diff --git a/sql/sylvania/mercenary.sql b/sql/sylvania/mercenary.sql
new file mode 100644
index 0000000..8a404b1
--- /dev/null
+++ b/sql/sylvania/mercenary.sql
@@ -0,0 +1,23 @@
+-- SylvaniaCore - Module "Mercenaires"
+-- Base : dc_world
+--
+-- Le Portail d Invocation de Mercenaire. Rang « boss » pour le contour dore,
+-- faction 35 (amical avec tout le monde) et unit_flags 770 pour qu il ne puisse
+-- ni etre attaque ni entrer en combat : c est un decor qui parle.
+--
+-- Le PNJ n est PAS spawne par ce fichier : il se pose en jeu avec
+-- .npc add 1000010
+
+DELETE FROM `creature_template` WHERE `entry` = 1000010;
+INSERT INTO `creature_template`
+ (`entry`, `modelid1`, `name`, `subname`, `gossip_menu_id`, `minlevel`, `maxlevel`,
+ `HealthScalingExpansion`, `faction`, `npcflag`, `speed_walk`, `speed_run`, `scale`,
+ `rank`, `unit_class`, `unit_flags`, `type`, `RegenHealth`, `flags_extra`,
+ `AIName`, `MovementType`, `InhabitType`, `HealthModifier`, `ManaModifier`,
+ `ArmorModifier`, `DamageModifier`, `ExperienceModifier`, `ScriptName`)
+VALUES
+ (1000010, 74465, "Portail d'Invocation de Mercenaire", 'Compagnie franche de Sylvania', 0, 110, 110,
+ 6, 35, 1, 1, 1.14286, 1,
+ 3, 1, 770, 10, 1, 2,
+ '', 0, 3, 1, 1,
+ 1, 1, 1, 'npc_mercenary_portal');
diff --git a/src/server/game/CapitalSiege/CapitalSiegeMgr.h b/src/server/game/CapitalSiege/CapitalSiegeMgr.h
index f3c6397..7887eae 100644
--- a/src/server/game/CapitalSiege/CapitalSiegeMgr.h
+++ b/src/server/game/CapitalSiege/CapitalSiegeMgr.h
@@ -103,6 +103,9 @@ public:
bool IsEnabled() const { return m_enabled; }
CapitalSiegeStatus GetStatus() const { return m_status; }
bool IsRunning() const { return m_status != SIEGE_STATUS_IDLE; }
+ // Un compte bot deja enrole dans l assaut ne doit pas etre debauche par
+ // un autre module (mercenaires).
+ bool IsAccountEngaged(uint32 accountId) const { return m_engagedAccounts.find(accountId) != m_engagedAccounts.end(); }
TeamId GetAttackerTeam() const { return m_attackerTeam; }
TeamId GetScheduledTeam() const { return m_scheduledTeam; }
CapitalSiegeTarget const* GetTarget() const { return m_target; }
diff --git a/src/server/game/Mercenary/MercenaryMgr.cpp b/src/server/game/Mercenary/MercenaryMgr.cpp
new file mode 100644
index 0000000..3adca0d
--- /dev/null
+++ b/src/server/game/Mercenary/MercenaryMgr.cpp
@@ -0,0 +1,580 @@
+/*
+ * This file is part of the DestinyCore Project. See AUTHORS file for Copyright information
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see .
+ */
+
+#include "MercenaryMgr.h"
+#include "CapitalSiegeMgr.h"
+#include "Chat.h"
+#include "Config.h"
+#include "DB2Stores.h"
+#include "Group.h"
+#include "GroupMgr.h"
+#include "Log.h"
+#include "ObjectAccessor.h"
+#include "Player.h"
+#include "PlayerBotMgr.h"
+#include "PlayerBotSession.h"
+#include "PlayerBotSetting.h"
+#include "SharedDefines.h"
+#include "World.h"
+#include "WorldSession.h"
+
+MercenaryMgr::MercenaryMgr() :
+ m_enabled(false), m_cost(100 * MERCENARY_COPPER_PER_GOLD), m_maxPerPlayer(MERCENARY_HARD_CAP),
+ m_minLevel(10), m_updateTimer(0), m_releasing(false)
+{
+}
+
+MercenaryMgr* MercenaryMgr::instance()
+{
+ static MercenaryMgr instance;
+ return &instance;
+}
+
+void MercenaryMgr::LoadConfig()
+{
+ m_enabled = sConfigMgr->GetIntDefault("pbotmerc", 0) != 0;
+
+ uint32 const gold = sConfigMgr->GetIntDefault("pbotmerc_cost", 100);
+ m_cost = gold * MERCENARY_COPPER_PER_GOLD;
+
+ m_maxPerPlayer = sConfigMgr->GetIntDefault("pbotmerc_max", MERCENARY_HARD_CAP);
+ if (m_maxPerPlayer > MERCENARY_HARD_CAP)
+ m_maxPerPlayer = MERCENARY_HARD_CAP; // au-dela, le groupe deborde
+
+ m_minLevel = sConfigMgr->GetIntDefault("pbotmerc_minlevel", 10);
+
+ TC_LOG_INFO("server.loading", "Mercenaires: %s, %u po par invocation, %u au maximum, niveau %u requis.",
+ m_enabled ? "actif" : "inactif", GetCostGold(), m_maxPerPlayer, m_minLevel);
+}
+
+char const* MercenaryMgr::GetRoleName(uint8 role)
+{
+ switch (role)
+ {
+ case ROLE_TANK: return "protecteur";
+ case ROLE_HEALER: return "guerisseur";
+ default: return "combattant";
+ }
+}
+
+char const* MercenaryMgr::GetErrorText(MercenaryResult result)
+{
+ switch (result)
+ {
+ case MERC_ERR_DISABLED: return "Le portail est éteint. Aucun mercenaire ne répond à l'appel.";
+ case MERC_ERR_BAD_PLACE: return "Le portail ne peut percer le voile depuis un champ de bataille ou une arène.";
+ case MERC_ERR_IN_COMBAT: return "Impossible d'invoquer un mercenaire en plein combat.";
+ case MERC_ERR_LEVEL: return "Vous êtes trop inexpérimenté pour commander un mercenaire.";
+ case MERC_ERR_NOT_LEADER: return "Seul le chef du groupe peut invoquer un mercenaire.";
+ case MERC_ERR_GROUP_FULL: return "Votre groupe est déjà complet.";
+ case MERC_ERR_MAX_REACHED: return "Vous commandez déjà autant de mercenaires que le portail l'autorise.";
+ case MERC_ERR_NO_MONEY: return "Vous n'avez pas assez d'or pour payer ce mercenaire.";
+ case MERC_ERR_NO_BOT: return "Aucun mercenaire de cette spécialité n'est disponible pour le moment. Votre or ne vous a pas été prélevé.";
+ case MERC_ERR_PENDING: return "Une invocation est déjà en cours. Patientez.";
+ default: return "";
+ }
+}
+
+// Seules ces classes disposent d une IA de groupe (AI/PlayerAI/BotGroupAI).
+// Le moine et le chasseur de demons n en ont pas : un mercenaire de ces classes
+// resterait plante sans rien faire.
+bool MercenaryMgr::IsMercenaryCapableClass(uint8 playerClass)
+{
+ switch (playerClass)
+ {
+ case CLASS_WARRIOR:
+ case CLASS_PALADIN:
+ case CLASS_HUNTER:
+ case CLASS_ROGUE:
+ case CLASS_PRIEST:
+ case CLASS_DEATH_KNIGHT:
+ case CLASS_SHAMAN:
+ case CLASS_MAGE:
+ case CLASS_WARLOCK:
+ case CLASS_DRUID:
+ return true;
+ default:
+ return false;
+ }
+}
+
+// Index de specialisation a passer au re-level (BGSType_Settting attend 1 a 3).
+// Au-dela de l index 2, le core refuse la specialisation.
+int32 MercenaryMgr::FindSpecIndexForRole(uint8 playerClass, uint8 role)
+{
+ for (uint32 i = 0; i < sChrSpecializationStore.GetNumRows(); ++i)
+ {
+ ChrSpecializationEntry const* spec = sChrSpecializationStore.LookupEntry(i);
+ if (!spec || spec->ClassID != int8(playerClass) || spec->IsPetSpecialization())
+ continue;
+ if (spec->Role != int8(role))
+ continue;
+ if (spec->OrderIndex > 2)
+ continue;
+ return int32(spec->OrderIndex);
+ }
+ return -1;
+}
+
+uint32 MercenaryMgr::CountContracts(ObjectGuid ownerGuid) const
+{
+ uint32 count = 0;
+ for (MercenaryContract const& contract : m_contracts)
+ if (contract.ownerGuid == ownerGuid)
+ ++count;
+ return count;
+}
+
+bool MercenaryMgr::IsMercenary(ObjectGuid botGuid) const
+{
+ for (MercenaryContract const& contract : m_contracts)
+ if (contract.botGuid == botGuid)
+ return true;
+ return false;
+}
+
+bool MercenaryMgr::IsAccountHired(uint32 accountId) const
+{
+ for (MercenaryContract const& contract : m_contracts)
+ if (contract.accountId == accountId)
+ return true;
+ return false;
+}
+
+// Reserve un compte bot capable de tenir le role demande dans la faction du
+// joueur. Priorite aux bots deja connectes et desoeuvres : pas de connexion a
+// payer et ils sont deja charges en memoire.
+bool MercenaryMgr::FindCandidate(Player* owner, uint8 role, uint32& accountId, uint64& charGuid, bool& alreadyOnline) const
+{
+ SessionMap const& sessions = sWorld->GetAllSessions();
+
+ for (SessionMap::const_iterator it = sessions.begin(); it != sessions.end(); ++it)
+ {
+ if (!it->second->IsBotSession())
+ continue;
+
+ PlayerBotSession* session = dynamic_cast(it->second);
+ if (!session || session->PlayerLoading() || session->HasSchedules() || session->IsAccountBotSession())
+ continue;
+ if (IsAccountHired(session->GetAccountId()))
+ continue;
+ if (sCapitalSiegeMgr->IsAccountEngaged(session->GetAccountId()))
+ continue; // deja enrole dans l assaut quotidien
+
+ Player* bot = session->GetPlayer();
+ if (!bot)
+ continue;
+
+ if (bot->IsLoading() || !bot->IsInWorld() || !bot->IsSettingFinish())
+ continue;
+ if (bot->GetGroup())
+ continue; // deja au service de quelqu un
+ if (bot->InBattleground() || bot->InArena() || bot->InBattlegroundQueue())
+ continue;
+ if (bot->GetMap()->IsDungeon() || bot->isUsingLfg())
+ continue;
+ if (bot->GetTeamId() != owner->GetTeamId())
+ continue;
+ if (!IsMercenaryCapableClass(bot->getClass()))
+ continue;
+ if (FindSpecIndexForRole(bot->getClass(), role) < 0)
+ continue;
+
+ accountId = session->GetAccountId();
+ charGuid = 0;
+ alreadyOnline = true;
+ return true;
+ }
+
+ // Second choix : une session bot hors ligne, sur laquelle on connecte
+ // explicitement un personnage de la bonne faction et du bon role. Laisser
+ // le core tirer au sort (BGSType_Online) ne garantit ni l un ni l autre.
+ for (SessionMap::const_iterator it = sessions.begin(); it != sessions.end(); ++it)
+ {
+ if (!it->second->IsBotSession())
+ continue;
+
+ PlayerBotSession* session = dynamic_cast(it->second);
+ if (!session || session->PlayerLoading() || session->HasSchedules() || session->IsAccountBotSession())
+ continue;
+ if (session->GetPlayer())
+ continue;
+ if (IsAccountHired(session->GetAccountId()))
+ continue;
+ if (sCapitalSiegeMgr->IsAccountEngaged(session->GetAccountId()))
+ continue;
+
+ PlayerBotBaseInfo* accountInfo = sPlayerBotMgr->GetPlayerBotAccountInfo(session->GetAccountId());
+ if (!accountInfo)
+ accountInfo = sPlayerBotMgr->GetAccountBotAccountInfo(session->GetAccountId());
+ if (!accountInfo)
+ continue;
+
+ for (PlayerBotBaseInfo::CharInfoMap::iterator itChar = accountInfo->characters.begin();
+ itChar != accountInfo->characters.end(); ++itChar)
+ {
+ PlayerBotCharBaseInfo& charInfo = itChar->second;
+ if (charInfo.GetCamp() != owner->GetTeamId())
+ continue;
+ if (!IsMercenaryCapableClass(uint8(charInfo.profession)))
+ continue;
+ if (FindSpecIndexForRole(uint8(charInfo.profession), role) < 0)
+ continue;
+
+ accountId = session->GetAccountId();
+ charGuid = charInfo.guid;
+ alreadyOnline = false;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+MercenaryResult MercenaryMgr::Summon(Player* owner, uint8 role)
+{
+ if (!m_enabled)
+ return MERC_ERR_DISABLED;
+ if (!owner || owner->IsPlayerBot())
+ return MERC_ERR_DISABLED;
+
+ if (owner->InBattleground() || owner->InArena() || owner->InBattlegroundQueue())
+ return MERC_ERR_BAD_PLACE;
+ if (owner->IsInCombat())
+ return MERC_ERR_IN_COMBAT;
+ if (owner->getLevel() < m_minLevel)
+ return MERC_ERR_LEVEL;
+
+ uint32 const contracts = CountContracts(owner->GetGUID());
+ if (contracts >= m_maxPerPlayer)
+ return MERC_ERR_MAX_REACHED;
+
+ if (Group* group = owner->GetGroup())
+ {
+ if (group->isBGGroup() || group->isBFGroup() || group->isLFGGroup() || group->isRaidGroup())
+ return MERC_ERR_BAD_PLACE;
+ if (group->GetLeaderGUID() != owner->GetGUID())
+ return MERC_ERR_NOT_LEADER;
+
+ // Les invocations en cours occupent deja leur place a venir.
+ uint32 pending = 0;
+ for (MercenaryContract const& contract : m_contracts)
+ if (contract.ownerGuid == owner->GetGUID() && contract.stage == MERC_STAGE_SUMMONING)
+ ++pending;
+
+ if (group->GetMembersCount() + pending >= MERCENARY_GROUP_SIZE)
+ return MERC_ERR_GROUP_FULL;
+ }
+
+ if (!owner->HasEnoughMoney(uint64(m_cost)))
+ return MERC_ERR_NO_MONEY;
+
+ uint32 accountId = 0;
+ uint64 charGuid = 0;
+ bool alreadyOnline = false;
+ if (!FindCandidate(owner, role, accountId, charGuid, alreadyOnline))
+ return MERC_ERR_NO_BOT;
+
+ WorldSession* worldSession = sWorld->FindSession(accountId);
+ PlayerBotSession* session = dynamic_cast(worldSession);
+ if (!session)
+ return MERC_ERR_NO_BOT;
+
+ // Le mercenaire existe : on encaisse.
+ owner->ModifyMoney(-int64(m_cost));
+
+ uint32 const level = PlayerBotSetting::CheckMaxLevel(owner->getLevel());
+
+ // Classe du personnage qui va porter le contrat : deja connue s il est en
+ // jeu, lue dans la fiche du compte s il faut encore le connecter.
+ uint8 botClass = 0;
+ if (alreadyOnline)
+ botClass = session->GetPlayer()->getClass();
+ else
+ {
+ PlayerBotBaseInfo* accountInfo = sPlayerBotMgr->GetPlayerBotAccountInfo(accountId);
+ if (!accountInfo)
+ accountInfo = sPlayerBotMgr->GetAccountBotAccountInfo(accountId);
+ if (accountInfo)
+ {
+ PlayerBotBaseInfo::CharInfoMap::iterator itChar = accountInfo->characters.find(uint32(charGuid));
+ if (itChar != accountInfo->characters.end())
+ botClass = uint8(itChar->second.profession);
+ }
+
+ BotGlobleSchedule online(BotGlobleScheduleType::BGSType_Online_GUID, charGuid);
+ session->PushScheduleToQueue(online);
+ }
+
+ int32 const specIndex = FindSpecIndexForRole(botClass, role);
+
+ // Mise au niveau du maitre et specialisation imposee par le role achete.
+ // 1 a 3 forcent la specialisation, 4 laisserait celle du personnage.
+ BotGlobleSchedule setting(BotGlobleScheduleType::BGSType_Settting, 0);
+ setting.parameter1 = level;
+ setting.parameter2 = level;
+ setting.parameter3 = specIndex >= 0 ? uint32(specIndex) + 1 : 4;
+ session->PushScheduleToQueue(setting);
+
+ MercenaryContract contract;
+ contract.accountId = accountId;
+ contract.ownerGuid = owner->GetGUID();
+ contract.role = role;
+ contract.stage = MERC_STAGE_SUMMONING;
+ m_contracts.push_back(contract);
+
+ TC_LOG_INFO("server.worldserver", "Mercenaires: %s a paye %u po pour un %s (compte bot %u).",
+ owner->GetName().c_str(), GetCostGold(), GetRoleName(role), accountId);
+
+ return MERC_OK;
+}
+
+void MercenaryMgr::Refund(ObjectGuid ownerGuid, char const* reason)
+{
+ Player* owner = ObjectAccessor::FindConnectedPlayer(ownerGuid);
+ if (!owner)
+ return;
+
+ owner->ModifyMoney(int64(m_cost));
+ ChatHandler(owner->GetSession()).PSendSysMessage("|cff00ff00[Portail]|r %s Vos %u pièces d'or vous sont rendues.",
+ reason, GetCostGold());
+}
+
+// Congedie le bot d un contrat : sortie du groupe puis deconnexion. Le contrat
+// doit avoir ete retire de la liste AVANT l appel (reentrance des hooks).
+void MercenaryMgr::ReleaseBot(MercenaryContract const& contract)
+{
+ WorldSession* session = sWorld->FindSession(contract.accountId);
+ if (!session || !session->IsBotSession())
+ return;
+
+ m_releasing = true;
+
+ if (Player* bot = session->GetPlayer())
+ {
+ if (bot->IsInWorld())
+ {
+ if (bot->IsInCombat())
+ bot->CombatStop(true);
+ if (bot->GetGroup())
+ bot->RemoveFromGroup(RemoveMethod::GROUP_REMOVEMETHOD_LEAVE);
+ }
+ session->LogoutPlayer(false);
+ }
+ else if (PlayerBotSession* botSession = dynamic_cast(session))
+ botSession->ClearAllSchedule(); // invocation abandonnee avant l arrivee
+
+ m_releasing = false;
+}
+
+void MercenaryMgr::DismissOne(ObjectGuid botGuid)
+{
+ if (m_releasing)
+ return;
+
+ for (std::vector::iterator it = m_contracts.begin(); it != m_contracts.end(); ++it)
+ {
+ if (it->botGuid != botGuid)
+ continue;
+
+ MercenaryContract const contract = *it;
+ m_contracts.erase(it);
+
+ if (Player* owner = ObjectAccessor::FindConnectedPlayer(contract.ownerGuid))
+ ChatHandler(owner->GetSession()).PSendSysMessage(
+ "|cff00ff00[Portail]|r Votre %s retourne d'où il vient. Il vous faudra payer de nouveau pour en invoquer un autre.",
+ GetRoleName(contract.role));
+
+ ReleaseBot(contract);
+ return;
+ }
+}
+
+void MercenaryMgr::DismissAll(ObjectGuid ownerGuid)
+{
+ if (m_releasing)
+ return;
+
+ std::vector released;
+ for (std::vector::iterator it = m_contracts.begin(); it != m_contracts.end(); )
+ {
+ if (it->ownerGuid != ownerGuid)
+ {
+ ++it;
+ continue;
+ }
+ released.push_back(*it);
+ it = m_contracts.erase(it);
+ }
+
+ for (MercenaryContract const& contract : released)
+ ReleaseBot(contract);
+}
+
+// Un membre a quitte un groupe, de son plein gre ou expulse. Si c est un
+// mercenaire, son contrat s arrete la ; si c est son employeur, toute sa
+// compagnie se dissout.
+void MercenaryMgr::OnPlayerLeftGroup(ObjectGuid guid)
+{
+ if (m_releasing)
+ return;
+
+ if (IsMercenary(guid))
+ DismissOne(guid);
+ else
+ DismissAll(guid);
+}
+
+void MercenaryMgr::OnGroupDisband(Group* group)
+{
+ if (m_releasing || !group || m_contracts.empty())
+ return;
+
+ Group::MemberSlotList const& members = group->GetMemberSlots();
+ std::vector guids;
+ for (Group::MemberSlot const& slot : members)
+ guids.push_back(slot.guid);
+
+ for (ObjectGuid const& guid : guids)
+ OnPlayerLeftGroup(guid);
+}
+
+void MercenaryMgr::OnPlayerLogout(Player* player)
+{
+ if (!player || player->IsPlayerBot())
+ return;
+
+ DismissAll(player->GetGUID());
+}
+
+void MercenaryMgr::Update(uint32 diff)
+{
+ if (!m_enabled || m_contracts.empty())
+ return;
+
+ m_updateTimer += diff;
+ if (m_updateTimer < 1000)
+ return;
+ m_updateTimer = 0;
+
+ for (std::vector::iterator it = m_contracts.begin(); it != m_contracts.end(); )
+ {
+ // L employeur d abord : plus de maitre, plus de contrat. C est le filet
+ // de securite si un hook n a pas ete appele (crash client, timeout...).
+ Player* owner = ObjectAccessor::FindConnectedPlayer(it->ownerGuid);
+ if (!owner || !owner->IsInWorld())
+ {
+ MercenaryContract const contract = *it;
+ it = m_contracts.erase(it);
+ ReleaseBot(contract);
+ continue;
+ }
+
+ if (it->stage == MERC_STAGE_ACTIVE)
+ {
+ Player* bot = ObjectAccessor::FindConnectedPlayer(it->botGuid);
+ Group* group = owner->GetGroup();
+ if (!bot || !bot->IsInWorld() || !group || bot->GetGroup() != group)
+ {
+ MercenaryContract const contract = *it;
+ it = m_contracts.erase(it);
+ ReleaseBot(contract);
+ continue;
+ }
+ ++it;
+ continue;
+ }
+
+ // Invocation en cours.
+ ++it->waitSeconds;
+
+ WorldSession* worldSession = sWorld->FindSession(it->accountId);
+ PlayerBotSession* session = dynamic_cast(worldSession);
+ if (!session)
+ {
+ Refund(it->ownerGuid, "Le mercenaire ne s'est jamais présenté.");
+ it = m_contracts.erase(it);
+ continue;
+ }
+
+ Player* bot = session->GetPlayer();
+ bool const ready = bot && bot->IsInWorld() && !session->PlayerLoading()
+ && !session->HasSchedules() && bot->IsSettingFinish();
+
+ if (!ready)
+ {
+ if (it->waitSeconds > MERCENARY_SUMMON_TIMEOUT)
+ {
+ MercenaryContract const contract = *it;
+ it = m_contracts.erase(it);
+ Refund(contract.ownerGuid, "Le mercenaire ne s'est jamais présenté.");
+ ReleaseBot(contract);
+ continue;
+ }
+ ++it;
+ continue;
+ }
+
+ // Le groupe est cree au moment ou le premier mercenaire arrive, pas a
+ // l achat : un paiement qui echoue ne doit pas laisser de groupe vide.
+ Group* group = owner->GetGroup();
+ if (!group)
+ {
+ group = new Group;
+ if (!group->Create(owner))
+ {
+ delete group;
+ MercenaryContract const contract = *it;
+ it = m_contracts.erase(it);
+ Refund(contract.ownerGuid, "Le pacte n'a pas pu être scellé.");
+ ReleaseBot(contract);
+ continue;
+ }
+ sGroupMgr->AddGroup(group);
+ }
+
+ if (group->IsFull() || !group->AddMember(bot))
+ {
+ MercenaryContract const contract = *it;
+ it = m_contracts.erase(it);
+ Refund(contract.ownerGuid, "Votre groupe est complet.");
+ ReleaseBot(contract);
+ continue;
+ }
+
+ it->botGuid = bot->GetGUID();
+ it->stage = MERC_STAGE_ACTIVE;
+
+ PlayerBotMgr::SwitchPlayerBotAI(bot, PlayerBotAIType::PBAIT_GROUP, true);
+
+ // Le mercenaire se materialise aupres de son employeur. L ajout au
+ // groupe precede volontairement le teleport : c est ce qui autorise
+ // l entree dans l instance ou se trouve deja le joueur.
+ float x, y, z;
+ owner->GetClosePoint(x, y, z, 2.0f, 3.0f);
+ bot->TeleportTo(owner->GetMapId(), x, y, z, owner->GetOrientation());
+
+ ChatHandler(owner->GetSession()).PSendSysMessage(
+ "|cff00ff00[Portail]|r %s, %s mercenaire, répond à votre appel.",
+ bot->GetName().c_str(), GetRoleName(it->role));
+
+ TC_LOG_INFO("server.worldserver", "Mercenaires: %s rejoint le groupe de %s (role %s).",
+ bot->GetName().c_str(), owner->GetName().c_str(), GetRoleName(it->role));
+
+ ++it;
+ }
+}
diff --git a/src/server/game/Mercenary/MercenaryMgr.h b/src/server/game/Mercenary/MercenaryMgr.h
new file mode 100644
index 0000000..baf760a
--- /dev/null
+++ b/src/server/game/Mercenary/MercenaryMgr.h
@@ -0,0 +1,139 @@
+/*
+ * This file is part of the DestinyCore 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 .
+ */
+
+// SylvaniaCore - Module "Mercenaires"
+//
+// Le Portail d Invocation de Mercenaire vend au joueur la compagnie temporaire
+// d un playerbot. Le contrat est volontairement fragile : il ne survit ni au
+// depart du groupe, ni au renvoi du mercenaire, ni a la deconnexion du client.
+// A chaque rupture, le bot se deconnecte et l invocation suivante se repaye.
+
+#ifndef __MERCENARYMGR_H__
+#define __MERCENARYMGR_H__
+
+#include "Common.h"
+#include "ObjectGuid.h"
+#include
+
+class Group;
+class Player;
+
+// Un groupe de World of Warcraft compte cinq places, le maitre inclus : il ne
+// peut donc jamais y avoir plus de quatre mercenaires sans convertir le groupe
+// en raid, ce qui interdirait les donjons.
+#define MERCENARY_GROUP_SIZE 5
+#define MERCENARY_HARD_CAP (MERCENARY_GROUP_SIZE - 1)
+#define MERCENARY_SUMMON_TIMEOUT 60 // secondes avant abandon + remboursement
+#define MERCENARY_COPPER_PER_GOLD 10000
+
+enum MercenaryStage
+{
+ MERC_STAGE_SUMMONING = 0, // connexion et mise a niveau en cours
+ MERC_STAGE_ACTIVE = 1 // dans le groupe, sous contrat
+};
+
+enum MercenaryResult
+{
+ MERC_OK = 0,
+ MERC_ERR_DISABLED,
+ MERC_ERR_BAD_PLACE,
+ MERC_ERR_IN_COMBAT,
+ MERC_ERR_LEVEL,
+ MERC_ERR_NOT_LEADER,
+ MERC_ERR_GROUP_FULL,
+ MERC_ERR_MAX_REACHED,
+ MERC_ERR_NO_MONEY,
+ MERC_ERR_NO_BOT,
+ MERC_ERR_PENDING
+};
+
+struct MercenaryContract
+{
+ MercenaryContract() : accountId(0), role(0), stage(MERC_STAGE_SUMMONING), waitSeconds(0) { }
+
+ uint32 accountId; // compte bot reserve
+ ObjectGuid ownerGuid; // joueur qui a paye
+ ObjectGuid botGuid; // renseigne une fois le mercenaire en jeu
+ uint8 role; // ROLE_TANK, ROLE_HEALER ou ROLE_DAMAGE
+ uint8 stage;
+ uint32 waitSeconds;
+};
+
+class TC_GAME_API MercenaryMgr
+{
+ public:
+ static MercenaryMgr* instance();
+
+ void LoadConfig();
+
+ bool IsEnabled() const { return m_enabled; }
+ uint32 GetCost() const { return m_cost; } // en cuivre
+ uint32 GetCostGold() const { return m_cost / MERCENARY_COPPER_PER_GOLD; }
+ uint32 GetMaxPerPlayer() const { return m_maxPerPlayer; }
+ uint32 GetMinLevel() const { return m_minLevel; }
+
+ // Nombre de contrats en cours pour ce joueur, invocations comprises.
+ uint32 CountContracts(ObjectGuid ownerGuid) const;
+
+ // Verifie, encaisse et lance l invocation. Le prelevement n a lieu que
+ // si un mercenaire du role demande a effectivement ete reserve.
+ MercenaryResult Summon(Player* owner, uint8 role);
+
+ void Update(uint32 diff);
+
+ bool IsMercenary(ObjectGuid botGuid) const;
+ bool IsAccountHired(uint32 accountId) const;
+
+ // Rupture du contrat. Le mercenaire quitte le groupe et se deconnecte ;
+ // aucune de ces routes ne rembourse, c est la regle du systeme.
+ void DismissOne(ObjectGuid botGuid);
+ void DismissAll(ObjectGuid ownerGuid);
+
+ // Hooks du module (GroupScript / PlayerScript).
+ void OnPlayerLeftGroup(ObjectGuid guid);
+ void OnGroupDisband(Group* group);
+ void OnPlayerLogout(Player* player);
+
+ static char const* GetRoleName(uint8 role);
+ static char const* GetErrorText(MercenaryResult result);
+
+ private:
+ MercenaryMgr();
+
+ bool FindCandidate(Player* owner, uint8 role, uint32& accountId, uint64& charGuid, bool& alreadyOnline) const;
+ void ReleaseBot(MercenaryContract const& contract);
+ void Refund(ObjectGuid ownerGuid, char const* reason);
+
+ static bool IsMercenaryCapableClass(uint8 playerClass);
+ static int32 FindSpecIndexForRole(uint8 playerClass, uint8 role);
+
+ std::vector m_contracts;
+
+ bool m_enabled;
+ uint32 m_cost;
+ uint32 m_maxPerPlayer;
+ uint32 m_minLevel;
+ uint32 m_updateTimer;
+
+ // Garde-fou de reentrance : congedier un mercenaire le retire du groupe,
+ // ce qui rappelle nos propres hooks GroupScript.
+ bool m_releasing;
+};
+
+#define sMercenaryMgr MercenaryMgr::instance()
+
+#endif // __MERCENARYMGR_H__
diff --git a/src/server/scripts/Mercenary/mercenary_script_loader.cpp b/src/server/scripts/Mercenary/mercenary_script_loader.cpp
new file mode 100644
index 0000000..da31ff7
--- /dev/null
+++ b/src/server/scripts/Mercenary/mercenary_script_loader.cpp
@@ -0,0 +1,25 @@
+/*
+ * This file is part of the DestinyCore Project. See AUTHORS file for Copyright information
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see .
+ */
+
+// This is where scripts' loading functions should be declared:
+
+void AddSC_npc_mercenary_portal();
+
+void AddMercenaryScripts()
+{
+ AddSC_npc_mercenary_portal();
+}
diff --git a/src/server/scripts/Mercenary/npc_mercenary_portal.cpp b/src/server/scripts/Mercenary/npc_mercenary_portal.cpp
new file mode 100644
index 0000000..b9b6414
--- /dev/null
+++ b/src/server/scripts/Mercenary/npc_mercenary_portal.cpp
@@ -0,0 +1,207 @@
+/*
+ * This file is part of the DestinyCore 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 .
+ */
+
+// SylvaniaCore - Module "Mercenaires"
+//
+// Le PNJ marchand et les trois hooks qui font vivre - et surtout mourir - le
+// contrat : depart du groupe, dissolution du groupe, deconnexion du joueur.
+// Aucun fichier du core n est modifie, tout passe par les hooks de ScriptMgr.
+
+#include "Chat.h"
+#include "Creature.h"
+#include "Group.h"
+#include "MercenaryMgr.h"
+#include "Player.h"
+#include "ScriptedGossip.h"
+#include "ScriptMgr.h"
+#include "SharedDefines.h"
+
+enum MercenaryGossipAction
+{
+ GOSSIP_ACTION_CLOSE = 0,
+ GOSSIP_ACTION_SUMMON_TANK = 1,
+ GOSSIP_ACTION_SUMMON_HEAL = 2,
+ GOSSIP_ACTION_SUMMON_DPS = 3,
+ GOSSIP_ACTION_DISMISS_ALL = 10,
+ GOSSIP_ACTION_EXPLAIN = 11
+};
+
+class npc_mercenary_portal : public CreatureScript
+{
+ public:
+ npc_mercenary_portal() : CreatureScript("npc_mercenary_portal") { }
+
+ // La boite de dialogue du client ne peut afficher qu un texte de la base
+ // de donnees (npc_text -> broadcast_text, cote DB2). Le boniment du
+ // portail passe donc par un chuchotement, qui accepte du texte libre.
+ static void WhisperPitch(Creature* creature, Player* player)
+ {
+ std::ostringstream pitch;
+ pitch << "Lance " << sMercenaryMgr->GetCostGold() << " pièces d'or dans le portail et un mercenaire répondra à ton appel. "
+ << "Jusqu'à " << sMercenaryMgr->GetMaxPerPlayer() << ", de quoi former un groupe complet. "
+ << "Mais sache-le : le lien est fragile.";
+ creature->Whisper(pitch.str(), LANG_UNIVERSAL, player);
+ }
+
+ bool OnGossipHello(Player* player, Creature* creature) override
+ {
+ ClearGossipMenuFor(player);
+
+ uint32 const cost = sMercenaryMgr->GetCostGold();
+ uint32 const hired = sMercenaryMgr->CountContracts(player->GetGUID());
+ uint32 const maximum = sMercenaryMgr->GetMaxPerPlayer();
+
+ WhisperPitch(creature, player);
+
+ if (!sMercenaryMgr->IsEnabled())
+ {
+ AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Le portail est éteint.", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_CLOSE);
+ SendGossipMenuFor(player, player->GetGossipTextId(creature), creature->GetGUID());
+ return true;
+ }
+
+ if (hired < maximum)
+ {
+ std::ostringstream tank;
+ tank << "Invoquer un protecteur (" << cost << " pièces d'or)";
+ AddGossipItemFor(player, GOSSIP_ICON_MONEY_BAG, tank.str(), GOSSIP_SENDER_MAIN, GOSSIP_ACTION_SUMMON_TANK);
+
+ std::ostringstream healer;
+ healer << "Invoquer un guérisseur (" << cost << " pièces d'or)";
+ AddGossipItemFor(player, GOSSIP_ICON_MONEY_BAG, healer.str(), GOSSIP_SENDER_MAIN, GOSSIP_ACTION_SUMMON_HEAL);
+
+ std::ostringstream damage;
+ damage << "Invoquer un combattant (" << cost << " pièces d'or)";
+ AddGossipItemFor(player, GOSSIP_ICON_MONEY_BAG, damage.str(), GOSSIP_SENDER_MAIN, GOSSIP_ACTION_SUMMON_DPS);
+ }
+ else
+ AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Le portail ne peut plus rien pour vous : votre compagnie est au complet.",
+ GOSSIP_SENDER_MAIN, GOSSIP_ACTION_CLOSE);
+
+ if (hired)
+ {
+ std::ostringstream dismiss;
+ dismiss << "Congédier mes mercenaires (" << hired << "/" << maximum << ")";
+ AddGossipItemFor(player, GOSSIP_ICON_TALK, dismiss.str(), GOSSIP_SENDER_MAIN, GOSSIP_ACTION_DISMISS_ALL);
+ }
+
+ AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Quelles sont les règles de ce pacte ?", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_EXPLAIN);
+ AddGossipItemFor(player, GOSSIP_ICON_CHAT, "Rien pour l'instant.", GOSSIP_SENDER_MAIN, GOSSIP_ACTION_CLOSE);
+
+ SendGossipMenuFor(player, player->GetGossipTextId(creature), creature->GetGUID());
+ return true;
+ }
+
+ bool OnGossipSelect(Player* player, Creature* creature, uint32 /*sender*/, uint32 action) override
+ {
+ uint8 role = 0;
+ switch (action)
+ {
+ case GOSSIP_ACTION_SUMMON_TANK: role = ROLE_TANK; break;
+ case GOSSIP_ACTION_SUMMON_HEAL: role = ROLE_HEALER; break;
+ case GOSSIP_ACTION_SUMMON_DPS: role = ROLE_DAMAGE; break;
+ case GOSSIP_ACTION_EXPLAIN:
+ CloseGossipMenuFor(player);
+ creature->Whisper("Le mercenaire te suit tant que le lien tient. Quitte le groupe, renvoie-le, ou "
+ "déconnecte-toi, et il retourne au néant sur-le-champ. Chaque nouvelle invocation se paie, sans exception.",
+ LANG_UNIVERSAL, player);
+ return true;
+ case GOSSIP_ACTION_DISMISS_ALL:
+ CloseGossipMenuFor(player);
+ sMercenaryMgr->DismissAll(player->GetGUID());
+ ChatHandler(player->GetSession()).PSendSysMessage(
+ "|cff00ff00[Portail]|r Vos mercenaires sont congédiés. Toute nouvelle invocation devra être payée.");
+ return true;
+ default:
+ CloseGossipMenuFor(player);
+ return true;
+ }
+
+ CloseGossipMenuFor(player);
+
+ MercenaryResult const result = sMercenaryMgr->Summon(player, role);
+ if (result != MERC_OK)
+ {
+ ChatHandler(player->GetSession()).PSendSysMessage("|cffff0000[Portail]|r %s",
+ MercenaryMgr::GetErrorText(result));
+ return true;
+ }
+
+ ChatHandler(player->GetSession()).PSendSysMessage(
+ "|cff00ff00[Portail]|r %u pièces d'or franchissent le voile. Votre %s ne va pas tarder...",
+ sMercenaryMgr->GetCostGold(), MercenaryMgr::GetRoleName(role));
+
+ return true;
+ }
+};
+
+// Depart d un membre : de son plein gre, expulse par le chef, ou parce que le
+// core a retire un personnage deconnecte. Les trois cas rompent le contrat.
+class mercenary_group_script : public GroupScript
+{
+ public:
+ mercenary_group_script() : GroupScript("mercenary_group_script") { }
+
+ void OnRemoveMember(Group* /*group*/, ObjectGuid guid, RemoveMethod /*method*/, ObjectGuid /*kicker*/, char const* /*reason*/) override
+ {
+ sMercenaryMgr->OnPlayerLeftGroup(guid);
+ }
+
+ void OnDisband(Group* group) override
+ {
+ sMercenaryMgr->OnGroupDisband(group);
+ }
+};
+
+// Deconnexion de l employeur : la compagnie entiere se dissout. Le core sait
+// deja renvoyer les bots d un groupe sans joueur reel (WorldSession.cpp), mais
+// il attend que le groupe soit vide de vrais joueurs ; ici le renvoi est
+// immediat et vise nommement les mercenaires de ce joueur.
+class mercenary_player_script : public PlayerScript
+{
+ public:
+ mercenary_player_script() : PlayerScript("mercenary_player_script") { }
+
+ void OnLogout(Player* player) override
+ {
+ sMercenaryMgr->OnPlayerLogout(player);
+ }
+};
+
+class mercenary_world_script : public WorldScript
+{
+ public:
+ mercenary_world_script() : WorldScript("mercenary_world_script") { }
+
+ void OnConfigLoad(bool /*reload*/) override
+ {
+ sMercenaryMgr->LoadConfig();
+ }
+
+ void OnUpdate(uint32 diff) override
+ {
+ sMercenaryMgr->Update(diff);
+ }
+};
+
+void AddSC_npc_mercenary_portal()
+{
+ new npc_mercenary_portal();
+ new mercenary_group_script();
+ new mercenary_player_script();
+ new mercenary_world_script();
+}