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 <noreply@anthropic.com>
This commit is contained in:
SylvaniaCore deploy
2026-08-15 23:53:15 +02:00
parent a5fb052b89
commit 47e707e8d6
6 changed files with 977 additions and 0 deletions
+23
View File
@@ -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');
@@ -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; }
+580
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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<PlayerBotSession*>(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<PlayerBotSession*>(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<PlayerBotSession*>(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<PlayerBotSession*>(session))
botSession->ClearAllSchedule(); // invocation abandonnee avant l arrivee
m_releasing = false;
}
void MercenaryMgr::DismissOne(ObjectGuid botGuid)
{
if (m_releasing)
return;
for (std::vector<MercenaryContract>::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<MercenaryContract> released;
for (std::vector<MercenaryContract>::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<ObjectGuid> 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<MercenaryContract>::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<PlayerBotSession*>(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;
}
}
+139
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
// 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 <vector>
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<MercenaryContract> 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__
@@ -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 <http://www.gnu.org/licenses/>.
*/
// This is where scripts' loading functions should be declared:
void AddSC_npc_mercenary_portal();
void AddMercenaryScripts()
{
AddSC_npc_mercenary_portal();
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
// 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();
}