From 96c937d5e4b68f951219da3f41d5ce91ea4e1cce Mon Sep 17 00:00:00 2001 From: Nico Date: Thu, 8 Oct 2026 09:40:02 +0200 Subject: [PATCH] full implementation of viano dungeon bots --- .../scripts/Custom/viano_adventurers.cpp | 124 +- ...ers.cpp.bak-vcore-alliance-20261007-220047 | 1685 ++++++++++++++++ ...p.bak-vcore-faction-models-20261008-002257 | 1739 +++++++++++++++++ src/server/scripts/World/item_scripts.cpp | 6 +- ...pts.cpp.bak-vcore-alliance-20261007-220047 | 1008 ++++++++++ 5 files changed, 4538 insertions(+), 24 deletions(-) create mode 100644 src/server/scripts/Custom/viano_adventurers.cpp.bak-vcore-alliance-20261007-220047 create mode 100644 src/server/scripts/Custom/viano_adventurers.cpp.bak-vcore-faction-models-20261008-002257 create mode 100644 src/server/scripts/World/item_scripts.cpp.bak-vcore-alliance-20261007-220047 diff --git a/src/server/scripts/Custom/viano_adventurers.cpp b/src/server/scripts/Custom/viano_adventurers.cpp index cbdfdf10f1..05160ce352 100644 --- a/src/server/scripts/Custom/viano_adventurers.cpp +++ b/src/server/scripts/Custom/viano_adventurers.cpp @@ -46,6 +46,39 @@ namespace VianoAdventurers || scriptName.rfind("npc_alliance_adventurer_tank", 0) == 0; } + + // VIANO_FACTION_MODELS_V1 + static uint32 GetVianoAdventurerDisplayId(Creature* creature) + { + static constexpr uint32 HORDE_MODELS[] = + { + 30063, + 30064, + 30065, + 30066 + }; + + static constexpr uint32 ALLIANCE_MODELS[] = + { + 24361, + 24541, + 24543, + 24542 + }; + + uint32 const* models = HORDE_MODELS; + + if (creature) + { + uint32 const entry = creature->GetEntry(); + + if (entry >= 900004 && entry <= 900006) + models = ALLIANCE_MODELS; + } + + return models[urand(0, 3)]; + } + static void GetVianoFollowSlot(Creature* bot, Player* owner, float& distance, float& angle) { distance = FOLLOW_DISTANCE; @@ -316,20 +349,18 @@ namespace VianoAdventurers 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)]; + uint32 const displayId = + GetVianoAdventurerDisplayId(me); me->SetDisplayId(displayId, true); - TC_LOG_INFO("scripts","[VianoAI] Horde adventurer display selected: %u", displayId); + TC_LOG_INFO( + "scripts", + "[VianoAI] adventurer display selected: " + "entry=%u display=%u", + me->GetEntry(), + displayId); + // REACT_PASSIVE lets AttackStart() briefly set a victim, but the // core subsequently drops that victim again. We already suppress @@ -1095,16 +1126,11 @@ namespace VianoAdventurers me->SetFaction(owner->GetFaction()); me->SetLevel(owner->getLevel()); me->UpdateLevelDependantStats(); + uint32 const displayId = + GetVianoAdventurerDisplayId(me); - static constexpr uint32 HORDE_ADVENTURER_MODELS[] = - { - 30063, - 30064, - 30065, - 30066 - }; + me->SetDisplayId(displayId, true); - me->SetDisplayId(HORDE_ADVENTURER_MODELS[urand(0, 3)], true); SetEquipmentSlots(false, 50179, 50794, EQUIP_NO_CHANGE); me->SetReactState(REACT_DEFENSIVE); @@ -1517,9 +1543,11 @@ namespace VianoAdventurers me->SetLevel(owner->getLevel()); me->UpdateLevelDependantStats(); ConfigureMana(true); + uint32 const displayId = + GetVianoAdventurerDisplayId(me); + + me->SetDisplayId(displayId, true); - static constexpr uint32 MODELS[] = { 30063, 30064, 30065, 30066 }; - me->SetDisplayId(MODELS[urand(0, 3)], true); SetEquipmentSlots(false, 39426, EQUIP_NO_CHANGE, EQUIP_NO_CHANGE); me->SetReactState(REACT_PASSIVE); FollowOwner(owner); @@ -1674,6 +1702,57 @@ void UpdateAI(uint32 diff) override return new npc_horde_adventurer_healerAI(creature); } }; + + // VIANO_ALLIANCE_ADVENTURERS_V1 + // + // Alliance adventurers intentionally reuse the same combat AI as their + // Horde counterparts. Faction, models and default equipment are supplied + // by their own creature_template rows. + + class npc_alliance_adventurer_melee : public CreatureScript + { + public: + npc_alliance_adventurer_melee() + : CreatureScript("npc_alliance_adventurer_melee") + { + } + + CreatureAI* GetAI(Creature* creature) const override + { + return new npc_horde_adventurer_melee:: + npc_horde_adventurer_meleeAI(creature); + } + }; + + class npc_alliance_adventurer_tank : public CreatureScript + { + public: + npc_alliance_adventurer_tank() + : CreatureScript("npc_alliance_adventurer_tank") + { + } + + CreatureAI* GetAI(Creature* creature) const override + { + return new npc_horde_adventurer_tank:: + npc_horde_adventurer_tankAI(creature); + } + }; + + class npc_alliance_adventurer_healer : public CreatureScript + { + public: + npc_alliance_adventurer_healer() + : CreatureScript("npc_alliance_adventurer_healer") + { + } + + CreatureAI* GetAI(Creature* creature) const override + { + return new npc_horde_adventurer_healer:: + npc_horde_adventurer_healerAI(creature); + } + }; } void AddSC_viano_adventurers() @@ -1682,4 +1761,7 @@ void AddSC_viano_adventurers() new npc_horde_adventurer_melee(); new npc_horde_adventurer_tank(); new npc_horde_adventurer_healer(); + new npc_alliance_adventurer_melee(); + new npc_alliance_adventurer_tank(); + new npc_alliance_adventurer_healer(); } diff --git a/src/server/scripts/Custom/viano_adventurers.cpp.bak-vcore-alliance-20261007-220047 b/src/server/scripts/Custom/viano_adventurers.cpp.bak-vcore-alliance-20261007-220047 new file mode 100644 index 0000000000..cbdfdf10f1 --- /dev/null +++ b/src/server/scripts/Custom/viano_adventurers.cpp.bak-vcore-alliance-20261007-220047 @@ -0,0 +1,1685 @@ +/* + * Viano custom dungeon adventurers + * Initial melee companion implementation. + */ + +#include "ScriptMgr.h" +#include "ScriptedCreature.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "Group.h" +#include "TemporarySummon.h" +#include "MotionMaster.h" +#include "Log.h" +#include "SpellInfo.h" +#include "SpellAuras.h" +#include +#include +#include "LFGMgr.h" +#include "LFG.h" +#include "ObjectMgr.h" +#include +#include + +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_FORMATION_V1 + static bool IsVianoTankUnit(Unit* unit) + { + Creature* creature = unit ? unit->ToCreature() : nullptr; + if (!creature || !creature->IsVianoAdventurer()) + return false; + + CreatureTemplate const* cinfo = creature->GetCreatureTemplate(); + if (!cinfo) + return false; + + std::string const scriptName = sObjectMgr->GetScriptName(cinfo->ScriptID); + return scriptName.rfind("npc_horde_adventurer_tank", 0) == 0 + || scriptName.rfind("npc_alliance_adventurer_tank", 0) == 0; + } + + static void GetVianoFollowSlot(Creature* bot, Player* owner, float& distance, float& angle) + { + distance = FOLLOW_DISTANCE; + angle = FOLLOW_ANGLE; + + if (!bot || !owner) + return; + + std::list groupUnits; + owner->GetPartyMembers(groupUnits); + + std::vector bots; + for (Unit* unit : groupUnits) + { + Creature* creature = unit ? unit->ToCreature() : nullptr; + if (!creature || !creature->IsVianoAdventurer()) + continue; + if (!creature->IsAlive() || !creature->IsInMap(owner)) + continue; + + bots.push_back(creature); + } + + std::sort(bots.begin(), bots.end(), [](Creature const* left, Creature const* right) + { + return left->GetGUID().GetRawValue() < right->GetGUID().GetRawValue(); + }); + + size_t slot = 0; + bool found = false; + for (size_t i = 0; i < bots.size(); ++i) + { + if (bots[i] == bot) + { + slot = i; + found = true; + break; + } + } + + if (!found) + return; + + struct FollowSlot + { + float distance; + float angle; + }; + + static FollowSlot const baseSlots[] = + { + { 2.7f, 3.14159265f }, + { 3.1f, 2.52f }, + { 3.1f, 3.76f }, + { 4.0f, 2.25f }, + { 4.0f, 4.03f }, + { 4.4f, 2.70f }, + { 4.4f, 3.58f }, + { 4.8f, 3.14159265f } + }; + + if (slot < sizeof(baseSlots) / sizeof(baseSlots[0])) + { + distance = baseSlots[slot].distance; + angle = baseSlots[slot].angle; + return; + } + + // Unlimited development-mode fallback: additional bots form wider rows + // instead of being stacked on one point. + static float const offsets[] = { -0.95f, -0.48f, 0.0f, 0.48f, 0.95f }; + size_t extra = slot - (sizeof(baseSlots) / sizeof(baseSlots[0])); + size_t row = extra / 5; + size_t column = extra % 5; + + distance = 5.4f + float(row) * 1.25f; + angle = 3.14159265f + offsets[column]; + } + + // 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; + + float followDistance = FOLLOW_DISTANCE; + float followAngle = FOLLOW_ANGLE; + GetVianoFollowSlot(me, owner, followDistance, followAngle); + me->GetMotionMaster()->MoveFollow(owner, followDistance, followAngle); + } + + 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->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); + 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())); + } + + 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; + + auto isCombatRelevant = [](Unit* target) -> bool + { + return target + && target->IsAlive() + && (target->IsInCombat() + || target->GetVictim() + || !target->getAttackers().empty()); + }; + + auto isDeclaredTank = [owner](Unit* unit) -> bool + { + if (!unit) + return false; + + if (Creature* creature = unit->ToCreature()) + { + if (CreatureTemplate const* creatureTemplate = creature->GetCreatureTemplate()) + { + std::string const scriptName = sObjectMgr->GetScriptName(creatureTemplate->ScriptID); + if (scriptName.find("_tank") != std::string::npos) + return true; + } + } + + if (Player* player = unit->ToPlayer()) + { + if (Group* group = owner->GetGroup()) + { + if (group->isLFGGroup() && player->GetGroup() == group) + { + uint8 roles = sLFGMgr->GetRoles(player->GetGUID()); + if (roles & lfg::PLAYER_ROLE_TANK) + return true; + } + } + } + + return false; + }; + + // VIANO_DD_TARGET_V1_1: + // If a declared tank is actively fighting, DDs follow the tank's + // victim. Merely selecting another hostile target must not pull it. + std::list groupUnits; + owner->GetPartyMembers(groupUnits); + + Unit* bestTankTarget = nullptr; + uint32 bestTankPressure = 0; + bool declaredTankPresent = false; + + auto considerTank = [&](Unit* tank) + { + if (!tank || !tank->IsAlive() || !tank->IsInMap(owner) || !isDeclaredTank(tank)) + return; + + declaredTankPresent = true; + + Unit* victim = tank->GetVictim(); + if (!victim || !IsValidCandidate(victim) || !isCombatRelevant(victim)) + return; + + // VIANO_DD_TANK_ENGAGEMENT_GATE_V1: + // AttackStart() gives the tank a victim immediately during a + // skull pull. DDs must not interpret that as "tank has taken + // the target" yet. Wait until the enemy is actually attacking + // the tank / appears in the tank's attacker set. + bool tankHasAggro = victim->GetVictim() == tank; + if (!tankHasAggro) + { + for (Unit* attacker : tank->getAttackers()) + { + if (attacker == victim) + { + tankHasAggro = true; + break; + } + } + } + + if (!tankHasAggro) + return; + + uint32 pressure = uint32(tank->getAttackers().size()); + if (!bestTankTarget || pressure > bestTankPressure) + { + bestTankTarget = victim; + bestTankPressure = pressure; + } + }; + + considerTank(owner); + for (Unit* member : groupUnits) + considerTank(member); + + if (bestTankTarget) + return bestTankTarget; + + // If a tank exists but has not actually established the pull yet, + // DDs wait. In particular they must NOT fall through to the + // player's selected skull target merely because the tank just put + // that target into combat. + if (declaredTankPresent) + { + if (Unit* target = me->GetVictim()) + if (IsValidCandidate(target) && isCombatRelevant(target)) + return target; + + if (Unit* target = SelectGroupCombatTarget(owner)) + if (IsValidCandidate(target) && isCombatRelevant(target)) + return target; + + for (Unit* attacker : me->getAttackers()) + if (IsValidCandidate(attacker) && isCombatRelevant(attacker)) + return attacker; + + return nullptr; + } + + // No declared tank: the owner controls DD focus, but only after + // the target is already genuinely combat-relevant. + if (Unit* target = owner->GetVictim()) + if (IsValidCandidate(target) && isCombatRelevant(target)) + return target; + + if (Unit* target = owner->GetSelectedUnit()) + if (IsValidCandidate(target) && isCombatRelevant(target)) + return target; + + // Keep a valid existing bot victim. + if (Unit* target = me->GetVictim()) + if (IsValidCandidate(target) && isCombatRelevant(target)) + return target; + + // Help the virtual/real group against enemies that are already + // attacking group members. + if (Unit* target = SelectGroupCombatTarget(owner)) + if (IsValidCandidate(target) && isCombatRelevant(target)) + return target; + + for (Unit* attacker : owner->getAttackers()) + if (IsValidCandidate(attacker) && isCombatRelevant(attacker)) + return attacker; + + for (Unit* attacker : me->getAttackers()) + if (IsValidCandidate(attacker) && isCombatRelevant(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 && !IsActivelyEngagedInCombat()) + { + // 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( + 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 (!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); + } + + // 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); + } + }; + + // VIANO_TANK_V1 + class npc_horde_adventurer_tank : public CreatureScript + { + public: + npc_horde_adventurer_tank() : CreatureScript("npc_horde_adventurer_tank") { } + + struct npc_horde_adventurer_tankAI : public ScriptedAI + { + npc_horde_adventurer_tankAI(Creature* creature) : ScriptedAI(creature) { } + + static constexpr uint8 SKULL_ICON_ID = 7; + static constexpr float AUTO_PICKUP_RADIUS = 10.0f; + static constexpr uint32 TANK_UPDATE_INTERVAL = 500; + static constexpr uint32 AOE_DAMAGE_INTERVAL = 6000; + + ObjectGuid ownerGuid; + uint32 updateTimer = 0; + uint32 recoveryTimer = 1000; + uint32 aoeDamageTimer = 3000; + + Player* GetOwnerPlayer() const + { + if (!ownerGuid) + return nullptr; + + return ObjectAccessor::GetPlayer(*me, ownerGuid); + } + + bool IsValidTankTarget(Unit* unit) const + { + if (!unit || !unit->IsAlive()) + return false; + + // Dungeon-role bot: do not turn this into a PvP player hunter. + if (!unit->ToCreature()) + return false; + + return me->IsValidAttackTarget(unit); + } + + bool IsActivelyEngagedInCombat() const + { + if (Unit* victim = me->GetVictim()) + if (victim->IsAlive() && me->IsValidAttackTarget(victim)) + return true; + + for (Unit* attacker : me->getAttackers()) + if (attacker && attacker->IsAlive() && me->IsValidAttackTarget(attacker)) + return true; + + return false; + } + + void FollowOwner(Player* owner) + { + if (!owner) + return; + + float followDistance = FOLLOW_DISTANCE; + float followAngle = FOLLOW_ANGLE; + GetVianoFollowSlot(me, owner, followDistance, followAngle); + me->GetMotionMaster()->MoveFollow(owner, followDistance, followAngle); + } + + void HandleOutOfCombatRecovery(uint32 diff) + { + if (!me->IsAlive() || IsActivelyEngagedInCombat()) + { + recoveryTimer = 1000; + return; + } + + if (recoveryTimer > diff) + { + recoveryTimer -= diff; + return; + } + + recoveryTimer = 1000; + + uint32 maxHealth = me->GetMaxHealth(); + if (me->GetHealth() < maxHealth) + { + uint32 gain = maxHealth / 10; + if (!gain) + gain = 1; + + uint32 health = me->GetHealth() + gain; + me->SetHealth(health > maxHealth ? maxHealth : health); + } + + uint32 maxMana = me->GetMaxPower(POWER_MANA); + if (maxMana && me->GetPower(POWER_MANA) < maxMana) + { + uint32 gain = maxMana / 10; + if (!gain) + gain = 1; + + uint32 mana = me->GetPower(POWER_MANA) + gain; + me->SetPower(POWER_MANA, mana > maxMana ? maxMana : mana); + } + } + + Unit* GetSkullTarget(Player* owner) const + { + if (!owner) + return nullptr; + + ObjectGuid guid; + + // A real Trinity Group is authoritative when it exists. + if (Group* group = owner->GetGroup()) guid = group->GetTargetIcon(SKULL_ICON_ID); + + if (!guid) guid = owner->GetVianoTargetIcon(SKULL_ICON_ID); + + if (!guid) return nullptr; + + Unit* target = ObjectAccessor::GetUnit(*me, guid); + return IsValidTankTarget(target) ? target : nullptr; + } + + Unit* FindGroupAttacker(Player* owner) const + { + if (!owner) + return nullptr; + + std::list groupUnits; + owner->GetPartyMembers(groupUnits); + + for (Unit* member : groupUnits) + { + if (!member || !member->IsAlive() || !member->IsInMap(owner)) + continue; + + for (Unit* attacker : member->getAttackers()) + if (IsValidTankTarget(attacker)) + return attacker; + } + + return nullptr; + } + + Unit* SelectPrimaryTarget(Player* owner) + { + if (!owner) + return nullptr; + + // Skull is the only explicit permission for the tank to initiate + // a pull. It also overrides the current primary target mid-fight. + if (Unit* skull = GetSkullTarget(owner)) + return skull; + + // Existing combat takes precedence over player selection. + if (Unit* victim = me->GetVictim()) + if (IsValidTankTarget(victim)) + return victim; + + // Pick up anything already attacking the group. + if (Unit* attacker = FindGroupAttacker(owner)) + return attacker; + + // The owner already attacking something is also a valid combat signal. + if (Unit* victim = owner->GetVictim()) + if (IsValidTankTarget(victim)) + return victim; + + // GetVictim() is not always populated for players. + if (owner->IsInCombat()) + if (Unit* selected = owner->GetSelectedUnit()) + if (IsValidTankTarget(selected)) + return selected; + + return nullptr; + } + + bool IsSafeAutoPickup(Unit* enemy) const + { + return IsValidTankTarget(enemy) + && me->GetDistance(enemy) <= AUTO_PICKUP_RADIUS + && me->IsWithinLOSInMap(enemy); + } + + // VIANO_MULTI_TANK_ARBITRATION_V1 + bool IsPreferredTankFor(Player* owner, Unit* enemy) const + { + if (!owner || !enemy) + return false; + + // Never steal an enemy that is already actively tanked by another + // living Viano tank. Normal damage threat still works; only the + // artificial recovery threat is suppressed. + if (Unit* victim = enemy->GetVictim()) + { + if (victim == me) + return false; + + if (victim->IsAlive() && IsVianoTankUnit(victim)) + return false; + } + + std::list groupUnits; + owner->GetPartyMembers(groupUnits); + + Creature* preferred = nullptr; + float preferredDistance = 0.0f; + + for (Unit* unit : groupUnits) + { + Creature* candidate = unit ? unit->ToCreature() : nullptr; + if (!candidate || !candidate->IsAlive() || !candidate->IsInMap(owner)) + continue; + if (!IsVianoTankUnit(candidate)) + continue; + if (!candidate->IsValidAttackTarget(enemy)) + continue; + if (candidate->GetDistance(enemy) > AUTO_PICKUP_RADIUS) + continue; + if (!candidate->IsWithinLOSInMap(enemy)) + continue; + + float candidateDistance = candidate->GetDistance(enemy); + if (!preferred + || candidateDistance < preferredDistance + || (candidateDistance == preferredDistance + && candidate->GetGUID().GetRawValue() < preferred->GetGUID().GetRawValue())) + { + preferred = candidate; + preferredDistance = candidateDistance; + } + } + + return preferred == me; + } + + void ForceTankThreat(Unit* enemy) + { + if (!IsSafeAutoPickup(enemy)) + return; + + Player* owner = GetOwnerPlayer(); + if (!owner || !IsPreferredTankFor(owner, enemy)) + return; + + ThreatManager& threat = enemy->GetThreatManager(); + + // Recovery only: reach the current highest threat and exceed it. + // Once this tank actually owns the victim, IsPreferredTankFor() + // stops further artificial inflation until aggro is lost again. + threat.MatchUnitThreatToHighestThreat(me); + + float current = threat.GetThreat(me, true); + float bonus = current * 0.35f; + if (bonus < 1.0f) + bonus = 1.0f; + + threat.AddThreat(me, bonus, nullptr, true, true); + } + + void CollectRelevantEnemyGuids(Player* owner, std::vector& targets) const + { + if (!owner) + return; + + auto addTarget = [this, &targets](Unit* enemy) + { + if (!IsSafeAutoPickup(enemy)) + return; + + ObjectGuid const guid = enemy->GetGUID(); + if (std::find(targets.begin(), targets.end(), guid) == targets.end()) + targets.push_back(guid); + }; + + std::list groupUnits; + owner->GetPartyMembers(groupUnits); + + for (Unit* member : groupUnits) + { + if (!member || !member->IsAlive() || !member->IsInMap(owner)) + continue; + + for (Unit* attacker : member->getAttackers()) + addTarget(attacker); + } + } + + void RecoverGroupThreat(Player* owner) + { + if (!owner || !IsActivelyEngagedInCombat()) + return; + + if (Unit* victim = me->GetVictim()) + ForceTankThreat(victim); + + std::vector targets; + CollectRelevantEnemyGuids(owner, targets); + + for (ObjectGuid const& guid : targets) + { + if (Unit* enemy = ObjectAccessor::GetUnit(*me, guid)) + ForceTankThreat(enemy); + } + } + + void DoTankAoeDamage(Player* owner) + { + if (!owner || !IsActivelyEngagedInCombat()) + return; + + uint32 damage = uint32(me->getLevel()) * 2u; + if (!damage) + damage = 1; + + std::vector targets; + CollectRelevantEnemyGuids(owner, targets); + + for (ObjectGuid const& guid : targets) + { + Unit* enemy = ObjectAccessor::GetUnit(*me, guid); + if (!IsSafeAutoPickup(enemy)) + continue; + + Unit::DealDamage( + me, + enemy, + damage, + 0, + DIRECT_DAMAGE, + SPELL_SCHOOL_MASK_NORMAL, + nullptr, + false); + } + } + + 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->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); + me->SetFaction(owner->GetFaction()); + me->SetLevel(owner->getLevel()); + me->UpdateLevelDependantStats(); + + static constexpr uint32 HORDE_ADVENTURER_MODELS[] = + { + 30063, + 30064, + 30065, + 30066 + }; + + me->SetDisplayId(HORDE_ADVENTURER_MODELS[urand(0, 3)], true); + SetEquipmentSlots(false, 50179, 50794, EQUIP_NO_CHANGE); + me->SetReactState(REACT_DEFENSIVE); + + FollowOwner(owner); + + TC_LOG_INFO( + "scripts", + "[VianoTank] initialized entry=%u owner='%s' level=%u health=%u armor=%u", + me->GetEntry(), + owner->GetName().c_str(), + uint32(me->getLevel()), + me->GetMaxHealth(), + me->GetArmor()); + } + + void Reset() override + { + updateTimer = 0; + recoveryTimer = 1000; + aoeDamageTimer = 3000; + + if (Player* owner = GetOwnerPlayer()) + if (!IsActivelyEngagedInCombat()) + FollowOwner(owner); + } + + void MoveInLineOfSight(Unit* /*who*/) override + { + // No autonomous proximity pulls. + } + + void UpdateAI(uint32 diff) override + { + HandleOutOfCombatRecovery(diff); + + if (aoeDamageTimer > diff) + aoeDamageTimer -= diff; + else + aoeDamageTimer = 0; + + if (updateTimer > diff) + { + updateTimer -= diff; + + if (me->GetVictim()) + DoMeleeAttackIfReady(); + + return; + } + + updateTimer = TANK_UPDATE_INTERVAL; + + Player* owner = GetOwnerPlayer(); + if (!owner || !owner->IsInWorld() || !me->IsInMap(owner)) + { + me->DespawnOrUnsummon(); + return; + } + + if (me->getLevel() != owner->getLevel()) + { + me->SetLevel(owner->getLevel()); + me->UpdateLevelDependantStats(); + } + + bool engaged = IsActivelyEngagedInCombat(); + + // Recovery teleport is strictly out-of-combat. + if (!engaged && me->GetDistance(owner) > TELEPORT_DISTANCE) + { + me->GetMotionMaster()->Clear(); + + me->NearTeleportTo( + owner->GetPositionX() + 1.5f, + owner->GetPositionY(), + owner->GetPositionZ(), + owner->GetOrientation()); + + FollowOwner(owner); + return; + } + + Unit* skullTarget = GetSkullTarget(owner); + bool const explicitSkullPull = skullTarget && !engaged; + Unit* desiredTarget = skullTarget ? skullTarget : SelectPrimaryTarget(owner); + + if (desiredTarget && me->GetVictim() != desiredTarget) + { + if (explicitSkullPull) + { + if (!me->IsWithinLOSInMap(desiredTarget)) + { + FollowOwner(owner); + return; + } + + me->SetInCombatWith(desiredTarget); + desiredTarget->SetInCombatWith(me); + } + + AttackStart(desiredTarget); + } + + engaged = IsActivelyEngagedInCombat(); + + if (engaged) + { + RecoverGroupThreat(owner); + + if (aoeDamageTimer == 0) + { + aoeDamageTimer = AOE_DAMAGE_INTERVAL; + DoTankAoeDamage(owner); + } + + if (me->GetVictim()) + DoMeleeAttackIfReady(); + } + else + FollowOwner(owner); + } + }; + + CreatureAI* GetAI(Creature* creature) const override + { + return new npc_horde_adventurer_tankAI(creature); + } + }; + + + // VIANO_HEALER_V1 + class npc_horde_adventurer_healer : public CreatureScript + { + public: + npc_horde_adventurer_healer() : CreatureScript("npc_horde_adventurer_healer") { } + + struct npc_horde_adventurer_healerAI : public ScriptedAI + { + npc_horde_adventurer_healerAI(Creature* creature) : ScriptedAI(creature) { } + + static constexpr uint32 SPELL_HEAL_EFFICIENT = 635; + static constexpr uint32 SPELL_HEAL_STRONG = 82326; + static constexpr uint32 SPELL_HEAL_EMERGENCY = 19750; + static constexpr float HEAL_RANGE = 40.0f; + + ObjectGuid ownerGuid; + uint32 updateTimer = 0; + uint32 recoveryTimer = 1000; + uint32 decisionLockTimer = 0; + + Player* GetOwnerPlayer() const + { + return ownerGuid ? ObjectAccessor::GetPlayer(*me, ownerGuid) : nullptr; + } + + static std::string GetScriptName(Creature const* creature) + { + if (!creature) + return {}; + if (CreatureTemplate const* ct = creature->GetCreatureTemplate()) + return sObjectMgr->GetScriptName(ct->ScriptID); + return {}; + } + + bool IsVianoTank(Unit* unit) const + { + Creature* creature = unit ? unit->ToCreature() : nullptr; + return creature && GetScriptName(creature).find("_tank") != std::string::npos; + } + + bool IsDeclaredPlayerTank(Unit* unit, Player* owner) const + { + Player* player = unit ? unit->ToPlayer() : nullptr; + if (!player || !owner) + return false; + + Group* group = owner->GetGroup(); + if (!group || !group->isLFGGroup() || player->GetGroup() != group) + return false; + + uint8 roles = sLFGMgr->GetRoles(player->GetGUID()); + return (roles & lfg::PLAYER_ROLE_TANK) != 0; + } + + bool IsDeclaredTank(Unit* unit, Player* owner) const + { + return IsVianoTank(unit) || IsDeclaredPlayerTank(unit, owner); + } + + bool IsActiveTank(Unit* unit) const + { + return unit && unit->IsAlive() && !unit->getAttackers().empty(); + } + + uint32 CountRelevantAttackers(Unit* unit) const + { + if (!unit) + return 0; + + uint32 count = 0; + for (Unit* attacker : unit->getAttackers()) + if (attacker && attacker->IsAlive() && attacker->GetVictim() == unit) + ++count; + return count; + } + + bool IsGroupCombatActive(Player* owner) const + { + if (!owner) + return false; + + std::list units; + owner->GetPartyMembers(units); + for (Unit* unit : units) + if (unit && unit->IsAlive() && (!unit->getAttackers().empty() || unit->IsInCombat())) + return true; + return false; + } + + bool IsValidHealTarget(Unit* unit, Player* owner) const + { + return unit + && owner + && unit->IsAlive() + && unit->IsInMap(owner) + && me->GetDistance(unit) <= HEAL_RANGE + && me->IsWithinLOSInMap(unit) + && me->IsValidAssistTarget(unit); + } + + float HealthPct(Unit* unit) const + { + return unit && unit->GetMaxHealth() + ? 100.0f * float(unit->GetHealth()) / float(unit->GetMaxHealth()) + : 100.0f; + } + + int32 HealPriority(Unit* unit, Player* owner) const + { + if (!IsValidHealTarget(unit, owner)) + return -1; + + float hp = HealthPct(unit); + bool declaredTank = IsDeclaredTank(unit, owner); + bool activeTank = IsActiveTank(unit); + uint32 attackers = CountRelevantAttackers(unit); + + if (!declaredTank && !activeTank && hp > 75.0f) + return -1; + if ((declaredTank || activeTank) && hp > 95.0f) + return -1; + + int32 score = int32(100.0f - hp); + if (declaredTank) + score += 45; + if (activeTank) + score += 65; + score += int32(std::min(attackers, 5u)) * 12; + + if (hp <= 35.0f) + score += 120; + else if (hp <= 50.0f) + score += 55; + + return score; + } + + Unit* SelectHealTarget(Player* owner) const + { + std::list units; + owner->GetPartyMembers(units); + + Unit* best = nullptr; + int32 bestScore = -1; + for (Unit* unit : units) + { + int32 score = HealPriority(unit, owner); + if (score > bestScore) + { + best = unit; + bestScore = score; + } + } + return best; + } + + uint32 SelectHealSpell(Unit* target, Player* owner) const + { + float hp = HealthPct(target); + bool tank = IsDeclaredTank(target, owner) || IsActiveTank(target); + + if (hp <= 35.0f || (tank && hp <= 45.0f)) + return SPELL_HEAL_EMERGENCY; + if (hp <= 50.0f || (tank && hp <= 68.0f)) + return SPELL_HEAL_STRONG; + return SPELL_HEAL_EFFICIENT; + } + + bool ManaPolicyAllows(Unit* target, Player* owner, uint32 spellId) const + { + uint32 maxMana = me->GetMaxPower(POWER_MANA); + if (!maxMana) + return true; + + float manaPct = 100.0f * float(me->GetPower(POWER_MANA)) / float(maxMana); + float hp = HealthPct(target); + bool tank = IsDeclaredTank(target, owner) || IsActiveTank(target); + + if (manaPct < 10.0f) + return spellId == SPELL_HEAL_EMERGENCY && hp <= 30.0f; + + if (manaPct < 25.0f) + { + if (spellId == SPELL_HEAL_EMERGENCY) + return true; + if (tank) + return hp <= 70.0f; + return hp <= 45.0f; + } + + return true; + } + + void ConfigureMana(bool refill) + { + // VIANO_HEALER_BALANCE_V1_2 + // Player-like mana pool: level 31 -> 1802 mana. + uint32 desired = 500u + uint32(me->getLevel()) * 42u; + if (desired < 900u) + desired = 900u; + + uint32 oldMax = me->GetMaxPower(POWER_MANA); + uint32 oldMana = me->GetPower(POWER_MANA); + me->SetMaxPower(POWER_MANA, desired); + + if (refill || !oldMax) + me->SetPower(POWER_MANA, desired); + else + me->SetPower(POWER_MANA, uint32(std::min( + uint64(oldMana) * uint64(desired) / uint64(oldMax), uint64(desired)))); + + // Disable generic creature regen; Viano owns healer mana regen. + me->RemoveFlag(UNIT_FIELD_FLAGS_2, UNIT_FLAG2_REGENERATE_POWER); + } + + void FollowOwner(Player* owner) + { + if (!owner) + return; + + uint32 slot = me->GetGUID().GetCounter() % 6u; + static float const distances[6] = { 3.0f, 3.2f, 3.2f, 4.0f, 4.0f, 4.5f }; + static float const angles[6] = { 3.14159265f, 2.55f, 3.73f, 2.80f, 3.48f, 3.14159265f }; + me->GetMotionMaster()->MoveFollow(owner, distances[slot], angles[slot]); + } + + void HandleOutOfCombatRecovery(uint32 diff, Player* owner) + { + if (!me->IsAlive() || !owner) + { + recoveryTimer = 1000; + return; + } + + if (recoveryTimer > diff) + { + recoveryTimer -= diff; + return; + } + + recoveryTimer = 1000; + bool const groupCombat = IsGroupCombatActive(owner); + + if (!groupCombat) + { + uint32 maxHealth = me->GetMaxHealth(); + if (me->GetHealth() < maxHealth) + { + uint32 gain = std::max(1u, maxHealth / 10u); + me->SetHealth(std::min(maxHealth, me->GetHealth() + gain)); + } + } + + uint32 maxMana = me->GetMaxPower(POWER_MANA); + if (maxMana && me->GetPower(POWER_MANA) < maxMana) + { + uint32 divisor = groupCombat ? 100u : 30u; // 1% combat, ~3.3% OOC + uint32 gain = std::max(1u, maxMana / divisor); + me->SetPower(POWER_MANA, + std::min(maxMana, me->GetPower(POWER_MANA) + gain)); + } + } + + void InitializeAI() override + { + ScriptedAI::InitializeAI(); + + TempSummon* summon = me->ToTempSummon(); + Player* owner = summon && summon->GetSummoner() ? summon->GetSummoner()->ToPlayer() : nullptr; + if (!owner) + { + me->DespawnOrUnsummon(); + return; + } + + ownerGuid = owner->GetGUID(); + me->SetOwnerGUID(ownerGuid); + me->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); + me->SetFaction(owner->GetFaction()); + me->SetLevel(owner->getLevel()); + me->UpdateLevelDependantStats(); + ConfigureMana(true); + + static constexpr uint32 MODELS[] = { 30063, 30064, 30065, 30066 }; + me->SetDisplayId(MODELS[urand(0, 3)], true); + SetEquipmentSlots(false, 39426, EQUIP_NO_CHANGE, EQUIP_NO_CHANGE); + me->SetReactState(REACT_PASSIVE); + FollowOwner(owner); + + TC_LOG_INFO("scripts", + "[VianoHealer] initialized entry=%u owner='%s' level=%u hp=%u/%u mana=%u/%u", + me->GetEntry(), owner->GetName().c_str(), uint32(me->getLevel()), + me->GetHealth(), me->GetMaxHealth(), + me->GetPower(POWER_MANA), me->GetMaxPower(POWER_MANA)); + } + + void Reset() override + { + if (Player* owner = GetOwnerPlayer()) + FollowOwner(owner); + } + + void MoveInLineOfSight(Unit* /*who*/) override + { + } + + + // VIANO_HEALER_DISPEL_V1 + bool TryDispelGroupMember(Player* owner) + { + if (!owner || me->HasUnitState(UNIT_STATE_CASTING)) + return false; + + std::list members; + owner->GetPartyMembers(members); + std::vector candidates; + candidates.push_back(owner); + + for (Unit* member : members) + { + if (!member) + continue; + bool duplicate = false; + for (Unit* existing : candidates) + if (existing && existing->GetGUID() == member->GetGUID()) + duplicate = true; + if (!duplicate) + candidates.push_back(member); + } + + // Healing wins if anyone is substantially hurt. + for (Unit* unit : candidates) + if (unit && unit->IsAlive() && unit->IsInMap(me) && unit->GetHealthPct() < 70.0f) + return false; + + uint32 const curseMask = (1u << DISPEL_CURSE); + uint32 const cleanseMask = (1u << DISPEL_MAGIC) | + (1u << DISPEL_DISEASE) | (1u << DISPEL_POISON); + + for (Unit* unit : candidates) + { + if (!unit || !unit->IsAlive() || !unit->IsInMap(me)) + continue; + if (!me->IsWithinDistInMap(unit, 40.0f) || !me->IsWithinLOSInMap(unit)) + continue; + + DispelChargesList curseAuras; + unit->GetDispellableAuraList(me, curseMask, curseAuras); + if (!curseAuras.empty()) + { + DoCast(unit, 2782); // Remove Corruption + return true; + } + + DispelChargesList cleanseAuras; + unit->GetDispellableAuraList(me, cleanseMask, cleanseAuras); + if (!cleanseAuras.empty()) + { + DoCast(unit, 4987); // Cleanse + return true; + } + } + return false; + } + +void UpdateAI(uint32 diff) override + { + if (updateTimer > diff) + { + updateTimer -= diff; + decisionLockTimer = decisionLockTimer > diff ? decisionLockTimer - diff : 0; + HandleOutOfCombatRecovery(diff, GetOwnerPlayer()); + return; + } + + updateTimer = 250; + decisionLockTimer = decisionLockTimer > diff ? decisionLockTimer - diff : 0; + + Player* owner = GetOwnerPlayer(); + + if (owner && TryDispelGroupMember(owner)) + return; + if (!owner || !owner->IsInWorld() || !me->IsInMap(owner)) + { + me->DespawnOrUnsummon(); + return; + } + + HandleOutOfCombatRecovery(diff, owner); + + if (me->getLevel() != owner->getLevel()) + { + me->SetLevel(owner->getLevel()); + me->UpdateLevelDependantStats(); + ConfigureMana(false); + } + + if (me->GetDistance(owner) > 45.0f && !me->HasUnitState(UNIT_STATE_CASTING)) + { + me->GetMotionMaster()->Clear(); + me->NearTeleportTo(owner->GetPositionX() + 2.0f, owner->GetPositionY() + 1.0f, + owner->GetPositionZ(), owner->GetOrientation()); + FollowOwner(owner); + return; + } + + if (me->HasUnitState(UNIT_STATE_CASTING) || decisionLockTimer) + return; + + Unit* target = SelectHealTarget(owner); + if (!target) + { + FollowOwner(owner); + return; + } + + uint32 spellId = SelectHealSpell(target, owner); + if (!ManaPolicyAllows(target, owner, spellId)) + return; + + me->CastSpell(target, spellId); + + TC_LOG_INFO("scripts", + "[VianoHealer] cast spell=%u target='%s' hp=%.1f declaredTank=%u activeTank=%u attackers=%u mana=%u/%u", + spellId, target->GetName().c_str(), HealthPct(target), + IsDeclaredTank(target, owner) ? 1u : 0u, + IsActiveTank(target) ? 1u : 0u, + CountRelevantAttackers(target), + me->GetPower(POWER_MANA), me->GetMaxPower(POWER_MANA)); + + decisionLockTimer = 500; + } + }; + + CreatureAI* GetAI(Creature* creature) const override + { + return new npc_horde_adventurer_healerAI(creature); + } + }; +} + +void AddSC_viano_adventurers() +{ + using namespace VianoAdventurers; + new npc_horde_adventurer_melee(); + new npc_horde_adventurer_tank(); + new npc_horde_adventurer_healer(); +} diff --git a/src/server/scripts/Custom/viano_adventurers.cpp.bak-vcore-faction-models-20261008-002257 b/src/server/scripts/Custom/viano_adventurers.cpp.bak-vcore-faction-models-20261008-002257 new file mode 100644 index 0000000000..7ade2ceb7f --- /dev/null +++ b/src/server/scripts/Custom/viano_adventurers.cpp.bak-vcore-faction-models-20261008-002257 @@ -0,0 +1,1739 @@ +/* + * Viano custom dungeon adventurers + * Initial melee companion implementation. + */ + +#include "ScriptMgr.h" +#include "ScriptedCreature.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "Group.h" +#include "TemporarySummon.h" +#include "MotionMaster.h" +#include "Log.h" +#include "SpellInfo.h" +#include "SpellAuras.h" +#include +#include +#include "LFGMgr.h" +#include "LFG.h" +#include "ObjectMgr.h" +#include +#include + +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_FORMATION_V1 + static bool IsVianoTankUnit(Unit* unit) + { + Creature* creature = unit ? unit->ToCreature() : nullptr; + if (!creature || !creature->IsVianoAdventurer()) + return false; + + CreatureTemplate const* cinfo = creature->GetCreatureTemplate(); + if (!cinfo) + return false; + + std::string const scriptName = sObjectMgr->GetScriptName(cinfo->ScriptID); + return scriptName.rfind("npc_horde_adventurer_tank", 0) == 0 + || scriptName.rfind("npc_alliance_adventurer_tank", 0) == 0; + } + + static void GetVianoFollowSlot(Creature* bot, Player* owner, float& distance, float& angle) + { + distance = FOLLOW_DISTANCE; + angle = FOLLOW_ANGLE; + + if (!bot || !owner) + return; + + std::list groupUnits; + owner->GetPartyMembers(groupUnits); + + std::vector bots; + for (Unit* unit : groupUnits) + { + Creature* creature = unit ? unit->ToCreature() : nullptr; + if (!creature || !creature->IsVianoAdventurer()) + continue; + if (!creature->IsAlive() || !creature->IsInMap(owner)) + continue; + + bots.push_back(creature); + } + + std::sort(bots.begin(), bots.end(), [](Creature const* left, Creature const* right) + { + return left->GetGUID().GetRawValue() < right->GetGUID().GetRawValue(); + }); + + size_t slot = 0; + bool found = false; + for (size_t i = 0; i < bots.size(); ++i) + { + if (bots[i] == bot) + { + slot = i; + found = true; + break; + } + } + + if (!found) + return; + + struct FollowSlot + { + float distance; + float angle; + }; + + static FollowSlot const baseSlots[] = + { + { 2.7f, 3.14159265f }, + { 3.1f, 2.52f }, + { 3.1f, 3.76f }, + { 4.0f, 2.25f }, + { 4.0f, 4.03f }, + { 4.4f, 2.70f }, + { 4.4f, 3.58f }, + { 4.8f, 3.14159265f } + }; + + if (slot < sizeof(baseSlots) / sizeof(baseSlots[0])) + { + distance = baseSlots[slot].distance; + angle = baseSlots[slot].angle; + return; + } + + // Unlimited development-mode fallback: additional bots form wider rows + // instead of being stacked on one point. + static float const offsets[] = { -0.95f, -0.48f, 0.0f, 0.48f, 0.95f }; + size_t extra = slot - (sizeof(baseSlots) / sizeof(baseSlots[0])); + size_t row = extra / 5; + size_t column = extra % 5; + + distance = 5.4f + float(row) * 1.25f; + angle = 3.14159265f + offsets[column]; + } + + // 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; + + float followDistance = FOLLOW_DISTANCE; + float followAngle = FOLLOW_ANGLE; + GetVianoFollowSlot(me, owner, followDistance, followAngle); + me->GetMotionMaster()->MoveFollow(owner, followDistance, followAngle); + } + + 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->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); + 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())); + } + + 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; + + auto isCombatRelevant = [](Unit* target) -> bool + { + return target + && target->IsAlive() + && (target->IsInCombat() + || target->GetVictim() + || !target->getAttackers().empty()); + }; + + auto isDeclaredTank = [owner](Unit* unit) -> bool + { + if (!unit) + return false; + + if (Creature* creature = unit->ToCreature()) + { + if (CreatureTemplate const* creatureTemplate = creature->GetCreatureTemplate()) + { + std::string const scriptName = sObjectMgr->GetScriptName(creatureTemplate->ScriptID); + if (scriptName.find("_tank") != std::string::npos) + return true; + } + } + + if (Player* player = unit->ToPlayer()) + { + if (Group* group = owner->GetGroup()) + { + if (group->isLFGGroup() && player->GetGroup() == group) + { + uint8 roles = sLFGMgr->GetRoles(player->GetGUID()); + if (roles & lfg::PLAYER_ROLE_TANK) + return true; + } + } + } + + return false; + }; + + // VIANO_DD_TARGET_V1_1: + // If a declared tank is actively fighting, DDs follow the tank's + // victim. Merely selecting another hostile target must not pull it. + std::list groupUnits; + owner->GetPartyMembers(groupUnits); + + Unit* bestTankTarget = nullptr; + uint32 bestTankPressure = 0; + bool declaredTankPresent = false; + + auto considerTank = [&](Unit* tank) + { + if (!tank || !tank->IsAlive() || !tank->IsInMap(owner) || !isDeclaredTank(tank)) + return; + + declaredTankPresent = true; + + Unit* victim = tank->GetVictim(); + if (!victim || !IsValidCandidate(victim) || !isCombatRelevant(victim)) + return; + + // VIANO_DD_TANK_ENGAGEMENT_GATE_V1: + // AttackStart() gives the tank a victim immediately during a + // skull pull. DDs must not interpret that as "tank has taken + // the target" yet. Wait until the enemy is actually attacking + // the tank / appears in the tank's attacker set. + bool tankHasAggro = victim->GetVictim() == tank; + if (!tankHasAggro) + { + for (Unit* attacker : tank->getAttackers()) + { + if (attacker == victim) + { + tankHasAggro = true; + break; + } + } + } + + if (!tankHasAggro) + return; + + uint32 pressure = uint32(tank->getAttackers().size()); + if (!bestTankTarget || pressure > bestTankPressure) + { + bestTankTarget = victim; + bestTankPressure = pressure; + } + }; + + considerTank(owner); + for (Unit* member : groupUnits) + considerTank(member); + + if (bestTankTarget) + return bestTankTarget; + + // If a tank exists but has not actually established the pull yet, + // DDs wait. In particular they must NOT fall through to the + // player's selected skull target merely because the tank just put + // that target into combat. + if (declaredTankPresent) + { + if (Unit* target = me->GetVictim()) + if (IsValidCandidate(target) && isCombatRelevant(target)) + return target; + + if (Unit* target = SelectGroupCombatTarget(owner)) + if (IsValidCandidate(target) && isCombatRelevant(target)) + return target; + + for (Unit* attacker : me->getAttackers()) + if (IsValidCandidate(attacker) && isCombatRelevant(attacker)) + return attacker; + + return nullptr; + } + + // No declared tank: the owner controls DD focus, but only after + // the target is already genuinely combat-relevant. + if (Unit* target = owner->GetVictim()) + if (IsValidCandidate(target) && isCombatRelevant(target)) + return target; + + if (Unit* target = owner->GetSelectedUnit()) + if (IsValidCandidate(target) && isCombatRelevant(target)) + return target; + + // Keep a valid existing bot victim. + if (Unit* target = me->GetVictim()) + if (IsValidCandidate(target) && isCombatRelevant(target)) + return target; + + // Help the virtual/real group against enemies that are already + // attacking group members. + if (Unit* target = SelectGroupCombatTarget(owner)) + if (IsValidCandidate(target) && isCombatRelevant(target)) + return target; + + for (Unit* attacker : owner->getAttackers()) + if (IsValidCandidate(attacker) && isCombatRelevant(attacker)) + return attacker; + + for (Unit* attacker : me->getAttackers()) + if (IsValidCandidate(attacker) && isCombatRelevant(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 && !IsActivelyEngagedInCombat()) + { + // 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( + 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 (!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); + } + + // 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); + } + }; + + // VIANO_TANK_V1 + class npc_horde_adventurer_tank : public CreatureScript + { + public: + npc_horde_adventurer_tank() : CreatureScript("npc_horde_adventurer_tank") { } + + struct npc_horde_adventurer_tankAI : public ScriptedAI + { + npc_horde_adventurer_tankAI(Creature* creature) : ScriptedAI(creature) { } + + static constexpr uint8 SKULL_ICON_ID = 7; + static constexpr float AUTO_PICKUP_RADIUS = 10.0f; + static constexpr uint32 TANK_UPDATE_INTERVAL = 500; + static constexpr uint32 AOE_DAMAGE_INTERVAL = 6000; + + ObjectGuid ownerGuid; + uint32 updateTimer = 0; + uint32 recoveryTimer = 1000; + uint32 aoeDamageTimer = 3000; + + Player* GetOwnerPlayer() const + { + if (!ownerGuid) + return nullptr; + + return ObjectAccessor::GetPlayer(*me, ownerGuid); + } + + bool IsValidTankTarget(Unit* unit) const + { + if (!unit || !unit->IsAlive()) + return false; + + // Dungeon-role bot: do not turn this into a PvP player hunter. + if (!unit->ToCreature()) + return false; + + return me->IsValidAttackTarget(unit); + } + + bool IsActivelyEngagedInCombat() const + { + if (Unit* victim = me->GetVictim()) + if (victim->IsAlive() && me->IsValidAttackTarget(victim)) + return true; + + for (Unit* attacker : me->getAttackers()) + if (attacker && attacker->IsAlive() && me->IsValidAttackTarget(attacker)) + return true; + + return false; + } + + void FollowOwner(Player* owner) + { + if (!owner) + return; + + float followDistance = FOLLOW_DISTANCE; + float followAngle = FOLLOW_ANGLE; + GetVianoFollowSlot(me, owner, followDistance, followAngle); + me->GetMotionMaster()->MoveFollow(owner, followDistance, followAngle); + } + + void HandleOutOfCombatRecovery(uint32 diff) + { + if (!me->IsAlive() || IsActivelyEngagedInCombat()) + { + recoveryTimer = 1000; + return; + } + + if (recoveryTimer > diff) + { + recoveryTimer -= diff; + return; + } + + recoveryTimer = 1000; + + uint32 maxHealth = me->GetMaxHealth(); + if (me->GetHealth() < maxHealth) + { + uint32 gain = maxHealth / 10; + if (!gain) + gain = 1; + + uint32 health = me->GetHealth() + gain; + me->SetHealth(health > maxHealth ? maxHealth : health); + } + + uint32 maxMana = me->GetMaxPower(POWER_MANA); + if (maxMana && me->GetPower(POWER_MANA) < maxMana) + { + uint32 gain = maxMana / 10; + if (!gain) + gain = 1; + + uint32 mana = me->GetPower(POWER_MANA) + gain; + me->SetPower(POWER_MANA, mana > maxMana ? maxMana : mana); + } + } + + Unit* GetSkullTarget(Player* owner) const + { + if (!owner) + return nullptr; + + ObjectGuid guid; + + // A real Trinity Group is authoritative when it exists. + if (Group* group = owner->GetGroup()) guid = group->GetTargetIcon(SKULL_ICON_ID); + + if (!guid) guid = owner->GetVianoTargetIcon(SKULL_ICON_ID); + + if (!guid) return nullptr; + + Unit* target = ObjectAccessor::GetUnit(*me, guid); + return IsValidTankTarget(target) ? target : nullptr; + } + + Unit* FindGroupAttacker(Player* owner) const + { + if (!owner) + return nullptr; + + std::list groupUnits; + owner->GetPartyMembers(groupUnits); + + for (Unit* member : groupUnits) + { + if (!member || !member->IsAlive() || !member->IsInMap(owner)) + continue; + + for (Unit* attacker : member->getAttackers()) + if (IsValidTankTarget(attacker)) + return attacker; + } + + return nullptr; + } + + Unit* SelectPrimaryTarget(Player* owner) + { + if (!owner) + return nullptr; + + // Skull is the only explicit permission for the tank to initiate + // a pull. It also overrides the current primary target mid-fight. + if (Unit* skull = GetSkullTarget(owner)) + return skull; + + // Existing combat takes precedence over player selection. + if (Unit* victim = me->GetVictim()) + if (IsValidTankTarget(victim)) + return victim; + + // Pick up anything already attacking the group. + if (Unit* attacker = FindGroupAttacker(owner)) + return attacker; + + // The owner already attacking something is also a valid combat signal. + if (Unit* victim = owner->GetVictim()) + if (IsValidTankTarget(victim)) + return victim; + + // GetVictim() is not always populated for players. + if (owner->IsInCombat()) + if (Unit* selected = owner->GetSelectedUnit()) + if (IsValidTankTarget(selected)) + return selected; + + return nullptr; + } + + bool IsSafeAutoPickup(Unit* enemy) const + { + return IsValidTankTarget(enemy) + && me->GetDistance(enemy) <= AUTO_PICKUP_RADIUS + && me->IsWithinLOSInMap(enemy); + } + + // VIANO_MULTI_TANK_ARBITRATION_V1 + bool IsPreferredTankFor(Player* owner, Unit* enemy) const + { + if (!owner || !enemy) + return false; + + // Never steal an enemy that is already actively tanked by another + // living Viano tank. Normal damage threat still works; only the + // artificial recovery threat is suppressed. + if (Unit* victim = enemy->GetVictim()) + { + if (victim == me) + return false; + + if (victim->IsAlive() && IsVianoTankUnit(victim)) + return false; + } + + std::list groupUnits; + owner->GetPartyMembers(groupUnits); + + Creature* preferred = nullptr; + float preferredDistance = 0.0f; + + for (Unit* unit : groupUnits) + { + Creature* candidate = unit ? unit->ToCreature() : nullptr; + if (!candidate || !candidate->IsAlive() || !candidate->IsInMap(owner)) + continue; + if (!IsVianoTankUnit(candidate)) + continue; + if (!candidate->IsValidAttackTarget(enemy)) + continue; + if (candidate->GetDistance(enemy) > AUTO_PICKUP_RADIUS) + continue; + if (!candidate->IsWithinLOSInMap(enemy)) + continue; + + float candidateDistance = candidate->GetDistance(enemy); + if (!preferred + || candidateDistance < preferredDistance + || (candidateDistance == preferredDistance + && candidate->GetGUID().GetRawValue() < preferred->GetGUID().GetRawValue())) + { + preferred = candidate; + preferredDistance = candidateDistance; + } + } + + return preferred == me; + } + + void ForceTankThreat(Unit* enemy) + { + if (!IsSafeAutoPickup(enemy)) + return; + + Player* owner = GetOwnerPlayer(); + if (!owner || !IsPreferredTankFor(owner, enemy)) + return; + + ThreatManager& threat = enemy->GetThreatManager(); + + // Recovery only: reach the current highest threat and exceed it. + // Once this tank actually owns the victim, IsPreferredTankFor() + // stops further artificial inflation until aggro is lost again. + threat.MatchUnitThreatToHighestThreat(me); + + float current = threat.GetThreat(me, true); + float bonus = current * 0.35f; + if (bonus < 1.0f) + bonus = 1.0f; + + threat.AddThreat(me, bonus, nullptr, true, true); + } + + void CollectRelevantEnemyGuids(Player* owner, std::vector& targets) const + { + if (!owner) + return; + + auto addTarget = [this, &targets](Unit* enemy) + { + if (!IsSafeAutoPickup(enemy)) + return; + + ObjectGuid const guid = enemy->GetGUID(); + if (std::find(targets.begin(), targets.end(), guid) == targets.end()) + targets.push_back(guid); + }; + + std::list groupUnits; + owner->GetPartyMembers(groupUnits); + + for (Unit* member : groupUnits) + { + if (!member || !member->IsAlive() || !member->IsInMap(owner)) + continue; + + for (Unit* attacker : member->getAttackers()) + addTarget(attacker); + } + } + + void RecoverGroupThreat(Player* owner) + { + if (!owner || !IsActivelyEngagedInCombat()) + return; + + if (Unit* victim = me->GetVictim()) + ForceTankThreat(victim); + + std::vector targets; + CollectRelevantEnemyGuids(owner, targets); + + for (ObjectGuid const& guid : targets) + { + if (Unit* enemy = ObjectAccessor::GetUnit(*me, guid)) + ForceTankThreat(enemy); + } + } + + void DoTankAoeDamage(Player* owner) + { + if (!owner || !IsActivelyEngagedInCombat()) + return; + + uint32 damage = uint32(me->getLevel()) * 2u; + if (!damage) + damage = 1; + + std::vector targets; + CollectRelevantEnemyGuids(owner, targets); + + for (ObjectGuid const& guid : targets) + { + Unit* enemy = ObjectAccessor::GetUnit(*me, guid); + if (!IsSafeAutoPickup(enemy)) + continue; + + Unit::DealDamage( + me, + enemy, + damage, + 0, + DIRECT_DAMAGE, + SPELL_SCHOOL_MASK_NORMAL, + nullptr, + false); + } + } + + 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->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); + me->SetFaction(owner->GetFaction()); + me->SetLevel(owner->getLevel()); + me->UpdateLevelDependantStats(); + + static constexpr uint32 HORDE_ADVENTURER_MODELS[] = + { + 30063, + 30064, + 30065, + 30066 + }; + + me->SetDisplayId(HORDE_ADVENTURER_MODELS[urand(0, 3)], true); + SetEquipmentSlots(false, 50179, 50794, EQUIP_NO_CHANGE); + me->SetReactState(REACT_DEFENSIVE); + + FollowOwner(owner); + + TC_LOG_INFO( + "scripts", + "[VianoTank] initialized entry=%u owner='%s' level=%u health=%u armor=%u", + me->GetEntry(), + owner->GetName().c_str(), + uint32(me->getLevel()), + me->GetMaxHealth(), + me->GetArmor()); + } + + void Reset() override + { + updateTimer = 0; + recoveryTimer = 1000; + aoeDamageTimer = 3000; + + if (Player* owner = GetOwnerPlayer()) + if (!IsActivelyEngagedInCombat()) + FollowOwner(owner); + } + + void MoveInLineOfSight(Unit* /*who*/) override + { + // No autonomous proximity pulls. + } + + void UpdateAI(uint32 diff) override + { + HandleOutOfCombatRecovery(diff); + + if (aoeDamageTimer > diff) + aoeDamageTimer -= diff; + else + aoeDamageTimer = 0; + + if (updateTimer > diff) + { + updateTimer -= diff; + + if (me->GetVictim()) + DoMeleeAttackIfReady(); + + return; + } + + updateTimer = TANK_UPDATE_INTERVAL; + + Player* owner = GetOwnerPlayer(); + if (!owner || !owner->IsInWorld() || !me->IsInMap(owner)) + { + me->DespawnOrUnsummon(); + return; + } + + if (me->getLevel() != owner->getLevel()) + { + me->SetLevel(owner->getLevel()); + me->UpdateLevelDependantStats(); + } + + bool engaged = IsActivelyEngagedInCombat(); + + // Recovery teleport is strictly out-of-combat. + if (!engaged && me->GetDistance(owner) > TELEPORT_DISTANCE) + { + me->GetMotionMaster()->Clear(); + + me->NearTeleportTo( + owner->GetPositionX() + 1.5f, + owner->GetPositionY(), + owner->GetPositionZ(), + owner->GetOrientation()); + + FollowOwner(owner); + return; + } + + Unit* skullTarget = GetSkullTarget(owner); + bool const explicitSkullPull = skullTarget && !engaged; + Unit* desiredTarget = skullTarget ? skullTarget : SelectPrimaryTarget(owner); + + if (desiredTarget && me->GetVictim() != desiredTarget) + { + if (explicitSkullPull) + { + if (!me->IsWithinLOSInMap(desiredTarget)) + { + FollowOwner(owner); + return; + } + + me->SetInCombatWith(desiredTarget); + desiredTarget->SetInCombatWith(me); + } + + AttackStart(desiredTarget); + } + + engaged = IsActivelyEngagedInCombat(); + + if (engaged) + { + RecoverGroupThreat(owner); + + if (aoeDamageTimer == 0) + { + aoeDamageTimer = AOE_DAMAGE_INTERVAL; + DoTankAoeDamage(owner); + } + + if (me->GetVictim()) + DoMeleeAttackIfReady(); + } + else + FollowOwner(owner); + } + }; + + CreatureAI* GetAI(Creature* creature) const override + { + return new npc_horde_adventurer_tankAI(creature); + } + }; + + + // VIANO_HEALER_V1 + class npc_horde_adventurer_healer : public CreatureScript + { + public: + npc_horde_adventurer_healer() : CreatureScript("npc_horde_adventurer_healer") { } + + struct npc_horde_adventurer_healerAI : public ScriptedAI + { + npc_horde_adventurer_healerAI(Creature* creature) : ScriptedAI(creature) { } + + static constexpr uint32 SPELL_HEAL_EFFICIENT = 635; + static constexpr uint32 SPELL_HEAL_STRONG = 82326; + static constexpr uint32 SPELL_HEAL_EMERGENCY = 19750; + static constexpr float HEAL_RANGE = 40.0f; + + ObjectGuid ownerGuid; + uint32 updateTimer = 0; + uint32 recoveryTimer = 1000; + uint32 decisionLockTimer = 0; + + Player* GetOwnerPlayer() const + { + return ownerGuid ? ObjectAccessor::GetPlayer(*me, ownerGuid) : nullptr; + } + + static std::string GetScriptName(Creature const* creature) + { + if (!creature) + return {}; + if (CreatureTemplate const* ct = creature->GetCreatureTemplate()) + return sObjectMgr->GetScriptName(ct->ScriptID); + return {}; + } + + bool IsVianoTank(Unit* unit) const + { + Creature* creature = unit ? unit->ToCreature() : nullptr; + return creature && GetScriptName(creature).find("_tank") != std::string::npos; + } + + bool IsDeclaredPlayerTank(Unit* unit, Player* owner) const + { + Player* player = unit ? unit->ToPlayer() : nullptr; + if (!player || !owner) + return false; + + Group* group = owner->GetGroup(); + if (!group || !group->isLFGGroup() || player->GetGroup() != group) + return false; + + uint8 roles = sLFGMgr->GetRoles(player->GetGUID()); + return (roles & lfg::PLAYER_ROLE_TANK) != 0; + } + + bool IsDeclaredTank(Unit* unit, Player* owner) const + { + return IsVianoTank(unit) || IsDeclaredPlayerTank(unit, owner); + } + + bool IsActiveTank(Unit* unit) const + { + return unit && unit->IsAlive() && !unit->getAttackers().empty(); + } + + uint32 CountRelevantAttackers(Unit* unit) const + { + if (!unit) + return 0; + + uint32 count = 0; + for (Unit* attacker : unit->getAttackers()) + if (attacker && attacker->IsAlive() && attacker->GetVictim() == unit) + ++count; + return count; + } + + bool IsGroupCombatActive(Player* owner) const + { + if (!owner) + return false; + + std::list units; + owner->GetPartyMembers(units); + for (Unit* unit : units) + if (unit && unit->IsAlive() && (!unit->getAttackers().empty() || unit->IsInCombat())) + return true; + return false; + } + + bool IsValidHealTarget(Unit* unit, Player* owner) const + { + return unit + && owner + && unit->IsAlive() + && unit->IsInMap(owner) + && me->GetDistance(unit) <= HEAL_RANGE + && me->IsWithinLOSInMap(unit) + && me->IsValidAssistTarget(unit); + } + + float HealthPct(Unit* unit) const + { + return unit && unit->GetMaxHealth() + ? 100.0f * float(unit->GetHealth()) / float(unit->GetMaxHealth()) + : 100.0f; + } + + int32 HealPriority(Unit* unit, Player* owner) const + { + if (!IsValidHealTarget(unit, owner)) + return -1; + + float hp = HealthPct(unit); + bool declaredTank = IsDeclaredTank(unit, owner); + bool activeTank = IsActiveTank(unit); + uint32 attackers = CountRelevantAttackers(unit); + + if (!declaredTank && !activeTank && hp > 75.0f) + return -1; + if ((declaredTank || activeTank) && hp > 95.0f) + return -1; + + int32 score = int32(100.0f - hp); + if (declaredTank) + score += 45; + if (activeTank) + score += 65; + score += int32(std::min(attackers, 5u)) * 12; + + if (hp <= 35.0f) + score += 120; + else if (hp <= 50.0f) + score += 55; + + return score; + } + + Unit* SelectHealTarget(Player* owner) const + { + std::list units; + owner->GetPartyMembers(units); + + Unit* best = nullptr; + int32 bestScore = -1; + for (Unit* unit : units) + { + int32 score = HealPriority(unit, owner); + if (score > bestScore) + { + best = unit; + bestScore = score; + } + } + return best; + } + + uint32 SelectHealSpell(Unit* target, Player* owner) const + { + float hp = HealthPct(target); + bool tank = IsDeclaredTank(target, owner) || IsActiveTank(target); + + if (hp <= 35.0f || (tank && hp <= 45.0f)) + return SPELL_HEAL_EMERGENCY; + if (hp <= 50.0f || (tank && hp <= 68.0f)) + return SPELL_HEAL_STRONG; + return SPELL_HEAL_EFFICIENT; + } + + bool ManaPolicyAllows(Unit* target, Player* owner, uint32 spellId) const + { + uint32 maxMana = me->GetMaxPower(POWER_MANA); + if (!maxMana) + return true; + + float manaPct = 100.0f * float(me->GetPower(POWER_MANA)) / float(maxMana); + float hp = HealthPct(target); + bool tank = IsDeclaredTank(target, owner) || IsActiveTank(target); + + if (manaPct < 10.0f) + return spellId == SPELL_HEAL_EMERGENCY && hp <= 30.0f; + + if (manaPct < 25.0f) + { + if (spellId == SPELL_HEAL_EMERGENCY) + return true; + if (tank) + return hp <= 70.0f; + return hp <= 45.0f; + } + + return true; + } + + void ConfigureMana(bool refill) + { + // VIANO_HEALER_BALANCE_V1_2 + // Player-like mana pool: level 31 -> 1802 mana. + uint32 desired = 500u + uint32(me->getLevel()) * 42u; + if (desired < 900u) + desired = 900u; + + uint32 oldMax = me->GetMaxPower(POWER_MANA); + uint32 oldMana = me->GetPower(POWER_MANA); + me->SetMaxPower(POWER_MANA, desired); + + if (refill || !oldMax) + me->SetPower(POWER_MANA, desired); + else + me->SetPower(POWER_MANA, uint32(std::min( + uint64(oldMana) * uint64(desired) / uint64(oldMax), uint64(desired)))); + + // Disable generic creature regen; Viano owns healer mana regen. + me->RemoveFlag(UNIT_FIELD_FLAGS_2, UNIT_FLAG2_REGENERATE_POWER); + } + + void FollowOwner(Player* owner) + { + if (!owner) + return; + + uint32 slot = me->GetGUID().GetCounter() % 6u; + static float const distances[6] = { 3.0f, 3.2f, 3.2f, 4.0f, 4.0f, 4.5f }; + static float const angles[6] = { 3.14159265f, 2.55f, 3.73f, 2.80f, 3.48f, 3.14159265f }; + me->GetMotionMaster()->MoveFollow(owner, distances[slot], angles[slot]); + } + + void HandleOutOfCombatRecovery(uint32 diff, Player* owner) + { + if (!me->IsAlive() || !owner) + { + recoveryTimer = 1000; + return; + } + + if (recoveryTimer > diff) + { + recoveryTimer -= diff; + return; + } + + recoveryTimer = 1000; + bool const groupCombat = IsGroupCombatActive(owner); + + if (!groupCombat) + { + uint32 maxHealth = me->GetMaxHealth(); + if (me->GetHealth() < maxHealth) + { + uint32 gain = std::max(1u, maxHealth / 10u); + me->SetHealth(std::min(maxHealth, me->GetHealth() + gain)); + } + } + + uint32 maxMana = me->GetMaxPower(POWER_MANA); + if (maxMana && me->GetPower(POWER_MANA) < maxMana) + { + uint32 divisor = groupCombat ? 100u : 30u; // 1% combat, ~3.3% OOC + uint32 gain = std::max(1u, maxMana / divisor); + me->SetPower(POWER_MANA, + std::min(maxMana, me->GetPower(POWER_MANA) + gain)); + } + } + + void InitializeAI() override + { + ScriptedAI::InitializeAI(); + + TempSummon* summon = me->ToTempSummon(); + Player* owner = summon && summon->GetSummoner() ? summon->GetSummoner()->ToPlayer() : nullptr; + if (!owner) + { + me->DespawnOrUnsummon(); + return; + } + + ownerGuid = owner->GetGUID(); + me->SetOwnerGUID(ownerGuid); + me->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_PLAYER_CONTROLLED); + me->SetFaction(owner->GetFaction()); + me->SetLevel(owner->getLevel()); + me->UpdateLevelDependantStats(); + ConfigureMana(true); + + static constexpr uint32 MODELS[] = { 30063, 30064, 30065, 30066 }; + me->SetDisplayId(MODELS[urand(0, 3)], true); + SetEquipmentSlots(false, 39426, EQUIP_NO_CHANGE, EQUIP_NO_CHANGE); + me->SetReactState(REACT_PASSIVE); + FollowOwner(owner); + + TC_LOG_INFO("scripts", + "[VianoHealer] initialized entry=%u owner='%s' level=%u hp=%u/%u mana=%u/%u", + me->GetEntry(), owner->GetName().c_str(), uint32(me->getLevel()), + me->GetHealth(), me->GetMaxHealth(), + me->GetPower(POWER_MANA), me->GetMaxPower(POWER_MANA)); + } + + void Reset() override + { + if (Player* owner = GetOwnerPlayer()) + FollowOwner(owner); + } + + void MoveInLineOfSight(Unit* /*who*/) override + { + } + + + // VIANO_HEALER_DISPEL_V1 + bool TryDispelGroupMember(Player* owner) + { + if (!owner || me->HasUnitState(UNIT_STATE_CASTING)) + return false; + + std::list members; + owner->GetPartyMembers(members); + std::vector candidates; + candidates.push_back(owner); + + for (Unit* member : members) + { + if (!member) + continue; + bool duplicate = false; + for (Unit* existing : candidates) + if (existing && existing->GetGUID() == member->GetGUID()) + duplicate = true; + if (!duplicate) + candidates.push_back(member); + } + + // Healing wins if anyone is substantially hurt. + for (Unit* unit : candidates) + if (unit && unit->IsAlive() && unit->IsInMap(me) && unit->GetHealthPct() < 70.0f) + return false; + + uint32 const curseMask = (1u << DISPEL_CURSE); + uint32 const cleanseMask = (1u << DISPEL_MAGIC) | + (1u << DISPEL_DISEASE) | (1u << DISPEL_POISON); + + for (Unit* unit : candidates) + { + if (!unit || !unit->IsAlive() || !unit->IsInMap(me)) + continue; + if (!me->IsWithinDistInMap(unit, 40.0f) || !me->IsWithinLOSInMap(unit)) + continue; + + DispelChargesList curseAuras; + unit->GetDispellableAuraList(me, curseMask, curseAuras); + if (!curseAuras.empty()) + { + DoCast(unit, 2782); // Remove Corruption + return true; + } + + DispelChargesList cleanseAuras; + unit->GetDispellableAuraList(me, cleanseMask, cleanseAuras); + if (!cleanseAuras.empty()) + { + DoCast(unit, 4987); // Cleanse + return true; + } + } + return false; + } + +void UpdateAI(uint32 diff) override + { + if (updateTimer > diff) + { + updateTimer -= diff; + decisionLockTimer = decisionLockTimer > diff ? decisionLockTimer - diff : 0; + HandleOutOfCombatRecovery(diff, GetOwnerPlayer()); + return; + } + + updateTimer = 250; + decisionLockTimer = decisionLockTimer > diff ? decisionLockTimer - diff : 0; + + Player* owner = GetOwnerPlayer(); + + if (owner && TryDispelGroupMember(owner)) + return; + if (!owner || !owner->IsInWorld() || !me->IsInMap(owner)) + { + me->DespawnOrUnsummon(); + return; + } + + HandleOutOfCombatRecovery(diff, owner); + + if (me->getLevel() != owner->getLevel()) + { + me->SetLevel(owner->getLevel()); + me->UpdateLevelDependantStats(); + ConfigureMana(false); + } + + if (me->GetDistance(owner) > 45.0f && !me->HasUnitState(UNIT_STATE_CASTING)) + { + me->GetMotionMaster()->Clear(); + me->NearTeleportTo(owner->GetPositionX() + 2.0f, owner->GetPositionY() + 1.0f, + owner->GetPositionZ(), owner->GetOrientation()); + FollowOwner(owner); + return; + } + + if (me->HasUnitState(UNIT_STATE_CASTING) || decisionLockTimer) + return; + + Unit* target = SelectHealTarget(owner); + if (!target) + { + FollowOwner(owner); + return; + } + + uint32 spellId = SelectHealSpell(target, owner); + if (!ManaPolicyAllows(target, owner, spellId)) + return; + + me->CastSpell(target, spellId); + + TC_LOG_INFO("scripts", + "[VianoHealer] cast spell=%u target='%s' hp=%.1f declaredTank=%u activeTank=%u attackers=%u mana=%u/%u", + spellId, target->GetName().c_str(), HealthPct(target), + IsDeclaredTank(target, owner) ? 1u : 0u, + IsActiveTank(target) ? 1u : 0u, + CountRelevantAttackers(target), + me->GetPower(POWER_MANA), me->GetMaxPower(POWER_MANA)); + + decisionLockTimer = 500; + } + }; + + CreatureAI* GetAI(Creature* creature) const override + { + return new npc_horde_adventurer_healerAI(creature); + } + }; + + // VIANO_ALLIANCE_ADVENTURERS_V1 + // + // Alliance adventurers intentionally reuse the same combat AI as their + // Horde counterparts. Faction, models and default equipment are supplied + // by their own creature_template rows. + + class npc_alliance_adventurer_melee : public CreatureScript + { + public: + npc_alliance_adventurer_melee() + : CreatureScript("npc_alliance_adventurer_melee") + { + } + + CreatureAI* GetAI(Creature* creature) const override + { + return new npc_horde_adventurer_melee:: + npc_horde_adventurer_meleeAI(creature); + } + }; + + class npc_alliance_adventurer_tank : public CreatureScript + { + public: + npc_alliance_adventurer_tank() + : CreatureScript("npc_alliance_adventurer_tank") + { + } + + CreatureAI* GetAI(Creature* creature) const override + { + return new npc_horde_adventurer_tank:: + npc_horde_adventurer_tankAI(creature); + } + }; + + class npc_alliance_adventurer_healer : public CreatureScript + { + public: + npc_alliance_adventurer_healer() + : CreatureScript("npc_alliance_adventurer_healer") + { + } + + CreatureAI* GetAI(Creature* creature) const override + { + return new npc_horde_adventurer_healer:: + npc_horde_adventurer_healerAI(creature); + } + }; +} + +void AddSC_viano_adventurers() +{ + using namespace VianoAdventurers; + new npc_horde_adventurer_melee(); + new npc_horde_adventurer_tank(); + new npc_horde_adventurer_healer(); + new npc_alliance_adventurer_melee(); + new npc_alliance_adventurer_tank(); + new npc_alliance_adventurer_healer(); +} diff --git a/src/server/scripts/World/item_scripts.cpp b/src/server/scripts/World/item_scripts.cpp index d29192665f..74186f40ba 100644 --- a/src/server/scripts/World/item_scripts.cpp +++ b/src/server/scripts/World/item_scripts.cpp @@ -436,9 +436,9 @@ enum VcoreDungeonHornNpcs NPC_HORDE_ADVENTURER_HEALER = 900003, // TODO: replace once Alliance creature templates exist. - NPC_ALLIANCE_ADVENTURER_DAMAGE = 0, - NPC_ALLIANCE_ADVENTURER_TANK = 0, - NPC_ALLIANCE_ADVENTURER_HEALER = 0 + NPC_ALLIANCE_ADVENTURER_DAMAGE = 900004, + NPC_ALLIANCE_ADVENTURER_TANK = 900005, + NPC_ALLIANCE_ADVENTURER_HEALER = 900006 }; static constexpr uint32 VCORE_DUNGEON_GROUP_LIMIT = 5; diff --git a/src/server/scripts/World/item_scripts.cpp.bak-vcore-alliance-20261007-220047 b/src/server/scripts/World/item_scripts.cpp.bak-vcore-alliance-20261007-220047 new file mode 100644 index 0000000000..d29192665f --- /dev/null +++ b/src/server/scripts/World/item_scripts.cpp.bak-vcore-alliance-20261007-220047 @@ -0,0 +1,1008 @@ +/* + * 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 . + */ + +/* ScriptData +SDName: Item_Scripts +SD%Complete: 100 +SDComment: Items for a range of different items. See content below (in script) +SDCategory: Items +EndScriptData */ + +/* ContentData +item_nether_wraith_beacon(i31742) Summons creatures for quest Becoming a Spellfire Tailor (q10832) +item_flying_machine(i34060, i34061) Engineering crafted flying machines +item_gor_dreks_ointment(i30175) Protecting Our Own(q10488) +item_only_for_flight Items which should only useable while flying +EndContentData */ + +#include "ScriptMgr.h" +#include "GameObject.h" +#include "Group.h" +#include "Item.h" +#include "Player.h" +#include "ScriptedCreature.h" +#include "Spell.h" +#include "SpellMgr.h" +#include "TemporarySummon.h" +#include "Log.h" +#include +#include +#include + +namespace ItemScripts +{ + +/*##### +# item_only_for_flight +#####*/ + +enum OnlyForFlight +{ + SPELL_ARCANE_CHARGES = 45072 +}; + +class item_only_for_flight : public ItemScript +{ +public: + item_only_for_flight() : ItemScript("item_only_for_flight") { } + + bool OnUse(Player* player, Item* item, SpellCastTargets const& /*targets*/) override + { + uint32 itemId = item->GetEntry(); + bool disabled = false; + + //for special scripts + switch (itemId) + { + case 24538: + if (player->GetAreaId() != 3628) + disabled = true; + break; + case 34489: + if (player->GetZoneId() != 4080) + disabled = true; + break; + case 34475: + if (SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(SPELL_ARCANE_CHARGES)) + Spell::SendCastResult(player, spellInfo, 1, SPELL_FAILED_NOT_ON_GROUND); + break; + } + + // allow use in flight only + if (player->IsInFlight() && !disabled) + return false; + + // error + player->SendEquipError(EQUIP_ERR_CLIENT_LOCKED_OUT, item, nullptr); + return true; + } +}; + +/*##### +# item_nether_wraith_beacon +#####*/ + +class item_nether_wraith_beacon : public ItemScript +{ +public: + item_nether_wraith_beacon() : ItemScript("item_nether_wraith_beacon") { } + + bool OnUse(Player* player, Item* /*item*/, SpellCastTargets const& /*targets*/) override + { + if (player->GetQuestStatus(10832) == QUEST_STATUS_INCOMPLETE) + { + if (Creature* nether = player->SummonCreature(22408, player->GetPositionX(), player->GetPositionY()+20, player->GetPositionZ(), 0, TEMPSUMMON_TIMED_DESPAWN, 180000)) + nether->AI()->AttackStart(player); + + if (Creature* nether = player->SummonCreature(22408, player->GetPositionX(), player->GetPositionY()-20, player->GetPositionZ(), 0, TEMPSUMMON_TIMED_DESPAWN, 180000)) + nether->AI()->AttackStart(player); + } + return false; + } +}; + +/*##### +# item_gor_dreks_ointment +#####*/ + +class item_gor_dreks_ointment : public ItemScript +{ +public: + item_gor_dreks_ointment() : ItemScript("item_gor_dreks_ointment") { } + + bool OnUse(Player* player, Item* item, SpellCastTargets const& targets) override + { + if (targets.GetUnitTarget() && targets.GetUnitTarget()->GetTypeId() == TYPEID_UNIT && + targets.GetUnitTarget()->GetEntry() == 20748 && !targets.GetUnitTarget()->HasAura(32578)) + return false; + + player->SendEquipError(EQUIP_ERR_CLIENT_LOCKED_OUT, item, nullptr); + return true; + } +}; + +/*##### +# item_incendiary_explosives +#####*/ + +class item_incendiary_explosives : public ItemScript +{ +public: + item_incendiary_explosives() : ItemScript("item_incendiary_explosives") { } + + bool OnUse(Player* player, Item* item, SpellCastTargets const & /*targets*/) override + { + if (player->FindNearestCreature(26248, 15) || player->FindNearestCreature(26249, 15)) + return false; + else + { + player->SendEquipError(EQUIP_ERR_OUT_OF_RANGE, item, nullptr); + return true; + } + } +}; + +/*##### +# item_mysterious_egg +#####*/ + +class item_mysterious_egg : public ItemScript +{ +public: + item_mysterious_egg() : ItemScript("item_mysterious_egg") { } + + bool OnExpire(Player* player, ItemTemplate const* /*pItemProto*/) override + { + ItemPosCountVec dest; + uint8 msg = player->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, 39883, 1); // Cracked Egg + if (msg == EQUIP_ERR_OK) + player->StoreNewItem(dest, 39883, true, GenerateItemRandomPropertyId(39883)); + + return true; + } +}; + +/*##### +# item_disgusting_jar +#####*/ + +class item_disgusting_jar : public ItemScript +{ +public: + item_disgusting_jar() : ItemScript("item_disgusting_jar") { } + + bool OnExpire(Player* player, ItemTemplate const* /*pItemProto*/) override + { + ItemPosCountVec dest; + uint8 msg = player->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, 44718, 1); // Ripe Disgusting Jar + if (msg == EQUIP_ERR_OK) + player->StoreNewItem(dest, 44718, true, GenerateItemRandomPropertyId(44718)); + + return true; + } +}; + +/*##### +# item_pile_fake_furs +#####*/ + +enum PileFakeFur +{ + GO_CARIBOU_TRAP_1 = 187982, + GO_CARIBOU_TRAP_2 = 187995, + GO_CARIBOU_TRAP_3 = 187996, + GO_CARIBOU_TRAP_4 = 187997, + GO_CARIBOU_TRAP_5 = 187998, + GO_CARIBOU_TRAP_6 = 187999, + GO_CARIBOU_TRAP_7 = 188000, + GO_CARIBOU_TRAP_8 = 188001, + GO_CARIBOU_TRAP_9 = 188002, + GO_CARIBOU_TRAP_10 = 188003, + GO_CARIBOU_TRAP_11 = 188004, + GO_CARIBOU_TRAP_12 = 188005, + GO_CARIBOU_TRAP_13 = 188006, + GO_CARIBOU_TRAP_14 = 188007, + GO_CARIBOU_TRAP_15 = 188008, + GO_HIGH_QUALITY_FUR = 187983, + NPC_NESINGWARY_TRAPPER = 25835 +}; + +#define CaribouTrapsNum 15 +const uint32 CaribouTraps[CaribouTrapsNum] = +{ + GO_CARIBOU_TRAP_1, GO_CARIBOU_TRAP_2, GO_CARIBOU_TRAP_3, GO_CARIBOU_TRAP_4, GO_CARIBOU_TRAP_5, + GO_CARIBOU_TRAP_6, GO_CARIBOU_TRAP_7, GO_CARIBOU_TRAP_8, GO_CARIBOU_TRAP_9, GO_CARIBOU_TRAP_10, + GO_CARIBOU_TRAP_11, GO_CARIBOU_TRAP_12, GO_CARIBOU_TRAP_13, GO_CARIBOU_TRAP_14, GO_CARIBOU_TRAP_15, +}; + +class item_pile_fake_furs : public ItemScript +{ +public: + item_pile_fake_furs() : ItemScript("item_pile_fake_furs") { } + + bool OnUse(Player* player, Item* /*item*/, SpellCastTargets const & /*targets*/) override + { + GameObject* go = nullptr; + for (uint8 i = 0; i < CaribouTrapsNum; ++i) + { + go = player->FindNearestGameObject(CaribouTraps[i], 5.0f); + if (go) + break; + } + + if (!go) + return false; + + if (go->FindNearestCreature(NPC_NESINGWARY_TRAPPER, 10.0f, true) || go->FindNearestCreature(NPC_NESINGWARY_TRAPPER, 10.0f, false) || go->FindNearestGameObject(GO_HIGH_QUALITY_FUR, 2.0f)) + return true; + + float x, y, z; + go->GetClosePoint(x, y, z, go->GetCombatReach() / 3, 7.0f); + go->SummonGameObject(GO_HIGH_QUALITY_FUR, *go, QuaternionData(), 1); + if (TempSummon* summon = player->SummonCreature(NPC_NESINGWARY_TRAPPER, x, y, z, go->GetOrientation(), TEMPSUMMON_DEAD_DESPAWN, 1000)) + { + summon->SetVisible(false); + summon->SetReactState(REACT_PASSIVE); + summon->SetImmuneToPC(true); + } + return false; + } +}; + +/*##### +# item_petrov_cluster_bombs +#####*/ + +enum PetrovClusterBombs +{ + SPELL_PETROV_BOMB = 42406, + AREA_ID_SHATTERED_STRAITS = 4064, + ZONE_ID_HOWLING = 495 +}; + +class item_petrov_cluster_bombs : public ItemScript +{ +public: + item_petrov_cluster_bombs() : ItemScript("item_petrov_cluster_bombs") { } + + bool OnUse(Player* player, Item* /*item*/, const SpellCastTargets & /*targets*/) override + { + if (player->GetZoneId() != ZONE_ID_HOWLING) + return false; + + if (!player->GetTransport() || player->GetAreaId() != AREA_ID_SHATTERED_STRAITS) + { + if (SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(SPELL_PETROV_BOMB)) + Spell::SendCastResult(player, spellInfo, 1, SPELL_FAILED_NOT_HERE); + + return true; + } + + return false; + } +}; + +/*###### +# item_dehta_trap_smasher +# For quest 11876, Help Those That Cannot Help Themselves +######*/ +enum HelpThemselves +{ + QUEST_CANNOT_HELP_THEMSELVES = 11876, + NPC_TRAPPED_MAMMOTH_CALF = 25850, + GO_MAMMOTH_TRAP_1 = 188022, + GO_MAMMOTH_TRAP_2 = 188024, + GO_MAMMOTH_TRAP_3 = 188025, + GO_MAMMOTH_TRAP_4 = 188026, + GO_MAMMOTH_TRAP_5 = 188027, + GO_MAMMOTH_TRAP_6 = 188028, + GO_MAMMOTH_TRAP_7 = 188029, + GO_MAMMOTH_TRAP_8 = 188030, + GO_MAMMOTH_TRAP_9 = 188031, + GO_MAMMOTH_TRAP_10 = 188032, + GO_MAMMOTH_TRAP_11 = 188033, + GO_MAMMOTH_TRAP_12 = 188034, + GO_MAMMOTH_TRAP_13 = 188035, + GO_MAMMOTH_TRAP_14 = 188036, + GO_MAMMOTH_TRAP_15 = 188037, + GO_MAMMOTH_TRAP_16 = 188038, + GO_MAMMOTH_TRAP_17 = 188039, + GO_MAMMOTH_TRAP_18 = 188040, + GO_MAMMOTH_TRAP_19 = 188041, + GO_MAMMOTH_TRAP_20 = 188042, + GO_MAMMOTH_TRAP_21 = 188043, + GO_MAMMOTH_TRAP_22 = 188044, +}; + +#define MammothTrapsNum 22 +const uint32 MammothTraps[MammothTrapsNum] = +{ + GO_MAMMOTH_TRAP_1, GO_MAMMOTH_TRAP_2, GO_MAMMOTH_TRAP_3, GO_MAMMOTH_TRAP_4, GO_MAMMOTH_TRAP_5, + GO_MAMMOTH_TRAP_6, GO_MAMMOTH_TRAP_7, GO_MAMMOTH_TRAP_8, GO_MAMMOTH_TRAP_9, GO_MAMMOTH_TRAP_10, + GO_MAMMOTH_TRAP_11, GO_MAMMOTH_TRAP_12, GO_MAMMOTH_TRAP_13, GO_MAMMOTH_TRAP_14, GO_MAMMOTH_TRAP_15, + GO_MAMMOTH_TRAP_16, GO_MAMMOTH_TRAP_17, GO_MAMMOTH_TRAP_18, GO_MAMMOTH_TRAP_19, GO_MAMMOTH_TRAP_20, + GO_MAMMOTH_TRAP_21, GO_MAMMOTH_TRAP_22 +}; + +class item_dehta_trap_smasher : public ItemScript +{ +public: + item_dehta_trap_smasher() : ItemScript("item_dehta_trap_smasher") { } + + bool OnUse(Player* player, Item* /*item*/, const SpellCastTargets & /*targets*/) override + { + if (player->GetQuestStatus(QUEST_CANNOT_HELP_THEMSELVES) != QUEST_STATUS_INCOMPLETE) + return false; + + Creature* pMammoth = player->FindNearestCreature(NPC_TRAPPED_MAMMOTH_CALF, 5.0f); + if (!pMammoth) + return false; + + GameObject* pTrap = nullptr; + for (uint8 i = 0; i < MammothTrapsNum; ++i) + { + pTrap = player->FindNearestGameObject(MammothTraps[i], 11.0f); + if (pTrap) + { + pMammoth->AI()->DoAction(1); + pTrap->SetGoState(GO_STATE_READY); + player->KilledMonsterCredit(NPC_TRAPPED_MAMMOTH_CALF); + return true; + } + } + return false; + } +}; + +enum CapturedFrog +{ + QUEST_THE_PERFECT_SPIES = 25444, + NPC_VANIRAS_SENTRY_TOTEM = 40187 +}; + +class item_captured_frog : public ItemScript +{ +public: + item_captured_frog() : ItemScript("item_captured_frog") { } + + bool OnUse(Player* player, Item* item, SpellCastTargets const& /*targets*/) override + { + if (player->GetQuestStatus(QUEST_THE_PERFECT_SPIES) == QUEST_STATUS_INCOMPLETE) + { + if (player->FindNearestCreature(NPC_VANIRAS_SENTRY_TOTEM, 10.0f)) + return false; + else + player->SendEquipError(EQUIP_ERR_OUT_OF_RANGE, item, nullptr); + } + else + player->SendEquipError(EQUIP_ERR_CLIENT_LOCKED_OUT, item, nullptr); + return true; + } +}; + +// Only used currently for +// 19169: Nightfall +class item_generic_limit_chance_above_60 : public ItemScript +{ + public: + item_generic_limit_chance_above_60() : ItemScript("item_generic_limit_chance_above_60") { } + + bool OnCastItemCombatSpell(Player* /*player*/, Unit* victim, SpellInfo const* /*spellInfo*/, Item* /*item*/) override + { + // spell proc chance gets severely reduced on victims > 60 (formula unknown) + if (victim->getLevel() > 60) + { + // gives ~0.1% proc chance at lvl 70 + float const lvlPenaltyFactor = 9.93f; + float const failureChance = (victim->getLevel() - 60) * lvlPenaltyFactor; + + // base ppm chance was already rolled, only roll success chance + return !roll_chance_f(failureChance); + } + + return true; + } +}; +/*##### +# Vcore dungeon horns +#####*/ + +enum VcoreDungeonHornSpells +{ + SPELL_VCORE_HORN_TANK = 120000, + SPELL_VCORE_HORN_HEALER = 120001, + SPELL_VCORE_HORN_DAMAGE = 120002, + SPELL_VCORE_HORN_DAMAGE_3 = 120003 +}; + +enum VcoreDungeonHornNpcs +{ + NPC_HORDE_ADVENTURER_DAMAGE = 900001, + NPC_HORDE_ADVENTURER_TANK = 900002, + NPC_HORDE_ADVENTURER_HEALER = 900003, + + // TODO: replace once Alliance creature templates exist. + NPC_ALLIANCE_ADVENTURER_DAMAGE = 0, + NPC_ALLIANCE_ADVENTURER_TANK = 0, + NPC_ALLIANCE_ADVENTURER_HEALER = 0 +}; + +static constexpr uint32 VCORE_DUNGEON_GROUP_LIMIT = 5; + +struct VcoreHornPlan +{ + uint32 occupancy = 0; + uint32 freeSlots = 0; + uint32 ownBots = 0; + uint32 spawnCount = 0; + uint32 replaceCount = 0; +}; + +static uint32 GetVcoreHornSpellForItem(uint32 itemId) +{ + switch (itemId) + { + case 80000: + case 80004: + return SPELL_VCORE_HORN_TANK; + + case 80001: + case 80005: + return SPELL_VCORE_HORN_HEALER; + + case 80002: + case 80006: + return SPELL_VCORE_HORN_DAMAGE; + + case 80003: + case 80007: + return SPELL_VCORE_HORN_DAMAGE_3; + + default: + return 0; + } +} + +static uint32 GetVcoreHornRequestedCount(uint32 spellId) +{ + return spellId == SPELL_VCORE_HORN_DAMAGE_3 ? 3u : 1u; +} + +static bool IsVcoreHordeHorn(uint32 itemId) +{ + return itemId >= 80000 && itemId <= 80003; +} + +static bool IsVcoreAllianceHorn(uint32 itemId) +{ + return itemId >= 80004 && itemId <= 80007; +} + +static uint32 GetVcoreHornNpcEntry(Player* player, uint32 spellId) +{ + if (!player) + return 0; + + if (player->GetTeam() == HORDE) + { + switch (spellId) + { + case SPELL_VCORE_HORN_TANK: + return NPC_HORDE_ADVENTURER_TANK; + + case SPELL_VCORE_HORN_HEALER: + return NPC_HORDE_ADVENTURER_HEALER; + + case SPELL_VCORE_HORN_DAMAGE: + case SPELL_VCORE_HORN_DAMAGE_3: + return NPC_HORDE_ADVENTURER_DAMAGE; + + default: + return 0; + } + } + + if (player->GetTeam() == ALLIANCE) + { + switch (spellId) + { + case SPELL_VCORE_HORN_TANK: + return NPC_ALLIANCE_ADVENTURER_TANK; + + case SPELL_VCORE_HORN_HEALER: + return NPC_ALLIANCE_ADVENTURER_HEALER; + + case SPELL_VCORE_HORN_DAMAGE: + case SPELL_VCORE_HORN_DAMAGE_3: + return NPC_ALLIANCE_ADVENTURER_DAMAGE; + + default: + return 0; + } + } + + return 0; +} + +static void CollectVcoreDungeonBots( + Player* player, + std::vector& allBots, + std::vector& ownBots) +{ + allBots.clear(); + ownBots.clear(); + + if (!player) + return; + + std::list units; + player->GetPartyMembers(units); + + for (Unit* unit : units) + { + Creature* creature = unit ? unit->ToCreature() : nullptr; + + if (!creature) + continue; + + if (!creature->IsAlive()) + continue; + + if (!creature->IsVianoAdventurer()) + continue; + + if (!creature->IsInMap(player)) + continue; + + if (std::find(allBots.begin(), allBots.end(), creature) == allBots.end()) + allBots.push_back(creature); + + Unit* owner = creature->GetOwner(); + + if (owner && owner->GetGUID() == player->GetGUID()) + ownBots.push_back(creature); + } + + // Current Viano summons receive increasing Creature GUIDs. + // The Viano formation code already uses the same ordering. + // Therefore this gives oldest -> newest. + std::sort( + ownBots.begin(), + ownBots.end(), + [](Creature const* left, Creature const* right) + { + return left->GetGUID().GetRawValue() + < right->GetGUID().GetRawValue(); + }); +} + +static uint32 GetVcoreRealPlayerCount(Player* player) +{ + if (!player) + return 0; + + if (Group* group = player->GetGroup()) + return group->GetMembersCount(); + + return 1; +} + +static VcoreHornPlan BuildVcoreHornPlan( + Player* player, + uint32 requestedCount, + std::vector& ownBots) +{ + VcoreHornPlan plan; + + std::vector allBots; + + CollectVcoreDungeonBots( + player, + allBots, + ownBots); + + uint32 const realPlayers = + GetVcoreRealPlayerCount(player); + + uint32 const botCount = + uint32(allBots.size()); + + plan.occupancy = + realPlayers + botCount; + + plan.ownBots = + uint32(ownBots.size()); + + // Do not mutate an already invalid >5 state. + if (plan.occupancy > VCORE_DUNGEON_GROUP_LIMIT) + return plan; + + plan.freeSlots = + VCORE_DUNGEON_GROUP_LIMIT - plan.occupancy; + + plan.spawnCount = + std::min( + requestedCount, + plan.freeSlots + plan.ownBots); + + plan.replaceCount = + plan.spawnCount > plan.freeSlots + ? plan.spawnCount - plan.freeSlots + : 0; + + return plan; +} + +class item_vcore_dungeon_horn : public ItemScript +{ +public: + item_vcore_dungeon_horn() + : ItemScript("item_vcore_dungeon_horn") + { + } + + bool OnUse( + Player* player, + Item* item, + SpellCastTargets const& /*targets*/) override + { + if (!player || !item) + return true; + + uint32 const itemId = + item->GetEntry(); + + uint32 const spellId = + GetVcoreHornSpellForItem(itemId); + + TC_LOG_INFO( + "scripts", + "[VianoHorn] OnUse player='%s' item=%u spell=%u", + player->GetName().c_str(), + itemId, + spellId); + + if (!spellId) + { + TC_LOG_ERROR( + "scripts", + "[VianoHorn] unknown horn item=%u", + itemId); + + return true; + } + + if ((IsVcoreHordeHorn(itemId) + && player->GetTeam() != HORDE) + || + (IsVcoreAllianceHorn(itemId) + && player->GetTeam() != ALLIANCE)) + { + TC_LOG_INFO( + "scripts", + "[VianoHorn] rejected: wrong faction player='%s' item=%u", + player->GetName().c_str(), + itemId); + + player->SendEquipError( + EQUIP_ERR_CLIENT_LOCKED_OUT, + item, + nullptr); + + return true; + } + + if (player->IsInCombat()) + { + TC_LOG_INFO( + "scripts", + "[VianoHorn] rejected: player is in combat"); + + player->SendEquipError( + EQUIP_ERR_CLIENT_LOCKED_OUT, + item, + nullptr); + + return true; + } + + if (!player->GetMap()->IsDungeon()) + { + TC_LOG_INFO( + "scripts", + "[VianoHorn] rejected: player is not inside a dungeon"); + + player->SendEquipError( + EQUIP_ERR_CLIENT_LOCKED_OUT, + item, + nullptr); + + return true; + } + + // Alliance NPC templates are not configured yet. + // Reject before the normal cast so no cooldown is consumed. + uint32 const npcEntry = + GetVcoreHornNpcEntry( + player, + spellId); + + if (!npcEntry) + { + TC_LOG_INFO( + "scripts", + "[VianoHorn] rejected: no creature entry configured " + "for team=%u spell=%u", + uint32(player->GetTeam()), + spellId); + + player->SendEquipError( + EQUIP_ERR_CLIENT_LOCKED_OUT, + item, + nullptr); + + return true; + } + + uint32 const requestedCount = + GetVcoreHornRequestedCount(spellId); + + std::vector ownBots; + + VcoreHornPlan const plan = + BuildVcoreHornPlan( + player, + requestedCount, + ownBots); + + TC_LOG_INFO( + "scripts", + "[VianoHorn] precheck occupancy=%u free=%u own=%u " + "requested=%u spawn=%u replace=%u", + plan.occupancy, + plan.freeSlots, + plan.ownBots, + requestedCount, + plan.spawnCount, + plan.replaceCount); + + // Full group and nothing belonging to this player + // can be replaced. Do not start the spell/cooldown. + if (!plan.spawnCount) + { + player->SendEquipError( + EQUIP_ERR_CLIENT_LOCKED_OUT, + item, + nullptr); + + return true; + } + + // Preserve the normal item -> spell lifecycle. + return false; + } +}; + +class spell_vcore_dungeon_horn : public SpellScriptLoader +{ +public: + spell_vcore_dungeon_horn() + : SpellScriptLoader("spell_vcore_dungeon_horn") + { + } + + class spell_vcore_dungeon_horn_SpellScript + : public SpellScript + { + void HandleAfterCast() + { + Player* player = + GetCaster() + ? GetCaster()->ToPlayer() + : nullptr; + + if (!player) + return; + + uint32 const spellId = + GetSpellInfo()->Id; + + if (spellId < SPELL_VCORE_HORN_TANK + || spellId > SPELL_VCORE_HORN_DAMAGE_3) + { + return; + } + + // Re-check immediately before changing world state. + if (player->IsInCombat()) + { + TC_LOG_INFO( + "scripts", + "[VianoHorn] AfterCast aborted: player entered combat"); + + return; + } + + if (!player->GetMap()->IsDungeon()) + { + TC_LOG_INFO( + "scripts", + "[VianoHorn] AfterCast aborted: player left dungeon"); + + return; + } + + uint32 const npcEntry = + GetVcoreHornNpcEntry( + player, + spellId); + + if (!npcEntry) + { + TC_LOG_ERROR( + "scripts", + "[VianoHorn] AfterCast aborted: no NPC entry " + "team=%u spell=%u", + uint32(player->GetTeam()), + spellId); + + return; + } + + uint32 const requestedCount = + GetVcoreHornRequestedCount(spellId); + + std::vector ownBots; + + VcoreHornPlan const plan = + BuildVcoreHornPlan( + player, + requestedCount, + ownBots); + + TC_LOG_INFO( + "scripts", + "[VianoHorn] AfterCast player='%s' spell=%u npc=%u " + "occupancy=%u free=%u own=%u requested=%u " + "spawn=%u replace=%u", + player->GetName().c_str(), + spellId, + npcEntry, + plan.occupancy, + plan.freeSlots, + plan.ownBots, + requestedCount, + plan.spawnCount, + plan.replaceCount); + + if (!plan.spawnCount) + return; + + // Replace only this player's oldest active bots. + for (uint32 i = 0; + i < plan.replaceCount + && i < ownBots.size(); + ++i) + { + Creature* oldBot = + ownBots[i]; + + if (!oldBot) + continue; + + TC_LOG_INFO( + "scripts", + "[VianoHorn] replacing own bot " + "entry=%u guid=%s", + oldBot->GetEntry(), + oldBot->GetGUID().ToString().c_str()); + + oldBot->DespawnOrUnsummon(); + } + + uint32 spawned = 0; + + for (uint32 i = 0; + i < plan.spawnCount; + ++i) + { + // Prevent 3-DD summons from stacking exactly + // on the same coordinates. + float const yOffset = + (float(i) + - (float(plan.spawnCount) - 1.0f) / 2.0f) + * 1.5f; + + Creature* summon = + player->SummonCreature( + npcEntry, + player->GetPositionX() + 2.0f, + player->GetPositionY() + yOffset, + player->GetPositionZ(), + player->GetOrientation(), + TEMPSUMMON_MANUAL_DESPAWN, + 0); + + if (!summon) + { + TC_LOG_ERROR( + "scripts", + "[VianoHorn] summon failed " + "player='%s' spell=%u npc=%u index=%u", + player->GetName().c_str(), + spellId, + npcEntry, + i); + + continue; + } + + ++spawned; + + TC_LOG_INFO( + "scripts", + "[VianoHorn] summoned " + "entry=%u guid=%s owner='%s'", + summon->GetEntry(), + summon->GetGUID().ToString().c_str(), + player->GetName().c_str()); + } + + TC_LOG_INFO( + "scripts", + "[VianoHorn] completed " + "player='%s' spell=%u spawned=%u " + "requested=%u replaced=%u", + player->GetName().c_str(), + spellId, + spawned, + requestedCount, + plan.replaceCount); + } + + void Register() override + { + AfterCast.Register( + &spell_vcore_dungeon_horn_SpellScript::HandleAfterCast); + } + }; + + SpellScript* GetSpellScript() const override + { + return new spell_vcore_dungeon_horn_SpellScript(); + } +}; +}// namespace ItemScripts + + +void AddSC_item_scripts() +{ + using namespace ItemScripts; + new item_only_for_flight(); + new item_nether_wraith_beacon(); + new item_gor_dreks_ointment(); + new item_incendiary_explosives(); + new item_mysterious_egg(); + new item_disgusting_jar(); + new item_pile_fake_furs(); + new item_petrov_cluster_bombs(); + new item_dehta_trap_smasher(); + new item_captured_frog(); + new item_generic_limit_chance_above_60(); + new item_vcore_dungeon_horn(); + new spell_vcore_dungeon_horn(); +}