whole lot of fixes
This commit is contained in:
1 parent
fec36215f6
commit
3cf7e70283
14 files changed
+3174
-83
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
+34
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// 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();
|
||||
|
||||
}
|
||||
@@ -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<std::string> 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<std::string> 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<uint64> 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<uint64> 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<std::string> 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())
|
||||
|
||||
@@ -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<Unit*> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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() : "<none>";
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -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 <string>
|
||||
|
||||
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("<empty>");
|
||||
}
|
||||
|
||||
static char const* UnitName(Unit const* unit)
|
||||
{
|
||||
return unit ? unit->GetName().c_str() : "<none>";
|
||||
}
|
||||
|
||||
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() : "<empty>");
|
||||
|
||||
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=<none>");
|
||||
|
||||
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() : "<empty>",
|
||||
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() : "<empty>");
|
||||
}
|
||||
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<ChatCommand> GetCommands() const override
|
||||
{
|
||||
static std::vector<ChatCommand> 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<ChatCommand> vianoCommands =
|
||||
{
|
||||
{ "bot", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", botCommands },
|
||||
};
|
||||
|
||||
static std::vector<ChatCommand> commandTable =
|
||||
{
|
||||
{ "viano", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", vianoCommands },
|
||||
};
|
||||
|
||||
return commandTable;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void AddSC_viano_bot_commands()
|
||||
{
|
||||
new VianoBotCommands::VianoBotCommandScript();
|
||||
}
|
||||
@@ -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 <string>
|
||||
|
||||
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("<empty>");
|
||||
}
|
||||
|
||||
static char const* UnitName(Unit const* unit)
|
||||
{
|
||||
return unit ? unit->GetName().c_str() : "<none>";
|
||||
}
|
||||
|
||||
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() : "<empty>");
|
||||
|
||||
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=<none>");
|
||||
|
||||
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() : "<empty>",
|
||||
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() : "<empty>");
|
||||
}
|
||||
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<ChatCommand> GetCommands() const override
|
||||
{
|
||||
static std::vector<ChatCommand> 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<ChatCommand> vianoCommands =
|
||||
{
|
||||
{ "bot", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", botCommands },
|
||||
};
|
||||
|
||||
static std::vector<ChatCommand> commandTable =
|
||||
{
|
||||
{ "viano", rbac::RBAC_PERM_COMMAND_GM, false, nullptr, "", vianoCommands },
|
||||
};
|
||||
|
||||
return commandTable;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void AddSC_viano_bot_commands()
|
||||
{
|
||||
new VianoBotCommands::VianoBotCommandScript();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+682
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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<Unit*> 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<Unit*> 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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* 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<npc_blastmaster_emi_shortfuseAI>(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<boss_grubbisAI>(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();
|
||||
}
|
||||
Reference in new issue
Block a user