/* * This file is part of the AzerothCore 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 Affero General Public License as published by the * Free Software Foundation; either version 3 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 Affero General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ #include "Trainer.h" #include "Creature.h" #include "NPCPackets.h" #include "Player.h" #include "SpellInfo.h" #include "SpellMgr.h" namespace Trainer { bool Spell::IsCastable() const { return sSpellMgr->AssertSpellInfo(SpellId)->HasEffect(SPELL_EFFECT_LEARN_SPELL); } Trainer::Trainer(uint32 trainerId, Type type, uint32 requirement, std::string greeting, std::vector spells) : _trainerId(trainerId), _type(type), _requirement(requirement), _spells(std::move(spells)) { _greeting[DEFAULT_LOCALE] = std::move(greeting); } void Trainer::SendSpells(Creature* npc, Player* player, LocaleConstant locale) const { float reputationDiscount = player->GetReputationPriceDiscount(npc); WorldPackets::NPC::TrainerList trainerList; trainerList.TrainerGUID = npc->GetGUID(); trainerList.TrainerType = AsUnderlyingType(_type); trainerList.Greeting = GetGreeting(locale); trainerList.Spells.reserve(_spells.size()); for (Spell const& trainerSpell : _spells) { if (!player->IsSpellFitByClassAndRace(trainerSpell.SpellId)) continue; SpellInfo const* trainerSpellInfo = sSpellMgr->AssertSpellInfo(trainerSpell.SpellId); bool primaryProfessionFirstRank = false; for (SpellEffectInfo const& spellEffectInfo : trainerSpellInfo->GetEffects()) { if (!spellEffectInfo.IsEffect(SPELL_EFFECT_LEARN_SPELL)) continue; SpellInfo const* learnedSpellInfo = sSpellMgr->GetSpellInfo(spellEffectInfo.TriggerSpell); if (learnedSpellInfo && learnedSpellInfo->IsPrimaryProfessionFirstRank()) primaryProfessionFirstRank = true; } trainerList.Spells.emplace_back(); WorldPackets::NPC::TrainerListSpell& trainerListSpell = trainerList.Spells.back(); trainerListSpell.SpellID = trainerSpell.SpellId; trainerListSpell.Usable = AsUnderlyingType(GetSpellState(player, &trainerSpell)); trainerListSpell.MoneyCost = int32(trainerSpell.MoneyCost * reputationDiscount); trainerListSpell.PointCost[0] = 0; // spells don't cost talent points trainerListSpell.PointCost[1] = (primaryProfessionFirstRank ? 1 : 0); trainerListSpell.ReqLevel = trainerSpell.ReqLevel; trainerListSpell.ReqSkillLine = trainerSpell.ReqSkillLine; trainerListSpell.ReqSkillRank = trainerSpell.ReqSkillRank; std::copy(trainerSpell.ReqAbility.begin(), trainerSpell.ReqAbility.end(), trainerListSpell.ReqAbility.begin()); } player->SendDirectMessage(trainerList.Write()); } void Trainer::TeachSpell(Creature* npc, Player* player, uint32 spellId) { if (!IsTrainerValidForPlayer(player)) return; Spell const* trainerSpell = GetSpell(spellId); if (!trainerSpell) { SendTeachFailure(npc, player, spellId, FailReason::Unavailable); return; } if (!CanTeachSpell(player, trainerSpell)) { SendTeachFailure(npc, player, spellId, FailReason::NotEnoughSkill); return; } float reputationDiscount = player->GetReputationPriceDiscount(npc); int32 moneyCost = int32(trainerSpell->MoneyCost * reputationDiscount); if (!player->HasEnoughMoney(moneyCost)) { SendTeachFailure(npc, player, spellId, FailReason::NotEnoughMoney); return; } player->ModifyMoney(-moneyCost); npc->SendPlaySpellVisual(179); // 53 SpellCastDirected npc->SendPlaySpellImpact(player->GetGUID(), 362); // 113 EmoteSalute // learn explicitly or cast explicitly if (trainerSpell->IsCastable()) player->CastSpell(player, trainerSpell->SpellId, true); else player->learnSpell(trainerSpell->SpellId, false); SendTeachSucceeded(npc, player, spellId); } Spell const* Trainer::GetSpell(uint32 spellId) const { auto itr = std::find_if(_spells.begin(), _spells.end(), [spellId](Spell const& trainerSpell) { return trainerSpell.SpellId == spellId; }); if (itr != _spells.end()) return &(*itr); return nullptr; } bool Trainer::CanTeachSpell(Player const* player, Spell const* trainerSpell) const { SpellState state = GetSpellState(player, trainerSpell); if (state != SpellState::Available) return false; SpellInfo const* trainerSpellInfo = sSpellMgr->AssertSpellInfo(trainerSpell->SpellId); for (SpellEffectInfo const& spellEffectInfo : trainerSpellInfo->GetEffects()) { if (!spellEffectInfo.IsEffect(SPELL_EFFECT_LEARN_SPELL)) continue; SpellInfo const* learnedSpellInfo = sSpellMgr->GetSpellInfo(spellEffectInfo.TriggerSpell); if (learnedSpellInfo && learnedSpellInfo->IsPrimaryProfessionFirstRank() && !player->GetFreePrimaryProfessionPoints()) return false; } return true; } SpellState Trainer::GetSpellState(Player const* player, Spell const* trainerSpell) const { if (player->HasSpell(trainerSpell->SpellId)) return SpellState::Known; // check race/class requirement if (!player->IsSpellFitByClassAndRace(trainerSpell->SpellId)) return SpellState::Unavailable; // check skill requirement if (trainerSpell->ReqSkillLine && player->GetBaseSkillValue(trainerSpell->ReqSkillLine) < trainerSpell->ReqSkillRank) return SpellState::Unavailable; for (int32 reqAbility : trainerSpell->ReqAbility) if (reqAbility && !player->HasSpell(reqAbility)) return SpellState::Unavailable; // check level requirement if (player->GetLevel() < trainerSpell->ReqLevel) return SpellState::Unavailable; // check ranks bool hasLearnSpellEffect = false; bool knowsAllLearnedSpells = true; for (SpellEffectInfo const& spellEffectInfo : sSpellMgr->AssertSpellInfo(trainerSpell->SpellId)->GetEffects()) { if (!spellEffectInfo.IsEffect(SPELL_EFFECT_LEARN_SPELL)) continue; hasLearnSpellEffect = true; if (!player->HasSpell(spellEffectInfo.TriggerSpell)) knowsAllLearnedSpells = false; if (uint32 previousRankSpellId = sSpellMgr->GetPrevSpellInChain(spellEffectInfo.TriggerSpell)) if (!player->HasSpell(previousRankSpellId)) return SpellState::Unavailable; } if (!hasLearnSpellEffect) { if (uint32 previousRankSpellId = sSpellMgr->GetPrevSpellInChain(trainerSpell->SpellId)) if (!player->HasSpell(previousRankSpellId)) return SpellState::Unavailable; } else if (knowsAllLearnedSpells) return SpellState::Known; // check additional spell requirement for (auto const& requirePair : sSpellMgr->GetSpellsRequiredForSpellBounds(trainerSpell->SpellId)) if (!player->HasSpell(requirePair.second)) return SpellState::Unavailable; return SpellState::Available; } bool Trainer::IsTrainerValidForPlayer(Player const* player) const { // Paragon (class 12) learns class abilities exclusively through the // Character Advancement panel (mod-paragon). Generic class trainers // refuse interaction. Pet trainers, mount/profession trainers, and // specialized portal/teleport trainers (mage portal NPCs) stay valid: // - Pet trainers: pet-skill purchases for hunter pets aren't covered // by the panel and should remain trainer-driven. // - Portal/teleport trainers: identified at runtime as a Class-type // trainer whose spells are ALL TELEPORT_UNITS or TRANS_DOOR // effects. The big general mage trainer fails this check (it // teaches Fireball, Frostbolt, etc.) and is correctly blocked. if (player && player->getClass() == CLASS_PARAGON && GetTrainerType() == Type::Class && !_spells.empty()) { bool onlyPortalsAndTeleports = true; for (Spell const& s : _spells) { SpellInfo const* info = sSpellMgr->GetSpellInfo(s.SpellId); if (!info) continue; bool isPortalOrTeleport = false; for (SpellEffectInfo const& eff : info->GetEffects()) { if (eff.Effect == SPELL_EFFECT_TELEPORT_UNITS || eff.Effect == SPELL_EFFECT_TRANS_DOOR) { isPortalOrTeleport = true; break; } } if (!isPortalOrTeleport) { onlyPortalsAndTeleports = false; break; } } if (!onlyPortalsAndTeleports) return false; } if (!GetTrainerRequirement()) return true; switch (GetTrainerType()) { case Type::Class: case Type::Pet: // check class for class trainers return player->getClass() == GetTrainerRequirement(); case Type::Mount: // check race for mount trainers return player->getRace() == GetTrainerRequirement(); case Type::Tradeskill: // check spell for profession trainers return player->HasSpell(GetTrainerRequirement()); default: break; } return true; } void Trainer::SendTeachFailure(Creature const* npc, Player const* player, uint32 spellId, FailReason reason) const { WorldPackets::NPC::TrainerBuyFailed trainerBuyFailed; trainerBuyFailed.TrainerGUID = npc->GetGUID(); trainerBuyFailed.SpellID = spellId; trainerBuyFailed.TrainerFailedReason = AsUnderlyingType(reason); player->SendDirectMessage(trainerBuyFailed.Write()); } void Trainer::SendTeachSucceeded(Creature const* npc, Player const* player, uint32 spellId) const { WorldPackets::NPC::TrainerBuySucceeded trainerBuySucceeded; trainerBuySucceeded.TrainerGUID = npc->GetGUID(); trainerBuySucceeded.SpellID = spellId; player->SendDirectMessage(trainerBuySucceeded.Write()); } std::string const& Trainer::GetGreeting(LocaleConstant locale) const { if (_greeting[locale].empty()) return _greeting[DEFAULT_LOCALE]; return _greeting[locale]; } void Trainer::AddGreetingLocale(LocaleConstant locale, std::string greeting) { _greeting[locale] = std::move(greeting); } }