Fixes for the bots.

This commit is contained in:
Justin Marshall
2026-05-19 19:43:23 -07:00
parent af56d4bfeb
commit ed6b826d23
15 changed files with 1419 additions and 1103 deletions
+318 -33
View File
@@ -9,6 +9,143 @@ idCVar bot_itemsfile( "bot_itemsfile", "items.c", CVAR_CHEAT, "" );
idBotGoalManager botGoalManager;
static bool BotGoalItemNameContains( const iteminfo_t* itemInfo, const char* needle ) {
if( itemInfo == NULL || needle == NULL ) {
return false;
}
return strstr( itemInfo->classname.c_str(), needle ) != NULL || strstr( itemInfo->name.c_str(), needle ) != NULL;
}
static bool BotGoalHasUsableWeapon( const int* inventory ) {
if( inventory == NULL ) {
return false;
}
return ( inventory[INVENTORY_ROCKETLAUNCHER] > 0 && inventory[INVENTORY_ROCKETS] > 0 ) ||
( inventory[INVENTORY_PLASMAGUN] > 0 && inventory[INVENTORY_CELLS] > 0 ) ||
( inventory[INVENTORY_SHOTGUN] > 0 && inventory[INVENTORY_SHELLS] > 0 ) ||
( inventory[INVENTORY_MACHINEGUN] > 0 && inventory[INVENTORY_BULLETS] > 0 );
}
static bool BotGoalHasPowerWeapon( const int* inventory ) {
if( inventory == NULL ) {
return false;
}
return ( inventory[INVENTORY_ROCKETLAUNCHER] > 0 && inventory[INVENTORY_ROCKETS] > 0 ) ||
( inventory[INVENTORY_PLASMAGUN] > 0 && inventory[INVENTORY_CELLS] > 0 ) ||
( inventory[INVENTORY_SHOTGUN] > 0 && inventory[INVENTORY_SHELLS] > 0 );
}
static bool BotGoalHasOnlyPistolOrDryWeapons( const int* inventory ) {
if( inventory == NULL ) {
return true;
}
const bool ownsNonPistolWeapon = inventory[INVENTORY_MACHINEGUN] > 0 ||
inventory[INVENTORY_SHOTGUN] > 0 ||
inventory[INVENTORY_PLASMAGUN] > 0 ||
inventory[INVENTORY_ROCKETLAUNCHER] > 0;
if( !ownsNonPistolWeapon ) {
return true;
}
if( !BotGoalHasUsableWeapon( inventory ) ) {
return true;
}
const bool onlyMachinegun = inventory[INVENTORY_MACHINEGUN] > 0 &&
inventory[INVENTORY_SHOTGUN] <= 0 &&
inventory[INVENTORY_PLASMAGUN] <= 0 &&
inventory[INVENTORY_ROCKETLAUNCHER] <= 0;
return onlyMachinegun && inventory[INVENTORY_BULLETS] < 25;
}
static float BotGoalCombatItemBias( const iteminfo_t* itemInfo, const int* inventory ) {
if( itemInfo == NULL || inventory == NULL ) {
return 0.0f;
}
const bool desperate = BotGoalHasOnlyPistolOrDryWeapons( inventory );
const bool lacksPowerWeapon = !BotGoalHasPowerWeapon( inventory );
float bias = 0.0f;
// Weapons: route through these during a fight, especially if the bot is stuck
// on pistol/machinegun or is out of usable ammo.
if( BotGoalItemNameContains( itemInfo, "weapon_rocketlauncher" ) || BotGoalItemNameContains( itemInfo, "rocketlauncher" ) ) {
if( inventory[INVENTORY_ROCKETLAUNCHER] <= 0 || desperate ) {
bias += desperate ? 9000.0f : 4200.0f;
}
else if( inventory[INVENTORY_ROCKETS] <= 3 ) {
bias += 2200.0f;
}
}
else if( BotGoalItemNameContains( itemInfo, "weapon_plasmagun" ) || BotGoalItemNameContains( itemInfo, "plasmagun" ) ) {
if( inventory[INVENTORY_PLASMAGUN] <= 0 || desperate ) {
bias += desperate ? 8200.0f : 3800.0f;
}
else if( inventory[INVENTORY_CELLS] <= 18 ) {
bias += 1800.0f;
}
}
else if( BotGoalItemNameContains( itemInfo, "weapon_shotgun" ) || BotGoalItemNameContains( itemInfo, "shotgun" ) ) {
if( inventory[INVENTORY_SHOTGUN] <= 0 || desperate || lacksPowerWeapon ) {
bias += desperate ? 7600.0f : 3200.0f;
}
else if( inventory[INVENTORY_SHELLS] <= 6 ) {
bias += 1600.0f;
}
}
else if( BotGoalItemNameContains( itemInfo, "weapon_machinegun" ) || BotGoalItemNameContains( itemInfo, "machinegun" ) ) {
if( inventory[INVENTORY_MACHINEGUN] <= 0 || desperate ) {
bias += desperate ? 6200.0f : 1800.0f;
}
else if( inventory[INVENTORY_BULLETS] <= 30 ) {
bias += 1200.0f;
}
}
else if( BotGoalItemNameContains( itemInfo, "ammo_rockets" ) || BotGoalItemNameContains( itemInfo, "rocket_ammo" ) ) {
if( inventory[INVENTORY_ROCKETLAUNCHER] > 0 && inventory[INVENTORY_ROCKETS] <= 5 ) {
bias += inventory[INVENTORY_ROCKETS] <= 1 ? 4200.0f : 2400.0f;
}
}
else if( BotGoalItemNameContains( itemInfo, "ammo_cells" ) || BotGoalItemNameContains( itemInfo, "cells" ) ) {
if( inventory[INVENTORY_PLASMAGUN] > 0 && inventory[INVENTORY_CELLS] <= 25 ) {
bias += inventory[INVENTORY_CELLS] <= 8 ? 3600.0f : 2000.0f;
}
}
else if( BotGoalItemNameContains( itemInfo, "ammo_shells" ) || BotGoalItemNameContains( itemInfo, "shells" ) ) {
if( inventory[INVENTORY_SHOTGUN] > 0 && inventory[INVENTORY_SHELLS] <= 10 ) {
bias += inventory[INVENTORY_SHELLS] <= 3 ? 3200.0f : 1700.0f;
}
}
else if( BotGoalItemNameContains( itemInfo, "ammo_clip" ) || BotGoalItemNameContains( itemInfo, "bullets" ) ) {
if( inventory[INVENTORY_MACHINEGUN] > 0 && inventory[INVENTORY_BULLETS] <= 45 ) {
bias += inventory[INVENTORY_BULLETS] <= 10 ? 2800.0f : 1300.0f;
}
}
// Health/armor should also interrupt a fight, but only when it is tactically
// worth the detour. This still lets fuzzy item weights handle normal roaming.
if( BotGoalItemNameContains( itemInfo, "health" ) ) {
if( inventory[INVENTORY_HEALTH] < 35 ) {
bias += 5200.0f;
}
else if( inventory[INVENTORY_HEALTH] < 65 ) {
bias += 2600.0f;
}
else if( inventory[INVENTORY_HEALTH] < 95 ) {
bias += 900.0f;
}
}
if( BotGoalItemNameContains( itemInfo, "armor" ) ) {
if( inventory[INVENTORY_ARMOR] < 25 ) {
bias += 2400.0f;
}
else if( inventory[INVENTORY_ARMOR] < 75 ) {
bias += 1100.0f;
}
}
return bias;
}
/*
==================
idBotGoalManager::idBotGoalManager
@@ -261,24 +398,131 @@ itemconfig_t* idBotGoalManager::LoadItemConfig( char* filename )
idBotGoalManager::ItemWeightIndex
========================
*/
int* idBotGoalManager::ItemWeightIndex( weightconfig_t* iwc, itemconfig_t* ic )
{
int* index, i;
/*
================
Bot_ItemWeightAlias
//initialize item weight index
index = new int[( sizeof( int ) * ic->numiteminfo )]; // jmarshall: Get this off of the heap!
Some itemconfig entries come from Quake 3 / Team Arena style bot data, but the
Doom 3 weight config usually does not contain every one of those classnames.
for( i = 0; i < ic->numiteminfo; i++ )
{
index[i] = botFuzzyWeightManager.FindFuzzyWeight( iwc, ( char* )ic->iteminfo[i].classname.c_str() );
if( index[i] < 0 )
{
common->Warning( "item info %d \"%s\" has no fuzzy weight\r\n", i, ic->iteminfo[i].classname.c_str() );
The item weight index must still point at a valid fuzzy weight, so we alias
unsupported / missing items to the closest useful existing Doom 3 weight.
================
*/
static const char* Bot_ItemWeightAlias(const char* classname) {
if (!classname || !classname[0]) {
return NULL;
}
struct itemWeightAlias_t {
const char* missingName;
const char* fallbackName;
};
static const itemWeightAlias_t aliases[] = {
// Missing weapon names commonly seen in Q3 bot item configs.
{ "weapon_fists", "weapon_pistol" },
{ "weapon_grapplinghook", "weapon_chainsaw" },
{ "weapon_grenadelauncher", "weapon_rocketlauncher" },
{ "weapon_lightning", "weapon_plasmagun" },
{ "weapon_machinegun", "weapon_machinegun" },
{ "weapon_railgun", "weapon_rocketlauncher" },
{ "weapon_nailgun", "weapon_chaingun" },
{ "weapon_prox_launcher", "weapon_rocketlauncher" },
// Team Arena / CTF objective entities. These are not normal pickup goals.
// Give them a valid low-risk fallback so the index is never invalid.
{ "team_redobelisk", "item_armor_shard" },
{ "team_blueobelisk", "item_armor_shard" },
{ "team_neutralobelisk", "item_armor_shard" },
{ NULL, NULL }
};
for (int i = 0; aliases[i].missingName != NULL; i++) {
if (idStr::Icmp(classname, aliases[i].missingName) == 0) {
return aliases[i].fallbackName;
}
}
return index;
return NULL;
}
/*
================
idBotGoalManager::ItemWeightIndex
================
*/
int* idBotGoalManager::ItemWeightIndex(weightconfig_t* iwc, itemconfig_t* ic) {
int* index;
int i;
if (iwc == NULL || ic == NULL || ic->numiteminfo <= 0) {
return NULL;
}
// initialize item weight index
index = new int[ic->numiteminfo]; // jmarshall: Get this off of the heap!
for (i = 0; i < ic->numiteminfo; i++) {
const char* classname = ic->iteminfo[i].classname.c_str();
index[i] = botFuzzyWeightManager.FindFuzzyWeight(iwc, (char*)classname);
if (index[i] < 0) {
const char* aliasName = Bot_ItemWeightAlias(classname);
if (aliasName != NULL) {
index[i] = botFuzzyWeightManager.FindFuzzyWeight(iwc, (char*)aliasName);
if (index[i] >= 0) {
common->Printf(
"Bot item weight alias: item info %d \"%s\" using \"%s\"\n",
i,
classname,
aliasName
);
continue;
}
}
// Last-resort fallback. This keeps the bot goal system from storing
// invalid fuzzy weight indexes even if the expected alias is missing.
static const char* safeFallbacks[] = {
"item_armor_shard",
"item_armor",
"ammo_clip",
"weapon_pistol",
"weapon_shotgun",
"weapon_machinegun",
NULL
};
for (int j = 0; safeFallbacks[j] != NULL; j++) {
index[i] = botFuzzyWeightManager.FindFuzzyWeight(iwc, (char*)safeFallbacks[j]);
if (index[i] >= 0) {
common->Printf(
"Bot item weight fallback: item info %d \"%s\" using \"%s\"\n",
i,
classname,
safeFallbacks[j]
);
break;
}
}
if (index[i] < 0) {
common->Warning(
"item info %d \"%s\" has no fuzzy weight and no fallback weight\r\n",
i,
classname
);
}
}
}
return index;
}
/*
========================
idBotGoalManager::InitLevelItemHeap
@@ -490,6 +734,11 @@ void idBotGoalManager::InitLevelItems( void )
//for (ent = AAS_NextBSPEntity(0); ent; ent = AAS_NextBSPEntity(ent))
for( int idx = 0; idx < gameLocal.num_entities; idx++ )
{
if( gameLocal.entities[idx] == NULL )
{
continue;
}
idItem* ent = gameLocal.entities[idx]->Cast<idItem>();
if( ent == nullptr )
@@ -606,6 +855,12 @@ void idBotGoalManager::BotGoalName( int number, char* name, int size )
{
levelitem_t* li;
if( name == NULL || size <= 0 )
{
return;
}
name[0] = '\0';
if( !itemconfig )
{
return;
@@ -615,8 +870,7 @@ void idBotGoalManager::BotGoalName( int number, char* name, int size )
{
if( li->number == number )
{
name[size - 1] = '\0';
strcpy( name, itemconfig->iteminfo[li->iteminfo].name );
idStr::Copynz( name, itemconfig->iteminfo[li->iteminfo].name, size );
return;
}
}
@@ -858,7 +1112,7 @@ int idBotGoalManager::BotGetLevelItemGoal( int index, char* name, bot_goal_t* go
{
// goal->areanum = li->goalareanum;
goal->origin = li->goalorigin;
goal->entitynum = li->item->entityNumber;
goal->entitynum = li->item != NULL ? li->item->entityNumber : 0;
goal->mins = itemconfig->iteminfo[li->iteminfo].mins;
goal->maxs = itemconfig->iteminfo[li->iteminfo].maxs;
goal->number = li->number;
@@ -953,9 +1207,14 @@ void idBotGoalManager::BotFindEntityForLevelItem( levelitem_t* li )
}
for( int idx = 0; idx < gameLocal.num_entities; idx++ )
{
if( gameLocal.entities[idx] == NULL )
{
continue;
}
idItem* ent = gameLocal.entities[idx]->Cast<idItem>();
if( ent == NULL )
if( ent == nullptr )
{
continue;
}
@@ -1032,6 +1291,11 @@ void idBotGoalManager::UpdateEntityItems( void )
//
for( int idx = 0; idx < gameLocal.num_entities; idx++ )
{
if( gameLocal.entities[idx] == NULL )
{
continue;
}
idItem* ent = gameLocal.entities[idx]->Cast<idItem>();
if( ent == nullptr )
@@ -1430,17 +1694,25 @@ int idBotGoalManager::BotChooseLTGItem( int goalstate, idVec3 origin, int* inven
}
//get the fuzzy weight function for this item
iteminfo = &ic->iteminfo[li->iteminfo];
const float tacticalBias = BotGoalCombatItemBias( iteminfo, inventory );
weightnum = gs->itemweightindex[iteminfo->number];
if( weightnum < 0 )
{
continue;
if( tacticalBias <= 0.0f )
{
continue;
}
weight = 0.0f;
}
else
{
#ifdef UNDECIDEDFUZZY
weight = FuzzyWeightUndecided( inventory, gs->itemweightconfig, weightnum );
weight = FuzzyWeightUndecided( inventory, gs->itemweightconfig, weightnum );
#else
weight = botFuzzyWeightManager.FuzzyWeight( inventory, gs->itemweightconfig, weightnum );
weight = botFuzzyWeightManager.FuzzyWeight( inventory, gs->itemweightconfig, weightnum );
#endif //UNDECIDEDFUZZY
}
weight += tacticalBias;
#ifdef DROPPEDWEIGHT
//HACK: to make dropped items more attractive
if( li->timeout )
@@ -1517,7 +1789,7 @@ int idBotGoalManager::BotChooseLTGItem( int goalstate, idVec3 origin, int* inven
VectorCopy( iteminfo->mins, goal.mins );
VectorCopy( iteminfo->maxs, goal.maxs );
// goal.areanum = bestitem->goalareanum;
goal.entitynum = bestitem->item->entityNumber;
goal.entitynum = bestitem->item != NULL ? bestitem->item->entityNumber : 0;
goal.number = bestitem->number;
goal.flags = GFL_ITEM;
if( bestitem->timeout )
@@ -1656,17 +1928,25 @@ int idBotGoalManager::BotChooseNBGItem( int goalstate, idVec3 origin, int* inven
}
//get the fuzzy weight function for this item
iteminfo = &ic->iteminfo[li->iteminfo];
const float tacticalBias = BotGoalCombatItemBias( iteminfo, inventory );
weightnum = gs->itemweightindex[iteminfo->number];
if( weightnum < 0 )
{
continue;
if( tacticalBias <= 0.0f )
{
continue;
}
weight = 0.0f;
}
//
else
{
#ifdef UNDECIDEDFUZZY
weight = FuzzyWeightUndecided( inventory, gs->itemweightconfig, weightnum );
weight = FuzzyWeightUndecided( inventory, gs->itemweightconfig, weightnum );
#else
weight = botFuzzyWeightManager.FuzzyWeight( inventory, gs->itemweightconfig, weightnum );
weight = botFuzzyWeightManager.FuzzyWeight( inventory, gs->itemweightconfig, weightnum );
#endif //UNDECIDEDFUZZY
}
weight += tacticalBias;
#ifdef DROPPEDWEIGHT
//HACK: to make dropped items more attractive
if( li->timeout )
@@ -1709,8 +1989,11 @@ int idBotGoalManager::BotChooseNBGItem( int goalstate, idVec3 origin, int* inven
t = gameLocal.TravelTimeToGoal( li->goalorigin, ltg->origin );
// jmarshall end
} //end if
//if the travel back is possible and doesn't take too long
if( t <= ltg_time )
// For ordinary detours, keep the original "on the way" check. For tactical
// combat pickups such as a missing weapon, ammo, or low-health pickup, allow
// a short off-route move because surviving the fight is more important than
// staying glued to the enemy/chase goal.
if( tacticalBias > 0.0f || t <= ltg_time )
{
bestweight = weight;
bestitem = li;
@@ -1730,7 +2013,7 @@ int idBotGoalManager::BotChooseNBGItem( int goalstate, idVec3 origin, int* inven
VectorCopy( iteminfo->mins, goal.mins );
VectorCopy( iteminfo->maxs, goal.maxs );
// goal.areanum = bestitem->goalareanum;
goal.entitynum = bestitem->item->entityNumber;
goal.entitynum = bestitem->item != NULL ? bestitem->item->entityNumber : 0;
goal.number = bestitem->number;
goal.flags = GFL_ITEM;
if( bestitem->timeout )
@@ -1818,7 +2101,7 @@ int idBotGoalManager::BotItemGoalInVisButNotVisible( int viewer, idVec3 eye, idA
return false;
}
VectorAdd( goal->mins, goal->mins, middle );
VectorAdd( goal->mins, goal->maxs, middle );
VectorScale( middle, 0.5, middle );
VectorAdd( goal->origin, middle, middle );
@@ -1926,11 +2209,13 @@ void idBotGoalManager::BotFreeItemWeights( int goalstate )
if( gs->itemweightconfig )
{
botFuzzyWeightManager.FreeWeightConfig( gs->itemweightconfig );
gs->itemweightconfig = NULL;
}
if( gs->itemweightindex )
{
delete[] gs->itemweightindex;
gs->itemweightindex = NULL;
}
// jmarshall - eval
//if (gs->itemweightindex)
// FreeMemory(gs->itemweightindex);
// jmarshall end
}
/*