18 KiB
Playerbot Weighting System Integration Guide
Overview
This guide explains how to integrate the utility-based weighting system with ClassAI specializations. The weighting system enables intelligent multi-criteria decision-making, allowing bots to make human-like choices by evaluating actions across 6 categories.
Reference Implementation: src/modules/Playerbot/AI/ClassAI/Mages/ArcaneMageWeighted.h
Quick Start (5 Steps)
- Include Headers
- Add ActionScoringEngine Member
- Initialize in Constructor
- Create Scoring Functions
- Replace Rotation Logic
Step 1: Include Required Headers
#include "../../Common/ActionScoringEngine.h"
#include "../../Common/CombatContextDetector.h"
Step 2: Add ActionScoringEngine Member
Add to your class definition:
class YourSpecRefactored : public SpecializationBase
{
private:
bot::ai::ActionScoringEngine _scoringEngine;
uint32 _lastContextUpdate;
uint32 _contextUpdateInterval; // e.g., 5000ms
};
Step 3: Initialize in Constructor
YourSpecRefactored(Player* bot)
: SpecializationBase(bot)
, _scoringEngine(
bot::ai::BotRole::RANGED_DPS, // Or TANK, HEALER, MELEE_DPS
bot::ai::CombatContext::SOLO) // Initial context
, _lastContextUpdate(0)
, _contextUpdateInterval(5000)
{
// Enable debug logging if configured
bool debugLogging = sConfigMgr->GetBoolDefault("Playerbot.AI.Weighting.LogScoring", false);
_scoringEngine.EnableDebugLogging(debugLogging);
}
Step 4: Create Scoring Functions
4.1 Main Scoring Entry Point
float ScoreAction(
uint32 actionId,
bot::ai::ScoringCategory category,
/* ... spec-specific state parameters ... */) const
{
using bot::ai::ScoringCategory;
switch (category)
{
case ScoringCategory::SURVIVAL:
return ScoreSurvival(actionId, healthPercent);
case ScoringCategory::GROUP_PROTECTION:
return ScoreGroupProtection(actionId, allyHealth);
case ScoringCategory::DAMAGE_OPTIMIZATION:
return ScoreDamage(actionId, resources, enemyCount);
case ScoringCategory::RESOURCE_EFFICIENCY:
return ScoreResource(actionId, manaPercent, comboPoints);
case ScoringCategory::POSITIONING_MECHANICS:
return ScorePositioning(actionId, range, inMechanic);
case ScoringCategory::STRATEGIC_VALUE:
return ScoreStrategic(actionId, fightPhase, cooldownsReady);
default:
return 0.0f;
}
}
4.2 Category-Specific Scoring Functions
Survival Scoring (All Roles)
float ScoreSurvival(uint32 actionId, float healthPercent) const
{
float urgency = (100.0f - healthPercent) / 100.0f; // 0.0-1.0
switch (actionId)
{
case DEFENSIVE_COOLDOWN_1:
if (healthPercent < 20.0f)
return 1.0f; // Critical - maximum urgency
else if (healthPercent < 40.0f)
return 0.7f * urgency;
else
return 0.0f; // Not needed
case HEAL_SELF:
return urgency; // Linear scaling with health loss
case SHIELD:
if (healthPercent < 60.0f)
return 0.5f + (0.5f * urgency);
else
return 0.0f;
default:
return 0.0f; // No survival value
}
}
Damage Optimization (DPS Priority)
float ScoreDamage(uint32 actionId, uint32 resources, uint32 enemyCount) const
{
bool isAoE = (enemyCount >= 3);
switch (actionId)
{
case MAJOR_COOLDOWN:
{
// Example: Score based on resource state and cooldown readiness
float resourceValue = static_cast<float>(resources) / GetMaxResource();
float contextBonus = 0.7f;
// Higher value in important contexts
bot::ai::CombatContext context = _scoringEngine.GetContext();
if (context == bot::ai::CombatContext::RAID_HEROIC ||
context == bot::ai::CombatContext::DUNGEON_BOSS)
contextBonus = 1.0f;
return resourceValue * contextBonus;
}
case BUILDER:
{
if (resources < GetMaxResource())
return 0.6f; // Good value when building
else
return 0.3f; // Lower value at cap
}
case SPENDER:
{
if (resources >= GetMaxResource() * 0.8f)
return 0.9f; // High value near cap
else if (resources >= GetMaxResource() * 0.5f)
return 0.5f; // Medium value
else
return 0.2f; // Low value, not enough resources
}
case AOE_ABILITY:
{
if (isAoE && enemyCount >= 5)
return 0.9f;
else if (isAoE)
return 0.6f;
else
return 0.1f; // Poor single target
}
default:
return 0.0f;
}
}
Resource Efficiency (Healer/Caster Priority)
float ScoreResource(uint32 actionId, uint32 manaPercent, uint32 comboPoints) const
{
switch (actionId)
{
case MANA_RECOVERY:
{
if (manaPercent < 20)
return 1.0f; // Critical recovery needed
else if (manaPercent < 40)
return 0.6f;
else
return 0.0f; // Not needed
}
case FREE_PROC_ABILITY:
{
// Free cast = perfect efficiency
if (HasProc())
return 1.0f;
else
return 0.0f; // Should never cast without proc
}
case EXPENSIVE_ABILITY:
{
if (manaPercent > 60)
return 0.7f; // Can afford it
else if (manaPercent > 30)
return 0.4f; // Risky but possible
else
return 0.0f; // Too expensive
}
case EFFICIENT_FILLER:
{
return 0.8f; // Always good efficiency
}
default:
return 0.5f; // Neutral efficiency
}
}
Group Protection (Healer/Tank Priority)
float ScoreGroupProtection(uint32 actionId, std::vector<AllyHealth> allies) const
{
switch (actionId)
{
case SINGLE_TARGET_HEAL:
{
// Find most urgent ally
AllyHealth* mostUrgent = FindMostUrgentAlly(allies);
if (!mostUrgent)
return 0.0f;
float urgency = (100.0f - mostUrgent->healthPct) / 100.0f;
float rolePriority = mostUrgent->isTank ? 2.0f : 1.0f;
return urgency * rolePriority;
}
case AOE_HEAL:
{
// Count allies below threshold
uint32 lowHealthCount = CountAlliesBelowHealth(allies, 70.0f);
if (lowHealthCount >= 4)
return 0.9f;
else if (lowHealthCount >= 2)
return 0.6f;
else
return 0.2f;
}
case INTERRUPT:
{
if (IsEnemyCastingDangerous())
return 1.0f; // Maximum urgency
else if (IsEnemyCasting())
return 0.6f;
else
return 0.0f; // No cast to interrupt
}
case DISPEL:
{
uint32 debuffCount = CountDispellableDebuffs(allies);
return std::min(static_cast<float>(debuffCount) * 0.3f, 1.0f);
}
default:
return 0.0f;
}
}
Strategic Value (All Roles)
float ScoreStrategic(uint32 actionId, uint32 fightPhase, bool cooldownsReady) const
{
bot::ai::CombatContext context = _scoringEngine.GetContext();
switch (actionId)
{
case MAJOR_COOLDOWN:
{
// Save for important fights
if (context == bot::ai::CombatContext::DUNGEON_BOSS ||
context == bot::ai::CombatContext::RAID_NORMAL ||
context == bot::ai::CombatContext::RAID_HEROIC)
return 1.0f; // Use on bosses
else if (context == bot::ai::CombatContext::DUNGEON_TRASH)
return 0.3f; // Save for boss
else
return 0.5f; // Use freely in solo/group
}
case HEROISM:
{
// Only use on critical encounters
if (context == bot::ai::CombatContext::RAID_HEROIC && fightPhase == BURN_PHASE)
return 1.0f;
else if (context == bot::ai::CombatContext::RAID_NORMAL && fightPhase == BURN_PHASE)
return 0.8f;
else
return 0.0f; // Don't waste
}
case RESOURCE_GENERATOR:
{
// Strategic value of generators before burst
if (cooldownsReady && GetResourcePercent() < 80)
return 0.7f; // Prepare for burst
else
return 0.3f;
}
default:
return 0.5f; // Neutral strategic value
}
}
Step 5: Replace Rotation Logic
5.1 Update Context Periodically
void UpdateCombatContext()
{
uint32 currentTime = getMSTime();
if (currentTime - _lastContextUpdate < _contextUpdateInterval)
return; // Not time to update yet
_lastContextUpdate = currentTime;
// Detect new context
bot::ai::CombatContext newContext = bot::ai::CombatContextDetector::DetectContext(this->GetBot());
// Update scoring engine if context changed
if (newContext != _scoringEngine.GetContext())
{
_scoringEngine.SetContext(newContext);
TC_LOG_DEBUG("playerbot", "YourSpec: Context changed to {}",
bot::ai::ActionScoringEngine::GetContextName(newContext));
}
}
5.2 Replace Rotation with Weighted Selection
Before (Order-Based):
void ExecuteRotation(::Unit* target)
{
// Old way: Hard-coded priority order
if (CanUse(MAJOR_COOLDOWN))
return Use(MAJOR_COOLDOWN);
if (CanUse(BUILDER))
return Use(BUILDER);
if (CanUse(SPENDER))
return Use(SPENDER);
// First viable option wins
}
After (Weight-Based):
void ExecuteWeightedRotation(::Unit* target)
{
Player* bot = this->GetBot();
if (!bot)
return;
// Gather current state
uint32 resources = GetCurrentResources();
uint32 manaPercent = bot->GetPowerPct(POWER_MANA);
float healthPercent = bot->GetHealthPct();
uint32 enemyCount = this->GetEnemiesInRange(40.0f);
// Build list of available actions
std::vector<uint32> availableActions;
if (this->IsSpellReady(BUILDER))
availableActions.push_back(BUILDER);
if (this->IsSpellReady(SPENDER))
availableActions.push_back(SPENDER);
if (this->IsSpellReady(MAJOR_COOLDOWN))
availableActions.push_back(MAJOR_COOLDOWN);
// Add defensives if needed
if (healthPercent < 50.0f)
{
if (this->IsSpellReady(DEFENSIVE_CD))
availableActions.push_back(DEFENSIVE_CD);
}
if (availableActions.empty())
return;
// Score all available actions
std::vector<bot::ai::ActionScore> scores = _scoringEngine.ScoreActions(
availableActions,
[this, resources, manaPercent, healthPercent, enemyCount]
(bot::ai::ScoringCategory category, uint32 actionId) -> float
{
return ScoreAction(actionId, category, resources, manaPercent, healthPercent, enemyCount);
}
);
// Execute best action
uint32 bestAction = _scoringEngine.GetBestAction(scores);
if (bestAction != 0)
{
ExecuteAction(bestAction, target);
}
}
Configuration
Enable Weighting System
Edit playerbots.conf:
# Enable weighting system
Playerbot.AI.Weighting.Enable = 1
# Enable debug logging (optional)
Playerbot.AI.Weighting.LogScoring = 1
Playerbot.AI.Weighting.LogTopActions = 3
Tune Weights (Optional)
Adjust base weights for your spec:
# Increase damage weight for pure DPS specs
Playerbot.AI.Weighting.DamageWeight = 180
# Increase group protection for healers
Playerbot.AI.Weighting.GroupProtectionWeight = 200
Testing Your Integration
1. Enable Debug Logging
Playerbot.AI.Weighting.LogScoring = 1
2. Check Logs
playerbot.weighting: ActionScoringEngine: Scored action 12345 = 287.50
Role: Ranged DPS, Context: Dungeon Boss
Category Breakdown:
Damage : 245.00 (weight: 270.00)
Resource : 42.50 (weight: 85.00)
3. Verify Context Detection
TC_LOG_DEBUG("playerbot", "Current context: {}",
bot::ai::CombatContextDetector::GetContextDescription(_scoringEngine.GetContext()));
Expected output:
playerbot: Current context: Dungeon Boss (5-man instance, boss encounter)
4. Test Decision Quality
Monitor bot decisions in various contexts:
- Solo questing: Should prioritize survival and damage
- Dungeon trash: Should prioritize AoE damage
- Dungeon boss: Should save cooldowns, use defensives wisely
- Raid: Should coordinate with group, follow mechanics
Common Patterns
Pattern 1: Cooldown with Conditions
case MAJOR_OFFENSIVE_CD:
{
float score = 0.0f;
// Damage category
if (category == ScoringCategory::DAMAGE_OPTIMIZATION)
{
// Require resources
if (resources >= GetMaxResource() * 0.8f)
score += 0.4f;
// Require target count (for AoE CDs)
if (enemyCount >= 3)
score += 0.3f;
// Context bonus
bot::ai::CombatContext ctx = _scoringEngine.GetContext();
if (ctx == bot::ai::CombatContext::RAID_HEROIC)
score += 0.3f;
return std::clamp(score, 0.0f, 1.0f);
}
return 0.0f;
}
Pattern 2: Emergency Defensive
case EMERGENCY_DEFENSIVE:
{
if (category == ScoringCategory::SURVIVAL)
{
if (healthPercent < 15.0f)
return 1.0f; // Always use when critical
// Otherwise not needed
return 0.0f;
}
return 0.0f;
}
Pattern 3: Resource Builder
case RESOURCE_BUILDER:
{
if (category == ScoringCategory::DAMAGE_OPTIMIZATION)
{
if (resources < GetMaxResource())
return 0.6f; // Good when building
else
return 0.2f; // Low value at cap
}
if (category == ScoringCategory::RESOURCE_EFFICIENCY)
{
// Efficient builder (low cost)
return 0.8f;
}
return 0.0f;
}
Pattern 4: Heal Target Selection
case SINGLE_TARGET_HEAL:
{
if (category == ScoringCategory::GROUP_PROTECTION)
{
// Get most urgent ally
AllyHealth* target = FindMostUrgentAlly();
if (!target)
return 0.0f;
float urgency = (100.0f - target->healthPct) / 100.0f;
// Tank priority
if (target->isTank)
urgency *= 2.0f;
return std::clamp(urgency, 0.0f, 1.0f);
}
return 0.0f;
}
Performance Considerations
CPU Cost
- Per Action Scoring: ~64 operations (~1-2 microseconds)
- 10 Actions per Frame: ~640 operations (~10-20 microseconds)
- Expected Overhead: <5% CPU for bot AI
Memory Cost
- ActionScoringEngine: ~36 bytes per bot
- 5000 Bots: ~180 KB total (negligible)
Optimization Tips
-
Limit Available Actions:
// Only add actions that are actually castable if (this->IsSpellReady(SPELL) && HasEnoughMana(SPELL)) availableActions.push_back(SPELL); -
Cache Context Detection:
// Update context every 5 seconds, not every frame if (currentTime - _lastContextUpdate >= 5000) UpdateCombatContext(); -
Skip Zero Categories:
// Return early for irrelevant categories if (category == ScoringCategory::GROUP_PROTECTION && !isHealer && !isTank) return 0.0f; // DPS doesn't score group protection
Troubleshooting
Issue: Bot always chooses same action
Cause: One action scores much higher than others
Solution: Review scoring functions, ensure proper value ranges (0.0-1.0)
// Bad: Always returns 1.0
return 1.0f;
// Good: Scale based on conditions
return std::clamp(urgency * modifier, 0.0f, 1.0f);
Issue: Bot ignores important actions
Cause: Action not in available actions list OR scores too low
Solution:
- Check
IsSpellReady()conditions - Enable debug logging to see scores
- Adjust category weights or scoring values
Issue: Performance degradation with many bots
Cause: Scoring too many actions per frame
Solution:
- Limit
availableActionslist - Skip scoring actions that are on cooldown
- Increase context update interval
Reference Implementation
See complete example:
- File:
src/modules/Playerbot/AI/ClassAI/Mages/ArcaneMageWeighted.h - Lines: 1-700+ (full spec with scoring)
Key sections:
- Constructor: Lines 39-48
- Context update: Lines 60-75
- Weighted rotation: Lines 80-145
- Scoring functions: Lines 200-520
- Action execution: Lines 550-580
FAQ
Q: Do I need to convert all specs at once? A: No! The system is backward compatible. Convert specs incrementally.
Q: Can I mix weighted and non-weighted logic? A: Yes! Use weighted selection for complex decisions, keep simple logic simple.
Q: How do I tune weights for my spec? A: Start with default weights, enable debug logging, observe bot behavior, adjust scoring functions.
Q: What if my spec doesn't fit the 6 categories? A: Return 0.0f for irrelevant categories. Focus on categories that matter for your spec.
Q: Can I add custom categories? A: Not easily. The 6 categories cover all common scenarios. Use strategic value for spec-specific logic.
Next Steps
- Choose a spec to integrate (start with one you know well)
- Follow the 5-step integration pattern
- Enable debug logging and test in solo content
- Tune scoring functions based on observed behavior
- Test in group/dungeon/raid content
- Disable debug logging for production
Happy coding!