From 47e707e8d63409fee9c7bb9699811b18e116fb75 Mon Sep 17 00:00:00 2001 From: SylvaniaCore deploy Date: Sat, 15 Aug 2026 23:53:15 +0200 Subject: [PATCH] Mercenaires : PNJ d invocation de playerbots payants Le Portail d Invocation de Mercenaire (creature 1000010) loue au joueur la compagnie d un playerbot contre 100 pieces d or, dans le role achete (tank, soigneur ou combattant) et au niveau de son employeur. Le contrat est volontairement fragile : quitter le groupe, dissoudre le groupe, expulser le mercenaire ou se deconnecter le renvoie hors du monde sur-le-champ, et l invocation suivante se repaie. Un filet de securite dans Update() rattrape les ruptures que les hooks n auraient pas vues. Aucun fichier du core n est modifie : tout passe par GroupScript::OnRemoveMember et OnDisband, PlayerScript::OnLogout et WorldScript. Seul ajout exterieur, un accesseur CapitalSiegeMgr::IsAccountEngaged() pour ne pas debaucher un bot deja enrole dans l assaut quotidien. Plafond a quatre mercenaires : un groupe compte cinq places, employeur inclus. Co-Authored-By: Claude Opus 5 --- sql/sylvania/mercenary.sql | 23 + .../game/CapitalSiege/CapitalSiegeMgr.h | 3 + src/server/game/Mercenary/MercenaryMgr.cpp | 580 ++++++++++++++++++ src/server/game/Mercenary/MercenaryMgr.h | 139 +++++ .../Mercenary/mercenary_script_loader.cpp | 25 + .../Mercenary/npc_mercenary_portal.cpp | 207 +++++++ 6 files changed, 977 insertions(+) create mode 100644 sql/sylvania/mercenary.sql create mode 100644 src/server/game/Mercenary/MercenaryMgr.cpp create mode 100644 src/server/game/Mercenary/MercenaryMgr.h create mode 100644 src/server/scripts/Mercenary/mercenary_script_loader.cpp create mode 100644 src/server/scripts/Mercenary/npc_mercenary_portal.cpp 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(); +}