feat: enemies now use weights on their attacks (#2004)

* feat: enemies now use weights on their attacks

tested that 8 times out of 10, in close range, spiders did a web attack instead of a melee attack, vs the prior behavior of always following a pattern

fixes #2002

* feedback
This commit is contained in:
David Markowitz
2026-06-18 23:27:14 -07:00
committed by GitHub
parent 0f17e1de3b
commit ce9d4e823c
4 changed files with 68 additions and 36 deletions

View File

@@ -38,3 +38,11 @@ std::vector<CDObjectSkills> CDObjectSkillsTable::Query(std::function<bool(CDObje
return data; return data;
} }
std::vector<CDObjectSkills> CDObjectSkillsTable::Get(const LOT lot) const {
std::vector<CDObjectSkills> toReturn;
for (const auto& entry : GetEntries()) {
if (entry.objectTemplate == lot) toReturn.push_back(entry);
}
return toReturn;
}

View File

@@ -4,12 +4,13 @@
#include "CDTable.h" #include "CDTable.h"
#include <cstdint> #include <cstdint>
#include <vector>
struct CDObjectSkills { struct CDObjectSkills {
uint32_t objectTemplate; //!< The LOT of the item uint32_t objectTemplate; //!< The LOT of the item
uint32_t skillID; //!< The Skill ID of the object uint32_t skillID; //!< The Skill ID of the object
uint32_t castOnType; //!< ??? uint32_t castOnType; //!< ???
uint32_t AICombatWeight; //!< ??? int32_t AICombatWeight; //!< ???
}; };
class CDObjectSkillsTable : public CDTable<CDObjectSkillsTable, std::vector<CDObjectSkills>> { class CDObjectSkillsTable : public CDTable<CDObjectSkillsTable, std::vector<CDObjectSkills>> {
@@ -17,5 +18,6 @@ public:
void LoadValuesFromDatabase(); void LoadValuesFromDatabase();
// Queries the table with a custom "where" clause // Queries the table with a custom "where" clause
std::vector<CDObjectSkills> Query(std::function<bool(CDObjectSkills)> predicate); std::vector<CDObjectSkills> Query(std::function<bool(CDObjectSkills)> predicate);
std::vector<CDObjectSkills> Get(const LOT lot) const;
}; };

View File

@@ -13,6 +13,8 @@
#include "CDClientDatabase.h" #include "CDClientDatabase.h"
#include "CDClientManager.h" #include "CDClientManager.h"
#include "CDObjectSkillsTable.h"
#include "CDSkillBehaviorTable.h"
#include "DestroyableComponent.h" #include "DestroyableComponent.h"
#include <algorithm> #include <algorithm>
@@ -43,7 +45,7 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t compo
//Grab the aggro information from BaseCombatAI: //Grab the aggro information from BaseCombatAI:
auto componentQuery = CDClientDatabase::CreatePreppedStmt( auto componentQuery = CDClientDatabase::CreatePreppedStmt(
"SELECT aggroRadius, tetherSpeed, pursuitSpeed, softTetherRadius, hardTetherRadius FROM BaseCombatAIComponent WHERE id = ?;"); "SELECT aggroRadius, tetherSpeed, pursuitSpeed, softTetherRadius, hardTetherRadius, minRoundLength, maxRoundLength, combatRoundLength FROM BaseCombatAIComponent WHERE id = ?;");
componentQuery.bind(1, static_cast<int>(componentID)); componentQuery.bind(1, static_cast<int>(componentID));
auto componentResult = componentQuery.execQuery(); auto componentResult = componentQuery.execQuery();
@@ -63,44 +65,37 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t compo
if (!componentResult.fieldIsNull("hardTetherRadius")) if (!componentResult.fieldIsNull("hardTetherRadius"))
m_HardTetherRadius = componentResult.getFloatField("hardTetherRadius"); m_HardTetherRadius = componentResult.getFloatField("hardTetherRadius");
}
componentResult.finalize(); m_MinRoundLength = componentResult.getFloatField("minRoundLength");
m_MaxRoundLength = componentResult.getFloatField("maxRoundLength");
m_CombatRoundLength = componentResult.getFloatField("combatRoundLength");
}
// Get aggro and tether radius from settings and use this if it is present. Only overwrite the // Get aggro and tether radius from settings and use this if it is present. Only overwrite the
// radii if it is greater than the one in the database. // radii if it is greater than the one in the database.
if (m_Parent) { m_AggroRadius = m_Parent->HasVar(u"aggroRadius") ? m_Parent->GetVar<float>(u"aggroRadius") : m_AggroRadius;
auto aggroRadius = m_Parent->GetVar<float>(u"aggroRadius"); m_HardTetherRadius = m_Parent->HasVar(u"tetherRadius") ? m_Parent->GetVar<float>(u"tetherRadius") : m_HardTetherRadius;
m_AggroRadius = aggroRadius != 0 ? aggroRadius : m_AggroRadius;
auto tetherRadius = m_Parent->GetVar<float>(u"tetherRadius");
m_HardTetherRadius = tetherRadius != 0 ? tetherRadius : m_HardTetherRadius;
}
/* /*
* Find skills * Find skills
*/ */
auto skillQuery = CDClientDatabase::CreatePreppedStmt( for (const auto objectSkill : CDClientManager::GetTable<CDObjectSkillsTable>()->Get(parent->GetLOT())) {
"SELECT skillID, cooldown, behaviorID FROM SkillBehavior WHERE skillID IN (SELECT skillID FROM ObjectSkills WHERE objectTemplate = ?);"); const auto skillBehavior = CDClientManager::GetTable<CDSkillBehaviorTable>()->GetSkillByID(objectSkill.skillID);
skillQuery.bind(1, static_cast<int>(parent->GetLOT())); if (skillBehavior.skillID == objectSkill.skillID) {
const auto skillId = skillBehavior.skillID;
auto result = skillQuery.execQuery(); const auto abilityCooldown = skillBehavior.cooldown;
while (!result.eof()) { const auto behaviorId = skillBehavior.behaviorID;
const auto skillId = static_cast<uint32_t>(result.getIntField("skillID"));
const auto abilityCooldown = static_cast<float>(result.getFloatField("cooldown")); const auto combatWeight = objectSkill.AICombatWeight;
const auto behaviorId = static_cast<uint32_t>(result.getIntField("behaviorID"));
auto* behavior = Behavior::CreateBehavior(behaviorId); auto* behavior = Behavior::CreateBehavior(behaviorId);
std::stringstream behaviorQuery; AiSkillEntry entry = { .skillId = skillId, .cooldown = 0.0f, .abilityCooldown = abilityCooldown, .behavior = behavior, .combatWeight = combatWeight };
AiSkillEntry entry = { skillId, 0, abilityCooldown, behavior };
m_SkillEntries.push_back(entry); m_SkillEntries.push_back(entry);
}
result.nextRow();
} }
Stun(1.0f); Stun(1.0f);
@@ -248,10 +243,12 @@ void BaseCombatAIComponent::Update(const float deltaTime) {
void BaseCombatAIComponent::CalculateCombat(const float deltaTime) { void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
bool hasSkillToCast = false; bool hasSkillToCast = false;
int32_t maxSkillWeights = 0;
for (auto& entry : m_SkillEntries) { for (auto& entry : m_SkillEntries) {
if (entry.cooldown > 0.0f) { if (entry.cooldown > 0.0f) {
entry.cooldown -= deltaTime; entry.cooldown -= deltaTime;
} else { } else {
maxSkillWeights += entry.combatWeight;
hasSkillToCast = true; hasSkillToCast = true;
} }
} }
@@ -337,10 +334,19 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
LookAt(target->GetPosition()); LookAt(target->GetPosition());
} }
for (auto i = 0; i < m_SkillEntries.size(); ++i) { // Roll to find which skill we'll try to cast
auto entry = m_SkillEntries.at(i); auto randomizedWeight = GeneralUtils::GenerateRandomNumber<int32_t>(0, maxSkillWeights);
if (entry.cooldown > 0) { for (auto& entry : m_SkillEntries) {
// Skill isn't cooled off yet
if (entry.cooldown > 0.0f) {
continue;
}
randomizedWeight -= entry.combatWeight;
// if the weight is still greater than 0 continue to the next rolled skill
if (randomizedWeight > 0) {
continue; continue;
} }
@@ -359,8 +365,6 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
entry.cooldown = entry.abilityCooldown + m_SkillTime; entry.cooldown = entry.abilityCooldown + m_SkillTime;
m_SkillEntries[i] = entry;
break; break;
} }
} }
@@ -914,8 +918,16 @@ bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReport
} }
auto& ignoredThreats = cmptType.PushDebug("Temp Ignored Threats"); auto& ignoredThreats = cmptType.PushDebug("Temp Ignored Threats");
for (const auto& [id, threat] : m_ThreatEntries) { for (const auto& [id, threat] : m_RemovedThreatList) {
ignoredThreats.PushDebug<AMFDoubleValue>(std::to_string(id) + " - Time") = threat; ignoredThreats.PushDebug<AMFDoubleValue>(std::to_string(id) + " - Time") = threat;
} }
auto& skillInfo = cmptType.PushDebug("Skill Info");
for (const auto& skill : m_SkillEntries) {
auto& skillDebug = skillInfo.PushDebug("Skill ID " + std::to_string(skill.skillId));
skillDebug.PushDebug<AMFDoubleValue>("Cooldown") = skill.cooldown;
skillDebug.PushDebug<AMFDoubleValue>("Ability Cooldown") = skill.abilityCooldown;
skillDebug.PushDebug<AMFIntValue>("AI Combat Weight") = skill.combatWeight;
}
return true; return true;
} }

View File

@@ -33,13 +33,15 @@ enum class AiState : uint32_t {
*/ */
struct AiSkillEntry struct AiSkillEntry
{ {
uint32_t skillId; uint32_t skillId{};
float cooldown; float cooldown{};
float abilityCooldown; float abilityCooldown{};
Behavior* behavior; Behavior* behavior{};
int32_t combatWeight{};
}; };
/** /**
@@ -396,9 +398,17 @@ private:
*/ */
bool m_DirtyStateOrTarget = false; bool m_DirtyStateOrTarget = false;
// Min amount of time to remain as in combat after casting a skill
float m_MinRoundLength = 0.0f;
// max amount of time to remain as in combat after casting a skill
float m_MaxRoundLength = 0.0f;
// The amount of time the entity will be forced to tether for // The amount of time the entity will be forced to tether for
float m_ForcedTetherTime = 0.0f; float m_ForcedTetherTime = 0.0f;
float m_CombatRoundLength = 0.0f;
// The amount of time a removed threat will be ignored for. // The amount of time a removed threat will be ignored for.
std::map<LWOOBJID, float> m_RemovedThreatList; std::map<LWOOBJID, float> m_RemovedThreatList;