Files
mod-playerbots/src/Ai/Base/Actions/LootRollAction.cpp
Hokken e9e79ad696 Fix LootRollLevel=1 to match documented 'greed' behavior (#2068)
## Summary

Fixes `AiPlayerbot.LootRollLevel = 1` to actually behave as "greed" mode
per the config documentation.

## Problem

The config documentation states:
```conf
# Bots' loot roll level (0 = pass, 1 = greed, 2 = need)
# Default: 1 (greed)
AiPlayerbot.LootRollLevel = 1
```

However, level 1 was converting **all GREED votes to PASS**, causing
bots to pass on almost everything:

| Item Type | AI Decision | Level 1 Behavior (Before) | Expected |
|-----------|-------------|---------------------------|----------|
| Gear upgrade | NEED | GREED ✓ | GREED |
| Usable gear (not upgrade) | GREED | **PASS** ✗ | GREED |
| Crafting materials | GREED | **PASS** ✗ | GREED |
| Recipes, consumables | GREED | **PASS** ✗ | GREED |

The only items bots would greed on were direct gear upgrades (originally
NEED, downgraded to GREED).

## Root Cause

In `LootRollAction.cpp`, lines 104-107 were converting GREED to PASS:

```cpp
else if (vote == GREED)
{
    vote = PASS;  // This breaks "greed" mode
}
```

## Fix

Remove the GREED→PASS conversion. Level 1 now only downgrades NEED to
GREED (as intended), preserving GREED votes for useful items.

## Behavior After Fix

| Level | Description | Behavior |
|-------|-------------|----------|
| 0 | Pass | Always pass on all items |
| 1 | Greed | Greed on useful items, never need |
| 2 | Need | Full AI logic (need/greed/pass) |

## Test Plan

- [ ] Set `AiPlayerbot.LootRollLevel = 1`
- [ ] Kill mobs that drop crafting materials, recipes, or non-upgrade
gear
- [ ] Verify bots greed on useful items instead of passing
- [ ] Verify bots still pass on junk items
- [ ] Verify bots never roll need (only greed)

Co-authored-by: Hokken <Hokken@users.noreply.github.com>
2026-02-08 12:45:03 +01:00

261 lines
7.8 KiB
C++

/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#include "LootRollAction.h"
#include "Event.h"
#include "Group.h"
#include "ItemUsageValue.h"
#include "LootAction.h"
#include "ObjectMgr.h"
#include "PlayerbotAIConfig.h"
#include "Playerbots.h"
bool LootRollAction::Execute(Event event)
{
Group* group = bot->GetGroup();
if (!group)
return false;
std::vector<Roll*> rolls = group->GetRolls();
for (Roll*& roll : rolls)
{
if (roll->playerVote.find(bot->GetGUID())->second != NOT_EMITED_YET)
{
continue;
}
ObjectGuid guid = roll->itemGUID;
uint32 slot = roll->itemSlot;
uint32 itemId = roll->itemid;
int32 randomProperty = 0;
if (roll->itemRandomPropId)
randomProperty = roll->itemRandomPropId;
else if (roll->itemRandomSuffix)
randomProperty = -((int)roll->itemRandomSuffix);
RollVote vote = PASS;
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
if (!proto)
continue;
std::string itemUsageParam;
if (randomProperty != 0)
{
itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty);
}
else
{
itemUsageParam = std::to_string(itemId);
}
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam);
// Armor Tokens are classed as MISC JUNK (Class 15, Subclass 0), luckily no other items I found have class bits and epic quality.
if (proto->Class == ITEM_CLASS_MISC && proto->SubClass == ITEM_SUBCLASS_JUNK && proto->Quality == ITEM_QUALITY_EPIC)
{
if (CanBotUseToken(proto, bot))
{
vote = NEED; // Eligible for "Need"
}
else
{
vote = GREED; // Not eligible, so "Greed"
}
}
else
{
switch (proto->Class)
{
case ITEM_CLASS_WEAPON:
case ITEM_CLASS_ARMOR:
if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP)
{
vote = NEED;
}
else if (usage != ITEM_USAGE_NONE)
{
vote = GREED;
}
break;
default:
if (StoreLootAction::IsLootAllowed(itemId, botAI))
vote = CalculateRollVote(proto); // Ensure correct Need/Greed behavior
break;
}
}
if (sPlayerbotAIConfig.lootRollLevel == 0)
{
vote = PASS;
}
else if (sPlayerbotAIConfig.lootRollLevel == 1)
{
// Level 1 = "greed" mode: bots greed on useful items but never need
// Only downgrade NEED to GREED, preserve GREED votes as-is
if (vote == NEED)
{
if (RollUniqueCheck(proto, bot))
{
vote = PASS;
}
else
{
vote = GREED;
}
}
}
switch (group->GetLootMethod())
{
case MASTER_LOOT:
case FREE_FOR_ALL:
group->CountRollVote(bot->GetGUID(), guid, PASS);
break;
default:
group->CountRollVote(bot->GetGUID(), guid, vote);
break;
}
// One item at a time
return true;
}
return false;
}
RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto)
{
std::ostringstream out;
out << proto->ItemId;
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", out.str());
RollVote needVote = PASS;
switch (usage)
{
case ITEM_USAGE_EQUIP:
case ITEM_USAGE_REPLACE:
case ITEM_USAGE_GUILD_TASK:
case ITEM_USAGE_BAD_EQUIP:
needVote = NEED;
break;
case ITEM_USAGE_SKILL:
case ITEM_USAGE_USE:
case ITEM_USAGE_DISENCHANT:
case ITEM_USAGE_AH:
case ITEM_USAGE_VENDOR:
needVote = GREED;
break;
default:
break;
}
return StoreLootAction::IsLootAllowed(proto->ItemId, GET_PLAYERBOT_AI(bot)) ? needVote : PASS;
}
bool MasterLootRollAction::isUseful() { return !botAI->HasActivePlayerMaster(); }
bool MasterLootRollAction::Execute(Event event)
{
Player* bot = QueryItemUsageAction::botAI->GetBot();
WorldPacket p(event.getPacket()); // WorldPacket packet for CMSG_LOOT_ROLL, (8+4+1)
ObjectGuid creatureGuid;
uint32 mapId;
uint32 itemSlot;
uint32 itemId;
uint32 randomSuffix;
uint32 randomPropertyId;
uint32 count;
uint32 timeout;
p.rpos(0); // reset packet pointer
p >> creatureGuid; // creature guid what we're looting
p >> mapId; /// 3.3.3 mapid
p >> itemSlot; // the itemEntryId for the item that shall be rolled for
p >> itemId; // the itemEntryId for the item that shall be rolled for
p >> randomSuffix; // randomSuffix
p >> randomPropertyId; // item random property ID
p >> count; // items in stack
p >> timeout; // the countdown time to choose "need" or "greed"
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
if (!proto)
return false;
Group* group = bot->GetGroup();
if (!group)
return false;
RollVote vote = CalculateRollVote(proto);
group->CountRollVote(bot->GetGUID(), creatureGuid, CalculateRollVote(proto));
return true;
}
bool CanBotUseToken(ItemTemplate const* proto, Player* bot)
{
// Get the bitmask for the bot's class
uint32 botClassMask = (1 << (bot->getClass() - 1));
// Check if the bot's class is allowed to use the token
if (proto->AllowableClass & botClassMask)
{
return true; // Bot's class is eligible to use this token
}
return false; // Bot's class cannot use this token
}
bool RollUniqueCheck(ItemTemplate const* proto, Player* bot)
{
// Count the total number of the item (equipped + in bags)
uint32 totalItemCount = bot->GetItemCount(proto->ItemId, true);
// Count the number of the item in bags only
uint32 bagItemCount = bot->GetItemCount(proto->ItemId, false);
// Determine if the unique item is already equipped
bool isEquipped = (totalItemCount > bagItemCount);
if (isEquipped && proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE))
{
return true; // Unique Item is already equipped
}
else if (proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE) && (bagItemCount > 1))
{
return true; // Unique item already in bag, don't roll for it
}
return false; // Item is not equipped or in bags, roll for it
}
bool RollAction::Execute(Event event)
{
std::string link = event.getParam();
if (link.empty())
{
bot->DoRandomRoll(0,100);
return false;
}
ItemIds itemIds = chat->parseItems(link);
if (itemIds.empty())
return false;
uint32 itemId = *itemIds.begin();
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
if (!proto)
{
return false;
}
std::string itemUsageParam;
itemUsageParam = std::to_string(itemId);
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam);
switch (proto->Class)
{
case ITEM_CLASS_WEAPON:
case ITEM_CLASS_ARMOR:
if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP)
{
bot->DoRandomRoll(0,100);
}
}
return true;
}