diff --git a/neo/doom3/game/bots/Bot.cpp b/neo/doom3/game/bots/Bot.cpp index fc12ba13..8a59659f 100644 --- a/neo/doom3/game/bots/Bot.cpp +++ b/neo/doom3/game/bots/Bot.cpp @@ -11,6 +11,29 @@ idCVar bot_showstate( "bot_showstate", "0", CVAR_BOOL | CVAR_CHEAT, "draws the b idCVar bot_debug( "bot_debug", "0", CVAR_BOOL, "shows debug info for the bot" ); idCVar bot_skill("bot_skill", "3", CVAR_INTEGER, ""); + +static int BotSafeAmmoCount( const idInventory& inv, const char* ammoName ) { + const int ammoNum = idWeapon::GetAmmoNumForName( ammoName ); + if( ammoNum < 0 || ammoNum >= AMMO_NUMTYPES ) { + return 0; + } + return inv.ammo[ammoNum]; +} + +static bool BotValidGameEntityNum( int entityNum ) { + return entityNum >= 0 && entityNum < gameLocal.num_entities && gameLocal.entities[entityNum] != NULL; +} + +static bool BotDirectPathIsClear( int passEntityNum, const idVec3& from, const idVec3& to ) { + trace_t trace; + gameLocal.Trace( trace, from, to, CONTENTS_SOLID | CONTENTS_PLAYERCLIP, passEntityNum ); + return trace.fraction >= 0.95f; +} + +static idVec3 bot_stuckOrigin[MAX_CLIENTS]; +static float bot_stuckTime[MAX_CLIENTS]; +static bool bot_stuckInit[MAX_CLIENTS]; + CLASS_DECLARATION( idPlayer, rvmBot ) END_CLASS @@ -43,12 +66,20 @@ rvmBot::SetEnemy */ void rvmBot::SetEnemy( idPlayer* player, idVec3 origin) { - if(bs.enemy == -1) + if( player == NULL || player == this || player->health <= 0 || player->spectating ) + { + return; + } + + const bool noEnemy = bs.enemy < 0 || !BotValidGameEntityNum( bs.enemy ) || EntityIsDead( gameLocal.entities[bs.enemy] ); + const bool sameEnemy = bs.enemy == player->entityNumber; + + if( noEnemy || sameEnemy ) { bs.enemy = player->entityNumber; bs.aggressiveAttackTime = gameLocal.SysScriptTime() + 2.0f; bs.lastenemyorigin = origin; - //bs.action = &botAIBattleRetreat; + bs.last_enemy_visible_position = origin; stateThread.SetState("state_Attacked"); } } @@ -71,12 +102,12 @@ void rvmBot::BotUpdateInventory( void ) bs.inventory[INVENTORY_PLASMAGUN] = HasWeapon( weapon_plasmagun ); bs.inventory[INVENTORY_BFG10K] = 0; bs.inventory[INVENTORY_GRAPPLINGHOOK] = 0; - bs.inventory[INVENTORY_SHELLS] = inventory.ammo[idWeapon::GetAmmoNumForName( "ammo_shells" )]; - bs.inventory[INVENTORY_BULLETS] = inventory.ammo[idWeapon::GetAmmoNumForName( "ammo_clip" )]; + bs.inventory[INVENTORY_SHELLS] = BotSafeAmmoCount( inventory, "ammo_shells" ); + bs.inventory[INVENTORY_BULLETS] = BotSafeAmmoCount( inventory, "ammo_clip" ); bs.inventory[INVENTORY_GRENADES] = 0; - bs.inventory[INVENTORY_CELLS] = inventory.ammo[idWeapon::GetAmmoNumForName( "ammo_cells" )]; + bs.inventory[INVENTORY_CELLS] = BotSafeAmmoCount( inventory, "ammo_cells" ); bs.inventory[INVENTORY_LIGHTNINGAMMO] = 0; - bs.inventory[INVENTORY_ROCKETS] = inventory.ammo[idWeapon::GetAmmoNumForName( "ammo_rockets" )]; + bs.inventory[INVENTORY_ROCKETS] = BotSafeAmmoCount( inventory, "ammo_rockets" ); bs.inventory[INVENTORY_SLUGS] = 0; bs.inventory[INVENTORY_BFGAMMO] = 0; bs.inventory[INVENTORY_HEALTH] = health; @@ -170,6 +201,7 @@ void rvmBot::Spawn( void ) hasSpawned = true; bs.botinput.respawn = true; + bs.botinput.lastWeaponNum = -1; stateThread.SetState("state_Respawn"); } @@ -182,22 +214,41 @@ rvmBot::Think */ void rvmBot::BotMoveToGoalOrigin(idVec3 goalOrigin) { - bs.botinput.dir = (goalOrigin - firstPersonViewOrigin); - idAngles desiredAngles = bs.botinput.dir.ToAngles(); + idVec3 moveDir = goalOrigin - GetPhysics()->GetOrigin(); + + // Usercmd forward/right movement is horizontal. Feeding pitch or a view-origin + // z delta into the movement vector makes the bot under-steer, drift, or push + // into walls while looking up/down at a target. + moveDir[2] = 0.0f; + + if( moveDir.LengthSqr() < Square( 4.0f ) ) + { + bs.botinput.dir = idVec3( 0.0f, 0.0f, 0.0f ); + bs.botinput.speed = 0.0f; + } + else + { + bs.botinput.dir = moveDir; + bs.botinput.dir.Normalize(); + bs.botinput.speed = pm_runspeed.GetInteger(); + } + if( bs.enemy >= 0 ) { - idPlayer* enemy = gameLocal.entities[bs.enemy]->Cast(); - if( enemy ) - { - desiredAngles = (enemy->firstPersonViewOrigin - firstPersonViewOrigin).ToAngles(); - } + // Combat states set bs.viewangles through BotAimAtEnemy. Do not replace that + // with a simple eye-to-eye angle here, or prediction/ground-rocket aiming is lost. + bs.botinput.viewangles = bs.viewangles; + } + else if( moveDir.LengthSqr() > Square( 1.0f ) ) + { + bs.botinput.viewangles = idAngles( 0.0f, moveDir.ToYaw(), 0.0f ); + bs.viewangles = bs.botinput.viewangles; + } + else + { + bs.botinput.viewangles = viewAngles; + bs.viewangles = viewAngles; } - - bs.botinput.viewangles = desiredAngles; - - bs.botinput.speed = pm_runspeed.GetInteger(); - - bs.botinput.dir.Normalize(); } /* @@ -236,14 +287,19 @@ void rvmBot::ServerThink( void ) bs.eye = GetEyePosition(); bs.thinktime = Bot_Time(); bs.botinput.actionflags = 0; + bs.botinput.dir = idVec3( 0.0f, 0.0f, 0.0f ); + bs.botinput.speed = 0.0f; + bs.botinput.viewangles = viewAngles; + bs.viewangles = viewAngles; BotUpdateInventory(); if( bot_pathdebug.IsModified() ) { - if( bot_pathdebug.GetBool() ) + idPlayer* localPlayer = gameLocal.GetLocalPlayer(); + if( bot_pathdebug.GetBool() && localPlayer != NULL ) { - bs.currentGoal.origin = gameLocal.GetLocalPlayer()->GetPhysics()->GetOrigin(); + bs.currentGoal.origin = localPlayer->GetPhysics()->GetOrigin(); bs.currentGoal.framenum = gameLocal.framenum; } @@ -253,43 +309,83 @@ void rvmBot::ServerThink( void ) stateThread.Execute(); - // If we are moving along a set of waypoints, let's move along. - aasPath_t path; - //int myArea = aas->PointAreaNum(GetOrigin()); - int goalArea = aas->PointAreaNum( bs.currentGoal.origin ); - idVec3 org = bs.origin; - int curAreaNum = aas->AdjustPositionAndGetArea( org ); - - if( bot_debug.GetBool() ) + if( BotIsDead( &bs ) || spectating || aas == NULL ) { - if (bs.useRandomPosition) - { - aas->ShowWalkPath(GetOrigin(), goalArea, bs.random_move_position); - } - else - { - aas->ShowWalkPath(GetOrigin(), goalArea, bs.currentGoal.origin); - } - - aas->ShowArea( GetOrigin() ); + bs.botinput.weapon = bs.weaponnum; + bs.attackerEntity = NULL; + return; } - if (bs.useRandomPosition) + idVec3 goalOrigin = bs.useRandomPosition ? bs.random_move_position : bs.currentGoal.origin; + idVec3 moveGoal = goalOrigin; + bool haveMoveGoal = false; + + if( goalOrigin.LengthSqr() > Square( 1.0f ) ) { - aas->WalkPathToGoal(path, curAreaNum, org, goalArea, bs.random_move_position, TFL_WALK | TFL_AIR); + aasPath_t path; + idVec3 org = bs.origin; + int curAreaNum = aas->AdjustPositionAndGetArea( org ); + idVec3 adjustedGoal = goalOrigin; + int goalArea = aas->PointAreaNum( adjustedGoal ); + + if( goalArea <= 0 ) + { + goalArea = aas->AdjustPositionAndGetArea( adjustedGoal ); + } + + if( bot_debug.GetBool() ) + { + aas->ShowWalkPath( GetOrigin(), goalArea, adjustedGoal ); + aas->ShowArea( GetOrigin() ); + } + + if( curAreaNum > 0 && goalArea > 0 && aas->WalkPathToGoal( path, curAreaNum, org, goalArea, adjustedGoal, TFL_WALK | TFL_AIR ) ) + { + moveGoal = path.moveGoal; + haveMoveGoal = true; + } + else if( BotDirectPathIsClear( entityNumber, bs.eye, adjustedGoal + idVec3( 0.0f, 0.0f, 24.0f ) ) ) + { + moveGoal = adjustedGoal; + haveMoveGoal = true; + } + } + + // If the selected route is not producing movement, invalidate the current path + // and choose a small local AAS point. This prevents repeatedly pushing into the + // same wall or corner when the item/chase goal is stale. + if( haveMoveGoal ) + { + const int stuckIndex = entityNumber >= 0 && entityNumber < MAX_CLIENTS ? entityNumber : 0; + if( !bot_stuckInit[stuckIndex] || ( bs.origin - bot_stuckOrigin[stuckIndex] ).LengthSqr() > Square( 8.0f ) ) + { + bot_stuckInit[stuckIndex] = true; + bot_stuckOrigin[stuckIndex] = bs.origin; + bot_stuckTime[stuckIndex] = Bot_Time(); + } + else if( Bot_Time() - bot_stuckTime[stuckIndex] > 1.25f ) + { + bs.ltg_time = 0; + bs.nbg_time = 0; + BotGetRandomPointNearPosition( bs.origin, bs.random_move_position, 96.0f ); + bs.useRandomPosition = true; + moveGoal = bs.random_move_position; + bot_stuckOrigin[stuckIndex] = bs.origin; + bot_stuckTime[stuckIndex] = Bot_Time(); + } + + BotMoveToGoalOrigin( moveGoal ); } else { - aas->WalkPathToGoal(path, curAreaNum, org, goalArea, bs.currentGoal.origin, TFL_WALK | TFL_AIR); + bs.botinput.dir = idVec3( 0.0f, 0.0f, 0.0f ); + bs.botinput.speed = 0.0f; + bs.botinput.viewangles = bs.viewangles; } - - idVec3 moveGoal = path.moveGoal; - BotMoveToGoalOrigin(path.moveGoal); bs.viewangles = bs.botinput.viewangles; - bs.useRandomPosition = false; - bs.attackerEntity = NULL; // Has to be consumed immedaitly. + bs.attackerEntity = NULL; bs.botinput.weapon = bs.weaponnum; } @@ -302,21 +398,23 @@ void rvmBot::Damage( idEntity* inflictor, idEntity* attacker, const idVec3& dir, { idPlayer::Damage( inflictor, attacker, dir, damageDefName, damageScale, location ); - idPlayer* player = attacker->Cast(); - if (health <= 0) - { - if (player) + idPlayer* player = attacker != NULL ? attacker->Cast() : NULL; + if( health <= 0 ) + { + if( player != NULL ) { - BotSendChatMessage(DEATH, player->netname ); + BotSendChatMessage( DEATH, player->netname ); } - } - - if (attacker == NULL) { return; } - //bs.attackerEntity = attacker; - SetEnemy(player, attacker->GetOrigin()); + if( player == NULL ) + { + return; + } + + bs.attackerEntity = player; + SetEnemy( player, player->GetOrigin() ); } /* @@ -325,10 +423,10 @@ rvmBot::InflictedDamageEvent ======================= */ void rvmBot::InflictedDamageEvent(idEntity* target) { - idPlayer* player = target->Cast(); + idPlayer* player = target != NULL ? target->Cast() : NULL; // Don't flood the chat with death and insults. - if (!player->IsBot() && player->health <= 0) + if( player != NULL && !player->IsBot() && player->health <= 0 ) { BotSendChatMessage(KILL, player->netname); } diff --git a/neo/doom3/game/bots/Bot.h b/neo/doom3/game/bots/Bot.h index 474acab3..a2870417 100644 --- a/neo/doom3/game/bots/Bot.h +++ b/neo/doom3/game/bots/Bot.h @@ -811,6 +811,7 @@ struct bot_state_t float enemysight_time; //time before reacting to enemy float enemydeath_time; //time the enemy died float aggressiveAttackTime; + float enemyposition_time; idVec3 origin; idVec3 aimtarget; idVec3 random_move_position; diff --git a/neo/doom3/game/bots/BotAI.cpp b/neo/doom3/game/bots/BotAI.cpp index 539686dc..ea9a5cc6 100644 --- a/neo/doom3/game/bots/BotAI.cpp +++ b/neo/doom3/game/bots/BotAI.cpp @@ -13,6 +13,138 @@ int rvmBot::WP_SHOTGUN = -1; int rvmBot::WP_PLASMAGUN = -1; int rvmBot::WP_ROCKET_LAUNCHER = -1; + + +static bool BotAIValidEntityNum( int entityNum ) { + return entityNum >= 0 && entityNum < gameLocal.num_entities && gameLocal.entities[entityNum] != NULL; +} + +static float BotAIClampFloat( float minValue, float maxValue, float value ) { + if( value < minValue ) { + return minValue; + } + if( value > maxValue ) { + return maxValue; + } + return value; +} + +static idVec3 BotAIEntityCenter( idEntity* ent ) { + if( ent == NULL || ent->GetPhysics() == NULL ) { + return idVec3( 0.0f, 0.0f, 0.0f ); + } + return ent->GetPhysics()->GetOrigin() + ent->GetPhysics()->GetBounds().GetCenter(); +} + +static bool BotAIPointWithinFOV( const idVec3& eye, const idAngles& viewangles, const idVec3& point, float fov ) { + if( fov >= 359.0f ) { + return true; + } + + idVec3 dir = point - eye; + if( dir.LengthSqr() < Square( 1.0f ) ) { + return true; + } + + idAngles targetAngles = dir.ToAngles(); + const float halfFov = fov * 0.5f; + const float yawDelta = idMath::Fabs( idMath::AngleDelta( targetAngles[YAW], viewangles[YAW] ) ); + const float pitchDelta = idMath::Fabs( idMath::AngleDelta( targetAngles[PITCH], viewangles[PITCH] ) ); + return yawDelta <= halfFov && pitchDelta <= halfFov; +} + +static bool BotAITraceCanHitEntity( int passEntityNum, int targetEntityNum, const idVec3& start, const idVec3& end, int mask ) { + trace_t trace; + gameLocal.Trace( trace, start, end, mask, passEntityNum ); + return trace.fraction >= 1.0f || trace.c.entityNum == targetEntityNum; +} + + +static bool BotAIHasUsableFightWeapon( 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 BotAIHasPowerFightWeapon( 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 BotAIHasOnlyPistolOrDryWeapons( const bot_state_t* bs ) { + if( bs == NULL ) { + return true; + } + const int* inventory = bs->inventory; + const bool ownsNonPistolWeapon = inventory[INVENTORY_MACHINEGUN] > 0 || + inventory[INVENTORY_SHOTGUN] > 0 || + inventory[INVENTORY_PLASMAGUN] > 0 || + inventory[INVENTORY_ROCKETLAUNCHER] > 0; + if( !ownsNonPistolWeapon ) { + return true; + } + if( !BotAIHasUsableFightWeapon( 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 bool BotAINeedsTacticalPickup( const bot_state_t* bs ) { + if( bs == NULL ) { + return false; + } + if( BotAIHasOnlyPistolOrDryWeapons( bs ) ) { + return true; + } + if( bs->inventory[INVENTORY_HEALTH] < 55 ) { + return true; + } + if( bs->inventory[INVENTORY_ROCKETLAUNCHER] > 0 && bs->inventory[INVENTORY_ROCKETS] <= 2 ) { + return true; + } + if( bs->inventory[INVENTORY_PLASMAGUN] > 0 && bs->inventory[INVENTORY_CELLS] <= 12 ) { + return true; + } + if( bs->inventory[INVENTORY_SHOTGUN] > 0 && bs->inventory[INVENTORY_SHELLS] <= 4 ) { + return true; + } + return false; +} + +static float BotAITacticalPickupRange( const bot_state_t* bs, float requestedRange ) { + float range = requestedRange; + if( bs == NULL || bs->enemy < 0 ) { + return range; + } + if( BotAINeedsTacticalPickup( bs ) ) { + if( range < 650.0f ) { + range = 650.0f; + } + } + else if( !BotAIHasPowerFightWeapon( bs->inventory ) ) { + if( range < 400.0f ) { + range = 400.0f; + } + } + else if( range < 260.0f ) { + range = 260.0f; + } + return range; +} + /* ========================= rvmBot::BotIsDead @@ -20,13 +152,13 @@ rvmBot::BotIsDead */ bool rvmBot::BotIsDead( bot_state_t* bs ) { - idPlayer* player = gameLocal.GetClientByNum( bs->client ); - if( player->health <= 0 ) + if( bs == NULL || !BotAIValidEntityNum( bs->client ) ) { return true; } - return false; + idPlayer* player = gameLocal.GetClientByNum( bs->client ); + return player == NULL || player->health <= 0; } /* @@ -71,24 +203,126 @@ rvmBot::BotChooseWeapon */ void rvmBot::BotChooseWeapon( bot_state_t* bs ) { - int newweaponnum; + if( bs == NULL ) + { + return; + } + + const bool haveRocketLauncher = WP_ROCKET_LAUNCHER >= 0 && bs->inventory[INVENTORY_ROCKETLAUNCHER] > 0 && bs->inventory[INVENTORY_ROCKETS] > 0; + const bool havePlasmaGun = WP_PLASMAGUN >= 0 && bs->inventory[INVENTORY_PLASMAGUN] > 0 && bs->inventory[INVENTORY_CELLS] > 0; + const bool haveShotgun = WP_SHOTGUN >= 0 && bs->inventory[INVENTORY_SHOTGUN] > 0 && bs->inventory[INVENTORY_SHELLS] > 0; + const bool haveMachinegun = WP_MACHINEGUN >= 0 && bs->inventory[INVENTORY_MACHINEGUN] > 0 && bs->inventory[INVENTORY_BULLETS] > 0; + + // The fuzzy weapon table is still evaluated, but it should not be able to + // pin a bot to the machinegun after the bot has picked up rockets, plasma, + // or shells. Doom 3 weapon slots are runtime values from SlotForWeapon(), so + // choose from those real slots here instead of trusting the Quake-style + // weapon config numbers blindly. + int practicalWeaponNum = -1; + const int enemyDist = ( bs->enemy >= 0 ) ? bs->inventory[ENEMY_HORIZONTAL_DIST] : 0; + + if( bs->enemy >= 0 && enemyDist > 0 ) + { + if( enemyDist < 160 ) + { + // Very close: avoid splash damage unless there is no safer weapon. + if( haveShotgun ) + { + practicalWeaponNum = WP_SHOTGUN; + } + else if( havePlasmaGun ) + { + practicalWeaponNum = WP_PLASMAGUN; + } + else if( haveMachinegun ) + { + practicalWeaponNum = WP_MACHINEGUN; + } + else if( haveRocketLauncher ) + { + practicalWeaponNum = WP_ROCKET_LAUNCHER; + } + } + else if( enemyDist < 700 ) + { + // Mid range: rockets are the highest value deathmatch weapon. + if( haveRocketLauncher ) + { + practicalWeaponNum = WP_ROCKET_LAUNCHER; + } + else if( havePlasmaGun ) + { + practicalWeaponNum = WP_PLASMAGUN; + } + else if( haveShotgun ) + { + practicalWeaponNum = WP_SHOTGUN; + } + else if( haveMachinegun ) + { + practicalWeaponNum = WP_MACHINEGUN; + } + } + else + { + // Long range: plasma is easier to land consistently; use rockets if + // plasma is unavailable. + if( havePlasmaGun ) + { + practicalWeaponNum = WP_PLASMAGUN; + } + else if( haveRocketLauncher ) + { + practicalWeaponNum = WP_ROCKET_LAUNCHER; + } + else if( haveMachinegun ) + { + practicalWeaponNum = WP_MACHINEGUN; + } + else if( haveShotgun ) + { + practicalWeaponNum = WP_SHOTGUN; + } + } + } + else + { + // No active enemy or no distance sample yet: keep the bot on the best + // generally useful weapon so it is ready when a fight starts. + if( haveRocketLauncher ) + { + practicalWeaponNum = WP_ROCKET_LAUNCHER; + } + else if( havePlasmaGun ) + { + practicalWeaponNum = WP_PLASMAGUN; + } + else if( haveShotgun ) + { + practicalWeaponNum = WP_SHOTGUN; + } + else if( haveMachinegun ) + { + practicalWeaponNum = WP_MACHINEGUN; + } + } + + int newweaponnum = practicalWeaponNum; + if( newweaponnum <= 0 ) + { + newweaponnum = botWeaponInfoManager.BotChooseBestFightWeapon( bs->ws, bs->inventory ); + } + if( newweaponnum <= 0 ) + { + newweaponnum = bs->weaponnum > 0 ? bs->weaponnum : 0; + } - //if (bs->cur_ps.weaponstate == WEAPON_RAISING || - // bs->cur_ps.weaponstate == WEAPON_DROPPING) { - // //trap_EA_SelectWeapon(bs->client, bs->weaponnum); - // bs->input.weapon = bs->weaponnum; - //} - //else { - newweaponnum = botWeaponInfoManager.BotChooseBestFightWeapon( bs->ws, bs->inventory ); if( bs->weaponnum != newweaponnum ) { bs->weaponchange_time = Bot_Time(); } bs->weaponnum = newweaponnum; - //BotAI_Print(PRT_MESSAGE, "bs->weaponnum = %d\n", bs->weaponnum); - //trap_EA_SelectWeapon(bs->client, bs->weaponnum); bs->botinput.weapon = bs->weaponnum; - //} } @@ -178,14 +412,13 @@ rvmBot::EntityIsDead */ bool rvmBot::EntityIsDead( idEntity* entity ) { + if( entity == NULL ) { - idPlayer* player = entity->Cast(); - if( player && player->health <= 0 ) - { - return true; - } + return true; } - return false; + + idPlayer* player = entity->Cast(); + return player != NULL && player->health <= 0; } /* @@ -197,153 +430,116 @@ returns visibility in the range [0, 1] taking fog and water surfaces into accoun */ float rvmBot::BotEntityVisibleTest( int viewer, idVec3 eye, idAngles viewangles, float fov, int ent, bool allowHeightTest ) { - int i, contents_mask, passent, hitent, infog, inwater, otherinfog, pc; - float squaredfogdist, waterfactor, vis, bestvis; - trace_t trace; - idEntity* entinfo; - idVec3 dir, start, end, middle; - idAngles entangles; - idPlayer* viewEnt; - - //calculate middle of bounding box - //BotEntityInfo(ent, &entinfo); - entinfo = gameLocal.entities[ent]; - viewEnt = gameLocal.entities[viewer]->Cast(); - - //VectorAdd(entinfo->r.mins, entinfo->r.maxs, middle); - //VectorScale(middle, 0.5, middle); - //VectorAdd(entinfo->GetOrigin(), middle, middle); - middle = entinfo->GetPhysics()->GetBounds().GetCenter(); - middle += entinfo->GetOrigin(); - - //check if entity is within field of vision - //VectorSubtract(middle, eye, dir); - dir = middle - eye; - entangles = dir.ToAngles(); - - if (!viewEnt->CheckFOV(entinfo->GetOrigin())) + if( !BotAIValidEntityNum( viewer ) || !BotAIValidEntityNum( ent ) ) { - return 0; + return 0.0f; } - pc = gameLocal.clip.PointContents( eye ); - infog = ( pc & CONTENTS_FOG ); - inwater = ( pc & ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ) ); - bestvis = 0; - for( i = 0; i < 3; i++ ) + idEntity* entinfo = gameLocal.entities[ent]; + if( entinfo == NULL || entinfo->GetPhysics() == NULL ) { - //if the point is not in potential visible sight - //if (!AAS_inPVS(eye, middle)) continue; - // - contents_mask = CONTENTS_SOLID | CONTENTS_PLAYERCLIP; - passent = viewer; - hitent = ent; - //VectorCopy(eye, start); - start = eye; - //VectorCopy(middle, end); - end = middle; - //if the entity is in water, lava or slime - if( gameLocal.clip.PointContents( middle ) & ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ) ) + return 0.0f; + } + + idBounds bounds = entinfo->GetPhysics()->GetBounds(); + idVec3 origin = entinfo->GetPhysics()->GetOrigin(); + idVec3 center = bounds.GetCenter(); + idVec3 testPoints[3]; + testPoints[0] = origin + center; + testPoints[1] = origin + idVec3( center[0], center[1], bounds[0][2] + 8.0f ); + testPoints[2] = origin + idVec3( center[0], center[1], bounds[1][2] - 4.0f ); + + if( !BotAIPointWithinFOV( eye, viewangles, testPoints[0], fov ) ) + { + return 0.0f; + } + + const int pc = gameLocal.clip.PointContents( eye ); + const int infog = ( pc & CONTENTS_FOG ); + const int inwater = ( pc & ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ) ); + float bestvis = 0.0f; + + for( int i = 0; i < 3; i++ ) + { + idVec3 start = eye; + idVec3 end = testPoints[i]; + int contents_mask = CONTENTS_SOLID | CONTENTS_PLAYERCLIP; + int passent = viewer; + int hitent = ent; + + if( gameLocal.clip.PointContents( end ) & ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ) ) { contents_mask |= ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ); } - //if eye is in water, lava or slime + if( inwater ) { if( !( contents_mask & ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ) ) ) { passent = ent; hitent = viewer; - VectorCopy( middle, start ); - VectorCopy( eye, end ); + start = end; + end = eye; } contents_mask ^= ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ); } - //trace from start to end + trace_t trace; gameLocal.Trace( trace, start, end, contents_mask, passent ); - // jmarshall if( trace.fraction < 0.9f && allowHeightTest ) { - end[2] += 50.0f; - gameLocal.Trace( trace, start, end, contents_mask, passent ); + idVec3 highEnd = end; + highEnd[2] += 50.0f; + gameLocal.Trace( trace, start, highEnd, contents_mask, passent ); } - // jmarshall end - //if water was hit - waterfactor = 1.0; + float waterfactor = 1.0f; if( trace.c.contents & ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ) ) { - //if the water surface is translucent - if( 1 ) - { - //trace through the water - contents_mask &= ~( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ); - //trap_Trace(&trace, trace.endpos, NULL, NULL, end, passent, contents_mask); - gameLocal.Trace( trace, trace.endpos, end, contents_mask, passent ); - waterfactor = 0.5; - } + contents_mask &= ~( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ); + gameLocal.Trace( trace, trace.endpos, end, contents_mask, passent ); + waterfactor = 0.5f; } - //if a full trace or the hitent was hit - if( trace.fraction >= 1 || trace.c.entityNum == hitent ) + if( trace.fraction >= 1.0f || trace.c.entityNum == hitent ) { - //check for fog, assuming there's only one fog brush where - //either the viewer or the entity is in or both are in - otherinfog = ( gameLocal.clip.PointContents( middle ) & CONTENTS_FOG ); + float squaredfogdist = 0.0f; + const int otherinfog = ( gameLocal.clip.PointContents( end ) & CONTENTS_FOG ); + idVec3 dir; + if( infog && otherinfog ) { - VectorSubtract( trace.endpos, eye, dir ); + dir = trace.endpos - eye; squaredfogdist = dir.LengthSqr(); } else if( infog ) { - VectorCopy( trace.endpos, start ); - //trap_Trace(&trace, start, NULL, NULL, eye, viewer, CONTENTS_FOG); - gameLocal.Trace( trace, start, eye, CONTENTS_FOG, viewer ); - VectorSubtract( eye, trace.endpos, dir ); + idVec3 fogStart = trace.endpos; + gameLocal.Trace( trace, fogStart, eye, CONTENTS_FOG, viewer ); + dir = eye - trace.endpos; squaredfogdist = dir.LengthSqr(); } else if( otherinfog ) { - VectorCopy( trace.endpos, end ); - //trap_Trace(&trace, eye, NULL, NULL, end, viewer, CONTENTS_FOG); - gameLocal.Trace( trace, eye, end, CONTENTS_FOG, viewer ); - VectorSubtract( end, trace.endpos, dir ); + idVec3 fogEnd = trace.endpos; + gameLocal.Trace( trace, eye, fogEnd, CONTENTS_FOG, viewer ); + dir = fogEnd - trace.endpos; squaredfogdist = dir.LengthSqr(); } - else - { - //if the entity and the viewer are not in fog assume there's no fog in between - squaredfogdist = 0; - } - //decrease visibility with the view distance through fog - vis = 1 / ( ( squaredfogdist * 0.001 ) < 1 ? 1 : ( squaredfogdist * 0.001 ) ); - //if entering water visibility is reduced + float vis = 1.0f / ( ( squaredfogdist * 0.001f ) < 1.0f ? 1.0f : ( squaredfogdist * 0.001f ) ); vis *= waterfactor; - if( vis > bestvis ) { bestvis = vis; } - - //if pretty much no fog - if( bestvis >= 0.95 ) + if( bestvis >= 0.95f ) { return bestvis; } } - //check bottom and top of bounding box as well - if( i == 0 ) - { - middle[2] += entinfo->GetPhysics()->GetBounds()[0][2]; // r.mins[2]; - } - else if( i == 1 ) - { - middle[2] += entinfo->GetPhysics()->GetBounds()[1][2] - entinfo->GetPhysics()->GetBounds()[0][2]; //entinfo->r.maxs[2] - entinfo->r.mins[2]; - } } + return bestvis; } @@ -364,16 +560,21 @@ rvmBot::BotUpdateBattleInventory */ void rvmBot::BotUpdateBattleInventory( bot_state_t* bs, int enemy ) { - idVec3 dir; - idEntity* entinfo; + if( bs == NULL || !BotAIValidEntityNum( enemy ) ) + { + return; + } - entinfo = gameLocal.entities[enemy]; + idEntity* entinfo = gameLocal.entities[enemy]; + if( entinfo == NULL || entinfo->GetPhysics() == NULL ) + { + return; + } - VectorSubtract( entinfo->GetOrigin(), bs->origin, dir ); + idVec3 dir = entinfo->GetPhysics()->GetOrigin() - bs->origin; bs->inventory[ENEMY_HEIGHT] = ( int )dir[2]; dir[2] = 0; bs->inventory[ENEMY_HORIZONTAL_DIST] = ( int )dir.Length(); - //FIXME: add num visible enemies and num visible team mates to the inventory } /* @@ -510,177 +711,111 @@ rvmBot::BotFindEnemy */ int rvmBot::BotFindEnemy( bot_state_t* bs, int curenemy ) { - int i, healthdecrease; - float f, alertness, easyfragger, vis; - float squaredist, cursquaredist; - idPlayer* entinfo = NULL; - idPlayer* curenemyinfo = NULL; - idVec3 dir; - idAngles angles; - idPlayer* clientEnt; + if( bs == NULL || !BotAIValidEntityNum( bs->client ) ) + { + return false; + } - alertness = botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_ALERTNESS, 0, 1 ); - easyfragger = botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_EASY_FRAGGER, 0, 1 ); + float alertness = botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_ALERTNESS, 0, 1 ); + float easyfragger = botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_EASY_FRAGGER, 0, 1 ); + idPlayer* clientEnt = gameLocal.entities[bs->client]->Cast(); + if( clientEnt == NULL ) + { + return false; + } - clientEnt = gameLocal.entities[bs->client]->Cast(); - - //check if the health decreased - healthdecrease = bs->lasthealth > bs->inventory[INVENTORY_HEALTH]; - - //remember the current health value + const bool healthdecrease = bs->lasthealth > bs->inventory[INVENTORY_HEALTH]; bs->lasthealth = bs->inventory[INVENTORY_HEALTH]; - // - if( curenemy >= 0 ) + + float cursquaredist = 0.0f; + if( curenemy >= 0 && BotAIValidEntityNum( curenemy ) ) { - //BotEntityInfo(curenemy, &curenemyinfo); - curenemyinfo = gameLocal.entities[curenemy]->Cast(); - // jmarshall - add flag support. - //if (EntityCarriesFlag(&curenemyinfo)) return qfalse; - // jmarshall end - //VectorSubtract(curenemyinfo->r.currentOrigin, bs->origin, dir); - dir = curenemyinfo->GetPhysics()->GetOrigin() - bs->origin; - cursquaredist = dir.LengthSqr();// VectorLengthSquared(dir); - } - else - { - cursquaredist = 0; + idPlayer* curenemyinfo = gameLocal.entities[curenemy]->Cast(); + if( curenemyinfo != NULL ) + { + idVec3 dir = curenemyinfo->GetPhysics()->GetOrigin() - bs->origin; + cursquaredist = dir.LengthSqr(); + } } - for( i = 0; i < MAX_CLIENTS; i++ ) + for( int i = 0; i < MAX_CLIENTS && i < gameLocal.num_entities; i++ ) { - - if( i == bs->client ) + if( i == bs->client || i == curenemy || !BotAIValidEntityNum( i ) ) { continue; } - //if it's the current enemy - if( i == curenemy ) + idPlayer* entinfo = gameLocal.entities[i]->Cast(); + if( entinfo == NULL ) + { + continue; + } + if( EntityIsDead( entinfo ) || i == bs->entitynum || entinfo->spectating ) { continue; } - entinfo = gameLocal.entities[i]->Cast(); - if( !entinfo ) - { - continue; - } - - //if the enemy isn't dead and the enemy isn't the bot self - if( EntityIsDead( entinfo ) || i == bs->entitynum ) - { - continue; - } - - if (entinfo->spectating) { - continue; - } - - //if the enemy is invisible and not shooting -// jmarshall - add invis - //if (EntityIsInvisible(&entinfo) && !EntityIsShooting(&entinfo)) { - // continue; - //} -// jmarshall end - -// jmarshall - eval, looks like code to not shoot chatting players or players that just spawned in. -// do we care about this? - //if not an easy fragger don't shoot at chatting players - //if (easyfragger < 0.5 && EntityIsChatting(&entinfo)) - // continue; - - // - //if (lastteleport_time > Bot_Time() - 3) { - // VectorSubtract(entinfo.origin, lastteleport_origin, dir); - // if (VectorLengthSquared(dir) < Square(70)) - // continue; - //} -// jmarshall end - - //calculate the distance towards the enemy idVec3 potentialTargetOrigin = entinfo->GetPhysics()->GetOrigin(); - dir = potentialTargetOrigin - bs->origin; - squaredist = dir.LengthSqr(); + idVec3 dir = potentialTargetOrigin - bs->origin; + float squaredist = dir.LengthSqr(); - // jmarshall - //if this entity is not carrying a flag - //if (!EntityCarriesFlag(&entinfo)) - //{ - //if this enemy is further away than the current one - if( curenemy >= 0 && squaredist > cursquaredist ) - { - continue; - } - //} -// jmarshall end - - //if the bot has no - if( squaredist > Square( 900.0 + alertness * 4000.0 ) ) + if( curenemy >= 0 && cursquaredist > 0.0f && squaredist > cursquaredist ) { continue; } - // jmarshall - teams! - //if on the same team - //if (BotSameTeam(bs, i)) - // continue; - // jmarshall end - //if the bot's health decreased or the enemy is shooting + if( squaredist > Square( 900.0f + alertness * 4000.0f ) ) + { + continue; + } + + float fov; if( curenemy < 0 && ( healthdecrease || entinfo->IsShooting() ) ) { - f = 360; + fov = 360.0f; } else { - f = 90 + 90 - ( 90 - ( squaredist > Square( 810 ) ? Square( 810 ) : squaredist ) / ( 810 * 9 ) ); + const float clampedDist = squaredist > Square( 810.0f ) ? Square( 810.0f ) : squaredist; + fov = 180.0f - ( 90.0f - clampedDist / ( 810.0f * 9.0f ) ); } - //check if the enemy is visible - // If we were last hit by someone then assume they are visible. + float vis = 1.0f; if( bs->attackerEntity != entinfo ) { - vis = BotEntityVisible( bs->entitynum, bs->eye, bs->viewangles, f, i ); - if( vis <= 0 ) + vis = BotEntityVisible( bs->entitynum, bs->eye, bs->viewangles, fov, i ); + if( vis <= 0.0f ) { continue; } } - //if the enemy is quite far away, not shooting and the bot is not damaged - if( curenemy < 0 && squaredist > Square( 100 ) && !healthdecrease && !entinfo->IsShooting() ) + // If the bot can avoid a distant, unaware enemy and does not want a fight, + // do not switch away from the current goal. + if( curenemy < 0 && squaredist > Square( 100.0f ) && !healthdecrease && !entinfo->IsShooting() ) { - //check if we can avoid this enemy - VectorSubtract( bs->origin, entinfo->GetOrigin(), dir ); - angles = dir.ToAngles(); - - //if the bot isn't in the fov of the enemy - if( !clientEnt->CheckFOV( entinfo->GetOrigin() ) ) + if( !entinfo->CheckFOV( clientEnt->GetPhysics()->GetOrigin() ) ) { - //update some stuff for this enemy BotUpdateBattleInventory( bs, i ); - - //if the bot doesn't really want to fight - if( BotWantsToRetreat( bs ) ) + if( BotWantsToRetreat( bs ) && easyfragger < 0.5f ) { continue; } } } - //found an enemy - bs->enemy = i;//entinfo.number; - if( curenemy >= 0 ) - { - bs->enemysight_time = Bot_Time() - 2; - } - else - { - bs->enemysight_time = Bot_Time(); - } + + bs->enemy = i; + bs->enemysight_time = curenemy >= 0 ? Bot_Time() - 2.0f : Bot_Time(); bs->enemysuicide = false; bs->enemydeath_time = 0; bs->enemyvisible_time = Bot_Time(); + bs->lastenemyorigin = potentialTargetOrigin; + bs->last_enemy_visible_position = potentialTargetOrigin; + bs->enemyorigin = potentialTargetOrigin; + bs->enemyposition_time = Bot_Time(); return true; } + return false; } @@ -702,78 +837,33 @@ rvmBot::BotAimAtEnemy */ void rvmBot::BotAimAtEnemy( bot_state_t* bs ) { - int i, enemyvisible; - float dist, f, aim_skill, aim_accuracy, speed, reactiontime; - idVec3 dir, bestorigin, end, start, groundtarget, cmdmove, enemyvelocity; - idVec3 mins( -4, -4, -4 ); - idVec3 maxs( 4, 4, 4 ); + if( bs == NULL || bs->enemy < 0 || !BotAIValidEntityNum( bs->enemy ) || !BotAIValidEntityNum( bs->entitynum ) ) + { + return; + } + + idPlayer* self = gameLocal.entities[bs->entitynum]->Cast(); + idPlayer* entinfo = gameLocal.entities[bs->enemy]->Cast(); + if( self == NULL || entinfo == NULL || entinfo->GetPhysics() == NULL ) + { + return; + } + + float aim_skill = botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_SKILL, 0, 1 ); + float aim_accuracy = botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_ACCURACY, 0, 1 ); + + if( aim_skill > 0.95f ) + { + const float reactiontime = 0.5f * botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_REACTIONTIME, 0, 1 ); + if( bs->enemysight_time > Bot_Time() - reactiontime || bs->teleport_time > Bot_Time() - reactiontime ) + { + return; + } + } + weaponinfo_t wi; - //aas_entityinfo_t entinfo; - idPlayer* entinfo; - bot_goal_t goal; - trace_t trace; - idVec3 target; - idPlayer* self; - - //if the bot has no enemy - if( bs->enemy < 0 ) - { - return; - } - - self = gameLocal.entities[bs->entitynum]->Cast(); - - //get the enemy entity information - //BotEntityInfo(bs->enemy, &entinfo); - entinfo = gameLocal.entities[bs->enemy]->Cast(); - - //if this is not a player (should be an obelisk) - if( bs->enemy >= MAX_CLIENTS ) - { - //if the obelisk is visible - VectorCopy( entinfo->GetOrigin(), target ); -#ifdef MISSIONPACK - // if attacking an obelisk - if( bs->enemy == redobelisk.entitynum || - bs->enemy == blueobelisk.entitynum ) - { - target[2] += 32; - } -#endif - //aim at the obelisk - VectorSubtract( target, bs->eye, dir ); - //vectoangles(dir, bs->viewangles); - bs->viewangles = dir.ToAngles(); - - //set the aim target before trying to attack - VectorCopy( target, bs->aimtarget ); - return; - } - - // - //BotAI_Print(PRT_MESSAGE, "client %d: aiming at client %d\n", bs->entitynum, bs->enemy); - // - aim_skill = botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_SKILL, 0, 1 ); - aim_accuracy = botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_ACCURACY, 0, 1 ); - // - if( aim_skill > 0.95 ) - { - //don't aim too early - reactiontime = 0.5 * botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_REACTIONTIME, 0, 1 ); - if( bs->enemysight_time > Bot_Time() - reactiontime ) - { - return; - } - if( bs->teleport_time > Bot_Time() - reactiontime ) - { - return; - } - } - - //get the weapon information botWeaponInfoManager.BotGetWeaponInfo( bs->ws, bs->weaponnum, &wi ); - //get the weapon specific aim accuracy and or aim skill if( wi.number == WP_MACHINEGUN ) { aim_accuracy = botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_ACCURACY_MACHINEGUN, 0, 1 ); @@ -782,305 +872,151 @@ void rvmBot::BotAimAtEnemy( bot_state_t* bs ) { aim_accuracy = botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_ACCURACY_SHOTGUN, 0, 1 ); } - //else if (wi.number == WP_GRENADE_LAUNCHER) { - // aim_accuracy = Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_GRENADELAUNCHER, 0, 1); - // aim_skill = Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL_GRENADELAUNCHER, 0, 1); - //} else if( wi.number == WP_ROCKET_LAUNCHER ) { aim_accuracy = botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_ACCURACY_ROCKETLAUNCHER, 0, 1 ); aim_skill = botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_SKILL_ROCKETLAUNCHER, 0, 1 ); } - //else if (wi.number == WP_LIGHTNING) { - // aim_accuracy = Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_LIGHTNING, 0, 1); - //} - //else if (wi.number == WP_RAILGUN) { - // aim_accuracy = Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_RAILGUN, 0, 1); - //} else if( wi.number == WP_PLASMAGUN ) { aim_accuracy = botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_ACCURACY_PLASMAGUN, 0, 1 ); aim_skill = botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_SKILL_PLASMAGUN, 0, 1 ); } - //else if (wi.number == WP_BFG) { - // aim_accuracy = Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_BFG10K, 0, 1); - // aim_skill = Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL_BFG10K, 0, 1); - //} - // - if( aim_accuracy <= 0 ) + aim_accuracy = BotAIClampFloat( 0.0001f, 1.0f, aim_accuracy ); + aim_skill = BotAIClampFloat( 0.0f, 1.0f, aim_skill ); + + const float enemyvisible = BotEntityVisible( bs->entitynum, bs->eye, bs->viewangles, 360.0f, bs->enemy ); + idVec3 bestorigin; + idVec3 start = bs->eye; + + if( enemyvisible > 0.0f ) { - aim_accuracy = 0.0001f; - } - //get the enemy entity information - //BotEntityInfo(bs->enemy, &entinfo); - entinfo = gameLocal.entities[bs->enemy]->Cast(); + bs->enemyvisible_time = Bot_Time(); + bs->lastenemyorigin = entinfo->GetPhysics()->GetOrigin(); + bs->last_enemy_visible_position = bs->lastenemyorigin; - //if the enemy is invisible then shoot crappy most of the time - //if (entinfo->IsInvisible()) { - // if (rvmBotUtil::random() > 0.1) - // aim_accuracy *= 0.4f; - //} - // jmarshall - fix aim accuracy. - //VectorSubtract(entinfo->GetOrigin(), entinfo.lastvisorigin, enemyvelocity); - //VectorScale(enemyvelocity, 1 / entinfo.update_time, enemyvelocity); - ////enemy origin and velocity is remembered every 0.5 seconds - //if (bs->enemyposition_time < Bot_Time()) { - // // - // bs->enemyposition_time = Bot_Time() + 0.5; - // VectorCopy(enemyvelocity, bs->enemyvelocity); - // VectorCopy(entinfo.origin, bs->enemyorigin); - //} - ////if not extremely skilled - //if (aim_skill < 0.9) { - // VectorSubtract(entinfo.origin, bs->enemyorigin, dir); - // //if the enemy moved a bit - // if (VectorLengthSquared(dir) > Square(48)) { - // //if the enemy changed direction - // if (DotProduct(bs->enemyvelocity, enemyvelocity) < 0) { - // //aim accuracy should be worse now - // aim_accuracy *= 0.7f; - // } - // } - //} - // jmarshall end + // Aim center-mass by default. Eye-only aiming misses badly when the bot is + // above the target on stairs because the head/eye ray is often clipped by the + // step edge while the torso is visible. + idVec3 enemyOrigin = entinfo->GetPhysics()->GetOrigin(); + idBounds enemyBounds = entinfo->GetPhysics()->GetBounds(); + const float enemyMinZ = enemyBounds[0][2]; + const float enemyMaxZ = enemyBounds[1][2]; + const float enemyCenterZ = ( enemyMinZ + enemyMaxZ ) * 0.5f; + const float enemyChestZ = enemyMinZ + ( enemyMaxZ - enemyMinZ ) * 0.65f; - //check visibility of enemy - enemyvisible = BotEntityVisible( bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy ); + idVec3 candidates[5]; + candidates[0] = enemyOrigin + idVec3( 0.0f, 0.0f, enemyChestZ ); + candidates[1] = enemyOrigin + idVec3( 0.0f, 0.0f, enemyCenterZ ); + candidates[2] = entinfo->GetEyePosition(); + candidates[3] = enemyOrigin + idVec3( 0.0f, 0.0f, enemyMinZ + 12.0f ); + candidates[4] = BotAIEntityCenter( entinfo ); - //if the enemy is visible - if( enemyvisible ) - { - // - VectorCopy( entinfo->GetOrigin(), bestorigin ); - bestorigin[2] += 8; - //get the start point shooting from - //NOTE: the x and y projectile start offsets are ignored - VectorCopy( bs->origin, start ); - start[2] += self->GetViewHeight();// bs->cur_ps.viewheight; - start[2] += wi.offset[2]; - // - //trap_Trace(&trace, start, mins, maxs, bestorigin, bs->entitynum, MASK_SHOT); - gameLocal.Trace( trace, start, bestorigin, MASK_SHOT, bs->entitynum ); - //if the enemy is NOT hit - if( trace.fraction <= 1 && trace.c.entityNum != bs->enemy ) + bestorigin = ( wi.proj.damagetype & DAMAGETYPE_RADIAL ) ? candidates[1] : candidates[0]; + for( int i = 0; i < 5; i++ ) { - bestorigin[2] += 16; - } - //if it is not an instant hit weapon the bot might want to predict the enemy - if( wi.speed ) - { - // - VectorSubtract( bestorigin, bs->origin, dir ); - dist = dir.Length(); - VectorSubtract( entinfo->GetOrigin(), bs->enemyorigin, dir ); - //if the enemy is NOT pretty far away and strafing just small steps left and right - if( !( dist > 100 && dir.LengthSqr() < Square( 32 ) ) ) + if( BotAITraceCanHitEntity( bs->entitynum, bs->enemy, start, candidates[i], MASK_SHOT ) ) { - //if skilled anough do exact prediction - if( aim_skill > 0.8 && - //if the weapon is ready to fire - !self->IsShooting() ) + bestorigin = candidates[i]; + break; + } + } + + // Linear lead for projectile weapons. The old code copied an uninitialized + // vector backwards and then changed the movement goal instead of changing aim. + if( wi.speed > 1.0f ) + { + idVec3 toTarget = bestorigin - start; + float leadTime = toTarget.Length() / wi.speed; + leadTime = BotAIClampFloat( 0.0f, 1.25f, leadTime ); + idVec3 enemyVelocity = entinfo->GetPhysics()->GetLinearVelocity(); + // Horizontal lead matters most. Clamp vertical lead so stairs/elevators do + // not make projectile aim sail over or under the body. + enemyVelocity[2] = BotAIClampFloat( -120.0f, 120.0f, enemyVelocity[2] ); + bestorigin += enemyVelocity * ( leadTime * aim_skill ); + } + + // Rockets are usually stronger when aimed at the floor near the enemy, but + // only do this if the impact point is visible and not dangerously close. + if( aim_skill > 0.6f && ( wi.proj.damagetype & DAMAGETYPE_RADIAL ) ) + { + idVec3 end = entinfo->GetPhysics()->GetOrigin(); + end[2] -= 96.0f; + trace_t trace; + gameLocal.Trace( trace, entinfo->GetPhysics()->GetOrigin(), end, MASK_SHOT, bs->enemy ); + idVec3 groundtarget = trace.endpos; + groundtarget[2] -= 4.0f; + + gameLocal.Trace( trace, start, groundtarget, MASK_SHOT, bs->entitynum ); + if( idMath::Fabs( trace.endpos[2] - groundtarget[2] ) < 50.0f ) + { + idVec3 fromBot = trace.endpos - start; + idVec3 fromEnemy = trace.endpos - entinfo->GetPhysics()->GetOrigin(); + if( fromBot.LengthSqr() > Square( 100.0f ) && fromEnemy.LengthSqr() < Square( 160.0f ) ) { - //aas_clientmove_t move; - idVec3 origin; - idVec3 last_enemy_visible_position; - VectorCopy( last_enemy_visible_position, bs->last_enemy_visible_position ); - last_enemy_visible_position[2] += 20.0f; - - // - VectorSubtract( entinfo->GetOrigin(), bs->origin, dir ); - - ////distance towards the enemy - //dist = VectorLength(dir); - ////direction the enemy is moving in - VectorSubtract( entinfo->GetOrigin(), last_enemy_visible_position, dir ); - //// - //VectorScale(dir, 1 / entinfo->update_time, dir); - //// - //VectorCopy(entinfo->origin, origin); - //origin[2] += 1; - - //// - //VectorClear(cmdmove); - ////AAS_ClearShownDebugLines(); - //trap_AAS_PredictClientMovement(&move, bs->enemy, origin, - // PRESENCE_CROUCH, qfalse, - // dir, cmdmove, 0, - // dist * 10 / wi.speed, 0.1f, 0, 0, qfalse); - //VectorCopy(move.endpos, bestorigin); - //BotAI_Print(PRT_MESSAGE, "%1.1f predicted speed = %f, frames = %f\n", Bot_Time(), VectorLength(dir), dist * 10 / wi.speed); - - bot_goal_t goal; - VectorMA( entinfo->GetOrigin(), 30, dir, goal.origin ); - BotMoveToGoal( bs, &goal ); - } - //if not that skilled do linear prediction - else if( aim_skill > 0.4 ) - { - // jmarshall - fix linear prediction. - idVec3 last_enemy_visible_position; - VectorCopy( last_enemy_visible_position, bs->last_enemy_visible_position ); - last_enemy_visible_position[2] += 20.0f; - - //VectorSubtract(entinfo->GetOrigin(), bs->origin, dir); - ////distance towards the enemy - //dist = VectorLength(dir); - ////direction the enemy is moving in - VectorSubtract( entinfo->GetOrigin(), last_enemy_visible_position, dir ); - //dir[2] = 0; - //// - //speed = VectorNormalize(dir) / entinfo.update_time; - ////botimport.Print(PRT_MESSAGE, "speed = %f, wi->speed = %f\n", speed, wi->speed); - ////best spot to aim at - //VectorMA(entinfo.origin, (dist / wi.speed) * speed, dir, bestorigin); - bot_goal_t goal; - VectorMA( entinfo->GetOrigin(), 30, dir, goal.origin ); - BotMoveToGoal( bs, &goal ); - // jmarshall end + bestorigin = trace.endpos; } } } - //if the projectile does radial damage - if( aim_skill > 0.6 && wi.proj.damagetype & DAMAGETYPE_RADIAL ) - { - //if the enemy isn't standing significantly higher than the bot - if( entinfo->GetOrigin()[2] < bs->origin[2] + 16 ) - { - //try to aim at the ground in front of the enemy - VectorCopy( entinfo->GetOrigin(), end ); - end[2] -= 64; - //trap_Trace(&trace, entinfo->GetOrigin(), NULL, NULL, end, bs->enemy, MASK_SHOT); - gameLocal.Trace( trace, entinfo->GetOrigin(), end, MASK_SHOT, bs->enemy ); - // - VectorCopy( bestorigin, groundtarget ); -// jmarshall - add start solid - //if (trace.startsolid) - // groundtarget[2] = entinfo->GetOrigin()[2] - 16; - //else - groundtarget[2] = trace.endpos[2] - 8; -// jmarshall end - //trace a line from projectile start to ground target - //trap_Trace(&trace, start, NULL, NULL, groundtarget, bs->entitynum, MASK_SHOT); - gameLocal.Trace( trace, start, groundtarget, MASK_SHOT, bs->entitynum ); - //if hitpoint is not vertically too far from the ground target - if( idMath::Fabs( trace.endpos[2] - groundtarget[2] ) < 50 ) - { - VectorSubtract( trace.endpos, groundtarget, dir ); - //if the hitpoint is near anough the ground target - if( dir.LengthSqr() < Square( 60 ) ) - { - VectorSubtract( trace.endpos, start, dir ); - //if the hitpoint is far anough from the bot - if( dir.LengthSqr() > Square( 100 ) ) - { - //check if the bot is visible from the ground target - trace.endpos[2] += 1; - //trap_Trace(&trace, trace.endpos, NULL, NULL, entinfo->GetOrigin(), bs->enemy, MASK_SHOT); - gameLocal.Trace( trace, trace.endpos, entinfo->GetOrigin(), MASK_SHOT, bs->enemy ); - if( trace.fraction >= 1 ) - { - //botimport.Print(PRT_MESSAGE, "%1.1f aiming at ground\n", AAS_Time()); - VectorCopy( groundtarget, bestorigin ); - } - } - } - } - } - } - bestorigin[0] += 20 * rvmBotUtil::crandom() * ( 1 - aim_accuracy ); - bestorigin[1] += 20 * rvmBotUtil::crandom() * ( 1 - aim_accuracy ); - bestorigin[2] += 10 * rvmBotUtil::crandom() * ( 1 - aim_accuracy ); + + const float miss = 1.0f - aim_accuracy; + bestorigin[0] += 10.0f * rvmBotUtil::crandom() * miss; + bestorigin[1] += 10.0f * rvmBotUtil::crandom() * miss; + bestorigin[2] += 6.0f * rvmBotUtil::crandom() * miss; } else { - // - VectorCopy( bs->lastenemyorigin, bestorigin ); - bestorigin[2] += 8; - // jmarshall - fix this. - //if the bot is skilled anough - //if (aim_skill > 0.5) { - // //do prediction shots around corners - // if (wi.number == WP_BFG || - // wi.number == WP_ROCKET_LAUNCHER || - // wi.number == WP_GRENADE_LAUNCHER) { - // //create the chase goal - // goal.entitynum = bs->client; - // goal.areanum = bs->areanum; - // VectorCopy(bs->eye, goal.origin); - // VectorSet(goal.mins, -8, -8, -8); - // VectorSet(goal.maxs, 8, 8, 8); - // // - // if (trap_BotPredictVisiblePosition(bs->lastenemyorigin, bs->lastenemyareanum, &goal, TFL_DEFAULT, target)) { - // VectorSubtract(target, bs->eye, dir); - // if (VectorLengthSquared(dir) > Square(80)) { - // VectorCopy(target, bestorigin); - // bestorigin[2] -= 20; - // } - // } - // aim_accuracy = 1; - // } - //} - // jmarshall end + bestorigin = bs->lastenemyorigin; + if( bestorigin.LengthSqr() < Square( 1.0f ) ) + { + bestorigin = entinfo->GetPhysics()->GetOrigin(); + } + bestorigin[2] += 16.0f; } - // - if( enemyvisible ) + + trace_t trace; + gameLocal.Trace( trace, bs->eye, bestorigin, MASK_SHOT, bs->entitynum ); + if( enemyvisible > 0.0f && ( trace.fraction >= 1.0f || trace.c.entityNum == bs->enemy ) ) { - //trap_Trace(&trace, bs->eye, NULL, NULL, bestorigin, bs->entitynum, MASK_SHOT); - gameLocal.Trace( trace, bs->eye, bestorigin, MASK_SHOT, bs->entitynum ); - VectorCopy( trace.endpos, bs->aimtarget ); + bs->aimtarget = bestorigin; } else { - VectorCopy( bestorigin, bs->aimtarget ); + bs->aimtarget = trace.endpos; } - //get aim direction - VectorSubtract( bestorigin, bs->eye, dir ); - // - if( wi.number == WP_MACHINEGUN || - wi.number == WP_SHOTGUN /*|| // jmarshall add lighting railgun. - wi.number == WP_LIGHTNING || - wi.number == WP_RAILGUN*/ ) + + idVec3 dir = bestorigin - bs->eye; + if( dir.LengthSqr() < Square( 1.0f ) ) { - //distance towards the enemy - dist = dir.Length();// VectorLength(dir); - if( dist > 150 ) - { - dist = 150; - } - f = 0.6 + dist / 150 * 0.4; - aim_accuracy *= f; + return; } - //add some random stuff to the aim direction depending on the aim accuracy - if( aim_accuracy < 0.8 ) + + if( wi.number == WP_MACHINEGUN || wi.number == WP_SHOTGUN ) + { + float dist = dir.Length(); + if( dist > 150.0f ) + { + dist = 150.0f; + } + aim_accuracy *= 0.6f + dist / 150.0f * 0.4f; + } + + if( aim_accuracy < 0.8f ) { dir.Normalize(); - for( i = 0; i < 3; i++ ) + const float miss = 1.0f - aim_accuracy; + for( int i = 0; i < 3; i++ ) { - dir[i] += 0.3 * rvmBotUtil::crandom() * ( 1 - aim_accuracy ); + dir[i] += 0.12f * rvmBotUtil::crandom() * miss; } } - //set the ideal view angles - //vectoangles(dir, bs->viewangles); - bs->viewangles = dir.ToAngles(); - //take the weapon spread into account for lower skilled bots - bs->viewangles[PITCH] += 6 * wi.vspread * rvmBotUtil::crandom() * ( 1 - aim_accuracy ); + bs->viewangles = dir.ToAngles(); + bs->viewangles[PITCH] += 2.0f * wi.vspread * rvmBotUtil::crandom() * ( 1.0f - aim_accuracy ); bs->viewangles[PITCH] = idMath::AngleMod( bs->viewangles[PITCH] ); - bs->viewangles[YAW] += 6 * wi.hspread * rvmBotUtil::crandom() * ( 1 - aim_accuracy ); + bs->viewangles[YAW] += 2.0f * wi.hspread * rvmBotUtil::crandom() * ( 1.0f - aim_accuracy ); bs->viewangles[YAW] = idMath::AngleMod( bs->viewangles[YAW] ); - // jmarshall - add bot_challenge. - //if the bots should be really challenging - //if (bot_challenge.integer) { - // //if the bot is really accurate and has the enemy in view for some time - // if (aim_accuracy > 0.9 && bs->enemysight_time < Bot_Time() - 1) { - // //set the view angles directly - // if (bs->ideal_viewangles[PITCH] > 180) bs->ideal_viewangles[PITCH] -= 360; - // VectorCopy(bs->ideal_viewangles, bs->viewangles); - // trap_EA_View(bs->client, bs->viewangles); - // } - //} - //vectoangles(bi->dir, bi->viewangles); - // jmarshall end + bs->botinput.viewangles = bs->viewangles; } @@ -1107,26 +1043,19 @@ rvmBot::BotNearbyGoal */ int rvmBot::BotNearbyGoal( bot_state_t* bs, int tfl, bot_goal_t* ltg, float range ) { - int ret; + if( bs == NULL ) + { + return false; + } - // jmarshall - check for air. - //check if the bot should go for air - //if (BotGoForAir(bs, tfl, ltg, range)) return qtrue; - ////if the bot is carrying the enemy flag - //if (BotCTFCarryingFlag(bs)) { - // //if the bot is just a few secs away from the base - // if (trap_AAS_AreaTravelTimeToGoalArea(bs->areanum, bs->origin, - // bs->teamgoal.areanum, TFL_DEFAULT) < 300) { - // //make the range really small - // range = 50; - // } - //} - // jmarshall end + // In battle the old code only considered tiny detours, so a bot with only a + // pistol or no ammo would keep strafing at the enemy instead of routing through + // a nearby weapon/ammo pickup. Expand the NBG budget only when combat inventory + // makes the pickup tactically useful; otherwise keep it as a short opportunistic + // detour. + range = BotAITacticalPickupRange( bs, range ); - // - ret = botGoalManager.BotChooseNBGItem( bs->gs, bs->origin, bs->inventory, tfl, ltg, range ); - - return ret; + return botGoalManager.BotChooseNBGItem( bs->gs, bs->origin, bs->inventory, tfl, ltg, range ); } @@ -1143,18 +1072,19 @@ void rvmBot::BotGetRandomPointNearPosition(const idVec3 point, idVec3& randomPoi return; } - const int areaNum = aas->PointAreaNum(point); - if (areaNum <= 0) { + idVec3 basePoint = point; + const int baseAreaNum = aas->AdjustPositionAndGetArea(basePoint); + if (baseAreaNum <= 0) { return; } if (radius <= 0.0f) { - randomPoint = aas->AreaCenter(areaNum); - aas->PushPointIntoAreaNum(areaNum, randomPoint); + randomPoint = aas->AreaCenter(baseAreaNum); + aas->PushPointIntoAreaNum(baseAreaNum, randomPoint); return; } - for (int i = 0; i < 16; i++) { + for (int i = 0; i < 24; i++) { const float angle = gameLocal.random.RandomFloat() * idMath::TWO_PI; const float dist = gameLocal.random.RandomFloat() * radius; @@ -1162,19 +1092,22 @@ void rvmBot::BotGetRandomPointNearPosition(const idVec3 point, idVec3& randomPoi testPoint.x += idMath::Cos(angle) * dist; testPoint.y += idMath::Sin(angle) * dist; - const int testAreaNum = aas->PointAreaNum(testPoint); + const int testAreaNum = aas->AdjustPositionAndGetArea(testPoint); if (testAreaNum <= 0) { continue; } - aas->PushPointIntoAreaNum(testAreaNum, testPoint); - - randomPoint = testPoint; - return; + aasPath_t path; + if( aas->WalkPathToGoal( path, baseAreaNum, basePoint, testAreaNum, testPoint, TFL_WALK | TFL_AIR ) ) + { + aas->PushPointIntoAreaNum(testAreaNum, testPoint); + randomPoint = testPoint; + return; + } } - randomPoint = aas->AreaCenter(areaNum); - aas->PushPointIntoAreaNum(areaNum, randomPoint); + randomPoint = aas->AreaCenter(baseAreaNum); + aas->PushPointIntoAreaNum(baseAreaNum, randomPoint); } /* @@ -1184,29 +1117,42 @@ rvmBot::BotMoveInRandomDirection */ int rvmBot::BotMoveInRandomDirection( bot_state_t* bs ) { - rvmBot* ent = gameLocal.entities[bs->client]->Cast(); + if( bs == NULL || !BotAIValidEntityNum( bs->client ) ) + { + return 0; + } - //ent->ResetPathFinding(); + rvmBot* ent = gameLocal.entities[bs->client]->Cast(); + if( ent == NULL ) + { + return 0; + } float dist = idMath::Distance( ent->GetPhysics()->GetOrigin(), bs->random_move_position ); - - if( !ent->PointVisible( bs->random_move_position ) ) + if( bs->random_move_position.LengthSqr() < Square( 1.0f ) || !ent->PointVisible( bs->random_move_position ) ) { - dist = 0; + dist = 0.0f; } - if( dist < 25 || bs->random_move_position.Length() == 0 ) + if( dist < 25.0f ) { - BotGetRandomPointNearPosition( ent->GetPhysics()->GetOrigin(), bs->random_move_position, 50.0f ); + BotGetRandomPointNearPosition( ent->GetPhysics()->GetOrigin(), bs->random_move_position, 80.0f ); } - VectorSubtract( bs->random_move_position, ent->GetPhysics()->GetOrigin(), bs->botinput.dir ); + idVec3 dir = bs->random_move_position - ent->GetPhysics()->GetOrigin(); + dir[2] = 0.0f; + if( dir.LengthSqr() > Square( 1.0f ) ) + { + dir.Normalize(); + } + else + { + dir = idVec3( 0.0f, 0.0f, 0.0f ); + } - idAngles ang( 0, bs->botinput.dir.ToYaw(), 0 ); - bs->botinput.speed = pm_runspeed.GetInteger(); - bs->botinput.dir.Normalize(); + bs->botinput.dir = dir; + bs->botinput.speed = 400.0f; bs->useRandomPosition = true; - bs->botinput.speed = 400; // 200 = walk, 400 = run. return 0; } @@ -1223,9 +1169,27 @@ void rvmBot::MoveToCoverPoint(void) aasObstacle_t obstacles[10]; idVec3 origin = GetOrigin(); idAAS* aas = gameLocal.GetBotAAS(); + if( aas == NULL ) + { + return; + } - areaNum = gameLocal.GetBotAAS()->PointReachableAreaNum(origin, aas->DefaultSearchBounds(), (AREA_REACHABLE_WALK | AREA_REACHABLE_FLY)); - target = aas->AreaCenter(aas->PointAreaNum(gameLocal.GetLocalPlayer()->GetOrigin())); + areaNum = aas->PointReachableAreaNum(origin, aas->DefaultSearchBounds(), (AREA_REACHABLE_WALK | AREA_REACHABLE_FLY)); + if( areaNum <= 0 ) + { + return; + } + idPlayer* localPlayer = gameLocal.GetLocalPlayer(); + if( localPlayer == NULL ) + { + return; + } + const int targetAreaNum = aas->PointAreaNum(localPlayer->GetOrigin()); + if( targetAreaNum <= 0 ) + { + return; + } + target = aas->AreaCenter(targetAreaNum); // consider the target an obstacle obstacles[0].absBounds = idBounds(target).Expand(16); @@ -1245,159 +1209,165 @@ rvmBot::BotCheckAttack */ void rvmBot::BotCheckAttack( bot_state_t* bs ) { - float points, reactiontime, fov, firethrottle; - int attackentity; - trace_t bsptrace; - //float selfpreservation; - idVec3 forward, right, start, end, dir; - idAngles angles; - weaponinfo_t wi; - trace_t trace; - //aas_entityinfo_t entinfo; - idEntity* entinfo; - idVec3 mins( -8, -8, -8 ); - idVec3 maxs( 8, 8, 8 ); - idPlayer* self; - - attackentity = bs->enemy; - // - //BotEntityInfo(attackentity, &entinfo); - entinfo = gameLocal.entities[attackentity]; - - self = gameLocal.entities[bs->client]->Cast(); - - // - reactiontime = botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_REACTIONTIME, 0, 1 ); - if( bs->enemysight_time > Bot_Time() - reactiontime ) - { - return; - } - if( bs->teleport_time > Bot_Time() - reactiontime ) + if( bs == NULL || bs->enemy < 0 || !BotAIValidEntityNum( bs->enemy ) || !BotAIValidEntityNum( bs->client ) ) { return; } - //if changing weapons - if( bs->weaponchange_time > Bot_Time() - 0.1 ) + const int attackentity = bs->enemy; + idEntity* entinfo = gameLocal.entities[attackentity]; + idPlayer* self = gameLocal.entities[bs->client]->Cast(); + if( entinfo == NULL || entinfo->GetPhysics() == NULL || self == NULL ) + { + return; + } + + // Make sure battle states that enter through "attacked" or chase still have + // a real weapon selected before the fire decision is made. + if( bs->weaponnum <= 0 ) + { + BotChooseWeapon( bs ); + } + + const float reactiontime = botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_REACTIONTIME, 0, 1 ); + if( bs->enemysight_time > Bot_Time() - reactiontime || bs->teleport_time > Bot_Time() - reactiontime ) + { + return; + } + + if( bs->weaponchange_time > Bot_Time() - 0.1f ) { return; } - //check fire throttle characteristic if( bs->firethrottlewait_time > Bot_Time() ) { return; } - // jmarshall - this wasn't original behaivor, but multiplying it by 40 feels better for gameplay - firethrottle = botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_FIRETHROTTLE, 0, 1 ) * 40.0f; - // jmarshall end + + float firethrottle = botCharacterStatsManager.Characteristic_BFloat( bs->character, CHARACTERISTIC_FIRETHROTTLE, 0, 1 ); + firethrottle = BotAIClampFloat( 0.0f, 1.0f, firethrottle ); if( bs->firethrottleshoot_time < Bot_Time() ) { if( rvmBotUtil::random() > firethrottle ) { bs->firethrottlewait_time = Bot_Time() + firethrottle; - bs->firethrottleshoot_time = 0; + bs->firethrottleshoot_time = 0.0f; } else { - bs->firethrottleshoot_time = Bot_Time() + 1 - firethrottle; - bs->firethrottlewait_time = 0; + bs->firethrottleshoot_time = Bot_Time() + 1.0f - firethrottle; + bs->firethrottlewait_time = 0.0f; } } - VectorSubtract( bs->aimtarget, bs->eye, dir ); - -// jmarshall - add gauntlet - //if (bs->weaponnum == WP_GAUNTLET) { - // if (dir.LengthSqr() > Square(60)) { - // return; - // } - //} -// jmarshall end - if( dir.LengthSqr() < Square( 100 ) ) - { - fov = 120; - } - else - { - fov = 50; - } - - //vectoangles(dir, angles); - angles = dir.ToAngles(); - if( !self->CheckFOV( entinfo->GetPhysics()->GetOrigin() ) ) - { - return; - } - - //trap_Trace(&bsptrace, bs->eye, NULL, NULL, bs->aimtarget, bs->client, CONTENTS_SOLID | CONTENTS_PLAYERCLIP); - gameLocal.Trace( bsptrace, bs->eye, bs->aimtarget, CONTENTS_SOLID | CONTENTS_PLAYERCLIP, bs->client ); - - if( bsptrace.fraction < 1 && bsptrace.c.entityNum != attackentity ) - { - return; - } - - //get the weapon info + weaponinfo_t wi; botWeaponInfoManager.BotGetWeaponInfo( bs->ws, bs->weaponnum, &wi ); - //get the start point shooting from - VectorCopy( bs->origin, start ); - start[2] += self->GetViewHeight();// bs->cur_ps.viewheight; - bs->viewangles.ToVectors( &forward, &right, NULL ); + idVec3 dir = bs->aimtarget - bs->eye; + if( dir.LengthSqr() < Square( 1.0f ) ) + { + bs->aimtarget = BotAIEntityCenter( entinfo ); + dir = bs->aimtarget - bs->eye; + if( dir.LengthSqr() < Square( 1.0f ) ) + { + return; + } + } + // Keep the safety checks from the firing-regression fix, but do not fire until + // the real player view is close enough to the computed aim point. Previously + // BotCheckAttack used only bs->viewangles, while BotInputToUserCommand still + // smoothed the actual usercmd view, so the bot often shot several frames early. + const float distToAim = dir.Length(); + const float idealAttackFov = distToAim < 100.0f ? 140.0f : 90.0f; + if( !BotAIPointWithinFOV( bs->eye, bs->viewangles, bs->aimtarget, idealAttackFov ) ) + { + return; + } + + float actualAttackFov = 14.0f; + if( distToAim < 120.0f ) + { + actualAttackFov = 45.0f; + } + else if( wi.number == WP_SHOTGUN ) + { + actualAttackFov = 32.0f; + } + else if( wi.proj.damagetype & DAMAGETYPE_RADIAL ) + { + actualAttackFov = 24.0f; + } + else if( wi.speed > 1.0f ) + { + actualAttackFov = 18.0f; + } + if( !BotAIPointWithinFOV( bs->eye, viewAngles, bs->aimtarget, actualAttackFov ) ) + { + return; + } + + trace_t sightTrace; + gameLocal.Trace( sightTrace, bs->eye, bs->aimtarget, MASK_SHOT, bs->entitynum ); + const bool eyeTraceClear = sightTrace.fraction >= 1.0f || sightTrace.c.entityNum == attackentity; + const bool enemyVisible = BotEntityVisibleTest( bs->entitynum, bs->eye, bs->viewangles, 360.0f, attackentity, false ) > 0.0f; + if( !eyeTraceClear && !enemyVisible ) + { + return; + } + + idVec3 forward, right, start, end; + bs->viewangles.ToVectors( &forward, &right, NULL ); + start = bs->eye; start[0] += forward[0] * wi.offset[0] + right[0] * wi.offset[1]; start[1] += forward[1] * wi.offset[0] + right[1] * wi.offset[1]; start[2] += forward[2] * wi.offset[0] + right[2] * wi.offset[1] + wi.offset[2]; - //end point aiming at - VectorMA( start, 1000, forward, end ); - //a little back to make sure not inside a very close enemy - VectorMA( start, -12, forward, start ); - //trap_Trace(&trace, start, mins, maxs, end, bs->entitynum, MASK_SHOT); - gameLocal.Trace( trace, start, end, MASK_SHOT, bs->entitynum ); + end = start + forward * 1000.0f; + start = start - forward * 12.0f; - //if the entity is a client - if( trace.c.entityNum > 0 && trace.c.entityNum <= MAX_CLIENTS ) + trace_t weaponTrace; + gameLocal.Trace( weaponTrace, start, end, MASK_SHOT, bs->entitynum ); + + // Do not shoot through another client. Team checks can be added here later; + // for now this prevents the regression fix from blindly firing through bodies. + if( weaponTrace.c.entityNum >= 0 && weaponTrace.c.entityNum < MAX_CLIENTS && weaponTrace.c.entityNum != attackentity ) { - if( trace.c.entityNum != attackentity ) - { - // jmarshall - teams - //if a teammate is hit - //if (BotSameTeam(bs, trace.entityNum)) - // return; - // jmarshall end - } + return; } - //if won't hit the enemy or not attacking a player (obelisk) - if( trace.c.entityNum != attackentity || attackentity >= MAX_CLIENTS ) + + if( weaponTrace.c.entityNum != attackentity ) { - //if the projectile does radial damage if( wi.proj.damagetype & DAMAGETYPE_RADIAL ) { - if( trace.fraction * 1000 < wi.proj.radius ) + // For splash weapons, only suppress shots that would explode close enough + // to hurt the bot. A ground/wall impact near the enemy is intentional. + if( weaponTrace.fraction < 1.0f && weaponTrace.fraction * 1000.0f < wi.proj.radius + 24.0f ) { - points = ( wi.proj.damage - 0.5 * trace.fraction * 1000 ) * 0.5; - if( points > 0 ) - { - return; - } + return; } - //FIXME: check if a teammate gets radial damage + } + else if( !eyeTraceClear ) + { + return; } } - //if fire has to be release to activate weapon + if( wi.flags & WFL_FIRERELEASED ) { if( bs->flags & BFL_ATTACKED ) { - //trap_EA_Attack(bs->client); bs->botinput.actionflags |= ACTION_ATTACK; + bs->flags &= ~BFL_ATTACKED; + } + else + { + bs->flags |= BFL_ATTACKED; } } else { - //trap_EA_Attack(bs->client); bs->botinput.actionflags |= ACTION_ATTACK; + bs->flags |= BFL_ATTACKED; } - bs->flags ^= BFL_ATTACKED; } diff --git a/neo/doom3/game/bots/BotAI_Battle_Attacked.cpp b/neo/doom3/game/bots/BotAI_Battle_Attacked.cpp index d873732d..957bf6e7 100644 --- a/neo/doom3/game/bots/BotAI_Battle_Attacked.cpp +++ b/neo/doom3/game/bots/BotAI_Battle_Attacked.cpp @@ -18,6 +18,38 @@ stateResult_t rvmBot::state_Attacked(stateParms_t* parms) { return SRESULT_DONE_FRAME; } + if( bs.enemy < 0 || bs.enemy >= gameLocal.num_entities || gameLocal.entities[bs.enemy] == NULL ) + { + stateThread.SetState("state_SeekLTG"); + return SRESULT_DONE_FRAME; + } + + // Update combat distance before weapon selection so the bot can pick + // shotgun/rocket/plasma appropriately instead of using stale inventory + // values from the previous state. + BotUpdateBattleInventory( &bs, bs.enemy ); + BotChooseWeapon( &bs ); + + // If we were ambushed while under-armed, do not stand and trade pistol shots. + // Use the existing BattleNBG state so the bot can keep aiming/firing while it + // moves through a nearby weapon, ammo, health, or armor pickup. + if( bs.check_time < Bot_Time() ) + { + bs.check_time = Bot_Time() + 0.35f; + bot_goal_t pickupAnchor; + pickupAnchor.Reset(); + pickupAnchor.entitynum = bs.enemy; + pickupAnchor.origin = bs.lastenemyorigin; + pickupAnchor.mins = idVec3( -8.0f, -8.0f, -8.0f ); + pickupAnchor.maxs = idVec3( 8.0f, 8.0f, 8.0f ); + if( BotNearbyGoal( &bs, 0, &pickupAnchor, 300.0f ) ) + { + bs.nbg_time = Bot_Time() + 18.0f; + stateThread.SetState("state_BattleNBG"); + return SRESULT_DONE_FRAME; + } + } + if (gameLocal.SysScriptTime() > bs.aggressiveAttackTime || bs.weaponnum == 0) { stateThread.SetState("state_Retreat"); return SRESULT_DONE; @@ -47,4 +79,4 @@ stateResult_t rvmBot::state_Attacked(stateParms_t* parms) { BotCheckAttack(&bs); return SRESULT_WAIT; -} \ No newline at end of file +} diff --git a/neo/doom3/game/bots/BotAI_Battle_Chase.cpp b/neo/doom3/game/bots/BotAI_Battle_Chase.cpp index f59d22f9..a8909495 100644 --- a/neo/doom3/game/bots/BotAI_Battle_Chase.cpp +++ b/neo/doom3/game/bots/BotAI_Battle_Chase.cpp @@ -36,8 +36,18 @@ stateResult_t rvmBot::state_Chase(stateParms_t* parms) return SRESULT_DONE_FRAME; } + if( parms->stage == 0 ) + { + if( bs.chase_time <= Bot_Time() ) + { + bs.chase_time = Bot_Time() + 10.0f; + } + parms->stage = 1; + } + //if no enemy - if( bs.enemy < 0 ) + if( bs.enemy < 0 || bs.enemy >= gameLocal.num_entities || gameLocal.entities[bs.enemy] == NULL || + bs.client < 0 || bs.client >= gameLocal.num_entities || gameLocal.entities[bs.client] == NULL ) { stateThread.SetState("state_SeekLTG"); return SRESULT_DONE_FRAME; @@ -92,7 +102,7 @@ stateResult_t rvmBot::state_Chase(stateParms_t* parms) // if for some reason we don't have line of sight to it, switch to LTG. trace_t trace; - gameLocal.Trace( trace, bs.last_enemy_visible_position, gameLocal.entities[bs.client]->GetOrigin(), CONTENTS_SOLID, 0 ); + gameLocal.Trace( trace, bs.last_enemy_visible_position, gameLocal.entities[bs.client]->GetOrigin(), CONTENTS_SOLID, bs.entitynum ); if( trace.fraction <= 0.9f ) { @@ -111,30 +121,29 @@ stateResult_t rvmBot::state_Chase(stateParms_t* parms) } //if there's no chase time left - if( !bs.chase_time || bs.chase_time < Bot_Time() - 10 ) + if( !bs.chase_time || bs.chase_time < Bot_Time() ) { //AIEnter_Seek_LTG(bs, "battle chase: time out"); stateThread.SetState("state_SeekLTG"); return SRESULT_DONE_FRAME; } - //check for nearby goals periodicly + //check for nearby goals periodically. Chasing should still route through a + // nearby weapon/ammo pickup instead of running past it, especially when the bot + // only has pistol/machinegun or is dry on ammo. if( bs.check_time < Bot_Time() ) { - bs.check_time = Bot_Time() + 1; - range = 150; - // + bs.check_time = Bot_Time() + 0.5f; + range = 260.0f; if( BotNearbyGoal( &bs, 0, &goal, range ) ) { - //the bot gets 5 seconds to pick up the nearby goal item - bs.nbg_time = Bot_Time() + 0.1 * range + 1; - //BotResetLastAvoidReach(bs.ms); - //AIEnter_Battle_NBG(bs, "battle chase: nbg"); + bs.nbg_time = Bot_Time() + 18.0f; stateThread.SetState("state_BattleNBG"); return SRESULT_DONE_FRAME; } } BotUpdateBattleInventory( &bs, bs.enemy ); + BotChooseWeapon( &bs ); ////initialize the movement state //BotSetupForMovement(bs); diff --git a/neo/doom3/game/bots/BotAI_Battle_Fight.cpp b/neo/doom3/game/bots/BotAI_Battle_Fight.cpp index 99d2c4b2..322740a4 100644 --- a/neo/doom3/game/bots/BotAI_Battle_Fight.cpp +++ b/neo/doom3/game/bots/BotAI_Battle_Fight.cpp @@ -12,7 +12,6 @@ rvmBot::state_BattleFight */ stateResult_t rvmBot::state_BattleFight(stateParms_t* parms) { - int areanum; idVec3 target; idPlayer* entinfo; @@ -30,14 +29,18 @@ stateResult_t rvmBot::state_BattleFight(stateParms_t* parms) } //if no enemy - if( bs.enemy < 0 ) + if( bs.enemy < 0 || bs.enemy >= gameLocal.num_entities || gameLocal.entities[bs.enemy] == NULL ) { stateThread.SetState("state_SeekLTG"); return SRESULT_DONE_FRAME; } - //BotEntityInfo(bs.enemy, &entinfo); - entinfo = gameLocal.entities[bs.enemy]->Cast();// &g_entities[bs.enemy]; + entinfo = gameLocal.entities[bs.enemy]->Cast(); + if( entinfo == NULL ) + { + stateThread.SetState("state_SeekLTG"); + return SRESULT_DONE_FRAME; + } //if the enemy is dead if( bs.enemydeath_time ) @@ -45,106 +48,72 @@ stateResult_t rvmBot::state_BattleFight(stateParms_t* parms) if( bs.enemydeath_time < Bot_Time() - 1.0 ) { bs.enemydeath_time = 0; - if( bs.enemysuicide ) - { - // jmarshall - bot chat - //BotChat_EnemySuicide(bs); - // jmarshall end - } - // jmarshall - bot chat stand - // if (bs.lastkilledplayer == bs.enemy && BotChat_Kill(bs)) { - // bs.stand_time = FloatTime() + BotChatTime(bs); - // AIEnter_Stand(bs, "battle fight: enemy dead"); - // } - // else { bs.ltg_time = 0; - //AIEnter_Seek_LTG(bs, "battle fight: enemy dead"); stateThread.SetState("state_SeekLTG"); - // } - // jmarshall end return SRESULT_DONE_FRAME; } } - else + else if( EntityIsDead( entinfo ) ) { - if( EntityIsDead( entinfo ) ) - { - bs.enemydeath_time = Bot_Time(); - } + bs.enemydeath_time = Bot_Time(); } -// jmarshall - isinvisible - //if the enemy is invisible and not shooting the bot looses track easily - //if (entinfo->IsInvisible() && !entinfo->IsShooting()) { - // if (rvmBotUtil::random() < 0.2) { - // stateThread.SetState("state_SeekLTG"); - // return; - // } - //} - // - target = entinfo->GetOrigin(); - //update the reachability area and origin if possible - //areanum = BotPointAreaNum(target); - //if (areanum && trap_AAS_AreaReachability(areanum)) { - // VectorCopy(target, bs.lastenemyorigin); - // bs.lastenemyareanum = areanum; - //} -// bs.lastenemyareanum = areanum; + target = entinfo->GetOrigin(); //update the attack inventory values BotUpdateBattleInventory( &bs, bs.enemy ); - //if the bot's health decreased -// jmarshall - bot chat - //if (bs.lastframe_health > bs.inventory[INVENTORY_HEALTH]) { - // if (BotChat_HitNoDeath(bs)) { - // bs.stand_time = FloatTime() + BotChatTime(bs); - // AIEnter_Stand(bs, "battle fight: chat health decreased"); - // return qfalse; - // } - //} - - //if the bot hit someone - //if (bs.cur_ps.persistant[PERS_HITS] > bs.lasthitcount) { - // if (BotChat_HitNoKill(bs)) { - // bs.stand_time = FloatTime() + BotChatTime(bs); - // AIEnter_Stand(bs, "battle fight: chat hit someone"); - // return qfalse; - // } - //} -// jmarshall end - //if the enemy is not visible if( !BotEntityVisible( bs.entitynum, bs.eye, bs.viewangles, 360, bs.enemy ) ) { if( BotWantsToChase( &bs ) ) { - //AIEnter_Battle_Chase(bs, "battle fight: enemy out of sight"); + bs.chase_time = Bot_Time() + 10.0f; stateThread.SetState("state_Chase"); return SRESULT_DONE_FRAME; } else { - //AIEnter_Seek_LTG(bs, "battle fight: enemy out of sight"); stateThread.SetState("state_SeekLTG"); return SRESULT_DONE_FRAME; } } + + bs.enemyvisible_time = Bot_Time(); + bs.lastenemyorigin = target; + bs.last_enemy_visible_position = target; + //use holdable items BotBattleUseItems( &bs ); - // - //bs.tfl = TFL_DEFAULT; - //if (bot_grapple.integer) bs.tfl |= TFL_GRAPPLEHOOK; - ////if in lava or slime the bot should be able to get out - //if (BotInLavaOrSlime(bs)) bs.tfl |= TFL_LAVA | TFL_SLIME; - //// - //if (BotCanAndWantsToRocketJump(bs)) { - // bs.tfl |= TFL_ROCKETJUMP; - //} + //choose the best weapon to fight with BotChooseWeapon( &bs ); - // Move randomly around our AAS area. + // Opportunistic combat pickup: keep fighting, but route through a nearby item + // instead of throwing the current goal away and random-strafing forever. The + // NBG selector itself expands this range when the bot is stuck on pistol/dry + // ammo, and otherwise keeps the detour short. + if( bs.check_time < Bot_Time() ) + { + bs.check_time = Bot_Time() + 0.45f; + + bot_goal_t pickupAnchor; + pickupAnchor.Reset(); + pickupAnchor.entitynum = bs.enemy; + pickupAnchor.origin = target; + pickupAnchor.mins = idVec3( -8.0f, -8.0f, -8.0f ); + pickupAnchor.maxs = idVec3( 8.0f, 8.0f, 8.0f ); + + const float pickupRange = 260.0f; + if( BotNearbyGoal( &bs, 0, &pickupAnchor, pickupRange ) ) + { + bs.nbg_time = Bot_Time() + 18.0f; + stateThread.SetState("state_BattleNBG"); + return SRESULT_DONE_FRAME; + } + } + + // Move randomly around our AAS area only when there is no useful nearby pickup. BotMoveInRandomDirection(&bs); //aim at the enemy @@ -158,11 +127,10 @@ stateResult_t rvmBot::state_BattleFight(stateParms_t* parms) { if( BotWantsToRetreat( &bs ) ) { - //AIEnter_Battle_Retreat(bs, "battle fight: wants to retreat"); stateThread.SetState("state_Retreat"); return SRESULT_DONE_FRAME; } } return SRESULT_WAIT; -} \ No newline at end of file +} diff --git a/neo/doom3/game/bots/BotAI_Battle_NBG.cpp b/neo/doom3/game/bots/BotAI_Battle_NBG.cpp index 371b2d2c..79ea7348 100644 --- a/neo/doom3/game/bots/BotAI_Battle_NBG.cpp +++ b/neo/doom3/game/bots/BotAI_Battle_NBG.cpp @@ -12,23 +12,9 @@ rvmBot::state_BattleNBG */ stateResult_t rvmBot::state_BattleNBG(stateParms_t* parms) { - int areanum; bot_goal_t goal; - //aas_entityinfo_t entinfo; - idEntity* entinfo; - //bot_moveresult_t moveresult; - float attack_skill; - idVec3 target, dir; - - //if (BotIsObserver(bs)) { - // AIEnter_Observer(bs, "battle nbg: observer"); - // return qfalse; - //} - ////if in the intermission - //if (BotIntermission(bs)) { - // AIEnter_Intermission(bs, "battle nbg: intermission"); - // return qfalse; - //} + idPlayer* entinfo; + idVec3 target; // respawn if dead. if( BotIsDead( &bs ) ) @@ -38,56 +24,31 @@ stateResult_t rvmBot::state_BattleNBG(stateParms_t* parms) } // if no enemy. - if( bs.enemy < 0 ) + if( bs.enemy < 0 || bs.enemy >= gameLocal.num_entities || gameLocal.entities[bs.enemy] == NULL ) { stateThread.SetState("state_SeekLTG"); return SRESULT_DONE_FRAME; } - //BotEntityInfo(bs.enemy, &entinfo); entinfo = gameLocal.entities[bs.enemy]->Cast(); + if( entinfo == NULL ) + { + stateThread.SetState("state_SeekLTG"); + return SRESULT_DONE_FRAME; + } if( entinfo->health <= 0 ) { - //AIEnter_Seek_NBG(bs, "battle nbg: enemy dead"); stateThread.SetState("state_SeekNBG"); return SRESULT_DONE_FRAME; } - //bs.tfl = TFL_DEFAULT; - //if (bot_grapple.integer) bs.tfl |= TFL_GRAPPLEHOOK; - ////if in lava or slime the bot should be able to get out - //if (BotInLavaOrSlime(bs)) bs.tfl |= TFL_LAVA | TFL_SLIME; - //// - //if (BotCanAndWantsToRocketJump(bs)) { - // bs.tfl |= TFL_ROCKETJUMP; - //} - ////map specific code - //BotMapScripts(bs); - //update the last time the enemy was visible if( BotEntityVisible( bs.entitynum, bs.eye, bs.viewangles, 360, bs.enemy ) ) { bs.enemyvisible_time = Bot_Time(); - //VectorCopy(entinfo->GetOrigin(), target); target = entinfo->GetOrigin(); - // if not a player enemy - if( bs.enemy >= MAX_CLIENTS ) - { -#ifdef MISSIONPACK - // if attacking an obelisk - if( bs.enemy == redobelisk.entitynum || - bs.enemy == blueobelisk.entitynum ) - { - target[2] += 16; - } -#endif - } - //update the reachability area and origin if possible - //areanum = BotPointAreaNum(target); - //if (areanum && trap_AAS_AreaReachability(areanum)) { - VectorCopy( target, bs.lastenemyorigin ); - // bs.lastenemyareanum = areanum; - //} + bs.lastenemyorigin = target; + bs.last_enemy_visible_position = target; } //if the bot has no goal or touches the current goal @@ -99,20 +60,16 @@ stateResult_t rvmBot::state_BattleNBG(stateParms_t* parms) { bs.nbg_time = 0; } - // + if( bs.nbg_time < Bot_Time() ) { - //pop the current goal from the stack botGoalManager.BotPopGoal( bs.gs ); - //if the bot still has a goal if( botGoalManager.BotGetTopGoal( bs.gs, &goal ) ) { - //AIEnter_Battle_Retreat(bs, "battle nbg: time out"); stateThread.SetState("state_Retreat"); } else { - //AIEnter_Battle_Fight(bs, "battle nbg: time out"); stateThread.SetState("state_BattleFight"); } @@ -122,57 +79,15 @@ stateResult_t rvmBot::state_BattleNBG(stateParms_t* parms) //move towards the goal BotMoveToGoal( &bs, &goal ); - //initialize the movement state - //BotSetupForMovement(bs); - ////move towards the goal - //trap_BotMoveToGoal(&moveresult, bs.ms, &goal, bs.tfl); - ////if the movement failed - //if (moveresult.failure) { - // //reset the avoid reach, otherwise bot is stuck in current area - // trap_BotResetAvoidReach(bs.ms); - // //BotAI_Print(PRT_MESSAGE, "movement failure %d\n", moveresult.traveltype); - // bs.nbg_time = 0; - //} - //// - //BotAIBlocked(bs, &moveresult, qfalse); - //update the attack inventory values BotUpdateBattleInventory( &bs, bs.enemy ); //choose the best weapon to fight with BotChooseWeapon( &bs ); - //if the view is fixed for the movement - //if (moveresult.flags & (MOVERESULT_MOVEMENTVIEW | MOVERESULT_SWIMVIEW)) { - // VectorCopy(moveresult.ideal_viewangles, bs.ideal_viewangles); - //} - //else if (!(moveresult.flags & MOVERESULT_MOVEMENTVIEWSET) - // && !(bs.flags & BFL_IDEALVIEWSET)) { - // attack_skill = trap_Characteristic_BFloat(bs.character, CHARACTERISTIC_ATTACK_SKILL, 0, 1); - // //if the bot is skilled anough and the enemy is visible - // if (attack_skill > 0.3) { - // //&& BotEntityVisible(bs.entitynum, bs.eye, bs.viewangles, 360, bs.enemy) - // BotAimAtEnemy(bs); - // } - // else { - // if (trap_BotMovementViewTarget(bs.ms, &goal, bs.tfl, 300, target)) { - // VectorSubtract(target, bs.origin, dir); - // vectoangles(dir, bs.ideal_viewangles); - // } - // else { - // vectoangles(moveresult.movedir, bs.ideal_viewangles); - // } - // bs.ideal_viewangles[2] *= 0.5; - // } - //} - //if (attack_skill > 0.3) { - //&& BotEntityVisible(bs.entitynum, bs.eye, bs.viewangles, 360, bs.enemy) BotAimAtEnemy( &bs ); - //} - //if the weapon is used for the bot movement - //if (moveresult.flags & MOVERESULT_MOVEMENTWEAPON) bs.weaponnum = moveresult.weapon; //attack the enemy if possible BotCheckAttack( &bs ); return SRESULT_WAIT; -} \ No newline at end of file +} diff --git a/neo/doom3/game/bots/BotAI_Battle_Retreat.cpp b/neo/doom3/game/bots/BotAI_Battle_Retreat.cpp index bcc37517..b9bddd6a 100644 --- a/neo/doom3/game/bots/BotAI_Battle_Retreat.cpp +++ b/neo/doom3/game/bots/BotAI_Battle_Retreat.cpp @@ -26,7 +26,7 @@ stateResult_t rvmBot::state_Retreat(stateParms_t* parms) } // if no enemy. - if( bs.enemy < 0 ) + if( bs.enemy < 0 || bs.enemy >= gameLocal.num_entities || gameLocal.entities[bs.enemy] == NULL ) { stateThread.SetState("state_SeekLTG"); return SRESULT_DONE_FRAME; @@ -57,6 +57,27 @@ stateResult_t rvmBot::state_Retreat(stateParms_t* parms) //update the attack inventory values BotUpdateBattleInventory( &bs, bs.enemy ); + BotChooseWeapon( &bs ); + + // Before switching back to chase, opportunistically grab a nearby tactical + // pickup. This keeps the bot from running past a rocket launcher, shells, or + // health while it is already committed to a fight. + if( bs.check_time < Bot_Time() ) + { + bs.check_time = Bot_Time() + 0.5f; + bot_goal_t pickupAnchor; + pickupAnchor.Reset(); + pickupAnchor.entitynum = bs.enemy; + pickupAnchor.origin = entinfo->GetOrigin(); + pickupAnchor.mins = idVec3( -8.0f, -8.0f, -8.0f ); + pickupAnchor.maxs = idVec3( 8.0f, 8.0f, 8.0f ); + if( BotNearbyGoal( &bs, 0, &pickupAnchor, 260.0f ) ) + { + bs.nbg_time = Bot_Time() + 18.0f; + stateThread.SetState("state_BattleNBG"); + return SRESULT_DONE_FRAME; + } + } //if the bot doesn't want to retreat anymore... probably picked up some nice items if( BotWantsToChase( &bs ) ) @@ -76,6 +97,7 @@ stateResult_t rvmBot::state_Retreat(stateParms_t* parms) bs.enemyvisible_time = Bot_Time(); target = entinfo->GetOrigin(); bs.lastenemyorigin = target; + bs.last_enemy_visible_position = target; } //if the enemy is NOT visible for 4 seconds @@ -108,19 +130,15 @@ stateResult_t rvmBot::state_Retreat(stateParms_t* parms) return SRESULT_DONE_FRAME; } - //check for nearby goals periodicly + //check for nearby goals periodically while retreating along the long-term item path if( bs.check_time < Bot_Time() ) { - bs.check_time = Bot_Time() + 1; - range = 150; + bs.check_time = Bot_Time() + 0.5f; + range = 260.0f; - // if( BotNearbyGoal( &bs, 0, &goal, range ) ) { - //trap_BotResetLastAvoidReach(bs.ms); - //time the bot gets to pick up the nearby goal item - bs.nbg_time = Bot_Time() + range / 100 + 1; - //AIEnter_Battle_NBG(bs, "battle retreat: nbg"); + bs.nbg_time = Bot_Time() + 18.0f; stateThread.SetState("state_BattleNBG"); return SRESULT_DONE_FRAME; } diff --git a/neo/doom3/game/bots/Bot_Input.cpp b/neo/doom3/game/bots/Bot_Input.cpp index f65896e5..730ace47 100644 --- a/neo/doom3/game/bots/Bot_Input.cpp +++ b/neo/doom3/game/bots/Bot_Input.cpp @@ -16,65 +16,47 @@ void rvmBot::BotInputToUserCommand(bot_input_t* bi, usercmd_t* ucmd, int time) { idVec3 forward, right; - short temp; - int j; + if( bi == NULL || ucmd == NULL ) + { + return; + } - //clear the whole structure -// memset(ucmd, 0, sizeof(usercmd_t)); - // - //common->Printf("dir = %f %f %f speed = %f\n", bi->dir[0], bi->dir[1], bi->dir[2], bi->speed); - //the duration for the user command in milli seconds - // if (bi->actionflags & ACTION_DELAYEDJUMP) { bi->actionflags |= ACTION_JUMP; bi->actionflags &= ~ACTION_DELAYEDJUMP; } - //set the buttons + if (bi->actionflags & ACTION_RESPAWN) { - ucmd->buttons = BUTTON_ATTACK; + ucmd->buttons |= BUTTON_ATTACK; } if (bi->actionflags & ACTION_ATTACK) { ucmd->buttons |= BUTTON_ATTACK; } - //if (bi->actionflags & ACTION_TALK) ucmd->buttons |= BUTTON_TALK; - //if (bi->actionflags & ACTION_GESTURE) ucmd->buttons |= BUTTON_GESTURE; - //if (bi->actionflags & ACTION_USE) ucmd->buttons |= BUTTON_USE_HOLDABLE; if (bi->actionflags & ACTION_WALK) { ucmd->buttons |= BUTTON_RUN; } - //if (bi->actionflags & ACTION_AFFIRMATIVE) ucmd->buttons |= BUTTON_AFFIRMATIVE; - //if (bi->actionflags & ACTION_NEGATIVE) ucmd->buttons |= BUTTON_NEGATIVE; - //if (bi->actionflags & ACTION_GETFLAG) ucmd->buttons |= BUTTON_GETFLAG; - //if (bi->actionflags & ACTION_GUARDBASE) ucmd->buttons |= BUTTON_GUARDBASE; - //if (bi->actionflags & ACTION_PATROL) ucmd->buttons |= BUTTON_PATROL; - //if (bi->actionflags & ACTION_FOLLOWME) ucmd->buttons |= BUTTON_FOLLOWME; - // - ucmd->impulse |= bi->weapon; + + ucmd->impulse = 0; if (bi->lastWeaponNum != bi->weapon) { - //ucmd->flags = UCF_IMPULSE_SEQUENCE; + ucmd->impulse = bi->weapon; bi->lastWeaponNum = bi->weapon; } - else - { - //ucmd->flags = 0; - } idAngles botViewAngles = viewAngles; - + // Slow smoothing made bots fire while their usercmd view was still several + // frames behind the computed aim, which is very noticeable when looking down + // stairs. Turn briskly in general and snap on the frames where the bot is + // actually firing. + const float turnScale = ( bi->actionflags & ACTION_ATTACK ) ? 1.0f : 0.35f; + for (int i = 0; i < 3; i++) { - int i; - float move; - float angMod = (1.0f / 12.0f); - - for (i = 0; i < 3; i++) { - move = idMath::AngleDelta(bi->viewangles[i], botViewAngles[i]); - botViewAngles[i] += (move * angMod); - } + const float move = idMath::AngleDelta( bi->viewangles[i], botViewAngles[i] ); + botViewAngles[i] += move * turnScale; } //set the view angles @@ -83,46 +65,57 @@ void rvmBot::BotInputToUserCommand(bot_input_t* bi, usercmd_t* ucmd, int time) ucmd->angles[1] = ANGLE2SHORT(botViewAngles[1] - deltaViewAngles[1]); ucmd->angles[2] = ANGLE2SHORT(botViewAngles[2] - deltaViewAngles[2]); - bi->viewangles.ToVectors(&forward, &right, NULL); + // Movement commands are relative to the view angles that are actually put into + // this usercmd, not the unsmoothed desired angles. Also keep movement on the + // horizontal plane so looking up/down at an enemy does not change speed. + idAngles moveAngles = botViewAngles; + moveAngles[PITCH] = 0.0f; + moveAngles[ROLL] = 0.0f; + moveAngles.ToVectors(&forward, &right, NULL); - //bot input speed is in the range [0, 400] - bi->speed = bi->speed * 127 / 400; - //set the view independent movement - ucmd->forwardmove = idMath::ClampChar(DotProduct(forward, bi->dir) * bi->speed); - ucmd->rightmove = idMath::ClampChar(DotProduct(right, bi->dir) * bi->speed); - //ucmd->upmove = abs(forward[2]) * bi->dir[2] * bi->speed; + idVec3 moveDir = bi->dir; + moveDir[2] = 0.0f; + if( moveDir.LengthSqr() > Square( 0.001f ) ) + { + moveDir.Normalize(); + } + else + { + moveDir = idVec3( 0.0f, 0.0f, 0.0f ); + } + + float scaledSpeed = bi->speed; + if( scaledSpeed < 0.0f ) + { + scaledSpeed = 0.0f; + } + else if( scaledSpeed > 400.0f ) + { + scaledSpeed = 400.0f; + } + scaledSpeed = scaledSpeed * 127.0f / 400.0f; + + ucmd->forwardmove = idMath::ClampChar( DotProduct(forward, moveDir) * scaledSpeed ); + ucmd->rightmove = idMath::ClampChar( DotProduct(right, moveDir) * scaledSpeed ); //normal keyboard movement if (bi->actionflags & ACTION_MOVEFORWARD) { - ucmd->forwardmove += 127; + ucmd->forwardmove = idMath::ClampChar( ucmd->forwardmove + 127 ); } if (bi->actionflags & ACTION_MOVEBACK) { - ucmd->forwardmove -= 127; + ucmd->forwardmove = idMath::ClampChar( ucmd->forwardmove - 127 ); } if (bi->actionflags & ACTION_MOVELEFT) { - ucmd->rightmove -= 127; + ucmd->rightmove = idMath::ClampChar( ucmd->rightmove - 127 ); } if (bi->actionflags & ACTION_MOVERIGHT) { - ucmd->rightmove += 127; + ucmd->rightmove = idMath::ClampChar( ucmd->rightmove + 127 ); } - //jump/moveup - //if (bi->actionflags & ACTION_JUMP) - // ucmd->buttons |= BUTTON_JUMP; - - // ucmd->upmove += 127; - // - ////crouch/movedown - //if (bi->actionflags & ACTION_CROUCH) - // ucmd->upmove -= 127; - // - //Com_Printf("forward = %d right = %d up = %d\n", ucmd.forwardmove, ucmd.rightmove, ucmd.upmove); - //Com_Printf("ucmd->serverTime = %d\n", ucmd->serverTime); - if( bi->respawn ) { ucmd->buttons |= BUTTON_ATTACK; diff --git a/neo/doom3/game/bots/Bot_char.cpp b/neo/doom3/game/bots/Bot_char.cpp index 7b37901d..53300a92 100644 --- a/neo/doom3/game/bots/Bot_char.cpp +++ b/neo/doom3/game/bots/Bot_char.cpp @@ -59,7 +59,6 @@ bot_character_t* idBotCharacterStatsManager::AllocBotCharacter( void ) { memset( &ch->c[0], 0, MAX_CHARACTERISTICS * sizeof( bot_characteristic_t ) ); ch->filename = ""; - ch->inUse = false; ch->skill = 0; } else @@ -70,6 +69,8 @@ bot_character_t* idBotCharacterStatsManager::AllocBotCharacter( void ) + ch->inUse = true; + return ch; } @@ -157,7 +158,7 @@ bot_character_t* idBotCharacterStatsManager::BotLoadCharacterFromFile( const cha } index = token.GetIntValue(); - if( index < 0 || index > MAX_CHARACTERISTICS ) + if( index < 0 || index >= MAX_CHARACTERISTICS ) { FreeCharacterFile( ch ); parser.Error( "characteristic index out of range [0, %d]\n", MAX_CHARACTERISTICS ); @@ -256,6 +257,12 @@ idBotCharacterStatsManager::CheckCharacteristicIndex */ int idBotCharacterStatsManager::CheckCharacteristicIndex( bot_character_t* ch, int index ) { + if( ch == NULL ) + { + gameLocal.Error( "NULL bot character +" ); + return false; + } if( index < 0 || index >= MAX_CHARACTERISTICS ) { gameLocal.Error( "characteristic %d does not exist\n", index ); @@ -309,6 +316,12 @@ Characteristic_String */ void idBotCharacterStatsManager::Characteristic_String( bot_character_t* ch, int index, char* buf, int size ) { + if( buf == NULL || size <= 0 ) + { + return; + } + buf[0] = '\0'; + //check if the index is in range if( !CheckCharacteristicIndex( ch, index ) ) { @@ -318,8 +331,7 @@ void idBotCharacterStatsManager::Characteristic_String( bot_character_t* ch, int //an integer will be converted to a float if( ch->c[index].type == CT_STRING ) { - strcpy( buf, ch->c[index].value.string ); - buf[size - 1] = '\0'; + idStr::Copynz( buf, ch->c[index].value.string, size ); return; } gameLocal.Error( "characteristic %d is not a string\n", index ); diff --git a/neo/doom3/game/bots/Bot_chat.cpp b/neo/doom3/game/bots/Bot_chat.cpp index 36d440e5..4795bbb9 100644 --- a/neo/doom3/game/bots/Bot_chat.cpp +++ b/neo/doom3/game/bots/Bot_chat.cpp @@ -35,6 +35,11 @@ rvmBot::BotSendChatMessage ==================== */ void rvmBot::BotSendChatMessage(botChat_t chat, const char* targetName) { + if( targetName == NULL ) + { + targetName = ""; + } + switch (chat) { case KILL: diff --git a/neo/doom3/game/bots/Bot_goal.cpp b/neo/doom3/game/bots/Bot_goal.cpp index 8217343f..f322187d 100644 --- a/neo/doom3/game/bots/Bot_goal.cpp +++ b/neo/doom3/game/bots/Bot_goal.cpp @@ -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(); 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(); - 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(); 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 } /* diff --git a/neo/doom3/game/bots/Bot_weapons.cpp b/neo/doom3/game/bots/Bot_weapons.cpp index ab0bb2cb..5e80013f 100644 --- a/neo/doom3/game/bots/Bot_weapons.cpp +++ b/neo/doom3/game/bots/Bot_weapons.cpp @@ -17,7 +17,7 @@ idBotWeaponInfoManager::BotValidWeaponNumber */ int idBotWeaponInfoManager::BotValidWeaponNumber( int weaponnum ) { - if( weaponnum <= 0 || weaponnum > BOT_MAX_WEAPONS ) + if( weaponnum < 0 || weaponnum >= BOT_MAX_WEAPONS ) { gameLocal.Error( "weapon number out of range\n" ); return false; @@ -35,6 +35,7 @@ bot_weaponstate_t* idBotWeaponInfoManager::BotWeaponStateFromHandle( int handle if( handle <= 0 || handle > MAX_CLIENTS ) { gameLocal.Error( "move state handle %d out of range\n", handle ); + return NULL; } return &botweaponstates[handle]; @@ -374,19 +375,27 @@ idBotWeaponInfoManager::BotGetWeaponInfo */ void idBotWeaponInfoManager::BotGetWeaponInfo( int weaponstate, int weapon, weaponinfo_t* weaponinfo ) { - bot_weaponstate_t* ws; - - if( !BotValidWeaponNumber( weapon ) ) - { - return; - } - ws = BotWeaponStateFromHandle( weaponstate ); - if( !ws ) + if( weaponinfo == NULL ) { return; } - *weaponinfo = this->weaponinfo[weapon]; + if( BotValidWeaponNumber( weapon ) && this->weaponinfo[weapon].valid ) + { + *weaponinfo = this->weaponinfo[weapon]; + return; + } + + // Fallback: return the first loaded weapon so callers never keep an + // uninitialized weaponinfo_t when a bot has not picked a valid weapon yet. + for( int i = 0; i < BOT_MAX_WEAPONS; i++ ) + { + if( this->weaponinfo[i].valid ) + { + *weaponinfo = this->weaponinfo[i]; + return; + } + } } /* diff --git a/neo/doom3/game/bots/Bot_weights.cpp b/neo/doom3/game/bots/Bot_weights.cpp index ad27de73..dc8b56a0 100644 --- a/neo/doom3/game/bots/Bot_weights.cpp +++ b/neo/doom3/game/bots/Bot_weights.cpp @@ -48,14 +48,15 @@ bool idBotFuzzyWeightManager::ReadValue( idParser& source, float* value ) { idToken token; - if( !source.ReadToken( &token ) ) + if( value == NULL || !source.ReadToken( &token ) ) { return false; } + bool negative = false; if( token == "-" ) { - source.Warning( "negative value set to zero\n" ); + negative = true; if( !source.ExpectTokenType( TT_NUMBER, 0, &token ) ) { return false; @@ -68,8 +69,7 @@ bool idBotFuzzyWeightManager::ReadValue( idParser& source, float* value ) return false; } - *value = token.GetFloatValue(); - + *value = negative ? -token.GetFloatValue() : token.GetFloatValue(); return true; } @@ -382,16 +382,17 @@ weightconfig_t* idBotFuzzyWeightManager::ReadWeightConfig( char* filename ) if( config->inUse ) { + if( config->filename == filename ) + { + return config; + } continue; } - if( config->filename == filename ) + if( avail == -1 ) { - return config; + avail = n; } - - avail = n; - break; } if( avail == -1 ) @@ -570,10 +571,10 @@ float idBotFuzzyWeightManager::FuzzyWeight_r( int* inventory, fuzzyseperator_t* } //the scale factor - scale = ( inventory[fs->index] - fs->value ) / ( fs->next->value - fs->value ); + scale = ( float )( inventory[fs->index] - fs->value ) / ( float )( fs->next->value - fs->value ); //scale between the two weights - return scale * w1 + ( 1 - scale ) * w2; + return ( 1 - scale ) * w1 + scale * w2; } return FuzzyWeight_r( inventory, fs->next ); } @@ -625,10 +626,10 @@ float idBotFuzzyWeightManager::FuzzyWeightUndecided_r( int* inventory, fuzzysepe } //the scale factor - scale = ( inventory[fs->index] - fs->value ) / ( fs->next->value - fs->value ); + scale = ( float )( inventory[fs->index] - fs->value ) / ( float )( fs->next->value - fs->value ); //scale between the two weights - return scale * w1 + ( 1 - scale ) * w2; + return ( 1 - scale ) * w1 + scale * w2; } return FuzzyWeightUndecided_r( inventory, fs->next ); } @@ -722,7 +723,7 @@ void idBotFuzzyWeightManager::ScaleFuzzySeperator_r( fuzzyseperator_t* fs, float } else if( fs->type == WT_BALANCE ) { - fs->weight = ( fs->maxweight + fs->minweight ) * scale; + fs->weight = fs->minweight + ( fs->maxweight - fs->minweight ) * scale; //get the weight between bounds if( fs->weight < fs->minweight ) @@ -832,7 +833,7 @@ int idBotFuzzyWeightManager::InterbreedFuzzySeperator_r( fuzzyseperator_t* fs1, gameLocal.Error( "cannot interbreed weight configs, unequal child\n" ); return false; } - if( !InterbreedFuzzySeperator_r( fs2->child, fs2->child, fsout->child ) ) + if( !InterbreedFuzzySeperator_r( fs1->child, fs2->child, fsout->child ) ) { return false; } @@ -849,7 +850,7 @@ int idBotFuzzyWeightManager::InterbreedFuzzySeperator_r( fuzzyseperator_t* fs1, { fsout->maxweight = fsout->weight; } - if( fsout->weight > fsout->minweight ) + if( fsout->weight < fsout->minweight ) { fsout->minweight = fsout->weight; } @@ -905,11 +906,10 @@ void idBotFuzzyWeightManager::BotShutdownWeights( void ) for( i = 0; i < MAX_WEIGHT_FILES; i++ ) { - weightFileList[i].inUse = false; - //if (weightFileList[i]) - //{ - // FreeWeightConfig2(weightFileList[i]); - // weightFileList[i] = NULL; - //} + if( weightFileList[i].inUse ) + { + FreeWeightConfig2( &weightFileList[i] ); + } + weightFileList[i].Reset(); } } \ No newline at end of file diff --git a/neo/doom3/game/gamesys/GameTypeInfo.h b/neo/doom3/game/gamesys/GameTypeInfo.h index 8530c68a..9828f263 100644 --- a/neo/doom3/game/gamesys/GameTypeInfo.h +++ b/neo/doom3/game/gamesys/GameTypeInfo.h @@ -8476,6 +8476,7 @@ static classVariableInfo_t bot_state_t_typeInfo[] = { { "float", "enemysight_time", (intptr_t)(&((bot_state_t *)0)->enemysight_time), sizeof( ((bot_state_t *)0)->enemysight_time ) }, { "float", "enemydeath_time", (intptr_t)(&((bot_state_t *)0)->enemydeath_time), sizeof( ((bot_state_t *)0)->enemydeath_time ) }, { "float", "aggressiveAttackTime", (intptr_t)(&((bot_state_t *)0)->aggressiveAttackTime), sizeof( ((bot_state_t *)0)->aggressiveAttackTime ) }, + { "float", "enemyposition_time", (intptr_t)(&((bot_state_t *)0)->enemyposition_time), sizeof( ((bot_state_t *)0)->enemyposition_time ) }, { "idVec3", "origin", (intptr_t)(&((bot_state_t *)0)->origin), sizeof( ((bot_state_t *)0)->origin ) }, { "idVec3", "aimtarget", (intptr_t)(&((bot_state_t *)0)->aimtarget), sizeof( ((bot_state_t *)0)->aimtarget ) }, { "idVec3", "random_move_position", (intptr_t)(&((bot_state_t *)0)->random_move_position), sizeof( ((bot_state_t *)0)->random_move_position ) },