diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp
index 898bfe9679..0e791443cb 100644
--- a/src/server/game/Entities/Player/Player.cpp
+++ b/src/server/game/Entities/Player/Player.cpp
@@ -15837,15 +15837,26 @@ void Player::KilledMonsterCredit(uint32 entry, ObjectGuid guid /*= ObjectGuid::E
uint16 curkillcount = q_status.CreatureOrGOCount[j];
if (curkillcount < reqkillcount)
{
- q_status.CreatureOrGOCount[j] = curkillcount + addkillcount;
+ uint16 newCount = curkillcount + addkillcount;
- m_QuestStatusSave[questid] = QUEST_DEFAULT_SAVE_TYPE;
+q_status.CreatureOrGOCount[j] = newCount;
- uint16 log_slot = FindQuestSlot(questid);
- if (log_slot < MAX_QUEST_LOG_SIZE)
- SetQuestSlotCounter(log_slot, j, curkillcount + addkillcount);
+m_QuestStatusSave[questid] = QUEST_DEFAULT_SAVE_TYPE;
- SendQuestUpdateAddCredit(qInfo, guid, j, curkillcount + addkillcount);
+uint16 log_slot = FindQuestSlot(questid);
+if (log_slot < MAX_QUEST_LOG_SIZE)
+ SetQuestSlotCounter(log_slot, j, newCount);
+
+sScriptMgr->OnQuestObjectiveCredit(
+ this,
+ questid,
+ j,
+ qInfo->RequiredNpcOrGo[j],
+ curkillcount,
+ newCount,
+ guid);
+
+SendQuestUpdateAddCredit(qInfo, guid, j, newCount);
}
if (CanCompleteQuest(questid))
CompleteQuest(questid);
@@ -15949,17 +15960,28 @@ void Player::KillCreditGO(uint32 entry, ObjectGuid guid)
uint32 reqCastCount = qInfo->RequiredNpcOrGoCount[j];
uint16 curCastCount = q_status.CreatureOrGOCount[j];
if (curCastCount < reqCastCount)
- {
- q_status.CreatureOrGOCount[j] = curCastCount + addCastCount;
+{
+ uint16 newCount = curCastCount + addCastCount;
- m_QuestStatusSave[questid] = QUEST_DEFAULT_SAVE_TYPE;
+ q_status.CreatureOrGOCount[j] = newCount;
- uint16 log_slot = FindQuestSlot(questid);
- if (log_slot < MAX_QUEST_LOG_SIZE)
- SetQuestSlotCounter(log_slot, j, curCastCount + addCastCount);
+ m_QuestStatusSave[questid] = QUEST_DEFAULT_SAVE_TYPE;
- SendQuestUpdateAddCredit(qInfo, guid, j, curCastCount + addCastCount);
- }
+ uint16 log_slot = FindQuestSlot(questid);
+ if (log_slot < MAX_QUEST_LOG_SIZE)
+ SetQuestSlotCounter(log_slot, j, newCount);
+
+ sScriptMgr->OnQuestObjectiveCredit(
+ this,
+ questid,
+ j,
+ qInfo->RequiredNpcOrGo[j],
+ curCastCount,
+ newCount,
+ guid);
+
+ SendQuestUpdateAddCredit(qInfo, guid, j, newCount);
+}
if (CanCompleteQuest(questid))
CompleteQuest(questid);
@@ -16011,15 +16033,26 @@ void Player::TalkedToCreature(uint32 entry, ObjectGuid guid)
uint16 curTalkCount = q_status.CreatureOrGOCount[j];
if (curTalkCount < reqTalkCount)
{
- q_status.CreatureOrGOCount[j] = curTalkCount + addTalkCount;
+ uint16 newCount = curTalkCount + addTalkCount;
- m_QuestStatusSave[questid] = QUEST_DEFAULT_SAVE_TYPE;
+q_status.CreatureOrGOCount[j] = newCount;
- uint16 log_slot = FindQuestSlot(questid);
- if (log_slot < MAX_QUEST_LOG_SIZE)
- SetQuestSlotCounter(log_slot, j, curTalkCount + addTalkCount);
+m_QuestStatusSave[questid] = QUEST_DEFAULT_SAVE_TYPE;
- SendQuestUpdateAddCredit(qInfo, guid, j, curTalkCount + addTalkCount);
+uint16 log_slot = FindQuestSlot(questid);
+if (log_slot < MAX_QUEST_LOG_SIZE)
+ SetQuestSlotCounter(log_slot, j, newCount);
+
+sScriptMgr->OnQuestObjectiveCredit(
+ this,
+ questid,
+ j,
+ qInfo->RequiredNpcOrGo[j],
+ curTalkCount,
+ newCount,
+ guid);
+
+SendQuestUpdateAddCredit(qInfo, guid, j, newCount);
}
if (CanCompleteQuest(questid))
CompleteQuest(questid);
diff --git a/src/server/game/Scripting/ScriptMgr.cpp b/src/server/game/Scripting/ScriptMgr.cpp
index 803f8767ca..a4fba0a408 100644
--- a/src/server/game/Scripting/ScriptMgr.cpp
+++ b/src/server/game/Scripting/ScriptMgr.cpp
@@ -1952,7 +1952,24 @@ void ScriptMgr::OnQuestStatusChange(Player* player, uint32 questId)
{
FOREACH_SCRIPT(PlayerScript)->OnQuestStatusChange(player, questId);
}
-
+void ScriptMgr::OnQuestObjectiveCredit(
+ Player* player,
+ uint32 questId,
+ uint8 objectiveIndex,
+ int32 objectiveEntry,
+ uint16 oldCount,
+ uint16 newCount,
+ ObjectGuid sourceGuid)
+{
+ FOREACH_SCRIPT(PlayerScript)->OnQuestObjectiveCredit(
+ player,
+ questId,
+ objectiveIndex,
+ objectiveEntry,
+ oldCount,
+ newCount,
+ sourceGuid);
+}
void ScriptMgr::OnPlayerRepop(Player* player)
{
FOREACH_SCRIPT(PlayerScript)->OnPlayerRepop(player);
diff --git a/src/server/game/Scripting/ScriptMgr.h b/src/server/game/Scripting/ScriptMgr.h
index bb6e9e5746..48e57ff2ed 100644
--- a/src/server/game/Scripting/ScriptMgr.h
+++ b/src/server/game/Scripting/ScriptMgr.h
@@ -708,6 +708,15 @@ class TC_GAME_API PlayerScript : public ScriptObject
// Called after a player's quest status has been changed
virtual void OnQuestStatusChange(Player* /*player*/, uint32 /*questId*/) { }
+ // Called after a creature/gameobject quest objective counter has changed
+virtual void OnQuestObjectiveCredit(
+ Player* /*player*/,
+ uint32 /*questId*/,
+ uint8 /*objectiveIndex*/,
+ int32 /*objectiveEntry*/,
+ uint16 /*oldCount*/,
+ uint16 /*newCount*/,
+ ObjectGuid /*sourceGuid*/) { }
// Called when a player presses release when he died
virtual void OnPlayerRepop(Player* /*player*/) { }
@@ -1035,6 +1044,14 @@ class TC_GAME_API ScriptMgr
void OnPlayerBindToInstance(Player* player, Difficulty difficulty, uint32 mapid, bool permanent, uint8 extendState);
void OnPlayerUpdateZone(Player* player, uint32 newZone, uint32 newArea);
void OnQuestStatusChange(Player* player, uint32 questId);
+ void OnQuestObjectiveCredit(
+ Player* player,
+ uint32 questId,
+ uint8 objectiveIndex,
+ int32 objectiveEntry,
+ uint16 oldCount,
+ uint16 newCount,
+ ObjectGuid sourceGuid);
void OnPlayerRepop(Player* player);
public: /* AccountScript */
diff --git a/src/server/scripts/Custom/custom_script_loader.cpp b/src/server/scripts/Custom/custom_script_loader.cpp
index e79a2ac228..c98bee0ad1 100644
--- a/src/server/scripts/Custom/custom_script_loader.cpp
+++ b/src/server/scripts/Custom/custom_script_loader.cpp
@@ -20,6 +20,7 @@
// VIANO_ADVENTURERS
void AddSC_viano_adventurers();
+void AddSC_viano_bot_commands();
// Viano Server diagnostics
void AddSC_vcore_diagnostics();
@@ -29,6 +30,7 @@ void AddSC_vcore_diagnostics();
void AddCustomScripts()
{
AddSC_viano_adventurers();
+ AddSC_viano_bot_commands();
AddSC_vcore_diagnostics();
}
diff --git a/src/server/scripts/Custom/custom_script_loader.cpp.bak-viano-botcommands-20260930-185547 b/src/server/scripts/Custom/custom_script_loader.cpp.bak-viano-botcommands-20260930-185547
new file mode 100644
index 0000000000..e79a2ac228
--- /dev/null
+++ b/src/server/scripts/Custom/custom_script_loader.cpp.bak-viano-botcommands-20260930-185547
@@ -0,0 +1,34 @@
+/*
+ * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see .
+ */
+
+// This is where scripts' loading functions should be declared:
+
+
+// VIANO_ADVENTURERS
+void AddSC_viano_adventurers();
+
+// Viano Server diagnostics
+void AddSC_vcore_diagnostics();
+
+// The name of this function should match:
+// void Add${NameOfDirectory}Scripts()
+void AddCustomScripts()
+{
+ AddSC_viano_adventurers();
+ AddSC_vcore_diagnostics();
+
+}
diff --git a/src/server/scripts/Custom/vcore_diagnostics.cpp b/src/server/scripts/Custom/vcore_diagnostics.cpp
index 8e6f950f83..6b4d3d2035 100644
--- a/src/server/scripts/Custom/vcore_diagnostics.cpp
+++ b/src/server/scripts/Custom/vcore_diagnostics.cpp
@@ -1,5 +1,5 @@
/*
- * Vcore Diagnostics Server v0.5
+ * Vcore Diagnostics Server v0.6.1
*
* Target: The Cataclysm Preservation Project / Vcore API line.
*
@@ -190,6 +190,139 @@ namespace VCoreDiagnostics
static QueryResult QuerySync(std::string const& sql);
+ static bool IsSafeSqlIdentifier(std::string const& value)
+ {
+ if (value.empty())
+ return false;
+
+ for (char c : value)
+ {
+ bool alpha = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
+ bool digit = c >= '0' && c <= '9';
+ if (!alpha && !digit && c != '_')
+ return false;
+ }
+
+ return true;
+ }
+
+ static std::optional PrimaryKeyColumn(
+ std::string const& database,
+ std::string const& table)
+ {
+ std::string safeDatabase = database;
+ std::string safeTable = table;
+ WorldDatabase.EscapeString(safeDatabase);
+ WorldDatabase.EscapeString(safeTable);
+
+ std::ostringstream sql;
+ sql << "SELECT COLUMN_NAME FROM information_schema.COLUMNS "
+ << "WHERE TABLE_SCHEMA='" << safeDatabase << "' "
+ << "AND TABLE_NAME='" << safeTable << "' "
+ << "AND COLUMN_KEY='PRI' "
+ << "ORDER BY ORDINAL_POSITION LIMIT 1";
+
+ QueryResult result = QuerySync(sql.str());
+ if (!result)
+ return std::nullopt;
+
+ std::string column = (*result)[0].GetString();
+ if (!IsSafeSqlIdentifier(column))
+ return std::nullopt;
+
+ return column;
+ }
+
+ static std::optional DisplayTextColumn(
+ std::string const& database,
+ std::string const& table)
+ {
+ std::string safeDatabase = database;
+ std::string safeTable = table;
+ WorldDatabase.EscapeString(safeDatabase);
+ WorldDatabase.EscapeString(safeTable);
+
+ std::ostringstream sql;
+ sql << "SELECT COLUMN_NAME FROM information_schema.COLUMNS "
+ << "WHERE TABLE_SCHEMA='" << safeDatabase << "' "
+ << "AND TABLE_NAME='" << safeTable << "' "
+ << "AND DATA_TYPE IN ('char','varchar','tinytext','text','mediumtext','longtext') "
+ << "ORDER BY ORDINAL_POSITION LIMIT 1";
+
+ QueryResult result = QuerySync(sql.str());
+ if (!result)
+ return std::nullopt;
+
+ std::string column = (*result)[0].GetString();
+ if (!IsSafeSqlIdentifier(column))
+ return std::nullopt;
+
+ return column;
+ }
+
+ static bool DatabaseEntityExists(
+ std::string const& database,
+ std::string const& table,
+ uint64 id)
+ {
+ if (!id)
+ return false;
+
+ auto keyColumn = PrimaryKeyColumn(database, table);
+ if (!keyColumn)
+ return false;
+
+ std::ostringstream sql;
+ sql << "SELECT 1 FROM `" << database << "`.`" << table << "` "
+ << "WHERE `" << *keyColumn << "`=" << id << " LIMIT 1";
+
+ return bool(QuerySync(sql.str()));
+ }
+
+ static bool ValidateDiagnosticFocus(
+ Player* player,
+ std::string const& focusType,
+ uint64 focusId)
+ {
+ if (focusType.empty())
+ return focusId == 0;
+
+ if (focusType == "quest")
+ return focusId <= 0xFFFFFFFFULL &&
+ IsQuestInPlayerLog(player, uint32(focusId));
+
+ if (focusType == "world_event")
+ {
+ // A tester may only select events explicitly exposed through the
+ // Vcore diagnostic event catalog.
+ std::ostringstream sql;
+ sql << "SELECT 1 "
+ << "FROM vcore_dev.diagnostic_event_catalog c "
+ << "INNER JOIN cata_world.game_event g "
+ << "ON g.eventEntry=c.event_entry "
+ << "WHERE c.enabled=1 "
+ << "AND c.event_entry=" << focusId << " "
+ << "LIMIT 1";
+
+ return bool(QuerySync(sql.str()));
+ }
+if (focusType == "creature")
+ return DatabaseEntityExists("cata_world", "creature_template", focusId);
+
+ if (focusType == "gameobject")
+ return DatabaseEntityExists("cata_world", "gameobject_template", focusId);
+
+ if (focusType == "zone")
+ return player && focusId == player->GetZoneId();
+
+ if (focusType == "other")
+ return focusId == 0;
+
+ return false;
+ }
+
+ static QueryResult QuerySync(std::string const& sql);
+
static std::string QuestTitle(uint32 questId)
{
if (!questId)
@@ -293,9 +426,20 @@ namespace VCoreDiagnostics
first = false;
out << "{";
- out << "\"id\":" << questId << ",";
- out << "\"status\":" << uint32(player->GetQuestStatus(questId));
- out << "}";
+out << "\"id\":" << questId << ",";
+out << "\"status\":" << uint32(player->GetQuestStatus(questId)) << ",";
+out << "\"objective_counters\":[";
+
+for (uint8 counter = 0; counter < QUEST_OBJECTIVES_COUNT; ++counter)
+{
+ if (counter)
+ out << ",";
+
+ out << player->GetQuestSlotCounter(slot, counter);
+}
+
+out << "]";
+out << "}";
}
out << "]";
@@ -566,11 +710,37 @@ namespace VCoreDiagnostics
static std::optional StartReproductionForReport(
Player* player,
uint64 reportId,
- uint32 questId = 0)
+ uint32 questId = 0,
+ std::string diagnosticFocusType = {},
+ uint64 diagnosticFocusId = 0)
{
if (!player || !reportId)
return std::nullopt;
+ // Backward compatibility: the old tester protocol supplied only questId.
+ // Treat that as an explicit quest focus so existing AddOn versions keep
+ // producing structured diagnostic context.
+ if (diagnosticFocusType.empty() && questId)
+ {
+ diagnosticFocusType = "quest";
+ diagnosticFocusId = questId;
+ }
+
+ if (diagnosticFocusType == "quest")
+ {
+ if (!diagnosticFocusId || diagnosticFocusId > 0xFFFFFFFFULL)
+ return std::nullopt;
+
+ uint32 focusQuestId = uint32(diagnosticFocusId);
+ if (questId && questId != focusQuestId)
+ return std::nullopt;
+
+ questId = focusQuestId;
+ }
+
+ if (!ValidateDiagnosticFocus(player, diagnosticFocusType, diagnosticFocusId))
+ return std::nullopt;
+
if (questId && !IsQuestInPlayerLog(player, questId))
return std::nullopt;
@@ -605,8 +775,8 @@ namespace VCoreDiagnostics
std::ostringstream sql;
sql << "INSERT INTO vcore_dev.bug_reproductions "
- << "(session_token, bug_id, report_id, quest_id, tester_account_id, tester_character_guid, "
- << "tester_character_name, result, map_id, zone_id, area_id, "
+ << "(session_token, bug_id, report_id, quest_id, diagnostic_focus_type, diagnostic_focus_id, "
+ << "tester_account_id, tester_character_guid, tester_character_name, result, map_id, zone_id, area_id, "
<< "position_x, position_y, position_z, orientation, started_at, created_at) "
<< "VALUES ('"
<< safeToken << "', ";
@@ -624,6 +794,21 @@ namespace VCoreDiagnostics
else
sql << "NULL";
+ std::string safeFocusType = diagnosticFocusType;
+ WorldDatabase.EscapeString(safeFocusType);
+
+ sql << ", ";
+ if (!diagnosticFocusType.empty())
+ sql << "'" << safeFocusType << "'";
+ else
+ sql << "NULL";
+
+ sql << ", ";
+ if (diagnosticFocusId)
+ sql << diagnosticFocusId;
+ else
+ sql << "NULL";
+
sql << ", "
<< player->GetSession()->GetAccountId() << ", "
<< player->GetGUID().GetCounter() << ", '"
@@ -654,11 +839,24 @@ namespace VCoreDiagnostics
<< "WHERE report_id=" << reportId;
ExecuteSync(updateReport.str());
+ std::ostringstream startData;
+ startData << "{\"diagnostic_focus\":";
+ if (!diagnosticFocusType.empty())
+ {
+ startData << "{\"type\":\"" << JsonEscape(diagnosticFocusType)
+ << "\",\"id\":" << diagnosticFocusId << "}";
+ }
+ else
+ {
+ startData << "null";
+ }
+ startData << ",\"snapshot\":" << BuildServerSnapshot(player) << "}";
+
InsertReproductionEvent(
*id,
"server",
"reproduction_started",
- "{\"snapshot\":" + BuildServerSnapshot(player) + "}");
+ startData.str());
return id;
}
@@ -913,9 +1111,16 @@ namespace VCoreDiagnostics
static std::optional StartTesterReproduction(
Player* player,
uint64 reportId,
- uint32 questId = 0)
+ uint32 questId = 0,
+ std::string diagnosticFocusType = {},
+ uint64 diagnosticFocusId = 0)
{
- return StartReproductionForReport(player, reportId, questId);
+ return StartReproductionForReport(
+ player,
+ reportId,
+ questId,
+ std::move(diagnosticFocusType),
+ diagnosticFocusId);
}
static bool AddTesterReproductionNote(
@@ -994,6 +1199,45 @@ namespace VCoreDiagnostics
SendAddonResponse(player, requestId, response.str());
}
+ static void SendWorldEventList(
+ Player* player,
+ std::string const& requestId)
+ {
+ // Tester-facing event selection is intentionally curated in
+ // vcore_dev.diagnostic_event_catalog. The authoritative game_event
+ // table remains the technical event source, while the catalog decides
+ // which events are useful diagnostic categories.
+ QueryResult result = QuerySync(
+ "SELECT c.event_entry, c.display_name "
+ "FROM vcore_dev.diagnostic_event_catalog c "
+ "INNER JOIN cata_world.game_event g "
+ "ON g.eventEntry=c.event_entry "
+ "WHERE c.enabled=1 "
+ "ORDER BY c.sort_order, c.display_name, c.event_entry");
+
+ std::ostringstream response;
+ response << "WORLD_EVENT_LIST|";
+
+ bool first = true;
+ if (result)
+ {
+ do
+ {
+ Field* fields = result->Fetch();
+ uint64 eventId = fields[0].GetUInt64();
+ std::string label = fields[1].GetString();
+
+ if (!first)
+ response << "\n";
+ first = false;
+
+ response << eventId << "|" << HexEncode(label);
+ } while (result->NextRow());
+ }
+
+ SendAddonResponse(player, requestId, response.str());
+ }
+
static void SendPermissions(Player* player, std::string const& requestId)
{
bool tester = HasTesterPermission(player);
@@ -1538,6 +1782,22 @@ namespace VCoreDiagnostics
return;
}
+ if (operation == 'V')
+ {
+ if (!HasTesterPermission(player))
+ {
+ SendAddonError(
+ player,
+ requestId,
+ "PERMISSION",
+ "Insufficient permission for tester diagnostic context.");
+ return;
+ }
+
+ SendWorldEventList(player, requestId);
+ return;
+ }
+
if (operation == 'R')
{
std::string severity;
@@ -1681,28 +1941,68 @@ namespace VCoreDiagnostics
{
uint32 reportId = 0;
uint32 questId = 0;
+ uint32 focusId = 0;
+ std::string focusType;
- std::string reportText = payload;
- std::string questText;
- std::string remainder;
+ // Backward compatible payloads:
+ // old: reportId|questId
+ // new: reportId|questId|focusType|focusId
+ std::vector fields;
+ std::size_t start = 0;
+ while (true)
+ {
+ std::size_t pos = payload.find('|', start);
+ if (pos == std::string::npos)
+ {
+ fields.push_back(payload.substr(start));
+ break;
+ }
- if (SplitFirst(payload, '|', reportText, remainder))
- questText = remainder;
+ fields.push_back(payload.substr(start, pos - start));
+ start = pos + 1;
+ }
- if (!ParseUnsigned(reportText, reportId) || !reportId)
+ if (fields.empty() || fields.size() > 4 ||
+ !ParseUnsigned(fields[0], reportId) || !reportId)
{
SendAddonError(player, requestId, "BAD_REPORT_ID", "Invalid report ID.");
return;
}
- if (!questText.empty() && !ParseUnsigned(questText, questId))
+ if (fields.size() >= 2 && !fields[1].empty() &&
+ !ParseUnsigned(fields[1], questId))
{
SendAddonError(player, requestId, "BAD_QUEST_ID", "Invalid quest ID.");
return;
}
- auto reproductionId =
- StartTesterReproduction(player, uint64(reportId), questId);
+ if (fields.size() >= 3)
+ focusType = fields[2];
+
+ if (fields.size() >= 4 && !fields[3].empty() &&
+ !ParseUnsigned(fields[3], focusId))
+ {
+ SendAddonError(player, requestId, "BAD_FOCUS_ID", "Invalid diagnostic focus ID.");
+ return;
+ }
+
+ if ((!focusType.empty() && !focusId && focusType != "other") ||
+ (focusType.empty() && focusId))
+ {
+ SendAddonError(
+ player,
+ requestId,
+ "BAD_FOCUS",
+ "Diagnostic focus type/ID combination is invalid.");
+ return;
+ }
+
+ auto reproductionId = StartTesterReproduction(
+ player,
+ uint64(reportId),
+ questId,
+ focusType,
+ uint64(focusId));
if (!reproductionId)
{
@@ -1710,7 +2010,7 @@ namespace VCoreDiagnostics
player,
requestId,
"REPRO_START_FAILED",
- "Could not start reproduction. Report/quest may be unavailable.");
+ "Could not start reproduction. Report or diagnostic focus may be unavailable.");
return;
}
@@ -2130,6 +2430,31 @@ namespace VCoreDiagnostics
Record(player, "quest_status_changed", data.str());
}
+ void OnQuestObjectiveCredit(
+ Player* player,
+ uint32 questId,
+ uint8 objectiveIndex,
+ int32 objectiveEntry,
+ uint16 oldCount,
+ uint16 newCount,
+ ObjectGuid sourceGuid) override
+{
+ if (!player)
+ return;
+
+ std::ostringstream data;
+ data << "{"
+ << "\"quest_id\":" << questId << ","
+ << "\"objective_index\":" << uint32(objectiveIndex) << ","
+ << "\"objective_entry\":" << objectiveEntry << ","
+ << "\"old_count\":" << oldCount << ","
+ << "\"new_count\":" << newCount << ","
+ << "\"source_guid\":" << sourceGuid.GetCounter()
+ << "}";
+
+ Record(player, "quest_objective_credit", data.str());
+}
+
void OnSpellCast(Player* player, Spell* spell, bool /*skipCheck*/) override
{
if (!spell || !spell->GetSpellInfo())
diff --git a/src/server/scripts/Custom/viano_adventurers.cpp b/src/server/scripts/Custom/viano_adventurers.cpp
index cd28a8c62b..a3032d11e5 100644
--- a/src/server/scripts/Custom/viano_adventurers.cpp
+++ b/src/server/scripts/Custom/viano_adventurers.cpp
@@ -329,49 +329,83 @@ namespace VianoAdventurers
UnitName(firstMeAttacker->GetVictim()), UnitEntry(firstMeAttacker->GetVictim()));
}
+ bool IsActivelyEngagedInCombat() const
+ {
+ // IsInCombat() alone is intentionally not enough here: the Core
+ // may keep the combat flag set after the bot has no live opponent.
+ if (Unit* victim = me->GetVictim())
+ if (IsValidCandidate(victim))
+ return true;
+
+ for (Unit* attacker : me->getAttackers())
+ if (IsValidCandidate(attacker))
+ return true;
+
+ return false;
+ }
+
+ Unit* SelectGroupCombatTarget(Player* owner)
+ {
+ if (!owner)
+ return nullptr;
+
+ std::list partyMembers;
+ owner->GetPartyMembers(partyMembers);
+
+ // In Vcore GetPartyMembers() returns living members of the same
+ // party/subgroup on the same map, plus their living guardian pets.
+ // An enemy attacking one of them is therefore already part of the
+ // group's current fight.
+ for (Unit* member : partyMembers)
+ {
+ if (!member || !member->IsAlive())
+ continue;
+
+ for (Unit* attacker : member->getAttackers())
+ if (IsValidCandidate(attacker))
+ return attacker;
+ }
+
+ return nullptr;
+ }
+
Unit* SelectAssistTarget(Player* owner)
-{
- if (!owner)
- return nullptr;
+ {
+ if (!owner)
+ return nullptr;
- // 1. Ein aktives Ziel des Besitzers ist eine ausdrückliche
- // Zielanweisung und darf das aktuelle Bot-Ziel überschreiben.
- if (Unit* target = owner->GetVictim())
- if (IsValidCandidate(target))
- return target;
+ // 1. An active owner victim is an explicit target instruction.
+ if (Unit* target = owner->GetVictim())
+ if (IsValidCandidate(target))
+ return target;
- // GetVictim() ist beim Spieler nicht immer zuverlässig gesetzt.
- // Während eines bereits laufenden Kampfes darf deshalb auch ein
- // bewusst ausgewähltes lebendes Feindziel als Anweisung gelten.
- // Reines Anwählen außerhalb des Kampfes darf weiterhin NICHT pullen.
- if (owner->IsInCombat())
- {
- if (Unit* target = owner->GetSelectedUnit())
- if (IsValidCandidate(target))
- return target;
- }
+ // During an already running fight, an explicitly selected living
+ // hostile target may also be treated as an instruction. Merely
+ // selecting an enemy out of combat still must not pull it.
+ if (owner->IsInCombat())
+ {
+ if (Unit* target = owner->GetSelectedUnit())
+ if (IsValidCandidate(target))
+ return target;
+ }
- // 2. Sobald der Bot selbst einen gültigen Gegner bekämpft,
- // hält er an diesem Ziel fest. ESC, eine Leiche als Target oder
- // fehlendes Spielertarget beendet den Kampf dadurch nicht mehr.
- if (Unit* target = me->GetVictim())
- if (IsValidCandidate(target))
- return target;
+ // 2. Keep the bot's own valid current victim.
+ if (Unit* target = me->GetVictim())
+ if (IsValidCandidate(target))
+ return target;
- // 3. Falls das bisherige Ziel tot/ungültig ist, helfen wir dem
- // Besitzer gegen einen Gegner, der ihn bereits angreift.
- for (Unit* attacker : owner->getAttackers())
- if (IsValidCandidate(attacker))
- return attacker;
+ // 3. Continue the group's fight even if the owner is a healer and
+ // currently targets a friendly tank or another party member.
+ if (Unit* target = SelectGroupCombatTarget(owner))
+ return target;
- // 4. Danach übernimmt der Bot einen Gegner, der ihn selbst angreift.
- // Besonders wichtig, wenn sein vorheriges Ziel gerade gestorben ist.
- for (Unit* attacker : me->getAttackers())
- if (IsValidCandidate(attacker))
- return attacker;
+ // 4. Finally, defend the bot itself if something is attacking it.
+ for (Unit* attacker : me->getAttackers())
+ if (IsValidCandidate(attacker))
+ return attacker;
- return nullptr;
-}
+ return nullptr;
+ }
void UpdateAI(uint32 diff) override
{
@@ -416,9 +450,11 @@ namespace VianoAdventurers
me->UpdateLevelDependantStats();
}
- if (me->GetDistance(owner) > TELEPORT_DISTANCE)
+ if (me->GetDistance(owner) > TELEPORT_DISTANCE && !IsActivelyEngagedInCombat())
{
- me->CombatStop(true);
+ // Keep the recovery teleport for geometry/jump problems, even
+ // if the group is already fighting elsewhere. Only the bot's
+ // own active engagement blocks teleporting.
me->GetMotionMaster()->Clear();
me->NearTeleportTo(
@@ -450,8 +486,11 @@ namespace VianoAdventurers
me->IsInCombat() ? 1u : 0u, UnitName(afterVictim), UnitEntry(afterVictim));
}
}
- else if (!me->IsInCombat() && !me->GetVictim())
+ else if (!IsActivelyEngagedInCombat())
{
+ // A stale Core combat flag must not prevent recovery/following.
+ // We deliberately do not clear the flag here; OOC regeneration
+ // will be handled as a separate change.
FollowOwner(owner);
}
diff --git a/src/server/scripts/Custom/viano_adventurers.cpp.bak-groupcombat-20260930-163130 b/src/server/scripts/Custom/viano_adventurers.cpp.bak-groupcombat-20260930-163130
new file mode 100644
index 0000000000..cd28a8c62b
--- /dev/null
+++ b/src/server/scripts/Custom/viano_adventurers.cpp.bak-groupcombat-20260930-163130
@@ -0,0 +1,477 @@
+/*
+ * Viano custom dungeon adventurers
+ * Initial melee companion implementation.
+ */
+
+#include "ScriptMgr.h"
+#include "ScriptedCreature.h"
+#include "ObjectAccessor.h"
+#include "Player.h"
+#include "TemporarySummon.h"
+#include "MotionMaster.h"
+#include "Log.h"
+
+namespace VianoAdventurers
+{
+ static constexpr float FOLLOW_DISTANCE = 2.5f;
+ static constexpr float FOLLOW_ANGLE = 3.14159265f;
+ static constexpr float TELEPORT_DISTANCE = 45.0f;
+ static constexpr uint32 UPDATE_INTERVAL = 500;
+ static constexpr uint32 DIAGNOSTIC_INTERVAL = 2000;
+
+ // VIANO_DD_PROFILE_V1_5
+ enum AdventurerSpells : uint32
+ {
+ SPELL_ADVENTURER_MORTAL_STRIKE = 65926,
+ SPELL_ADVENTURER_OVERPOWER = 65924,
+ SPELL_ADVENTURER_BLADESTORM = 65947
+ };
+
+ enum AdventurerEvents : uint32
+ {
+ EVENT_ADVENTURER_MORTAL_STRIKE = 1,
+ EVENT_ADVENTURER_OVERPOWER,
+ EVENT_ADVENTURER_BLADESTORM
+ };
+
+ // VIANO_AI_DIAGNOSTICS_V1
+
+ class npc_horde_adventurer_melee : public CreatureScript
+ {
+ public:
+ npc_horde_adventurer_melee() : CreatureScript("npc_horde_adventurer_melee") { }
+
+ struct npc_horde_adventurer_meleeAI : public ScriptedAI
+ {
+ npc_horde_adventurer_meleeAI(Creature* creature) : ScriptedAI(creature) { }
+
+ ObjectGuid ownerGuid;
+ EventMap combatEvents;
+ uint32 updateTimer = 0;
+ uint32 diagnosticTimer = 0;
+ uint32 recoveryTimer = 1000;
+ Player* GetOwnerPlayer() const
+ {
+ if (!ownerGuid)
+ return nullptr;
+
+ return ObjectAccessor::GetPlayer(*me, ownerGuid);
+ }
+
+ void FollowOwner(Player* owner)
+ {
+ if (!owner)
+ return;
+
+ me->GetMotionMaster()->MoveFollow(owner, FOLLOW_DISTANCE, FOLLOW_ANGLE);
+ }
+
+ void ResetCombatEvents()
+ {
+ combatEvents.Reset();
+
+ combatEvents.ScheduleEvent(EVENT_ADVENTURER_MORTAL_STRIKE, 2 * IN_MILLISECONDS);
+ combatEvents.ScheduleEvent(EVENT_ADVENTURER_OVERPOWER, 5 * IN_MILLISECONDS);
+ combatEvents.ScheduleEvent(EVENT_ADVENTURER_BLADESTORM, 12 * IN_MILLISECONDS);
+ }
+
+ void HandleCombatActions(uint32 diff)
+ {
+ Unit* victim = me->GetVictim();
+ if (!victim || !victim->IsAlive())
+ return;
+
+ combatEvents.Update(diff);
+
+ if (me->HasUnitState(UNIT_STATE_CASTING))
+ return;
+
+ if (uint32 eventId = combatEvents.ExecuteEvent())
+ {
+ switch (eventId)
+ {
+ case EVENT_ADVENTURER_MORTAL_STRIKE:
+ TC_LOG_INFO("scripts", "[VianoAI] cast Mortal Strike: target='%s'(%u)",
+ UnitName(victim), UnitEntry(victim));
+ DoCastVictim(SPELL_ADVENTURER_MORTAL_STRIKE);
+ combatEvents.ScheduleEvent(EVENT_ADVENTURER_MORTAL_STRIKE,
+ urand(6 * IN_MILLISECONDS, 8 * IN_MILLISECONDS));
+ return;
+
+ case EVENT_ADVENTURER_OVERPOWER:
+ TC_LOG_INFO("scripts", "[VianoAI] cast Overpower: target='%s'(%u)",
+ UnitName(victim), UnitEntry(victim));
+ DoCastVictim(SPELL_ADVENTURER_OVERPOWER);
+ combatEvents.ScheduleEvent(EVENT_ADVENTURER_OVERPOWER,
+ urand(10 * IN_MILLISECONDS, 14 * IN_MILLISECONDS));
+ return;
+
+ case EVENT_ADVENTURER_BLADESTORM:
+ TC_LOG_INFO("scripts", "[VianoAI] cast Bladestorm: target='%s'(%u)",
+ UnitName(victim), UnitEntry(victim));
+ DoCastVictim(SPELL_ADVENTURER_BLADESTORM);
+ combatEvents.ScheduleEvent(EVENT_ADVENTURER_BLADESTORM,
+ urand(30 * IN_MILLISECONDS, 45 * IN_MILLISECONDS));
+ return;
+
+ default:
+ break;
+ }
+ }
+
+ DoMeleeAttackIfReady();
+ }
+ void HandleOutOfCombatRecovery(uint32 diff)
+ {
+ if (!me->IsAlive())
+ {
+ recoveryTimer = 1000;
+ return;
+ }
+
+ // Keine künstliche Regeneration während eines Kampfes.
+ if (me->IsInCombat())
+ {
+ recoveryTimer = 1000;
+ return;
+ }
+
+ if (recoveryTimer > diff)
+ {
+ recoveryTimer -= diff;
+ return;
+ }
+
+ recoveryTimer = 1000;
+
+ // 10 % der maximalen Lebenspunkte pro Sekunde.
+ uint32 maxHealth = me->GetMaxHealth();
+
+ if (me->GetHealth() < maxHealth)
+ {
+ uint32 healthGain = maxHealth / 10;
+
+ if (healthGain == 0)
+ healthGain = 1;
+
+ uint32 newHealth = me->GetHealth() + healthGain;
+
+ if (newHealth > maxHealth)
+ newHealth = maxHealth;
+
+ me->SetHealth(newHealth);
+ }
+
+ // 10 % des maximalen Manas pro Sekunde.
+ uint32 maxMana = me->GetMaxPower(POWER_MANA);
+
+ if (maxMana > 0 && me->GetPower(POWER_MANA) < maxMana)
+ {
+ uint32 manaGain = maxMana / 10;
+
+ if (manaGain == 0)
+ manaGain = 1;
+
+ uint32 newMana = me->GetPower(POWER_MANA) + manaGain;
+
+ if (newMana > maxMana)
+ newMana = maxMana;
+
+ me->SetPower(POWER_MANA, newMana);
+ }
+}
+
+ void InitializeAI() override
+ {
+ ScriptedAI::InitializeAI();
+
+ TempSummon* summon = me->ToTempSummon();
+ if (!summon)
+ {
+ me->DespawnOrUnsummon();
+ return;
+ }
+
+ Unit* summoner = summon->GetSummoner();
+ Player* owner = summoner ? summoner->ToPlayer() : nullptr;
+ if (!owner)
+ {
+ me->DespawnOrUnsummon();
+ return;
+ }
+
+ ownerGuid = owner->GetGUID();
+
+ me->SetOwnerGUID(ownerGuid);
+ me->SetFaction(owner->GetFaction());
+ me->SetLevel(owner->getLevel());
+ me->UpdateLevelDependantStats();
+
+
+ static constexpr uint32 HORDE_ADVENTURER_MODELS[] =
+ {
+ 30063,
+ 30064,
+ 30065,
+ 30066
+ };
+ uint32 displayId = HORDE_ADVENTURER_MODELS[urand(0, 3)];
+
+ me->SetDisplayId(displayId, true);
+
+ TC_LOG_INFO("scripts","[VianoAI] Horde adventurer display selected: %u", displayId);
+
+ // REACT_PASSIVE lets AttackStart() briefly set a victim, but the
+ // core subsequently drops that victim again. We already suppress
+ // autonomous proximity aggro in MoveInLineOfSight(), so DEFENSIVE
+ // keeps an explicitly assigned assist target without making the
+ // adventurer independently pull nearby creatures.
+ me->SetReactState(REACT_DEFENSIVE);
+
+ TC_LOG_INFO("scripts",
+ "[VianoAI] initialized me='%s' entry=%u owner='%s' ownerGuid=%s meFaction=%u ownerFaction=%u level=%u",
+ me->GetName().c_str(), me->GetEntry(), owner->GetName().c_str(), ownerGuid.ToString().c_str(),
+ me->GetFaction(), owner->GetFaction(), uint32(me->getLevel()));
+
+ ResetCombatEvents();
+ FollowOwner(owner);
+ }
+
+ void Reset() override
+ {
+ ResetCombatEvents();
+
+ if (Player* owner = GetOwnerPlayer())
+ FollowOwner(owner);
+ }
+
+ void MoveInLineOfSight(Unit* /*who*/) override
+ {
+ }
+
+ char const* UnitName(Unit* unit) const
+ {
+ return unit ? unit->GetName().c_str() : "";
+ }
+
+ uint32 UnitEntry(Unit* unit) const
+ {
+ return unit ? unit->GetEntry() : 0;
+ }
+
+ bool IsValidCandidate(Unit* unit) const
+ {
+ return unit && unit->IsAlive() && me->IsValidAttackTarget(unit);
+ }
+
+ void LogDiagnostics(Player* owner)
+ {
+ if (!owner)
+ return;
+
+ Unit* ownerVictim = owner->GetVictim();
+ Unit* selected = owner->GetSelectedUnit();
+ Unit* meVictim = me->GetVictim();
+
+ Unit* firstOwnerAttacker = nullptr;
+ for (Unit* attacker : owner->getAttackers())
+ {
+ if (attacker)
+ {
+ firstOwnerAttacker = attacker;
+ break;
+ }
+ }
+
+ Unit* firstMeAttacker = nullptr;
+ for (Unit* attacker : me->getAttackers())
+ {
+ if (attacker)
+ {
+ firstMeAttacker = attacker;
+ break;
+ }
+ }
+
+ TC_LOG_INFO("scripts",
+ "[VianoAI] me='%s' entry=%u lvl=%u faction=%u react=%u alive=%u combat=%u hp=%u/%u victim='%s'(%u) attackers=%u distance=%.2f",
+ me->GetName().c_str(), me->GetEntry(), uint32(me->getLevel()), me->GetFaction(), uint32(me->GetReactState()),
+ me->IsAlive() ? 1u : 0u, me->IsInCombat() ? 1u : 0u, me->GetHealth(), me->GetMaxHealth(),
+ UnitName(meVictim), UnitEntry(meVictim), uint32(me->getAttackers().size()), me->GetDistance(owner));
+
+ TC_LOG_INFO("scripts",
+ "[VianoAI] owner='%s' lvl=%u faction=%u alive=%u combat=%u victim='%s'(%u) selected='%s'(%u) attackers=%u",
+ owner->GetName().c_str(), uint32(owner->getLevel()), owner->GetFaction(), owner->IsAlive() ? 1u : 0u,
+ owner->IsInCombat() ? 1u : 0u, UnitName(ownerVictim), UnitEntry(ownerVictim), UnitName(selected), UnitEntry(selected),
+ uint32(owner->getAttackers().size()));
+
+ if (ownerVictim)
+ TC_LOG_INFO("scripts", "[VianoAI] ownerVictim='%s' entry=%u faction=%u validForBot=%u botCanAttack=%u ownerCanAttack=%u victim='%s'(%u)",
+ ownerVictim->GetName().c_str(), ownerVictim->GetEntry(), ownerVictim->GetFaction(), IsValidCandidate(ownerVictim) ? 1u : 0u,
+ me->IsValidAttackTarget(ownerVictim) ? 1u : 0u, owner->IsValidAttackTarget(ownerVictim) ? 1u : 0u,
+ UnitName(ownerVictim->GetVictim()), UnitEntry(ownerVictim->GetVictim()));
+
+ if (selected)
+ TC_LOG_INFO("scripts", "[VianoAI] selected='%s' entry=%u faction=%u validForBot=%u botCanAttack=%u ownerCanAttack=%u victim='%s'(%u)",
+ selected->GetName().c_str(), selected->GetEntry(), selected->GetFaction(), IsValidCandidate(selected) ? 1u : 0u,
+ me->IsValidAttackTarget(selected) ? 1u : 0u, owner->IsValidAttackTarget(selected) ? 1u : 0u,
+ UnitName(selected->GetVictim()), UnitEntry(selected->GetVictim()));
+
+ if (firstOwnerAttacker)
+ TC_LOG_INFO("scripts", "[VianoAI] firstOwnerAttacker='%s' entry=%u faction=%u validForBot=%u botCanAttack=%u ownerCanAttack=%u victim='%s'(%u)",
+ firstOwnerAttacker->GetName().c_str(), firstOwnerAttacker->GetEntry(), firstOwnerAttacker->GetFaction(), IsValidCandidate(firstOwnerAttacker) ? 1u : 0u,
+ me->IsValidAttackTarget(firstOwnerAttacker) ? 1u : 0u, owner->IsValidAttackTarget(firstOwnerAttacker) ? 1u : 0u,
+ UnitName(firstOwnerAttacker->GetVictim()), UnitEntry(firstOwnerAttacker->GetVictim()));
+
+ if (firstMeAttacker)
+ TC_LOG_INFO("scripts", "[VianoAI] firstMeAttacker='%s' entry=%u faction=%u victim='%s'(%u)",
+ firstMeAttacker->GetName().c_str(), firstMeAttacker->GetEntry(), firstMeAttacker->GetFaction(),
+ UnitName(firstMeAttacker->GetVictim()), UnitEntry(firstMeAttacker->GetVictim()));
+ }
+
+ Unit* SelectAssistTarget(Player* owner)
+{
+ if (!owner)
+ return nullptr;
+
+ // 1. Ein aktives Ziel des Besitzers ist eine ausdrückliche
+ // Zielanweisung und darf das aktuelle Bot-Ziel überschreiben.
+ if (Unit* target = owner->GetVictim())
+ if (IsValidCandidate(target))
+ return target;
+
+ // GetVictim() ist beim Spieler nicht immer zuverlässig gesetzt.
+ // Während eines bereits laufenden Kampfes darf deshalb auch ein
+ // bewusst ausgewähltes lebendes Feindziel als Anweisung gelten.
+ // Reines Anwählen außerhalb des Kampfes darf weiterhin NICHT pullen.
+ if (owner->IsInCombat())
+ {
+ if (Unit* target = owner->GetSelectedUnit())
+ if (IsValidCandidate(target))
+ return target;
+ }
+
+ // 2. Sobald der Bot selbst einen gültigen Gegner bekämpft,
+ // hält er an diesem Ziel fest. ESC, eine Leiche als Target oder
+ // fehlendes Spielertarget beendet den Kampf dadurch nicht mehr.
+ if (Unit* target = me->GetVictim())
+ if (IsValidCandidate(target))
+ return target;
+
+ // 3. Falls das bisherige Ziel tot/ungültig ist, helfen wir dem
+ // Besitzer gegen einen Gegner, der ihn bereits angreift.
+ for (Unit* attacker : owner->getAttackers())
+ if (IsValidCandidate(attacker))
+ return attacker;
+
+ // 4. Danach übernimmt der Bot einen Gegner, der ihn selbst angreift.
+ // Besonders wichtig, wenn sein vorheriges Ziel gerade gestorben ist.
+ for (Unit* attacker : me->getAttackers())
+ if (IsValidCandidate(attacker))
+ return attacker;
+
+ return nullptr;
+}
+
+ void UpdateAI(uint32 diff) override
+ {
+ HandleOutOfCombatRecovery(diff);
+ if (diagnosticTimer > diff)
+ diagnosticTimer -= diff;
+ else
+ diagnosticTimer = 0;
+
+ if (updateTimer > diff)
+ {
+ updateTimer -= diff;
+
+ // VIANO_TARGET_CONTROL_V1_4
+ // Keep the explicitly assigned victim. UpdateVictim() may
+ // select another creature from the threat list.
+ if (me->GetVictim())
+ HandleCombatActions(diff);
+
+ return;
+ }
+
+ updateTimer = UPDATE_INTERVAL;
+
+ Player* owner = GetOwnerPlayer();
+ if (!owner || !owner->IsInWorld() || !me->IsInMap(owner))
+ {
+ TC_LOG_INFO("scripts", "[VianoAI] despawn: owner invalid or not on same map (ownerFound=%u)", owner ? 1u : 0u);
+ me->DespawnOrUnsummon();
+ return;
+ }
+
+ if (diagnosticTimer == 0)
+ {
+ diagnosticTimer = DIAGNOSTIC_INTERVAL;
+ LogDiagnostics(owner);
+ }
+
+ if (me->getLevel() != owner->getLevel())
+ {
+ me->SetLevel(owner->getLevel());
+ me->UpdateLevelDependantStats();
+ }
+
+ if (me->GetDistance(owner) > TELEPORT_DISTANCE)
+ {
+ me->CombatStop(true);
+ me->GetMotionMaster()->Clear();
+
+ me->NearTeleportTo(
+ owner->GetPositionX() + 1.5f,
+ owner->GetPositionY(),
+ owner->GetPositionZ(),
+ owner->GetOrientation()
+ );
+
+ FollowOwner(owner);
+ return;
+ }
+
+ Unit* desiredTarget = SelectAssistTarget(owner);
+
+ if (desiredTarget)
+ {
+ if (me->GetVictim() != desiredTarget)
+ {
+ TC_LOG_INFO("scripts",
+ "[VianoAI] AttackStart requested: target='%s' entry=%u targetFaction=%u valid=%u meCombatBefore=%u",
+ desiredTarget->GetName().c_str(), desiredTarget->GetEntry(), desiredTarget->GetFaction(),
+ me->IsValidAttackTarget(desiredTarget) ? 1u : 0u, me->IsInCombat() ? 1u : 0u);
+
+ AttackStart(desiredTarget);
+
+ Unit* afterVictim = me->GetVictim();
+ TC_LOG_INFO("scripts", "[VianoAI] AttackStart result: meCombatAfter=%u victim='%s'(%u)",
+ me->IsInCombat() ? 1u : 0u, UnitName(afterVictim), UnitEntry(afterVictim));
+ }
+ }
+ else if (!me->IsInCombat() && !me->GetVictim())
+ {
+ FollowOwner(owner);
+ }
+
+ // VIANO_TARGET_CONTROL_V1_4
+ // Keep the explicitly assigned victim. UpdateVictim() may
+ // select another creature from the threat list.
+ if (me->GetVictim())
+ HandleCombatActions(diff);
+ }
+ };
+
+ CreatureAI* GetAI(Creature* creature) const override
+ {
+ return new npc_horde_adventurer_meleeAI(creature);
+ }
+ };
+}
+
+void AddSC_viano_adventurers()
+{
+ using namespace VianoAdventurers;
+ new npc_horde_adventurer_melee();
+}
diff --git a/src/server/scripts/Custom/viano_bot_commands.cpp b/src/server/scripts/Custom/viano_bot_commands.cpp
new file mode 100644
index 0000000000..decd1afe75
--- /dev/null
+++ b/src/server/scripts/Custom/viano_bot_commands.cpp
@@ -0,0 +1,401 @@
+/*
+ * Viano bot GM diagnostics.
+ *
+ * Read-only commands:
+ * .viano bot info
+ * .viano bot relations
+ * .viano bot combat
+ * .viano bot owner
+ * .viano bot role
+ */
+
+#include "ScriptMgr.h"
+#include "Chat.h"
+#include "Creature.h"
+#include "ObjectAccessor.h"
+#include "ObjectMgr.h"
+#include "Player.h"
+#include "RBAC.h"
+#include "TemporarySummon.h"
+#include "Unit.h"
+
+#include
+
+namespace VianoBotCommands
+{
+ static char const* BoolText(bool value)
+ {
+ return value ? "yes" : "no";
+ }
+
+ static std::string GuidText(ObjectGuid const& guid)
+ {
+ return guid ? guid.ToString() : std::string("");
+ }
+
+ static char const* UnitName(Unit const* unit)
+ {
+ return unit ? unit->GetName().c_str() : "";
+ }
+
+ static uint32 UnitEntry(Unit const* unit)
+ {
+ if (Creature const* creature = unit ? unit->ToCreature() : nullptr)
+ return creature->GetEntry();
+
+ return 0;
+ }
+
+ static std::string GetScriptName(Creature const* creature)
+ {
+ if (!creature)
+ return {};
+
+ if (CreatureTemplate const* creatureTemplate = creature->GetCreatureTemplate())
+ return sObjectMgr->GetScriptName(creatureTemplate->ScriptID);
+
+ return {};
+ }
+
+ static bool IsVianoBot(Creature const* creature)
+ {
+ if (!creature)
+ return false;
+
+ std::string const scriptName = GetScriptName(creature);
+
+ return scriptName.rfind("npc_horde_adventurer_", 0) == 0
+ || scriptName.rfind("npc_alliance_adventurer_", 0) == 0;
+ }
+
+ static Creature* GetSelectedVianoBot(ChatHandler* handler)
+ {
+ if (!handler || !handler->GetSession())
+ return nullptr;
+
+ Player* player = handler->GetSession()->GetPlayer();
+ if (!player)
+ return nullptr;
+
+ Unit* selected = player->GetSelectedUnit();
+ Creature* creature = selected ? selected->ToCreature() : nullptr;
+
+ if (!creature)
+ {
+ handler->SendSysMessage("Select a Viano bot creature first.");
+ return nullptr;
+ }
+
+ if (!IsVianoBot(creature))
+ {
+ handler->PSendSysMessage(
+ "Selected creature is not a recognized Viano bot. entry=%u script='%s'",
+ creature->GetEntry(), GetScriptName(creature).c_str());
+ return nullptr;
+ }
+
+ return creature;
+ }
+
+ static Player* GetCommandPlayer(ChatHandler* handler)
+ {
+ return handler && handler->GetSession() ? handler->GetSession()->GetPlayer() : nullptr;
+ }
+
+ static Unit* GetResolvedController(Creature* bot)
+ {
+ return bot ? bot->GetCharmerOrOwnerOrCreatorOrSelf() : nullptr;
+ }
+
+ static char const* DetectRole(Creature const* bot)
+ {
+ std::string const scriptName = GetScriptName(bot);
+
+ if (scriptName.find("_melee") != std::string::npos)
+ return "damage-melee";
+ if (scriptName.find("_tank") != std::string::npos)
+ return "tank";
+ if (scriptName.find("_healer") != std::string::npos)
+ return "healer";
+ if (scriptName.find("_ranged") != std::string::npos)
+ return "damage-ranged";
+
+ return "unknown";
+ }
+
+ static void PrintHeader(ChatHandler* handler, Creature* bot, char const* section)
+ {
+ handler->PSendSysMessage(
+ "[VianoBot:%s] name='%s' entry=%u guid=%s script='%s'",
+ section,
+ bot->GetName().c_str(),
+ bot->GetEntry(),
+ GuidText(bot->GetGUID()).c_str(),
+ GetScriptName(bot).c_str());
+ }
+
+ static bool HandleInfo(ChatHandler* handler, char const* /*args*/)
+ {
+ Creature* bot = GetSelectedVianoBot(handler);
+ if (!bot)
+ return true;
+
+ Player* gm = GetCommandPlayer(handler);
+ Unit* victim = bot->GetVictim();
+
+ PrintHeader(handler, bot, "info");
+
+ handler->PSendSysMessage(
+ "level=%u alive=%s health=%u/%u faction=%u react=%u combat=%s attackers=%u",
+ uint32(bot->getLevel()),
+ BoolText(bot->IsAlive()),
+ bot->GetHealth(),
+ bot->GetMaxHealth(),
+ bot->GetFaction(),
+ uint32(bot->GetReactState()),
+ BoolText(bot->IsInCombat()),
+ uint32(bot->getAttackers().size()));
+
+ handler->PSendSysMessage(
+ "unitFlags=0x%08X immuneToPC=%s immuneToNPC=%s selectable=%s",
+ bot->GetUInt32Value(UNIT_FIELD_FLAGS),
+ BoolText(bot->IsImmuneToPC()),
+ BoolText(bot->IsImmuneToNPC()),
+ BoolText(!bot->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NOT_SELECTABLE)));
+
+ handler->PSendSysMessage(
+ "victim='%s' entry=%u victimGuid=%s",
+ UnitName(victim),
+ UnitEntry(victim),
+ victim ? GuidText(victim->GetGUID()).c_str() : "");
+
+ if (gm)
+ handler->PSendSysMessage(
+ "distanceToGM=%.2f gmAssistTarget=%s gmAttackTarget=%s",
+ bot->GetDistance(gm),
+ BoolText(gm->IsValidAssistTarget(bot)),
+ BoolText(gm->IsValidAttackTarget(bot)));
+
+ return true;
+ }
+
+ static bool HandleRelations(ChatHandler* handler, char const* /*args*/)
+ {
+ Creature* bot = GetSelectedVianoBot(handler);
+ if (!bot)
+ return true;
+
+ Player* player = GetCommandPlayer(handler);
+ if (!player)
+ return true;
+
+ PrintHeader(handler, bot, "relations");
+
+ handler->PSendSysMessage(
+ "player='%s' playerGuid=%s botFaction=%u playerFaction=%u",
+ player->GetName().c_str(),
+ GuidText(player->GetGUID()).c_str(),
+ bot->GetFaction(),
+ player->GetFaction());
+
+ handler->PSendSysMessage(
+ "player->bot: party=%s raid=%s assist=%s attack=%s reaction=%d",
+ BoolText(player->IsInPartyWith(bot)),
+ BoolText(player->IsInRaidWith(bot)),
+ BoolText(player->IsValidAssistTarget(bot)),
+ BoolText(player->IsValidAttackTarget(bot)),
+ int32(player->GetReactionTo(bot)));
+
+ handler->PSendSysMessage(
+ "bot->player: party=%s raid=%s assist=%s attack=%s reaction=%d",
+ BoolText(bot->IsInPartyWith(player)),
+ BoolText(bot->IsInRaidWith(player)),
+ BoolText(bot->IsValidAssistTarget(player)),
+ BoolText(bot->IsValidAttackTarget(player)),
+ int32(bot->GetReactionTo(player)));
+
+ handler->PSendSysMessage(
+ "bot immuneToPC=%s immuneToNPC=%s unitFlags=0x%08X",
+ BoolText(bot->IsImmuneToPC()),
+ BoolText(bot->IsImmuneToNPC()),
+ bot->GetUInt32Value(UNIT_FIELD_FLAGS));
+
+ return true;
+ }
+
+ static bool HandleCombat(ChatHandler* handler, char const* /*args*/)
+ {
+ Creature* bot = GetSelectedVianoBot(handler);
+ if (!bot)
+ return true;
+
+ Player* player = GetCommandPlayer(handler);
+ Unit* victim = bot->GetVictim();
+
+ PrintHeader(handler, bot, "combat");
+
+ handler->PSendSysMessage(
+ "botCombat=%s victim='%s' entry=%u attackers=%u casting=%s",
+ BoolText(bot->IsInCombat()),
+ UnitName(victim),
+ UnitEntry(victim),
+ uint32(bot->getAttackers().size()),
+ BoolText(bot->HasUnitState(UNIT_STATE_CASTING)));
+
+ uint32 index = 0;
+ for (Unit* attacker : bot->getAttackers())
+ {
+ if (!attacker)
+ continue;
+
+ handler->PSendSysMessage(
+ "attacker[%u]='%s' entry=%u guid=%s alive=%s victim='%s'(%u)",
+ index++,
+ attacker->GetName().c_str(),
+ UnitEntry(attacker),
+ GuidText(attacker->GetGUID()).c_str(),
+ BoolText(attacker->IsAlive()),
+ UnitName(attacker->GetVictim()),
+ UnitEntry(attacker->GetVictim()));
+ }
+
+ if (index == 0)
+ handler->SendSysMessage("attackers=");
+
+ if (player)
+ {
+ Unit* playerVictim = player->GetVictim();
+ Unit* selected = player->GetSelectedUnit();
+
+ handler->PSendSysMessage(
+ "playerCombat=%s playerVictim='%s'(%u) selected='%s'(%u)",
+ BoolText(player->IsInCombat()),
+ UnitName(playerVictim),
+ UnitEntry(playerVictim),
+ UnitName(selected),
+ UnitEntry(selected));
+ }
+
+ return true;
+ }
+
+ static bool HandleOwner(ChatHandler* handler, char const* /*args*/)
+ {
+ Creature* bot = GetSelectedVianoBot(handler);
+ if (!bot)
+ return true;
+
+ Unit* resolved = GetResolvedController(bot);
+ Unit* owner = bot->GetOwner();
+ Unit* creator = bot->GetCreatorGUID()
+ ? ObjectAccessor::GetUnit(*bot, bot->GetCreatorGUID())
+ : nullptr;
+ Unit* charmer = bot->GetCharmer();
+
+ PrintHeader(handler, bot, "owner");
+
+ handler->PSendSysMessage(
+ "OwnerGUID=%s CreatorGUID=%s CharmerGUID=%s",
+ GuidText(bot->GetOwnerGUID()).c_str(),
+ GuidText(bot->GetCreatorGUID()).c_str(),
+ GuidText(bot->GetCharmerGUID()).c_str());
+
+ handler->PSendSysMessage(
+ "owner='%s'(%u) creator='%s'(%u) charmer='%s'(%u)",
+ UnitName(owner), UnitEntry(owner),
+ UnitName(creator), UnitEntry(creator),
+ UnitName(charmer), UnitEntry(charmer));
+
+ handler->PSendSysMessage(
+ "resolvedController='%s' entry=%u guid=%s typeId=%u",
+ UnitName(resolved),
+ UnitEntry(resolved),
+ resolved ? GuidText(resolved->GetGUID()).c_str() : "",
+ resolved ? uint32(resolved->GetTypeId()) : 0u);
+
+ if (TempSummon* summon = bot->ToTempSummon())
+ {
+ Unit* summoner = summon->GetSummoner();
+ handler->PSendSysMessage(
+ "tempSummon=yes summoner='%s' entry=%u guid=%s",
+ UnitName(summoner),
+ UnitEntry(summoner),
+ summoner ? GuidText(summoner->GetGUID()).c_str() : "");
+ }
+ else
+ handler->SendSysMessage("tempSummon=no");
+
+ return true;
+ }
+
+ static bool HandleRole(ChatHandler* handler, char const* /*args*/)
+ {
+ Creature* bot = GetSelectedVianoBot(handler);
+ if (!bot)
+ return true;
+
+ Player* player = GetCommandPlayer(handler);
+ Unit* victim = bot->GetVictim();
+
+ PrintHeader(handler, bot, "role");
+
+ handler->PSendSysMessage(
+ "role=%s react=%u level=%u combat=%s victim='%s'(%u)",
+ DetectRole(bot),
+ uint32(bot->GetReactState()),
+ uint32(bot->getLevel()),
+ BoolText(bot->IsInCombat()),
+ UnitName(victim),
+ UnitEntry(victim));
+
+ if (player)
+ {
+ handler->PSendSysMessage(
+ "ownerCandidate='%s' distance=%.2f canAssistBot=%s canAttackBot=%s",
+ player->GetName().c_str(),
+ bot->GetDistance(player),
+ BoolText(player->IsValidAssistTarget(bot)),
+ BoolText(player->IsValidAttackTarget(bot)));
+ }
+
+ handler->SendSysMessage(
+ "role-specific AI state/cooldowns can be added here as tank/healer roles are implemented.");
+
+ return true;
+ }
+
+ class VianoBotCommandScript : public CommandScript
+ {
+ public:
+ VianoBotCommandScript() : CommandScript("VianoBotCommandScript") { }
+
+ std::vector GetCommands() const override
+ {
+ static std::vector botCommands =
+ {
+ { "info", rbac::RBAC_PERM_COMMAND_GM, false, &HandleInfo, "" },
+ { "relations", rbac::RBAC_PERM_COMMAND_GM, false, &HandleRelations, "" },
+ { "combat", rbac::RBAC_PERM_COMMAND_GM, false, &HandleCombat, "" },
+ { "owner", rbac::RBAC_PERM_COMMAND_GM, false, &HandleOwner, "" },
+ { "role", rbac::RBAC_PERM_COMMAND_GM, false, &HandleRole, "" },
+ };
+
+ static std::vector vianoCommands =
+ {
+ { "bot", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", botCommands },
+ };
+
+ static std::vector commandTable =
+ {
+ { "viano", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", vianoCommands },
+ };
+
+ return commandTable;
+ }
+ };
+}
+
+void AddSC_viano_bot_commands()
+{
+ new VianoBotCommands::VianoBotCommandScript();
+}
diff --git a/src/server/scripts/Custom/viano_bot_commands.cpp.bak-rbac-20260930-190232 b/src/server/scripts/Custom/viano_bot_commands.cpp.bak-rbac-20260930-190232
new file mode 100644
index 0000000000..c8d3ccd88a
--- /dev/null
+++ b/src/server/scripts/Custom/viano_bot_commands.cpp.bak-rbac-20260930-190232
@@ -0,0 +1,400 @@
+/*
+ * Viano bot GM diagnostics.
+ *
+ * Read-only commands:
+ * .viano bot info
+ * .viano bot relations
+ * .viano bot combat
+ * .viano bot owner
+ * .viano bot role
+ */
+
+#include "ScriptMgr.h"
+#include "Chat.h"
+#include "Creature.h"
+#include "ObjectAccessor.h"
+#include "ObjectMgr.h"
+#include "Player.h"
+#include "TemporarySummon.h"
+#include "Unit.h"
+
+#include
+
+namespace VianoBotCommands
+{
+ static char const* BoolText(bool value)
+ {
+ return value ? "yes" : "no";
+ }
+
+ static std::string GuidText(ObjectGuid const& guid)
+ {
+ return guid ? guid.ToString() : std::string("");
+ }
+
+ static char const* UnitName(Unit const* unit)
+ {
+ return unit ? unit->GetName().c_str() : "";
+ }
+
+ static uint32 UnitEntry(Unit const* unit)
+ {
+ if (Creature const* creature = unit ? unit->ToCreature() : nullptr)
+ return creature->GetEntry();
+
+ return 0;
+ }
+
+ static std::string GetScriptName(Creature const* creature)
+ {
+ if (!creature)
+ return {};
+
+ if (CreatureTemplate const* creatureTemplate = creature->GetCreatureTemplate())
+ return sObjectMgr->GetScriptName(creatureTemplate->ScriptID);
+
+ return {};
+ }
+
+ static bool IsVianoBot(Creature const* creature)
+ {
+ if (!creature)
+ return false;
+
+ std::string const scriptName = GetScriptName(creature);
+
+ return scriptName.rfind("npc_horde_adventurer_", 0) == 0
+ || scriptName.rfind("npc_alliance_adventurer_", 0) == 0;
+ }
+
+ static Creature* GetSelectedVianoBot(ChatHandler* handler)
+ {
+ if (!handler || !handler->GetSession())
+ return nullptr;
+
+ Player* player = handler->GetSession()->GetPlayer();
+ if (!player)
+ return nullptr;
+
+ Unit* selected = player->GetSelectedUnit();
+ Creature* creature = selected ? selected->ToCreature() : nullptr;
+
+ if (!creature)
+ {
+ handler->SendSysMessage("Select a Viano bot creature first.");
+ return nullptr;
+ }
+
+ if (!IsVianoBot(creature))
+ {
+ handler->PSendSysMessage(
+ "Selected creature is not a recognized Viano bot. entry=%u script='%s'",
+ creature->GetEntry(), GetScriptName(creature).c_str());
+ return nullptr;
+ }
+
+ return creature;
+ }
+
+ static Player* GetCommandPlayer(ChatHandler* handler)
+ {
+ return handler && handler->GetSession() ? handler->GetSession()->GetPlayer() : nullptr;
+ }
+
+ static Unit* GetResolvedController(Creature* bot)
+ {
+ return bot ? bot->GetCharmerOrOwnerOrCreatorOrSelf() : nullptr;
+ }
+
+ static char const* DetectRole(Creature const* bot)
+ {
+ std::string const scriptName = GetScriptName(bot);
+
+ if (scriptName.find("_melee") != std::string::npos)
+ return "damage-melee";
+ if (scriptName.find("_tank") != std::string::npos)
+ return "tank";
+ if (scriptName.find("_healer") != std::string::npos)
+ return "healer";
+ if (scriptName.find("_ranged") != std::string::npos)
+ return "damage-ranged";
+
+ return "unknown";
+ }
+
+ static void PrintHeader(ChatHandler* handler, Creature* bot, char const* section)
+ {
+ handler->PSendSysMessage(
+ "[VianoBot:%s] name='%s' entry=%u guid=%s script='%s'",
+ section,
+ bot->GetName().c_str(),
+ bot->GetEntry(),
+ GuidText(bot->GetGUID()).c_str(),
+ GetScriptName(bot).c_str());
+ }
+
+ static bool HandleInfo(ChatHandler* handler, char const* /*args*/)
+ {
+ Creature* bot = GetSelectedVianoBot(handler);
+ if (!bot)
+ return true;
+
+ Player* gm = GetCommandPlayer(handler);
+ Unit* victim = bot->GetVictim();
+
+ PrintHeader(handler, bot, "info");
+
+ handler->PSendSysMessage(
+ "level=%u alive=%s health=%u/%u faction=%u react=%u combat=%s attackers=%u",
+ uint32(bot->getLevel()),
+ BoolText(bot->IsAlive()),
+ bot->GetHealth(),
+ bot->GetMaxHealth(),
+ bot->GetFaction(),
+ uint32(bot->GetReactState()),
+ BoolText(bot->IsInCombat()),
+ uint32(bot->getAttackers().size()));
+
+ handler->PSendSysMessage(
+ "unitFlags=0x%08X immuneToPC=%s immuneToNPC=%s selectable=%s",
+ bot->GetUInt32Value(UNIT_FIELD_FLAGS),
+ BoolText(bot->IsImmuneToPC()),
+ BoolText(bot->IsImmuneToNPC()),
+ BoolText(!bot->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NOT_SELECTABLE)));
+
+ handler->PSendSysMessage(
+ "victim='%s' entry=%u victimGuid=%s",
+ UnitName(victim),
+ UnitEntry(victim),
+ victim ? GuidText(victim->GetGUID()).c_str() : "");
+
+ if (gm)
+ handler->PSendSysMessage(
+ "distanceToGM=%.2f gmAssistTarget=%s gmAttackTarget=%s",
+ bot->GetDistance(gm),
+ BoolText(gm->IsValidAssistTarget(bot)),
+ BoolText(gm->IsValidAttackTarget(bot)));
+
+ return true;
+ }
+
+ static bool HandleRelations(ChatHandler* handler, char const* /*args*/)
+ {
+ Creature* bot = GetSelectedVianoBot(handler);
+ if (!bot)
+ return true;
+
+ Player* player = GetCommandPlayer(handler);
+ if (!player)
+ return true;
+
+ PrintHeader(handler, bot, "relations");
+
+ handler->PSendSysMessage(
+ "player='%s' playerGuid=%s botFaction=%u playerFaction=%u",
+ player->GetName().c_str(),
+ GuidText(player->GetGUID()).c_str(),
+ bot->GetFaction(),
+ player->GetFaction());
+
+ handler->PSendSysMessage(
+ "player->bot: party=%s raid=%s assist=%s attack=%s reaction=%d",
+ BoolText(player->IsInPartyWith(bot)),
+ BoolText(player->IsInRaidWith(bot)),
+ BoolText(player->IsValidAssistTarget(bot)),
+ BoolText(player->IsValidAttackTarget(bot)),
+ int32(player->GetReactionTo(bot)));
+
+ handler->PSendSysMessage(
+ "bot->player: party=%s raid=%s assist=%s attack=%s reaction=%d",
+ BoolText(bot->IsInPartyWith(player)),
+ BoolText(bot->IsInRaidWith(player)),
+ BoolText(bot->IsValidAssistTarget(player)),
+ BoolText(bot->IsValidAttackTarget(player)),
+ int32(bot->GetReactionTo(player)));
+
+ handler->PSendSysMessage(
+ "bot immuneToPC=%s immuneToNPC=%s unitFlags=0x%08X",
+ BoolText(bot->IsImmuneToPC()),
+ BoolText(bot->IsImmuneToNPC()),
+ bot->GetUInt32Value(UNIT_FIELD_FLAGS));
+
+ return true;
+ }
+
+ static bool HandleCombat(ChatHandler* handler, char const* /*args*/)
+ {
+ Creature* bot = GetSelectedVianoBot(handler);
+ if (!bot)
+ return true;
+
+ Player* player = GetCommandPlayer(handler);
+ Unit* victim = bot->GetVictim();
+
+ PrintHeader(handler, bot, "combat");
+
+ handler->PSendSysMessage(
+ "botCombat=%s victim='%s' entry=%u attackers=%u casting=%s",
+ BoolText(bot->IsInCombat()),
+ UnitName(victim),
+ UnitEntry(victim),
+ uint32(bot->getAttackers().size()),
+ BoolText(bot->HasUnitState(UNIT_STATE_CASTING)));
+
+ uint32 index = 0;
+ for (Unit* attacker : bot->getAttackers())
+ {
+ if (!attacker)
+ continue;
+
+ handler->PSendSysMessage(
+ "attacker[%u]='%s' entry=%u guid=%s alive=%s victim='%s'(%u)",
+ index++,
+ attacker->GetName().c_str(),
+ UnitEntry(attacker),
+ GuidText(attacker->GetGUID()).c_str(),
+ BoolText(attacker->IsAlive()),
+ UnitName(attacker->GetVictim()),
+ UnitEntry(attacker->GetVictim()));
+ }
+
+ if (index == 0)
+ handler->SendSysMessage("attackers=");
+
+ if (player)
+ {
+ Unit* playerVictim = player->GetVictim();
+ Unit* selected = player->GetSelectedUnit();
+
+ handler->PSendSysMessage(
+ "playerCombat=%s playerVictim='%s'(%u) selected='%s'(%u)",
+ BoolText(player->IsInCombat()),
+ UnitName(playerVictim),
+ UnitEntry(playerVictim),
+ UnitName(selected),
+ UnitEntry(selected));
+ }
+
+ return true;
+ }
+
+ static bool HandleOwner(ChatHandler* handler, char const* /*args*/)
+ {
+ Creature* bot = GetSelectedVianoBot(handler);
+ if (!bot)
+ return true;
+
+ Unit* resolved = GetResolvedController(bot);
+ Unit* owner = bot->GetOwner();
+ Unit* creator = bot->GetCreatorGUID()
+ ? ObjectAccessor::GetUnit(*bot, bot->GetCreatorGUID())
+ : nullptr;
+ Unit* charmer = bot->GetCharmer();
+
+ PrintHeader(handler, bot, "owner");
+
+ handler->PSendSysMessage(
+ "OwnerGUID=%s CreatorGUID=%s CharmerGUID=%s",
+ GuidText(bot->GetOwnerGUID()).c_str(),
+ GuidText(bot->GetCreatorGUID()).c_str(),
+ GuidText(bot->GetCharmerGUID()).c_str());
+
+ handler->PSendSysMessage(
+ "owner='%s'(%u) creator='%s'(%u) charmer='%s'(%u)",
+ UnitName(owner), UnitEntry(owner),
+ UnitName(creator), UnitEntry(creator),
+ UnitName(charmer), UnitEntry(charmer));
+
+ handler->PSendSysMessage(
+ "resolvedController='%s' entry=%u guid=%s typeId=%u",
+ UnitName(resolved),
+ UnitEntry(resolved),
+ resolved ? GuidText(resolved->GetGUID()).c_str() : "",
+ resolved ? uint32(resolved->GetTypeId()) : 0u);
+
+ if (TempSummon* summon = bot->ToTempSummon())
+ {
+ Unit* summoner = summon->GetSummoner();
+ handler->PSendSysMessage(
+ "tempSummon=yes summoner='%s' entry=%u guid=%s",
+ UnitName(summoner),
+ UnitEntry(summoner),
+ summoner ? GuidText(summoner->GetGUID()).c_str() : "");
+ }
+ else
+ handler->SendSysMessage("tempSummon=no");
+
+ return true;
+ }
+
+ static bool HandleRole(ChatHandler* handler, char const* /*args*/)
+ {
+ Creature* bot = GetSelectedVianoBot(handler);
+ if (!bot)
+ return true;
+
+ Player* player = GetCommandPlayer(handler);
+ Unit* victim = bot->GetVictim();
+
+ PrintHeader(handler, bot, "role");
+
+ handler->PSendSysMessage(
+ "role=%s react=%u level=%u combat=%s victim='%s'(%u)",
+ DetectRole(bot),
+ uint32(bot->GetReactState()),
+ uint32(bot->getLevel()),
+ BoolText(bot->IsInCombat()),
+ UnitName(victim),
+ UnitEntry(victim));
+
+ if (player)
+ {
+ handler->PSendSysMessage(
+ "ownerCandidate='%s' distance=%.2f canAssistBot=%s canAttackBot=%s",
+ player->GetName().c_str(),
+ bot->GetDistance(player),
+ BoolText(player->IsValidAssistTarget(bot)),
+ BoolText(player->IsValidAttackTarget(bot)));
+ }
+
+ handler->SendSysMessage(
+ "role-specific AI state/cooldowns can be added here as tank/healer roles are implemented.");
+
+ return true;
+ }
+
+ class VianoBotCommandScript : public CommandScript
+ {
+ public:
+ VianoBotCommandScript() : CommandScript("VianoBotCommandScript") { }
+
+ std::vector GetCommands() const override
+ {
+ static std::vector botCommands =
+ {
+ { "info", rbac::RBAC_PERM_COMMAND_GM, false, &HandleInfo, "" },
+ { "relations", rbac::RBAC_PERM_COMMAND_GM, false, &HandleRelations, "" },
+ { "combat", rbac::RBAC_PERM_COMMAND_GM, false, &HandleCombat, "" },
+ { "owner", rbac::RBAC_PERM_COMMAND_GM, false, &HandleOwner, "" },
+ { "role", rbac::RBAC_PERM_COMMAND_GM, false, &HandleRole, "" },
+ };
+
+ static std::vector vianoCommands =
+ {
+ { "bot", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", botCommands },
+ };
+
+ static std::vector commandTable =
+ {
+ { "viano", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", vianoCommands },
+ };
+
+ return commandTable;
+ }
+ };
+}
+
+void AddSC_viano_bot_commands()
+{
+ new VianoBotCommands::VianoBotCommandScript();
+}
diff --git a/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp
index 0e7237ea81..493bb80626 100644
--- a/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp
+++ b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp
@@ -288,10 +288,24 @@ struct npc_worgen_runt : public ScriptedAI
_events.ScheduleEvent(EVENT_AGGRO_PLAYER, 2s);
break;
case EVENT_AGGRO_PLAYER:
- if (Unit* player = ObjectAccessor::GetPlayer(*me, _playerGuid))
- if (me->IsInDist(player->GetPosition(), 100.0f))
- AttackStart(player);
+ {
+ Creature* crowley = me->FindNearestCreature(35077, 100.0f, true);
+ Creature* tobias = me->FindNearestCreature(35124, 100.0f, true);
+
+ if (crowley && tobias)
+ {
+ if (me->GetDistance(crowley) <= me->GetDistance(tobias))
+ AttackStart(crowley);
+ else
+ AttackStart(tobias);
+ }
+ else if (crowley)
+ AttackStart(crowley);
+ else if (tobias)
+ AttackStart(tobias);
+
break;
+ }
default:
break;
}
diff --git a/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug14-20260930-234127 b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug14-20260930-234127
new file mode 100644
index 0000000000..0e7237ea81
--- /dev/null
+++ b/src/server/scripts/EasternKingdoms/Gilneas/gilneas_chapter_1.cpp.bak-bug14-20260930-234127
@@ -0,0 +1,682 @@
+/*
+* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
+*
+* This program is free software; you can redistribute it and/or modify it
+* under the terms of the GNU General Public License as published by the
+* Free Software Foundation; either version 2 of the License, or (at your
+* option) any later version.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT
+* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+* more details.
+*
+* You should have received a copy of the GNU General Public License along
+* with this program. If not, see .
+*/
+
+#include "gilneas.h"
+#include "ScriptMgr.h"
+#include "CombatAI.h"
+#include "MotionMaster.h"
+#include "ObjectAccessor.h"
+#include "Player.h"
+#include "PassiveAI.h"
+#include "PhasingHandler.h"
+#include "ScriptedCreature.h"
+#include "SpellInfo.h"
+#include "SpellScript.h"
+#include "TemporarySummon.h"
+#include "Vehicle.h"
+
+namespace Gilneas::Chapter1
+{
+
+/*######
+## Quest 14098 - Evacuate the Merchant Square
+######*/
+
+enum FrightenedCitizen
+{
+ SAY_FRIGHTENED_CITIZEN_RESCUE = 0,
+
+ NPC_EVACUATION_STALKER_FIRST = 35830,
+ NPC_EVACUATION_STALKER_NEAR = 35010,
+ NPC_EVACUATION_STALKER_FAR = 35011,
+ NPC_RAMPAGING_WORGEN = 35660,
+
+ CREDIT_35830 = 35830,
+
+ POINT_STALKER_FIRST = 1,
+ POINT_STALKER_NEAR = 2,
+ POINT_STALKER_FAR = 3,
+
+ EVENT_TALK_FRIGHTENED = 1,
+ EVENT_MOVE_TO_NEAR_STALKER = 2,
+ EVENT_MOVE_TO_FAR_STALKER = 3,
+ EVENT_DESPAWN = 4
+};
+
+struct npc_frightened_citizen : public PassiveAI
+{
+ npc_frightened_citizen(Creature* creature) : PassiveAI(creature) { }
+
+ void IsSummonedBy(Unit* /*summoner*/) override
+ {
+ if (Creature* stalkerNear = me->FindNearestCreature(NPC_EVACUATION_STALKER_FIRST, 20.0f))
+ me->GetMotionMaster()->MovePoint(POINT_STALKER_FIRST, stalkerNear->GetPosition(), true);
+ }
+
+ void MovementInform(uint32 type, uint32 id) override
+ {
+ if (type != POINT_MOTION_TYPE)
+ return;
+
+ switch (id)
+ {
+ case POINT_STALKER_FIRST:
+ _events.ScheduleEvent(EVENT_TALK_FRIGHTENED, 1s);
+ break;
+ case POINT_STALKER_NEAR:
+ _events.ScheduleEvent(EVENT_MOVE_TO_FAR_STALKER, 1ms);
+ break;
+ case POINT_STALKER_FAR:
+ _events.ScheduleEvent(EVENT_DESPAWN, 1ms);
+ break;
+ default:
+ break;
+ }
+ }
+
+ void UpdateAI(uint32 diff) override
+ {
+ _events.Update(diff);
+
+ while (uint32 eventId = _events.ExecuteEvent())
+ {
+ switch (eventId)
+ {
+ case EVENT_TALK_FRIGHTENED:
+ if (Unit* summoner = me->ToTempSummon()->GetSummoner())
+ {
+ if (Player* player = summoner->ToPlayer())
+ {
+ player->KilledMonsterCredit(CREDIT_35830);
+ Talk(SAY_FRIGHTENED_CITIZEN_RESCUE, summoner);
+ }
+ }
+ _events.ScheduleEvent(EVENT_MOVE_TO_NEAR_STALKER, 2s);
+ break;
+ case EVENT_MOVE_TO_NEAR_STALKER:
+ if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_FAR, 50.0f))
+ me->GetMotionMaster()->MovePoint(POINT_STALKER_FAR, stalker->GetPosition(), true);
+ else if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_NEAR, 100.0f))
+ me->GetMotionMaster()->MovePoint(POINT_STALKER_NEAR, stalker->GetPosition(), true);
+ break;
+ case EVENT_MOVE_TO_FAR_STALKER:
+ if (Creature* stalker = me->FindNearestCreature(NPC_EVACUATION_STALKER_FAR, 500.0f))
+ me->GetMotionMaster()->MovePoint(POINT_STALKER_FAR, stalker->GetPosition(), true);
+ break;
+ case EVENT_DESPAWN:
+ me->DespawnOrUnsummon();
+ break;
+ default:
+ break;
+ }
+ }
+ }
+private:
+ EventMap _events;
+};
+
+/*######
+## Quest 14154 - By the Skin of his Teeth
+######*/
+
+enum WorgenRunt
+{
+ EVENT_JUMP_TO_PRISON = 1,
+ EVENT_AGGRO_PLAYER = 2,
+ EVENT_FORCE_DESPAWN = 3,
+
+ PHASE_ROOF = 0,
+ PHASE_COMBAT = 1,
+
+ WORGEN_ID_ROOF_1 = 0,
+ WORGEN_ID_ROOF_2 = 1,
+ WORGEN_ID_ROOF_3 = 2,
+ WORGEN_ID_ROOF_4 = 3,
+ WORGEN_ID_ROOF_5 = 4,
+ WORGEN_ID_ROOF_6 = 5,
+ WORGEN_ID_ROOF_7 = 6,
+
+ WORGEN_ID_CATHEDRAL_1 = 7,
+ WORGEN_ID_CATHEDRAL_2 = 8,
+ WORGEN_ID_CATHEDRAL_3 = 9,
+ WORGEN_ID_CATHEDRAL_4 = 10,
+ WORGEN_ID_CATHEDRAL_5 = 11,
+ WORGEN_ID_CATHEDRAL_6 = 12,
+ WORGEN_ID_CATHEDRAL_7 = 13,
+ WORGEN_ID_CATHEDRAL_8 = 14,
+ WORGEN_ID_CATHEDRAL_9 = 15,
+
+ NPC_WORGEN_RUNT_SPELL = 35188,
+};
+
+static Position const RuntSpellSummonJumpPos = { -1671.915f, 1446.734f, 52.28712f };
+
+struct npc_worgen_runt : public ScriptedAI
+{
+ npc_worgen_runt(Creature* creature) : ScriptedAI(creature), _worgenID(0), _wayPointCounter(0), _jumped(false) { }
+
+ void JustEngagedWith(Unit* /*who*/) override
+ {
+ _events.SetPhase(PHASE_COMBAT);
+ }
+
+ void IsSummonedBy(Unit* summoner) override
+ {
+ me->setActive(true); // we are in a phased and cut off map so we're fine to use that here
+ _events.SetPhase(PHASE_ROOF);
+ _events.ScheduleEvent(EVENT_FORCE_DESPAWN, 70s, 0, PHASE_ROOF);
+ _playerGuid = summoner->GetGUID();
+ if (me->GetEntry() == NPC_WORGEN_RUNT_SPELL)
+ me->GetMotionMaster()->MoveJump(RuntSpellSummonJumpPos, 16.0f, 4.371286f);
+ }
+
+ void DoAction(int32 action) override
+ {
+ _worgenID = action;
+ switch (_worgenID) // ugly as fuck but needed for individual controls
+ {
+ // House roof cases
+ case WORGEN_ID_ROOF_1:
+ _wayPointCounter = runtHousePathSize1;
+ me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize1, worgenRuntHousePath1, runtHousePathSize1);
+ break;
+ case WORGEN_ID_ROOF_2:
+ _wayPointCounter = runtHousePathSize2;
+ me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize2, worgenRuntHousePath2, runtHousePathSize2);
+ break;
+ case WORGEN_ID_ROOF_3:
+ _wayPointCounter = runtHousePathSize3;
+ me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize3, worgenRuntHousePath3, runtHousePathSize3);
+ break;
+ case WORGEN_ID_ROOF_4:
+ _wayPointCounter = runtHousePathSize4;
+ me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize4, worgenRuntHousePath4, runtHousePathSize4);
+ break;
+ case WORGEN_ID_ROOF_5:
+ _wayPointCounter = runtHousePathSize5;
+ me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize5, worgenRuntHousePath5, runtHousePathSize5);
+ break;
+ case WORGEN_ID_ROOF_6:
+ _wayPointCounter = runtHousePathSize6;
+ me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize6, worgenRuntHousePath6, runtHousePathSize6);
+ break;
+ case WORGEN_ID_ROOF_7:
+ _wayPointCounter = runtHousePathSize7;
+ me->GetMotionMaster()->MoveSmoothPath(runtHousePathSize7, worgenRuntHousePath7, runtHousePathSize7);
+ break;
+ // Cathedral cases
+ case WORGEN_ID_CATHEDRAL_1:
+ _wayPointCounter = runtCathedralPathSize1;
+ me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize1, worgenRuntCathedralPath1, runtCathedralPathSize1);
+ break;
+ case WORGEN_ID_CATHEDRAL_2:
+ _wayPointCounter = runtCathedralPathSize2;
+ me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize2, worgenRuntCathedralPath1, runtCathedralPathSize2);
+ break;
+ case WORGEN_ID_CATHEDRAL_3:
+ _wayPointCounter = runtCathedralPathSize3;
+ me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize3, worgenRuntCathedralPath1, runtCathedralPathSize3);
+ break;
+ case WORGEN_ID_CATHEDRAL_4:
+ _wayPointCounter = runtCathedralPathSize4;
+ me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize4, worgenRuntCathedralPath1, runtCathedralPathSize4);
+ break;
+ case WORGEN_ID_CATHEDRAL_5:
+ _wayPointCounter = runtCathedralPathSize5;
+ me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize5, worgenRuntCathedralPath1, runtCathedralPathSize5);
+ break;
+ case WORGEN_ID_CATHEDRAL_6:
+ _wayPointCounter = runtCathedralPathSize6;
+ me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize6, worgenRuntCathedralPath1, runtCathedralPathSize6);
+ break;
+ case WORGEN_ID_CATHEDRAL_7:
+ _wayPointCounter = runtCathedralPathSize7;
+ me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize7, worgenRuntCathedralPath1, runtCathedralPathSize7);
+ break;
+ case WORGEN_ID_CATHEDRAL_8:
+ _wayPointCounter = runtCathedralPathSize8;
+ me->GetMotionMaster()->MoveSmoothPath(runtCathedralPathSize8, worgenRuntCathedralPath1, runtCathedralPathSize8);
+ break;
+ }
+ }
+
+ void JustDied(Unit* /*killer*/) override
+ {
+ me->DespawnOrUnsummon(5s);
+ }
+
+ void MovementInform(uint32 type, uint32 pointId) override
+ {
+ if (type == EFFECT_MOTION_TYPE && pointId == _wayPointCounter && !_jumped)
+ {
+ _jumped = true;
+ _events.ScheduleEvent(EVENT_JUMP_TO_PRISON, 1ms);
+ }
+ }
+
+ void UpdateAI(uint32 diff) override
+ {
+ if (!UpdateVictim() && !_events.IsInPhase(PHASE_ROOF))
+ return;
+
+ _events.Update(diff);
+
+ while (uint32 eventId = _events.ExecuteEvent())
+ {
+ switch (eventId)
+ {
+ case EVENT_FORCE_DESPAWN:
+ me->DespawnOrUnsummon();
+ break;
+ case EVENT_JUMP_TO_PRISON:
+ me->GetMotionMaster()->MoveJump(worgenRuntJumpPos[_worgenID], 16.0f, _worgenID < WORGEN_ID_CATHEDRAL_1 ? 19.2911f : frand(3.945607f, 4.852813f));
+ me->SetHomePosition(worgenRuntJumpPos[_worgenID]);
+ _events.ScheduleEvent(EVENT_AGGRO_PLAYER, 2s);
+ break;
+ case EVENT_AGGRO_PLAYER:
+ if (Unit* player = ObjectAccessor::GetPlayer(*me, _playerGuid))
+ if (me->IsInDist(player->GetPosition(), 100.0f))
+ AttackStart(player);
+ break;
+ default:
+ break;
+ }
+ }
+
+ DoMeleeAttackIfReady();
+ }
+private:
+ uint32 _worgenID;
+ uint32 _wayPointCounter;
+ bool _jumped;
+ ObjectGuid _playerGuid;
+ EventMap _events;
+};
+
+/*######
+## Quest 14159 - The Rebel Lord's Arsenal
+######*/
+
+enum TheRebelLordsArsena
+{
+ PHASE_ID_SUMMON = 170,
+ PHASE_ID_WOUND = 171,
+ NPC_LORNA_CROWLEY = 35378,
+ NPC_GENERIC_TRIGGER_LAB = 35374,
+
+ SPELL_SHOOT_INSTAKILL = 67593,
+ SPELL_COSMETIC_ATTACK = 42880,
+ SPELL_PULL_TO = 67357,
+
+ EVENT_COSMETIC_ATTACK = 1,
+ EVENT_JUMP_TO_PLAYER,
+ EVENT_SHOOT_JOSIAH
+};
+
+static Position const JosiahJumpPos = { -1796.63f, 1427.73f, 12.4624f };
+
+struct npc_josiah_avery : public PassiveAI
+{
+ npc_josiah_avery(Creature* creature) : PassiveAI(creature) { }
+
+ void IsSummonedBy(Unit* summoner) override
+ {
+ PhasingHandler::AddPhase(me, PHASE_ID_SUMMON, true);
+ PhasingHandler::AddPhase(me, PHASE_ID_WOUND, true);
+ _playerGuid = summoner->GetGUID();
+ _events.ScheduleEvent(EVENT_COSMETIC_ATTACK, 500ms);
+ }
+
+ void JustDied(Unit* /*killer*/) override
+ {
+ me->DespawnOrUnsummon(5s);
+ }
+
+ void UpdateAI(uint32 diff) override
+ {
+ _events.Update(diff);
+
+ while (uint32 eventId = _events.ExecuteEvent())
+ {
+ switch (eventId)
+ {
+ case EVENT_COSMETIC_ATTACK:
+ if (Player* player = ObjectAccessor::GetPlayer(*me, _playerGuid))
+ {
+ DoCast(player, SPELL_COSMETIC_ATTACK);
+ if (Creature* lorna = me->FindNearestCreature(NPC_LORNA_CROWLEY, 30.0f, true))
+ if (Creature* labTrigger = lorna->FindNearestCreature(NPC_GENERIC_TRIGGER_LAB, 5.0f, true))
+ labTrigger->CastSpell(player, SPELL_PULL_TO);
+
+ _events.ScheduleEvent(EVENT_JUMP_TO_PLAYER, 1s);
+ }
+ break;
+ case EVENT_JUMP_TO_PLAYER:
+ me->GetMotionMaster()->MoveJump(JosiahJumpPos, 10.0f, 14.18636f);
+ _events.ScheduleEvent(EVENT_SHOOT_JOSIAH, 500ms);
+ break;
+ case EVENT_SHOOT_JOSIAH:
+ if (Creature* lorna = me->FindNearestCreature(NPC_LORNA_CROWLEY, 30.0f, true))
+ lorna->CastSpell(me, SPELL_SHOOT_INSTAKILL, true);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+private:
+ ObjectGuid _playerGuid;
+ EventMap _events;
+};
+
+/*######
+## Quest 14293 - Save Krennan Aranas
+######*/
+
+static Position const GreymanesHorseJumpPos = { -1676.16f, 1346.19f, 15.1349f };
+
+enum GreymanesHorse
+{
+ SAY_ANNOUNCE_RESCUE = 0,
+ SAY_RESCUED = 0,
+ SAY_TRAPPED = 0,
+
+ PATH_KRENNAN_TREE = 0,
+ PATH_KRENNAN_BACK = 1,
+
+ EVENT_START_PATH_1 = 1,
+ EVENT_START_PATH_2,
+ EVENT_JUMP_TO_KRENNAN,
+ EVENT_ANNOUNCE_RESCUE,
+ EVENT_DISMOUNT_PLAYER,
+
+ NPC_RESCUED_KRENNAN = 35907,
+ NPC_TRAPPED_KRENNAN = 35753,
+};
+
+struct npc_greymanes_horse : public VehicleAI
+{
+ npc_greymanes_horse(Creature* creature) : VehicleAI(creature), _currentPath(PATH_KRENNAN_TREE) { }
+
+ void PassengerBoarded(Unit* passenger, int8 /*seatId*/, bool apply) override
+ {
+ if (apply && passenger->GetTypeId() == TYPEID_PLAYER)
+ {
+ me->SetControlled(true, UNIT_STATE_ROOT);
+ _events.ScheduleEvent(EVENT_START_PATH_1, 1s);
+ }
+ else if (apply && passenger->GetEntry() == NPC_RESCUED_KRENNAN)
+ _events.ScheduleEvent(EVENT_START_PATH_2, 1s);
+ }
+
+ void MovementInform(uint32 type, uint32 pointId) override
+ {
+ if (type == EFFECT_MOTION_TYPE && pointId == pathSize1 && _currentPath == PATH_KRENNAN_TREE)
+ _events.ScheduleEvent(EVENT_JUMP_TO_KRENNAN, 1ms);
+ else if (type == EFFECT_MOTION_TYPE && pointId == pathSize2 && _currentPath == PATH_KRENNAN_BACK)
+ _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms);
+ }
+
+ void UpdateAI(uint32 diff) override
+ {
+ _events.Update(diff);
+
+ if (me->HasUnitState(UNIT_STATE_CASTING))
+ return;
+
+ while (uint32 eventId = _events.ExecuteEvent())
+ {
+ switch (eventId)
+ {
+ case EVENT_START_PATH_1:
+ me->SetControlled(false, UNIT_STATE_ROOT);
+ _currentPath = PATH_KRENNAN_TREE;
+ me->GetMotionMaster()->MoveSmoothPath(pathSize1, greymanesHorsePath1, pathSize1);
+ break;
+ case EVENT_JUMP_TO_KRENNAN:
+ me->GetMotionMaster()->MoveJump(GreymanesHorseJumpPos, 16.0f, 14.00968f);
+ _events.ScheduleEvent(EVENT_ANNOUNCE_RESCUE, 2s);
+ break;
+ case EVENT_ANNOUNCE_RESCUE:
+ me->SetControlled(true, UNIT_STATE_ROOT);
+ if (Unit* passenger = me->GetVehicleKit()->GetPassenger(0))
+ {
+ Talk(SAY_ANNOUNCE_RESCUE, passenger);
+ if (Creature* krennan = me->FindNearestCreature(NPC_TRAPPED_KRENNAN, 30.0f, true))
+ krennan->AI()->Talk(SAY_TRAPPED, passenger);
+ }
+ break;
+ case EVENT_START_PATH_2:
+ me->SetControlled(true, UNIT_STATE_ROOT);
+ _currentPath = PATH_KRENNAN_BACK;
+ me->GetMotionMaster()->MoveSmoothPath(pathSize2, greymanesHorsePath2, pathSize2);
+ break;
+ case EVENT_DISMOUNT_PLAYER:
+ if (Unit* passenger = me->GetVehicleKit()->GetPassenger(1))
+ {
+ if (Unit* player = me->GetVehicleKit()->GetPassenger(0))
+ {
+ if (Creature* krennan = passenger->ToCreature())
+ {
+ std::vector storedAttackers;
+
+ for (Unit* attacker : me->getAttackers())
+ storedAttackers.push_back(attacker);
+
+ for (Unit* attacker : storedAttackers)
+ {
+ if (Creature* creature = attacker->ToCreature())
+ if (creature->IsAIEnabled())
+ creature->AI()->EnterEvadeMode();
+ }
+
+ player->ExitVehicle();
+ krennan->AddUnitState(UNIT_STATE_ROOT);
+ krennan->ExitVehicle();
+ krennan->AI()->Talk(SAY_RESCUED, me);
+ krennan->DespawnOrUnsummon(7s);
+ }
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+private:
+ EventMap _events;
+ uint32 _currentPath;
+};
+
+/*######
+## Quest 14212 - Sacrifices
+######*/
+
+enum CrowleysHorse
+{
+ EVENT_JUMP_OVER_BARRICADES_1 = 1,
+ EVENT_JUMP_OVER_BARRICADES_2,
+ EVENT_MOVE_PATH_MAIN_1,
+ EVENT_MOVE_PATH_MAIN_2,
+ EVENT_MOVE_OFF_PATH = 6,
+
+ PATH_ID_CROWLEYS_HORSE_1 = 352310,
+ PATH_ID_CROWLEYS_HORSE_2 = 352311,
+ PATH_ID_CROWLEYS_HORSE_3 = 444280,
+
+ SPELL_THROW_TORCH = 67063,
+ NPC_CROWLEYS_HORSE_2 = 44428,
+};
+
+static Position const CrowleysHorseJumpPos = { -1714.762f, 1673.16f, 20.49182f };
+static Position const CrowleysHorseJumpPos2 = { -1566.71f, 1708.04f, 20.4849f };
+
+struct npc_crowleys_horse : public VehicleAI
+{
+ npc_crowleys_horse(Creature* creature) : VehicleAI(creature), _currentPath(0) { }
+
+ void PassengerBoarded(Unit* passenger, int8 /*seatId*/, bool apply) override
+ {
+ if (apply && passenger->GetTypeId() == TYPEID_PLAYER && me->GetEntry() != NPC_CROWLEYS_HORSE_2)
+ {
+ me->SetControlled(true, UNIT_STATE_ROOT);
+ _events.ScheduleEvent(EVENT_JUMP_OVER_BARRICADES_1, 2s);
+ }
+ else if (apply && passenger->GetTypeId() == TYPEID_PLAYER)
+ {
+ me->SetControlled(true, UNIT_STATE_ROOT);
+ _events.ScheduleEvent(EVENT_MOVE_OFF_PATH, 2s);
+ }
+ }
+
+ void MovementInform(uint32 type, uint32 pointId) override
+ {
+ if (type == WAYPOINT_MOTION_TYPE && pointId == 14 && _currentPath == PATH_ID_CROWLEYS_HORSE_1)
+ _events.ScheduleEvent(EVENT_JUMP_OVER_BARRICADES_2, 1s);
+ else if (type == WAYPOINT_MOTION_TYPE && pointId == 15 && _currentPath == PATH_ID_CROWLEYS_HORSE_2)
+ _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms);
+ else if (type == WAYPOINT_MOTION_TYPE && pointId == 16 && _currentPath == PATH_ID_CROWLEYS_HORSE_3)
+ _events.ScheduleEvent(EVENT_DISMOUNT_PLAYER, 1ms);
+ }
+
+ void UpdateAI(uint32 diff) override
+ {
+ _events.Update(diff);
+
+ while (uint32 eventId = _events.ExecuteEvent())
+ {
+ switch (eventId)
+ {
+ case EVENT_JUMP_OVER_BARRICADES_1:
+ me->SetControlled(false, UNIT_STATE_ROOT);
+ me->GetMotionMaster()->MoveJump(CrowleysHorseJumpPos, 16.0f, 18.56182f);
+ _events.ScheduleEvent(EVENT_MOVE_PATH_MAIN_1, 2s);
+ break;
+ case EVENT_MOVE_PATH_MAIN_1:
+ me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_1, false);
+ _currentPath = PATH_ID_CROWLEYS_HORSE_1;
+ break;
+ case EVENT_JUMP_OVER_BARRICADES_2:
+ me->GetMotionMaster()->MoveJump(CrowleysHorseJumpPos2, 16.0f, 18.56182f);
+ _events.ScheduleEvent(EVENT_MOVE_PATH_MAIN_2, 2s);
+ break;
+ case EVENT_MOVE_PATH_MAIN_2:
+ _currentPath = PATH_ID_CROWLEYS_HORSE_2;
+ me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_2, false);
+ break;
+ case EVENT_DISMOUNT_PLAYER:
+ {
+ std::set attackersCopy = me->getAttackers();
+ for (Unit* attacker : attackersCopy)
+ {
+ if (Creature* creature = attacker->ToCreature())
+ if (creature->IsAIEnabled())
+ creature->AI()->EnterEvadeMode();
+ }
+
+ me->RemoveAurasDueToSpell(VEHICLE_SPELL_RIDE_HARDCODED);
+ me->DespawnOrUnsummon(5s);
+ break;
+ }
+ case EVENT_MOVE_OFF_PATH:
+ me->SetControlled(false, UNIT_STATE_ROOT);
+ _currentPath = PATH_ID_CROWLEYS_HORSE_3;
+ me->GetMotionMaster()->MovePath(PATH_ID_CROWLEYS_HORSE_3, false);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+private:
+ EventMap _events;
+ uint32 _currentPath;
+};
+
+enum GileanCrow
+{
+ SPELL_GILNEAN_CROW = 93275,
+ EVENT_APPLY_HOVER_BYTES = 1,
+ EVENT_FLY_AWAY_1 = 2,
+ EVENT_FLY_AWAY_2 = 3,
+
+ POINT_NONE = 0,
+ POINT_CROW_FLIGHT = 1
+};
+
+struct npc_gilnean_crow : public PassiveAI
+{
+ npc_gilnean_crow(Creature* creature) : PassiveAI(creature) { }
+
+ void SpellHit(WorldObject* /*caster*/, SpellInfo const* spell) override
+ {
+ if (spell->Id == SPELL_GILNEAN_CROW)
+ _events.ScheduleEvent(EVENT_APPLY_HOVER_BYTES, 500ms);
+ }
+
+ void MovementInform(uint32 type, uint32 pointId) override
+ {
+ if (type == POINT_MOTION_TYPE && pointId == POINT_CROW_FLIGHT)
+ {
+ Position pos = me->GetRandomNearPosition(8.0f);
+ pos.m_positionZ = me->GetPositionZ() + 10.0f;
+ me->GetMotionMaster()->MovePoint(POINT_NONE, pos);
+ me->DespawnOrUnsummon(14s);
+ }
+ }
+
+ void UpdateAI(uint32 diff) override
+ {
+ _events.Update(diff);
+
+ while (uint32 eventId = _events.ExecuteEvent())
+ {
+ switch (eventId)
+ {
+ case EVENT_APPLY_HOVER_BYTES:
+ me->SetStandState(UNIT_STAND_STATE_SIT);
+ me->SetAnimTier(AnimTier::Submerged);
+ _events.ScheduleEvent(EVENT_FLY_AWAY_1, 1s);
+ break;
+ case EVENT_FLY_AWAY_1:
+ {
+ Position pos = me->GetRandomNearPosition(7.0f);
+ pos.m_positionZ = me->GetPositionZ() + 8.0f;
+ me->GetMotionMaster()->MovePoint(POINT_CROW_FLIGHT, pos);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ }
+private:
+ EventMap _events;
+};
+}
+
+void AddSC_gilneas_chapter_1()
+{
+ using namespace Gilneas::Chapter1;
+ RegisterCreatureAI(npc_frightened_citizen);
+ RegisterCreatureAI(npc_worgen_runt);
+ RegisterCreatureAI(npc_josiah_avery);
+ RegisterCreatureAI(npc_greymanes_horse);
+ RegisterCreatureAI(npc_crowleys_horse);
+ RegisterCreatureAI(npc_gilnean_crow);
+}
diff --git a/src/server/scripts/EasternKingdoms/Gnomeregan/gnomeregan.cpp b/src/server/scripts/EasternKingdoms/Gnomeregan/gnomeregan.cpp
index 83659bd90b..a2a3d9e69e 100644
--- a/src/server/scripts/EasternKingdoms/Gnomeregan/gnomeregan.cpp
+++ b/src/server/scripts/EasternKingdoms/Gnomeregan/gnomeregan.cpp
@@ -33,6 +33,7 @@ Script Data End */
#include "ScriptedGossip.h"
#include "SpellScript.h"
#include "TemporarySummon.h"
+#include "GameObjectAI.h"
namespace Gnomeregan
{
@@ -544,6 +545,36 @@ public:
}
};
+// 208325 - Extra Parachutes
+enum ExtraParachutes
+{
+ ITEM_SAFE_PARACHUTE = 60680
+};
+
+class go_gnomeregan_extra_parachutes : public GameObjectScript
+{
+public:
+ go_gnomeregan_extra_parachutes() : GameObjectScript("go_gnomeregan_extra_parachutes") { }
+
+ struct go_gnomeregan_extra_parachutesAI : public GameObjectAI
+ {
+ go_gnomeregan_extra_parachutesAI(GameObject* go) : GameObjectAI(go) { }
+
+ bool GossipHello(Player* player) override
+ {
+ if (!player->HasItemCount(60680, 1))
+ player->AddItem(60680, 1);
+
+ return true;
+ }
+ };
+
+ GameObjectAI* GetAI(GameObject* go) const override
+ {
+ return new go_gnomeregan_extra_parachutesAI(go);
+ }
+};
+
// 12709 - Collecting Fallout
class spell_collecting_fallout : public SpellScriptLoader
{
@@ -588,6 +619,7 @@ void AddSC_gnomeregan()
using namespace Gnomeregan;
new npc_blastmaster_emi_shortfuse();
new boss_grubbis();
+ new go_gnomeregan_extra_parachutes();
new spell_collecting_fallout();
}
diff --git a/src/server/scripts/EasternKingdoms/Gnomeregan/gnomeregan.cpp.bak_parachutes b/src/server/scripts/EasternKingdoms/Gnomeregan/gnomeregan.cpp.bak_parachutes
new file mode 100644
index 0000000000..fe7b217999
--- /dev/null
+++ b/src/server/scripts/EasternKingdoms/Gnomeregan/gnomeregan.cpp.bak_parachutes
@@ -0,0 +1,618 @@
+/*
+ * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see .
+ */
+
+/* Script Data Start
+SDName: Gnomeregan
+SDAuthor: Manuel
+SD%Complete: 90%
+SDComment: Some visual effects are not implemented.
+Script Data End */
+
+#include "ScriptMgr.h"
+#include "GameObject.h"
+#include "gnomeregan.h"
+#include "InstanceScript.h"
+#include "Map.h"
+#include "ObjectAccessor.h"
+#include "Player.h"
+#include "ScriptedEscortAI.h"
+#include "ScriptedGossip.h"
+#include "SpellScript.h"
+#include "TemporarySummon.h"
+
+namespace Gnomeregan
+{
+enum BlastmasterEmi
+{
+ SAY_BLASTMASTER_0 = 0,
+ SAY_BLASTMASTER_1 = 1,
+ SAY_BLASTMASTER_2 = 2,
+ SAY_BLASTMASTER_3 = 3,
+ SAY_BLASTMASTER_4 = 4,
+ SAY_BLASTMASTER_5 = 5,
+ SAY_BLASTMASTER_6 = 6,
+ SAY_BLASTMASTER_7 = 7,
+ SAY_BLASTMASTER_8 = 8,
+ SAY_BLASTMASTER_9 = 9,
+ SAY_BLASTMASTER_10 = 10,
+ SAY_BLASTMASTER_11 = 11,
+ SAY_BLASTMASTER_12 = 12,
+ SAY_BLASTMASTER_13 = 13,
+ SAY_BLASTMASTER_14 = 14,
+ SAY_BLASTMASTER_15 = 15,
+ SAY_BLASTMASTER_16 = 16,
+ SAY_BLASTMASTER_17 = 17,
+ SAY_BLASTMASTER_18 = 18,
+ SAY_BLASTMASTER_19 = 19,
+
+ SAY_GRUBBIS = 0
+};
+
+const Position SpawnPosition[] =
+{
+ {-557.630f, -114.514f, -152.209f, 0.641f},
+ {-555.263f, -113.802f, -152.737f, 0.311f},
+ {-552.154f, -112.476f, -153.349f, 0.621f},
+ {-548.692f, -111.089f, -154.090f, 0.621f},
+ {-546.905f, -108.340f, -154.877f, 0.729f},
+ {-547.736f, -105.154f, -155.176f, 0.372f},
+ {-547.274f, -114.109f, -153.952f, 0.735f},
+ {-552.534f, -110.012f, -153.577f, 0.747f},
+ {-550.708f, -116.436f, -153.103f, 0.679f},
+ {-554.030f, -115.983f, -152.635f, 0.695f},
+ {-494.595f, -87.516f, -149.116f, 3.344f},
+ {-493.349f, -90.845f, -148.882f, 3.717f},
+ {-491.995f, -87.619f, -148.197f, 3.230f},
+ {-490.732f, -90.739f, -148.091f, 3.230f},
+ {-490.554f, -89.114f, -148.055f, 3.230f},
+ {-495.240f, -90.808f, -149.493f, 3.238f},
+ {-494.195f, -89.553f, -149.131f, 3.254f},
+ {-511.3304f, -139.9622f, -152.4761f, 0.7504908f},
+ {-510.6754f, -139.4371f, -152.6167f, 3.33359f},
+ {-511.8976f, -139.3562f, -152.4785f, 3.961899f}
+};
+
+class npc_blastmaster_emi_shortfuse : public CreatureScript
+{
+public:
+ npc_blastmaster_emi_shortfuse() : CreatureScript("npc_blastmaster_emi_shortfuse") { }
+
+ struct npc_blastmaster_emi_shortfuseAI : public EscortAI
+ {
+ npc_blastmaster_emi_shortfuseAI(Creature* creature) : EscortAI(creature)
+ {
+ instance = creature->GetInstanceScript();
+ creature->RestoreFaction();
+ Reset();
+ }
+
+ InstanceScript* instance;
+
+ uint8 uiPhase;
+ uint32 uiTimer;
+
+ GuidList SummonList;
+ GuidList GoSummonList;
+
+ void Reset() override
+ {
+ if (!HasEscortState(STATE_ESCORT_ESCORTING))
+ {
+ uiTimer = 0;
+ uiPhase = 0;
+
+ RestoreAll();
+
+ SummonList.clear();
+ GoSummonList.clear();
+ }
+ }
+
+ bool GossipSelect(Player* player, uint32 /*menuId*/, uint32 gossipListId) override
+ {
+ if (gossipListId == 0)
+ {
+ Start(true, false, player->GetGUID());
+
+ me->SetFaction(player->GetFaction());
+ SetData(1, 0);
+
+ player->PlayerTalkClass->SendCloseGossip();
+ }
+ return false;
+ }
+
+ void NextStep(uint32 uiTimerStep, bool bNextStep = true, uint8 uiPhaseStep = 0)
+ {
+ uiTimer = uiTimerStep;
+ if (bNextStep)
+ ++uiPhase;
+ else
+ uiPhase = uiPhaseStep;
+ }
+
+ void CaveDestruction(bool isRight)
+ {
+ if (GoSummonList.empty())
+ return;
+
+ for (GuidList::const_iterator itr = GoSummonList.begin(); itr != GoSummonList.end(); ++itr)
+ {
+ if (GameObject* go = ObjectAccessor::GetGameObject(*me, *itr))
+ {
+ if (Creature* trigger = go->SummonTrigger(go->GetPositionX(), go->GetPositionY(), go->GetPositionZ(), 0, 1))
+ {
+ //visual effects are not working!
+ trigger->CastSpell(trigger, 11542, true);
+ trigger->CastSpell(trigger, 35470, true);
+ }
+ go->RemoveFromWorld();
+ //go->CastSpell(me, 12158); makes all die?!
+ }
+ }
+
+ if (GameObject* go = ObjectAccessor::GetGameObject(*me, instance->GetGuidData(isRight ? DATA_GO_CAVE_IN_RIGHT : DATA_GO_CAVE_IN_LEFT)))
+ instance->HandleGameObject(ObjectGuid::Empty, false, go);
+ }
+
+ void SetInFace(bool isRight)
+ {
+ if (GameObject* go = ObjectAccessor::GetGameObject(*me, instance->GetGuidData(isRight ? DATA_GO_CAVE_IN_RIGHT : DATA_GO_CAVE_IN_LEFT)))
+ me->SetFacingToObject(go);
+ }
+
+ void RestoreAll()
+ {
+ if (GameObject* go = ObjectAccessor::GetGameObject(*me, instance->GetGuidData(DATA_GO_CAVE_IN_RIGHT)))
+ instance->HandleGameObject(ObjectGuid::Empty, false, go);
+
+ if (GameObject* go = ObjectAccessor::GetGameObject(*me, instance->GetGuidData(DATA_GO_CAVE_IN_LEFT)))
+ instance->HandleGameObject(ObjectGuid::Empty, false, go);
+
+ if (!GoSummonList.empty())
+ for (GuidList::const_iterator itr = GoSummonList.begin(); itr != GoSummonList.end(); ++itr)
+ {
+ if (GameObject* go = ObjectAccessor::GetGameObject(*me, *itr))
+ go->RemoveFromWorld();
+ }
+
+ if (!SummonList.empty())
+ for (GuidList::const_iterator itr = SummonList.begin(); itr != SummonList.end(); ++itr)
+ {
+ if (Creature* summon = ObjectAccessor::GetCreature(*me, *itr))
+ {
+ summon->DespawnOrUnsummon();
+ }
+ }
+ }
+
+ void AggroAllPlayers(Creature* temp)
+ {
+ Map::PlayerList const& PlList = me->GetMap()->GetPlayers();
+ for (Map::PlayerList::const_iterator i = PlList.begin(); i != PlList.end(); ++i)
+ {
+ if (Player* player = i->GetSource())
+ {
+ if (player->IsGameMaster())
+ continue;
+
+ if (player->IsAlive())
+ AddThreat(player, 0.0f, temp);
+ }
+ }
+ }
+
+ void WaypointReached(uint32 waypointId, uint32 /*pathId*/) override
+ {
+ //just in case
+ if (GetPlayerForEscort())
+ if (me->GetFaction() != GetPlayerForEscort()->GetFaction())
+ me->SetFaction(GetPlayerForEscort()->GetFaction());
+
+ switch (waypointId)
+ {
+ case 3:
+ SetEscortPaused(true);
+ NextStep(2000, false, 3);
+ break;
+ case 7:
+ SetEscortPaused(true);
+ NextStep(2000, false, 4);
+ break;
+ case 9:
+ NextStep(1000, false, 8);
+ break;
+ case 10:
+ NextStep(25000, false, 10);
+ break;
+ case 11:
+ SetEscortPaused(true);
+ SetInFace(true);
+ NextStep(1000, false, 11);
+ break;
+ case 12:
+ NextStep(25000, false, 18);
+ break;
+ case 13:
+ Summon(6);
+ NextStep(25000, false, 19);
+ break;
+ case 14:
+ SetInFace(false);
+ Talk(SAY_BLASTMASTER_17);
+ SetEscortPaused(true);
+ NextStep(5000, false, 20);
+ break;
+ }
+ }
+
+ void SetData(uint32 uiI, uint32 uiValue) override
+ {
+ switch (uiI)
+ {
+ case 1:
+ SetEscortPaused(true);
+ Talk(SAY_BLASTMASTER_0);
+ NextStep(2000, true);
+ break;
+ case 2:
+ switch (uiValue)
+ {
+ case 1:
+ instance->SetData(TYPE_EVENT, IN_PROGRESS);
+ break;
+ case 2:
+ instance->SetData(TYPE_EVENT, DONE);
+ NextStep(5000, false, 22);
+ break;
+ }
+ break;
+ }
+ }
+
+ void Summon(uint8 uiCase)
+ {
+ switch (uiCase)
+ {
+ case 1:
+ me->SummonCreature(NPC_CAVERNDEEP_AMBUSHER, SpawnPosition[0], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ me->SummonCreature(NPC_CAVERNDEEP_AMBUSHER, SpawnPosition[1], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ me->SummonCreature(NPC_CAVERNDEEP_AMBUSHER, SpawnPosition[2], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ me->SummonCreature(NPC_CAVERNDEEP_AMBUSHER, SpawnPosition[3], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ me->SummonCreature(NPC_CAVERNDEEP_AMBUSHER, SpawnPosition[4], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ me->SummonCreature(NPC_CAVERNDEEP_AMBUSHER, SpawnPosition[5], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ me->SummonCreature(NPC_CAVERNDEEP_AMBUSHER, SpawnPosition[6], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ me->SummonCreature(NPC_CAVERNDEEP_AMBUSHER, SpawnPosition[7], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ me->SummonCreature(NPC_CAVERNDEEP_AMBUSHER, SpawnPosition[8], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ me->SummonCreature(NPC_CAVERNDEEP_AMBUSHER, SpawnPosition[9], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ break;
+ case 2:
+ if (GameObject* go = me->SummonGameObject(183410, -533.140f, -105.322f, -156.016f, 0.f, QuaternionData(), 1))
+ {
+ GoSummonList.push_back(go->GetGUID());
+ go->SetFlag(GAMEOBJECT_FLAGS, GO_FLAG_NOT_SELECTABLE); //We can't use it!
+ }
+ Summon(3);
+ break;
+ case 3:
+ me->SummonCreature(NPC_CAVERNDEEP_AMBUSHER, SpawnPosition[0], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ me->SummonCreature(NPC_CAVERNDEEP_AMBUSHER, SpawnPosition[1], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ me->SummonCreature(NPC_CAVERNDEEP_AMBUSHER, SpawnPosition[2], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ me->SummonCreature(NPC_CAVERNDEEP_AMBUSHER, SpawnPosition[3], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ Talk(SAY_BLASTMASTER_7);
+ break;
+ case 4:
+ if (GameObject* go = me->SummonGameObject(183410, -542.199f, -96.854f, -155.790f, 0.f, QuaternionData(), 1))
+ {
+ GoSummonList.push_back(go->GetGUID());
+ go->SetFlag(GAMEOBJECT_FLAGS, GO_FLAG_NOT_SELECTABLE);
+ }
+ break;
+ case 5:
+ me->SummonCreature(NPC_CAVERNDEEP_AMBUSHER, SpawnPosition[10], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ me->SummonCreature(NPC_CAVERNDEEP_AMBUSHER, SpawnPosition[11], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ me->SummonCreature(NPC_CAVERNDEEP_AMBUSHER, SpawnPosition[12], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ me->SummonCreature(NPC_CAVERNDEEP_AMBUSHER, SpawnPosition[13], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ me->SummonCreature(NPC_CAVERNDEEP_AMBUSHER, SpawnPosition[14], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ break;
+ case 6:
+ if (GameObject* go = me->SummonGameObject(183410, -507.820f, -103.333f, -151.353f, 0.f, QuaternionData(), 1))
+ {
+ GoSummonList.push_back(go->GetGUID());
+ go->SetFlag(GAMEOBJECT_FLAGS, GO_FLAG_NOT_SELECTABLE); //We can't use it!
+ Summon(5);
+ }
+ break;
+ case 7:
+ if (GameObject* go = me->SummonGameObject(183410, -511.829f, -86.249f, -151.431f, 0.f, QuaternionData(), 1))
+ {
+ GoSummonList.push_back(go->GetGUID());
+ go->SetFlag(GAMEOBJECT_FLAGS, GO_FLAG_NOT_SELECTABLE); //We can't use it!
+ }
+ break;
+ case 8:
+ if (Creature* grubbis = me->SummonCreature(NPC_GRUBBIS, SpawnPosition[15], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000))
+ grubbis->AI()->Talk(SAY_GRUBBIS);
+ me->SummonCreature(NPC_CHOMPER, SpawnPosition[16], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 1800000);
+ break;
+ case 9:
+ me->SummonGameObject(GO_RED_ROCKET, SpawnPosition[17], QuaternionData(), 7200);
+ me->SummonGameObject(GO_RED_ROCKET, SpawnPosition[18], QuaternionData(), 7200);
+ me->SummonGameObject(GO_RED_ROCKET, SpawnPosition[19], QuaternionData(), 7200);
+ break;
+ }
+ }
+
+ void UpdateEscortAI(uint32 uiDiff) override
+ {
+ if (uiPhase)
+ {
+ if (uiTimer <= uiDiff)
+ {
+ switch (uiPhase)
+ {
+ case 1:
+ Talk(SAY_BLASTMASTER_1);
+ NextStep(2000, true);
+ break;
+ case 2:
+ SetEscortPaused(false);
+ NextStep(0, false, 0);
+ break;
+ case 3:
+ Talk(SAY_BLASTMASTER_2);
+ SetEscortPaused(false);
+ NextStep(0, false, 0);
+ break;
+ case 4:
+ Talk(SAY_BLASTMASTER_3);
+ NextStep(3000, true);
+ break;
+ case 5:
+ Talk(SAY_BLASTMASTER_4);
+ NextStep(3000, true);
+ break;
+ case 6:
+ SetInFace(true);
+ Talk(SAY_BLASTMASTER_5);
+ Summon(1);
+ if (GameObject* go = ObjectAccessor::GetGameObject(*me, instance->GetGuidData(DATA_GO_CAVE_IN_RIGHT)))
+ instance->HandleGameObject(ObjectGuid::Empty, true, go);
+ NextStep(3000, true);
+ break;
+ case 7:
+ Talk(SAY_BLASTMASTER_6);
+ SetEscortPaused(false);
+ NextStep(0, false, 0);
+ break;
+ case 8:
+ me->HandleEmoteCommand(EMOTE_STATE_USE_STANDING);
+ NextStep(25000, true);
+ break;
+ case 9:
+ Summon(2);
+ NextStep(0, false);
+ break;
+ case 10:
+ Summon(4);
+ Talk(SAY_BLASTMASTER_8);
+ NextStep(0, false);
+ break;
+ case 11:
+ Talk(SAY_BLASTMASTER_9);
+ NextStep(5000, true);
+ break;
+ case 12:
+ Talk(SAY_BLASTMASTER_10);
+ NextStep(5000, true);
+ break;
+ case 13:
+ Talk(SAY_BLASTMASTER_11);
+ CaveDestruction(true);
+ NextStep(8000, true);
+ break;
+ case 14:
+ Talk(SAY_BLASTMASTER_12);
+ NextStep(8500, true);
+ break;
+ case 15:
+ Talk(SAY_BLASTMASTER_13);
+ NextStep(2000, true);
+ break;
+ case 16:
+ Talk(SAY_BLASTMASTER_14);
+ SetInFace(false);
+ if (GameObject* go = ObjectAccessor::GetGameObject(*me, instance->GetGuidData(DATA_GO_CAVE_IN_LEFT)))
+ instance->HandleGameObject(ObjectGuid::Empty, true, go);
+ NextStep(2000, true);
+ break;
+ case 17:
+ SetEscortPaused(false);
+ Talk(SAY_BLASTMASTER_15);
+ Summon(5);
+ NextStep(0, false);
+ break;
+ case 18:
+ Summon(6);
+ NextStep(0, false);
+ break;
+ case 19:
+ SetInFace(false);
+ Summon(7);
+ Talk(SAY_BLASTMASTER_16);
+ NextStep(0, false);
+ break;
+ case 20:
+ Talk(SAY_BLASTMASTER_18);
+ NextStep(2000, true);
+ break;
+ case 21:
+ Summon(8);
+ NextStep(0, false);
+ break;
+ case 22:
+ CaveDestruction(false);
+ Talk(SAY_BLASTMASTER_11);
+ NextStep(3000, true);
+ break;
+ case 23:
+ Summon(9);
+ Talk(SAY_BLASTMASTER_19);
+ NextStep(0, false);
+ break;
+ }
+ } else uiTimer -= uiDiff;
+ }
+
+ if (!UpdateVictim())
+ return;
+
+ DoMeleeAttackIfReady();
+ }
+
+ void JustSummoned(Creature* summon) override
+ {
+ SummonList.push_back(summon->GetGUID());
+ AggroAllPlayers(summon);
+ }
+ };
+
+ CreatureAI* GetAI(Creature* creature) const override
+ {
+ return GetGnomereganAI(creature);
+ }
+};
+
+class boss_grubbis : public CreatureScript
+{
+public:
+ boss_grubbis() : CreatureScript("boss_grubbis") { }
+
+ struct boss_grubbisAI : public ScriptedAI
+ {
+ boss_grubbisAI(Creature* creature) : ScriptedAI(creature)
+ {
+ SetDataSummoner();
+ }
+
+ void SetDataSummoner()
+ {
+ if (!me->IsSummon())
+ return;
+
+ if (Unit* summon = me->ToTempSummon()->GetSummoner())
+ if (Creature* creature = summon->ToCreature())
+ creature->AI()->SetData(2, 1);
+ }
+
+ void UpdateAI(uint32 /*diff*/) override
+ {
+ if (!UpdateVictim())
+ return;
+
+ DoMeleeAttackIfReady();
+ }
+
+ void JustDied(Unit* /*killer*/) override
+ {
+ if (!me->IsSummon())
+ return;
+
+ if (Unit* summoner = me->ToTempSummon()->GetSummoner())
+ if (Creature* creature = summoner->ToCreature())
+ creature->AI()->SetData(2, 2);
+ }
+ };
+
+ CreatureAI* GetAI(Creature* creature) const override
+ {
+ return GetGnomereganAI(creature);
+ }
+};
+
+// 208325 - Extra Parachutes
+enum ExtraParachutes
+{
+ ITEM_SAFE_PARACHUTE = 60680
+};
+
+class go_gnomeregan_extra_parachutes : public GameObjectScript
+{
+public:
+ go_gnomeregan_extra_parachutes() : GameObjectScript("go_gnomeregan_extra_parachutes") { }
+
+ bool OnGossipHello(Player* player, GameObject* /*go*/) override
+ {
+ // Retail behavior: the crate directly provides one S.A.F.E. Parachute.
+ // Do not hand out another one while the player already owns one.
+ if (!player->HasItemCount(ITEM_SAFE_PARACHUTE, 1, true))
+ player->AddItem(ITEM_SAFE_PARACHUTE, 1);
+
+ // The original server-side spell 96364 is incomplete in this branch.
+ // Handling the click here also prevents the broken default spell cast.
+ return true;
+ }
+};
+
+// 12709 - Collecting Fallout
+class spell_collecting_fallout : public SpellScriptLoader
+{
+ public:
+ spell_collecting_fallout() : SpellScriptLoader("spell_collecting_fallout") { }
+
+ class spell_collecting_fallout_SpellScript : public SpellScript
+ {
+ void OnLaunch(SpellEffIndex effIndex)
+ {
+ // estimated 25% chance of success
+ if (roll_chance_i(25))
+ _spellFail = false;
+ else
+ PreventHitDefaultEffect(effIndex);
+ }
+
+ void HandleFail(SpellEffIndex effIndex)
+ {
+ if (!_spellFail)
+ PreventHitDefaultEffect(effIndex);
+ }
+
+ void Register() override
+ {
+ OnEffectLaunch.Register(&spell_collecting_fallout_SpellScript::OnLaunch, EFFECT_0, SPELL_EFFECT_TRIGGER_SPELL);
+ OnEffectLaunch.Register(&spell_collecting_fallout_SpellScript::HandleFail, EFFECT_1, SPELL_EFFECT_TRIGGER_SPELL);
+ }
+
+ bool _spellFail = true;
+ };
+
+ SpellScript* GetSpellScript() const override
+ {
+ return new spell_collecting_fallout_SpellScript();
+ }
+};
+}
+
+void AddSC_gnomeregan()
+{
+ using namespace Gnomeregan;
+ new npc_blastmaster_emi_shortfuse();
+ new boss_grubbis();
+ new go_gnomeregan_extra_parachutes();
+
+ new spell_collecting_fallout();
+}