Playerbot: auto-learn riding skill + mount; owner-mount party sync
- Riding (skill 762) granted automatically at modern thresholds (L10 Journeyman 150, L20 Expert 225, L30 Master 375). One tier-appropriate mount spell learned per tier, data-driven from the Mount DB2. - Runs on bot login, level-up and alt finalize. - Owner-mount sync: Unit::Mount/Unit::Dismount hooks push MountIntent/DismountIntent to owned bots so the party travels together. - Add Unit.cpp mount/dismount hunks to the core-hooks patch.
This commit is contained in:
@@ -13,6 +13,8 @@ namespace Playerbot::Hooks {
|
||||
void OnPlayerLogin(Player* p) { V2::Module::instance().OnPlayerLogin(p); }
|
||||
void OnPlayerLogout(Player* p) { V2::Module::instance().OnPlayerLogout(p); }
|
||||
void OnLevelUp(Player* p, uint8 new_level) { V2::Module::instance().OnLevelUp(p, new_level); }
|
||||
void OnPlayerMounted(Player* p) { V2::Module::instance().OnMounted(p); }
|
||||
void OnPlayerDismounted(Player* p) { V2::Module::instance().OnDismounted(p); }
|
||||
void OnDeath(Unit* victim, Unit* killer) { V2::Module::instance().OnDeath(victim, killer); }
|
||||
void OnResurrect(Player* p) { V2::Module::instance().OnResurrect(p); }
|
||||
void OnSpecChanged(Player* p, uint8 new_spec) { V2::Module::instance().OnSpecChanged(p, new_spec); }
|
||||
|
||||
@@ -26,6 +26,8 @@ namespace Playerbot::Hooks {
|
||||
void OnPlayerLogin(Player* p);
|
||||
void OnPlayerLogout(Player* p);
|
||||
void OnLevelUp(Player* p, uint8 new_level);
|
||||
void OnPlayerMounted(Player* p);
|
||||
void OnPlayerDismounted(Player* p);
|
||||
void OnDeath(Unit* victim, Unit* killer);
|
||||
void OnResurrect(Player* p);
|
||||
void OnSpecChanged(Player* p, uint8 new_spec);
|
||||
@@ -199,6 +201,8 @@ void OnPathOutcome(uint8 outcome);
|
||||
inline void OnPlayerLogin(Player*) {}
|
||||
inline void OnPlayerLogout(Player*) {}
|
||||
inline void OnLevelUp(Player*, uint8) {}
|
||||
inline void OnPlayerMounted(Player*) {}
|
||||
inline void OnPlayerDismounted(Player*) {}
|
||||
inline void OnDeath(Unit*, Unit*) {}
|
||||
inline void OnResurrect(Player*) {}
|
||||
inline void OnSpecChanged(Player*, uint8) {}
|
||||
|
||||
@@ -2506,6 +2506,12 @@ void Module::SampleFleetVitals(std::chrono::milliseconds total, uint32 now_ms,
|
||||
// Every hook is wired but inert until the corresponding subsystem lands.
|
||||
// Each one will dispatch to a Fleet or Bot-layer component per FEATURE_MATRIX.md.
|
||||
|
||||
// Riding/mount helpers are defined below (before OnLevelUp); forward-declare
|
||||
// here because OnPlayerLogin and the alt finalize path call them earlier.
|
||||
static uint16 RidingSkillForLevel(uint8 level);
|
||||
static uint32 MountSpellForRidingSkill(uint16 ridingSkill);
|
||||
static void EnsureRidingSkillAndMount(Player* bot);
|
||||
|
||||
void Module::OnPlayerLogin(Player* p)
|
||||
{
|
||||
if (!initialized_ || !p) return;
|
||||
@@ -2597,6 +2603,10 @@ void Module::OnPlayerLogin(Player* p)
|
||||
// for sessionless bots.)
|
||||
p->SetPlayerLocalFlag(PLAYER_LOCAL_FLAG_OVERRIDE_TRANSPORT_SERVER_TIME);
|
||||
|
||||
// Riding + mount: bots that enter the world already >= L20 need their
|
||||
// riding skill and a mount spell (level-ups are covered by OnLevelUp).
|
||||
EnsureRidingSkillAndMount(p);
|
||||
|
||||
// Cross-map travel on-ramp (2026-06-16): ensure the bot KNOWS its faction's
|
||||
// flight paths. The travel graph prunes every taxi edge to a flight master
|
||||
// NOT in the bot's taximask (UnifiedTravelGraph EdgeUsable → IsTaximaskNodeKnown),
|
||||
@@ -2808,12 +2818,130 @@ void Module::OnPlayerLogout(Player* p)
|
||||
TC_LOG_INFO("playerbot.v2", "[PlayerbotV2] Bot AI detached: {} (id {})", p->GetName(), id);
|
||||
}
|
||||
|
||||
void Module::OnLevelUp(Player* /*p*/, uint8 /*new_level*/)
|
||||
// ----------------------------------------------------------------------------
|
||||
// RIDING + MOUNT — bots learn riding skill automatically at the modern
|
||||
// (9.0+/11.0+, client 12.x) thresholds and are taught ONE tier-appropriate
|
||||
// mount spell (data-driven from the Mount DB2). Riding skill 762 is granted
|
||||
// without a trainer visit in this client generation:
|
||||
// L10 -> Journeyman (150) ground +100% (and Skyriding)
|
||||
// L20 -> Expert (225) Steady Flight +220%
|
||||
// L30 -> Master (375) Steady Flight +420%
|
||||
// The bot AI's best_mount_spell scoring picks the fastest known mount, so a
|
||||
// single per-tier mount is all that's needed. Called on bot login, on level
|
||||
// up, and from the alt finalize success path (covers bots already past a
|
||||
// threshold when they (re)enter the world).
|
||||
// ----------------------------------------------------------------------------
|
||||
static uint16 RidingSkillForLevel(uint8 level)
|
||||
{
|
||||
if (!initialized_) return;
|
||||
// Intentionally a no-op. Snapshot's `level` field updates next tick;
|
||||
// newly-trained spells appear in `known_spells` as the bot learns them.
|
||||
// Rules already gate on `knows_spell` so unlocks light up automatically.
|
||||
if (level >= 30) return 375;
|
||||
if (level >= 20) return 225;
|
||||
if (level >= 10) return 150;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Pick the mount spell for the bot's riding tier: the mount whose LOWEST
|
||||
// riding requirement is the highest value <= the bot's skill (so each tier
|
||||
// upgrades). Mounts gated behind a PlayerCondition (race/rep/quest) are
|
||||
// skipped, as are flying-capability-only mounts below Expert (225) riding.
|
||||
static uint32 MountSpellForRidingSkill(uint16 ridingSkill)
|
||||
{
|
||||
uint32 best = 0;
|
||||
uint16 bestReq = 0;
|
||||
for (uint32 id = 0; id < sMountStore.GetNumRows(); ++id)
|
||||
{
|
||||
MountEntry const* m = sMountStore.LookupEntry(id);
|
||||
if (!m || m->SourceSpellID <= 0 || m->PlayerConditionID != 0)
|
||||
continue;
|
||||
auto const* caps = sDB2Manager.GetMountCapabilities(m->MountTypeID);
|
||||
if (!caps || caps->empty())
|
||||
continue;
|
||||
uint16 minReq = UINT16_MAX;
|
||||
bool flight = false;
|
||||
for (MountTypeXCapabilityEntry const* cx : *caps)
|
||||
{
|
||||
MountCapabilityEntry const* cap = sMountCapabilityStore.LookupEntry(cx->MountCapabilityID);
|
||||
if (!cap) continue;
|
||||
minReq = std::min(minReq, cap->ReqRidingSkill);
|
||||
if (cap->ReqSpellAuraID != 0)
|
||||
flight = true; // capability needs the "can fly" aura
|
||||
}
|
||||
if (minReq == UINT16_MAX || minReq > ridingSkill)
|
||||
continue;
|
||||
if (ridingSkill < 225 && flight)
|
||||
continue; // not flying-capable yet — skip flying-only mounts
|
||||
if (best == 0 || minReq > bestReq)
|
||||
{
|
||||
best = m->SourceSpellID;
|
||||
bestReq = minReq;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
static void EnsureRidingSkillAndMount(Player* bot)
|
||||
{
|
||||
if (!bot || !bot->IsInWorld()) return;
|
||||
uint16 const want = RidingSkillForLevel(bot->GetLevel());
|
||||
if (want == 0) return;
|
||||
|
||||
if (bot->GetSkillValue(SKILL_RIDING) < want)
|
||||
bot->SetSkill(SKILL_RIDING, 0, want, want);
|
||||
|
||||
uint32 const mount = MountSpellForRidingSkill(want);
|
||||
if (mount && !bot->HasSpell(mount))
|
||||
bot->LearnSpell(mount, false);
|
||||
}
|
||||
|
||||
void Module::OnLevelUp(Player* p, uint8 /*new_level*/)
|
||||
{
|
||||
if (!initialized_ || !p) return;
|
||||
EnsureRidingSkillAndMount(p);
|
||||
}
|
||||
|
||||
void Module::OnMounted(Player* owner)
|
||||
{
|
||||
if (!initialized_ || !owner) return;
|
||||
WorldSession* sess = owner->GetSession();
|
||||
if (!sess || sess->IsBot()) return;
|
||||
uint32 const account_id = sess->GetAccountId();
|
||||
if (account_id == 0) return;
|
||||
|
||||
auto bots = Services::Altbots().AltsOfAccount(account_id);
|
||||
for (BotId id : bots)
|
||||
{
|
||||
Player* bot = ObjectAccessor::FindConnectedPlayer(
|
||||
ObjectGuid::Create<HighGuid::Player>(id));
|
||||
if (!bot || !bot->IsInWorld()) continue;
|
||||
if (bot->IsInCombat() || bot->IsMounted()) continue;
|
||||
if (!Services::HasIntents(id)) continue;
|
||||
Intent it{};
|
||||
it.bot_id = id;
|
||||
it.body = MountIntent{0}; // 0 = best mount for context
|
||||
Services::Intents(id).push(std::move(it));
|
||||
}
|
||||
}
|
||||
|
||||
void Module::OnDismounted(Player* owner)
|
||||
{
|
||||
if (!initialized_ || !owner) return;
|
||||
WorldSession* sess = owner->GetSession();
|
||||
if (!sess || sess->IsBot()) return;
|
||||
uint32 const account_id = sess->GetAccountId();
|
||||
if (account_id == 0) return;
|
||||
|
||||
auto bots = Services::Altbots().AltsOfAccount(account_id);
|
||||
for (BotId id : bots)
|
||||
{
|
||||
Player* bot = ObjectAccessor::FindConnectedPlayer(
|
||||
ObjectGuid::Create<HighGuid::Player>(id));
|
||||
if (!bot || !bot->IsInWorld()) continue;
|
||||
if (!bot->IsMounted()) continue;
|
||||
if (!Services::HasIntents(id)) continue;
|
||||
Intent it{};
|
||||
it.bot_id = id;
|
||||
it.body = DismountIntent{};
|
||||
Services::Intents(id).push(std::move(it));
|
||||
}
|
||||
}
|
||||
|
||||
void Module::OnDeath(Unit* victim, Unit* /*killer*/)
|
||||
@@ -3841,6 +3969,7 @@ void Module::DrainAltFinalizes(uint32 /*now_ms*/)
|
||||
bot->InitTalentForLevel();
|
||||
bot->SetXP(0);
|
||||
}
|
||||
EnsureRidingSkillAndMount(bot);
|
||||
|
||||
float const ang = owner->GetOrientation() + float(M_PI)
|
||||
+ (float(id % 7) - 3.f) * 0.25f;
|
||||
|
||||
@@ -48,6 +48,8 @@ public:
|
||||
void OnPlayerLogin(Player* p);
|
||||
void OnPlayerLogout(Player* p);
|
||||
void OnLevelUp(Player* p, uint8 new_level);
|
||||
void OnMounted(Player* owner);
|
||||
void OnDismounted(Player* owner);
|
||||
void OnDeath(Unit* victim, Unit* killer);
|
||||
void OnResurrect(Player* p);
|
||||
void OnSpecChanged(Player* p, uint8 new_spec);
|
||||
|
||||
@@ -1396,6 +1396,36 @@ index 1f376653f2..971fb43af8 100644
|
||||
Unit* unit = healer;
|
||||
if (healer && healer->GetTypeId() == TYPEID_UNIT && healer->IsTotem())
|
||||
unit = healer->GetOwner();
|
||||
@@ -8430,6 +8431,15 @@ void Unit::Mount(uint32 mount, uint32 VehicleId, uint32 creatureEntry)
|
||||
|
||||
SetUnitFlag(UNIT_FLAG_MOUNT);
|
||||
|
||||
+#if TRINITY_PLAYERBOT_V2
|
||||
+ // Owner-mount sync (PlayerbotV2): when a human owner mounts a real
|
||||
+ // mount (mount display id != 0 - vehicle mounts pass 0 here), tell all
|
||||
+ // owned bots to mount too so the party travels at the same speed.
|
||||
+ if (Player* mp = ToPlayer(); mp && mount && mp->GetSession() &&
|
||||
+ !mp->GetSession()->IsBot())
|
||||
+ Playerbot::Hooks::OnPlayerMounted(mp);
|
||||
+#endif
|
||||
+
|
||||
CalculateHoverHeight();
|
||||
|
||||
if (Player* player = ToPlayer())
|
||||
@@ -8460,6 +8470,13 @@ void Unit::Dismount()
|
||||
if (!IsMounted())
|
||||
return;
|
||||
|
||||
+#if TRINITY_PLAYERBOT_V2
|
||||
+ // Owner-dismount sync (PlayerbotV2): tell owned bots to dismount when
|
||||
+ // the human owner dismounts (combat, cast, water, manual, ...).
|
||||
+ if (Player* mp = ToPlayer(); mp && mp->GetSession() && !mp->GetSession()->IsBot())
|
||||
+ Playerbot::Hooks::OnPlayerDismounted(mp);
|
||||
+#endif
|
||||
+
|
||||
SetMountDisplayId(0);
|
||||
RemoveUnitFlag(UNIT_FLAG_MOUNT);
|
||||
|
||||
@@ -11459,6 +11478,13 @@ void Unit::SetMeleeAnimKitId(uint16 animKitId)
|
||||
if (attacker && !attacker->IsInMap(victim))
|
||||
attacker = nullptr;
|
||||
|
||||
Reference in New Issue
Block a user