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