Siege des Capitales : composition de la horde par roles

La horde etait recrutee sans regard pour les roles : n importe quelle classe,
avec la specialisation que le personnage portait deja. Les IA de classe savent
pourtant distinguer les roles - BotPaladinAI soigne en Sacre, BotPriestAI hors
Ombre, BotShamanAI en Restauration - mais cette logique ne servait a rien
puisque la specialisation n etait jamais imposee.

Le recrutement vise desormais des quotas, nouvelles cles siege_tank_pct et
siege_healer_pct (10 et 20 par defaut, somme plafonnee a 60). Les soigneurs et
les tanks sont engages en premier : sans eux la horde fond avant le trone.
FindSpecIndexForRole interroge ChrSpecialization pour trouver la specialisation
que la classe doit prendre, et le re-level la lui impose via parameter3.

Les specialisations d index superieur a 2 sont ecartees : elles sont hors de
portee du re-level, qui refuse talent > 2. Cela concerne la Restauration du
druide, dont l IA de classe attend de toute facon un druide a trois
specialisations, calibrage d avant Legion.

L imposition de la specialisation passe par Player::ActivateTalentGroup, sur son
propre tick du re-level. Une premiere tentative ecrivait
PLAYER_FIELD_CURRENT_SPEC_ID a la main pendant que stats et auras etaient
recalculees, et provoquait un debordement de pile.

Verifie en conditions reelles, assaut a 50 bots : apparition de guerriers
Protection et de druides Gardien en tank, de chamans Restauration en soigneur,
sept talents par bot et par specialisation, aucun plantage, progression 29 a 30
sur 30.

Reste a affiner : le quota de soigneurs n est pas toujours atteint faute de
comptes disponibles dans les classes soignantes au moment du recrutement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
SylvaniaCore deploy
2026-08-16 23:08:23 +02:00
parent 5aeeb91c52
commit 6b89200c47
2 changed files with 86 additions and 7 deletions
@@ -21,6 +21,7 @@
#include "DatabaseEnv.h"
#include "Log.h"
#include "Creature.h"
#include "DB2Stores.h"
#include "Map.h"
#include "MapManager.h"
#include "ObjectAccessor.h"
@@ -52,7 +53,7 @@ namespace
CapitalSiegeMgr::CapitalSiegeMgr() :
m_enabled(false), m_hourMin(18), m_hourMax(24), m_botCount(50), m_botLevel(110),
m_duration(HOUR), m_spawnRate(5), m_pvpMode(1), m_engageRange(18.0f), m_bossEngageRange(40.0f), m_stallTimeout(20), m_advanceWindow(12),
m_duration(HOUR), m_spawnRate(5), m_pvpMode(1), m_tankPct(10), m_healerPct(20), m_engageRange(18.0f), m_bossEngageRange(40.0f), m_stallTimeout(20), m_advanceWindow(12),
m_maxDiff(400), m_announce(true),
m_lastEventDay(0), m_lastAttackerTeam(TEAM_NEUTRAL), m_scheduledDay(0),
m_scheduledTime(0), m_scheduledTeam(TEAM_NEUTRAL),
@@ -60,7 +61,7 @@ CapitalSiegeMgr::CapitalSiegeMgr() :
m_commander(nullptr),
m_bossGuid(ObjectGuid::Empty), m_bossClearedFlags(0), m_bossOriginalLevel(0), m_bossOriginalFaction(0),
m_bossOriginalMaxHealth(0), m_bossAwakened(false), m_bossLookupFailed(false),
m_bossRetryTimer(0), m_totalRecruited(0),
m_bossRetryTimer(0), m_totalRecruited(0), m_recruitedByRole{ 0, 0, 0 },
m_elapsed(0), m_updateTimer(0), m_overloadTimer(0),
m_botsSpawned(0), m_botsLost(0), m_startTime(0)
{
@@ -116,6 +117,8 @@ void CapitalSiegeMgr::LoadConfig()
m_duration = sConfigMgr->GetIntDefault("siege_duration", HOUR);
m_spawnRate = sConfigMgr->GetIntDefault("siege_spawn_rate", 5);
m_pvpMode = sConfigMgr->GetIntDefault("siege_pvp", 1);
m_tankPct = sConfigMgr->GetIntDefault("siege_tank_pct", 10);
m_healerPct = sConfigMgr->GetIntDefault("siege_healer_pct", 20);
m_engageRange = sConfigMgr->GetFloatDefault("siege_engage_range", 18.0f);
m_bossEngageRange = sConfigMgr->GetFloatDefault("siege_boss_engage_range", 40.0f);
m_stallTimeout = sConfigMgr->GetIntDefault("siege_stall_timeout", 20);
@@ -146,6 +149,14 @@ void CapitalSiegeMgr::LoadConfig()
m_duration = MINUTE;
if (m_pvpMode > 2)
m_pvpMode = 1;
// Il faut laisser la majorite de l effectif aux degats, sinon la horde ne
// tue plus rien.
if (m_tankPct + m_healerPct > 60)
{
TC_LOG_ERROR("server.worldserver", "Siege des Capitales: tanks + soigneurs = %u%%, ramene a 10/20.", m_tankPct + m_healerPct);
m_tankPct = 10;
m_healerPct = 20;
}
// Une portee trop courte rendrait les bots inoffensifs, une portee au-dela
// de BOTAI_SEARCH_RANGE (32 yards, BotAITool.h) les ferait courir apres
// toute la ville au lieu de suivre leur route.
@@ -354,6 +365,7 @@ bool CapitalSiegeMgr::StartSiege(TeamId attacker, std::string const& trigger)
m_recruits.clear();
m_engagedAccounts.clear();
m_totalRecruited = 0;
m_recruitedByRole[0] = m_recruitedByRole[1] = m_recruitedByRole[2] = 0;
m_bossLookupFailed = false;
m_bossRetryTimer = 0;
@@ -684,6 +696,8 @@ void CapitalSiegeMgr::ProcessPendingRecruits()
m_engagedAccounts.erase(it->accountId);
if (m_totalRecruited)
--m_totalRecruited;
if (m_recruitedByRole[it->role])
--m_recruitedByRole[it->role];
it = m_recruits.erase(it);
continue;
}
@@ -724,6 +738,8 @@ void CapitalSiegeMgr::ProcessPendingRecruits()
m_engagedAccounts.erase(it->accountId);
if (m_totalRecruited)
--m_totalRecruited;
if (m_recruitedByRole[it->role])
--m_recruitedByRole[it->role];
it = m_recruits.erase(it);
continue;
}
@@ -733,6 +749,8 @@ void CapitalSiegeMgr::ProcessPendingRecruits()
m_engagedAccounts.erase(it->accountId);
if (m_totalRecruited)
--m_totalRecruited;
if (m_recruitedByRole[it->role])
--m_recruitedByRole[it->role];
it = m_recruits.erase(it);
continue;
}
@@ -758,6 +776,8 @@ void CapitalSiegeMgr::ProcessPendingRecruits()
m_engagedAccounts.erase(it->accountId);
if (m_totalRecruited)
--m_totalRecruited;
if (m_recruitedByRole[it->role])
--m_recruitedByRole[it->role];
it = m_recruits.erase(it);
continue;
}
@@ -766,9 +786,44 @@ void CapitalSiegeMgr::ProcessPendingRecruits()
}
}
int32 CapitalSiegeMgr::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;
// Au-dela de l index 2 la specialisation est hors de portee du re-level,
// qui refuse talent > 2. C est le cas de la Restauration du druide, dont
// l IA de classe attend de toute facon un druide a trois specialisations.
if (spec->OrderIndex > 2)
continue;
return int32(spec->OrderIndex);
}
return -1;
}
// Choisit le role manquant le plus prioritaire au regard des quotas. Les
// soigneurs et les tanks passent avant : sans eux la horde fond, et c est
// exactement ce qui se produisait quand le recrutement prenait n importe qui.
uint8 CapitalSiegeMgr::PickRoleToRecruit() const
{
uint32 const wantedTanks = std::max<uint32>(1, m_botCount * m_tankPct / 100);
uint32 const wantedHealers = std::max<uint32>(1, m_botCount * m_healerPct / 100);
if (m_recruitedByRole[ROLE_HEALER] < wantedHealers)
return ROLE_HEALER;
if (m_recruitedByRole[ROLE_TANK] < wantedTanks)
return ROLE_TANK;
return ROLE_DAMAGE;
}
bool CapitalSiegeMgr::RecruitOneBot()
{
SessionMap const& sessions = sWorld->GetAllSessions();
uint8 const wantedRole = PickRoleToRecruit();
// Premier choix : un bot deja connecte et inoccupe. Pas de connexion a
// payer, et il est deja charge en memoire.
@@ -798,16 +853,22 @@ bool CapitalSiegeMgr::RecruitOneBot()
if (!IsSiegeCapableClass(player->getClass()))
continue;
int32 const specIndex = FindSpecIndexForRole(player->getClass(), wantedRole);
if (specIndex < 0)
continue; // cette classe ne peut pas tenir le role demande
uint32 const level = PlayerBotSetting::CheckMaxLevel(m_botLevel);
BotGlobleSchedule setting(BotGlobleScheduleType::BGSType_Settting, 0);
setting.parameter1 = level;
setting.parameter2 = level;
setting.parameter3 = 4;
// 1 a 3 impose la specialisation, 4 laisserait celle du personnage.
setting.parameter3 = uint32(specIndex) + 1;
session->PushScheduleToQueue(setting);
m_engagedAccounts.insert(session->GetAccountId());
m_recruits.push_back(SiegeRecruit(session->GetAccountId()));
m_recruits.push_back(SiegeRecruit(session->GetAccountId(), wantedRole));
++m_totalRecruited;
++m_recruitedByRole[wantedRole];
return true;
}
@@ -837,6 +898,7 @@ bool CapitalSiegeMgr::RecruitOneBot()
continue;
uint64 pickedGuid = 0;
uint8 pickedClass = 0;
for (PlayerBotBaseInfo::CharInfoMap::iterator itChar = accountInfo->characters.begin();
itChar != accountInfo->characters.end(); ++itChar)
{
@@ -845,7 +907,10 @@ bool CapitalSiegeMgr::RecruitOneBot()
continue;
if (!IsSiegeCapableClass(uint8(charInfo.profession)))
continue;
if (FindSpecIndexForRole(uint8(charInfo.profession), wantedRole) < 0)
continue; // cette classe ne peut pas tenir le role demande
pickedGuid = charInfo.guid;
pickedClass = uint8(charInfo.profession);
break;
}
@@ -861,12 +926,13 @@ bool CapitalSiegeMgr::RecruitOneBot()
BotGlobleSchedule setting(BotGlobleScheduleType::BGSType_Settting, 0);
setting.parameter1 = level;
setting.parameter2 = level;
setting.parameter3 = 4;
setting.parameter3 = uint32(FindSpecIndexForRole(pickedClass, wantedRole)) + 1;
session->PushScheduleToQueue(setting);
m_engagedAccounts.insert(session->GetAccountId());
m_recruits.push_back(SiegeRecruit(session->GetAccountId()));
m_recruits.push_back(SiegeRecruit(session->GetAccountId(), wantedRole));
++m_totalRecruited;
++m_recruitedByRole[wantedRole];
return true;
}
+14 -1
View File
@@ -154,6 +154,13 @@ private:
// moine ni de chasseur de demons dans la horde d invasion.
static bool IsSiegeCapableClass(uint8 playerClass);
// Composition par roles. Renvoie l index de specialisation (0 a 2) que la
// classe doit prendre pour tenir le role, ou -1 si elle ne le peut pas dans
// la limite des trois premieres specialisations, seules atteignables par le
// re-level (PlayerBotSetting::ResetPlayerToLevel refuse talent > 2).
static int32 FindSpecIndexForRole(uint8 playerClass, uint8 role);
uint8 PickRoleToRecruit() const;
// Persistance ------------------------------------------------------------
void SaveState();
void OpenHistoryRow(std::string const& trigger);
@@ -175,6 +182,8 @@ private:
uint32 m_duration; // secondes
uint32 m_spawnRate; // bots par seconde
uint32 m_pvpMode; // 0 = PNJ seuls, 1 = joueurs flagges PvP, 2 = tous
uint32 m_tankPct; // part de tanks dans la horde, en pourcent
uint32 m_healerPct; // part de soigneurs dans la horde, en pourcent
float m_engageRange; // yards, portee d engagement en mode siege
float m_bossEngageRange; // yards, rayon de prise a partie du dirigeant
uint32 m_stallTimeout; // secondes sans progresser avant de lacher la cible
@@ -199,8 +208,10 @@ private:
uint32 accountId;
uint32 stage; // 0 = connexion et reglage, 1 = trajet
uint32 waitSeconds;
uint8 role; // ROLE_TANK, ROLE_HEALER ou ROLE_DAMAGE
explicit SiegeRecruit(uint32 account) : accountId(account), stage(0), waitSeconds(0) { }
SiegeRecruit(uint32 account, uint8 wantedRole)
: accountId(account), stage(0), waitSeconds(0), role(wantedRole) { }
};
CapitalSiegeStatus m_status;
@@ -223,6 +234,8 @@ private:
// Total engage depuis le debut de l evenement. Plafonne le recrutement :
// un bot tue n est jamais remplace, il n y a pas de vague infinie.
uint32 m_totalRecruited;
// Effectif engage par role depuis le debut, pour tenir la composition.
uint32 m_recruitedByRole[3];
uint32 m_elapsed; // ms depuis le debut de l evenement
uint32 m_updateTimer; // ms, cadence le tick a la seconde
uint32 m_overloadTimer; // ms passees au-dessus du seuil de charge