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:
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user