// BotAI.cpp // #pragma hdrstop #include "precompiled.h" #include "../Game_local.h" #include "../ai/NAVCallback_FindCoverArea.h" #define IDEAL_ATTACKDIST 140 int rvmBot::WP_MACHINEGUN = -1; 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 ========================= */ bool rvmBot::BotIsDead(bot_state_t* bs) { if (bs == NULL || !BotAIValidEntityNum(bs->client)) { return true; } idPlayer* player = gameLocal.GetClientByNum(bs->client); return player == NULL || player->health <= 0; } /* ================== rvmBot::BotReachedGoal ================== */ bool rvmBot::BotReachedGoal(bot_state_t* bs, bot_goal_t* goal) { if (goal->flags & GFL_ITEM) { //if touching the goal if (botGoalManager.BotTouchingGoal(bs->origin, goal)) { if (!(goal->flags & GFL_DROPPED)) { botGoalManager.BotSetAvoidGoalTime(bs->gs, goal->number, -1); } return true; } //if the goal isn't there if (botGoalManager.BotItemGoalInVisButNotVisible(bs->entitynum, bs->eye, bs->viewangles, goal)) { return true; } } else { //if touching the goal if (botGoalManager.BotTouchingGoal(bs->origin, goal)) { return true; } } return false; } /* ================== rvmBot::BotChooseWeapon ================== */ void rvmBot::BotChooseWeapon(bot_state_t* bs) { 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) { // BotChooseBestFightWeapon returns bot weapon-config numbers. Only use // the fuzzy result if it already matches one of the engine runtime slots; // otherwise keep the current weapon instead of sending a bogus impulse. const int fuzzyWeaponNum = botWeaponInfoManager.BotChooseBestFightWeapon(bs->ws, bs->inventory); if (fuzzyWeaponNum == WP_MACHINEGUN || fuzzyWeaponNum == WP_SHOTGUN || fuzzyWeaponNum == WP_PLASMAGUN || fuzzyWeaponNum == WP_ROCKET_LAUNCHER) { newweaponnum = fuzzyWeaponNum; } } if (newweaponnum < 0) { if (bs->weaponnum >= 0) { newweaponnum = bs->weaponnum; } else { newweaponnum = spawnArgs.GetInt("current_weapon", "1"); } } if (bs->weaponnum != newweaponnum) { bs->weaponchange_time = Bot_Time(); bs->botinput.lastWeaponNum = -1; } bs->weaponnum = newweaponnum; bs->botinput.weapon = bs->weaponnum; SelectWeapon(bs->weaponnum, false); } /* ================== rvmBot::BotGetItemLongTermGoal ================== */ int rvmBot::BotGetItemLongTermGoal(bot_state_t* bs, int tfl, bot_goal_t* goal) { //if the bot has no goal if (!botGoalManager.BotGetTopGoal(bs->gs, goal)) { //BotAI_Print(PRT_MESSAGE, "no ltg on stack\n"); bs->ltg_time = 0; } //if the bot touches the current goal else if (BotReachedGoal(bs, goal)) { BotChooseWeapon(bs); bs->ltg_time = 0; } // Check to see that we can get to our goal, if not get a new goal. //if (bs->numMovementWaypoints > 0) //{ // trace_t tr; // gentity_t* ent = &g_entities[bs->client]; // vec3_t waypoint; // // VectorCopy(bs->movement_waypoints[bs->currentWaypoint], waypoint); // waypoint[2] += 5.0f; // // trap_Trace(&tr, ent->r.currentOrigin, NULL, NULL, waypoint, bs->client, CONTENTS_SOLID); // // if (tr.fraction <= 0.7f) // { // BotChooseWeapon(bs); // bs->ltg_time = 0; // } //} //if it is time to find a new long term goal if (bs->ltg_time == 0) { //pop the current goal from the stack botGoalManager.BotPopGoal(bs->gs); //BotAI_Print(PRT_MESSAGE, "%s: choosing new ltg\n", ClientName(bs->client, netname, sizeof(netname))); //choose a new goal //BotAI_Print(PRT_MESSAGE, "%6.1f client %d: BotChooseLTGItem\n", Bot_Time(), bs->client); if (botGoalManager.BotChooseLTGItem(bs->gs, bs->origin, bs->inventory, tfl)) { char buf[128]; //get the goal at the top of the stack botGoalManager.BotGetTopGoal(bs->gs, goal); botGoalManager.BotGoalName(goal->number, buf, sizeof(buf)); common->Printf("%1.1f: new long term goal %s\n", Bot_Time(), buf); bs->ltg_time = Bot_Time() + 20; bs->currentGoal.framenum = gameLocal.framenum; } else //the bot gets sorta stuck with all the avoid timings, shouldn't happen though { // //trap_BotDumpAvoidGoals(bs->gs); //reset the avoid goals and the avoid reach botGoalManager.BotResetAvoidGoals(bs->gs); //BotResetAvoidReach(bs->ms); } //get the goal at the top of the stack if (!botGoalManager.BotGetTopGoal(bs->gs, goal)) { return false; } bs->currentGoal.framenum = gameLocal.framenum; return true; } return true; } /* ================== rvmBot::EntityIsDead ================== */ bool rvmBot::EntityIsDead(idEntity* entity) { if (entity == NULL) { return true; } idPlayer* player = entity->Cast(); return player != NULL && player->health <= 0; } /* ================== BotEntityVisibleTest returns visibility in the range [0, 1] taking fog and water surfaces into account ================== */ float rvmBot::BotEntityVisibleTest(int viewer, idVec3 eye, idAngles viewangles, float fov, int ent, bool allowHeightTest) { if (!BotAIValidEntityNum(viewer) || !BotAIValidEntityNum(ent)) { return 0.0f; } idEntity* entinfo = gameLocal.entities[ent]; if (entinfo == NULL || entinfo->GetPhysics() == NULL) { 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 (inwater) { if (!(contents_mask & (CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER))) { passent = ent; hitent = viewer; start = end; end = eye; } contents_mask ^= (CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER); } trace_t trace; gameLocal.Trace(trace, start, end, contents_mask, passent); if (trace.fraction < 0.9f && allowHeightTest) { idVec3 highEnd = end; highEnd[2] += 50.0f; gameLocal.Trace(trace, start, highEnd, contents_mask, passent); } float waterfactor = 1.0f; if (trace.c.contents & (CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER)) { contents_mask &= ~(CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER); gameLocal.Trace(trace, trace.endpos, end, contents_mask, passent); waterfactor = 0.5f; } if (trace.fraction >= 1.0f || trace.c.entityNum == hitent) { float squaredfogdist = 0.0f; const int otherinfog = (gameLocal.clip.PointContents(end) & CONTENTS_FOG); idVec3 dir; if (infog && otherinfog) { dir = trace.endpos - eye; squaredfogdist = dir.LengthSqr(); } else if (infog) { idVec3 fogStart = trace.endpos; gameLocal.Trace(trace, fogStart, eye, CONTENTS_FOG, viewer); dir = eye - trace.endpos; squaredfogdist = dir.LengthSqr(); } else if (otherinfog) { idVec3 fogEnd = trace.endpos; gameLocal.Trace(trace, eye, fogEnd, CONTENTS_FOG, viewer); dir = fogEnd - trace.endpos; squaredfogdist = dir.LengthSqr(); } float vis = 1.0f / ((squaredfogdist * 0.001f) < 1.0f ? 1.0f : (squaredfogdist * 0.001f)); vis *= waterfactor; if (vis > bestvis) { bestvis = vis; } if (bestvis >= 0.95f) { return bestvis; } } } return bestvis; } /* ================== rvmBot::BotEntityVisible ================== */ float rvmBot::BotEntityVisible(int viewer, idVec3 eye, idAngles viewangles, float fov, int ent) { return BotEntityVisibleTest(viewer, eye, viewangles, fov, ent, true); } /* ================== rvmBot::BotUpdateBattleInventory ================== */ void rvmBot::BotUpdateBattleInventory(bot_state_t* bs, int enemy) { if (bs == NULL || !BotAIValidEntityNum(enemy)) { return; } idEntity* entinfo = gameLocal.entities[enemy]; if (entinfo == NULL || entinfo->GetPhysics() == NULL) { return; } 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(); } /* ================== rvmBot::BotAggression ================== */ float rvmBot::BotAggression(bot_state_t* bs) { //if the bot has quad if (bs->inventory[INVENTORY_QUAD]) { //if the bot is not holding the gauntlet or the enemy is really nearby if (bs->weaponnum != 0 || bs->inventory[ENEMY_HORIZONTAL_DIST] < 80) { return 70; } } //if the enemy is located way higher than the bot if (bs->inventory[ENEMY_HEIGHT] > 200) { return 0; } //if the bot is very low on health if (bs->inventory[INVENTORY_HEALTH] < 60) { return 0; } //if the bot is low on health if (bs->inventory[INVENTORY_HEALTH] < 80) { //if the bot has insufficient armor if (bs->inventory[INVENTORY_ARMOR] < 40) { return 0; } } //if the bot can use the bfg if (bs->inventory[INVENTORY_BFG10K] > 0 && bs->inventory[INVENTORY_BFGAMMO] > 7) { return 100; } //if the bot can use the railgun if (bs->inventory[INVENTORY_RAILGUN] > 0 && bs->inventory[INVENTORY_SLUGS] > 5) { return 95; } //if the bot can use the lightning gun if (bs->inventory[INVENTORY_LIGHTNING] > 0 && bs->inventory[INVENTORY_LIGHTNINGAMMO] > 50) { return 90; } //if the bot can use the rocketlauncher if (bs->inventory[INVENTORY_ROCKETLAUNCHER] > 0 && bs->inventory[INVENTORY_ROCKETS] > 5) { return 90; } //if the bot can use the plasmagun if (bs->inventory[INVENTORY_PLASMAGUN] > 0 && bs->inventory[INVENTORY_CELLS] > 40) { return 85; } //if the bot can use the grenade launcher if (bs->inventory[INVENTORY_GRENADELAUNCHER] > 0 && bs->inventory[INVENTORY_GRENADES] > 10) { return 80; } //if the bot can use the shotgun if (bs->inventory[INVENTORY_SHOTGUN] > 0 && bs->inventory[INVENTORY_SHELLS] > 10) { return 50; } if (bs->inventory[INVENTORY_BULLETS] > 0) { return 60; } //otherwise the bot is not feeling too good return 0; } /* ================== rvmBot::BotWantsToRetreat ================== */ int rvmBot::BotWantsToRetreat(bot_state_t* bs) { if (BotAggression(bs) < 50) { return true; } return false; } /* ================== rvmBot::BotBattleUseItems ================== */ void rvmBot::BotBattleUseItems(bot_state_t* bs) { if (bs->inventory[INVENTORY_HEALTH] < 40) { if (bs->inventory[INVENTORY_TELEPORTER] > 0) { bs->botinput.actionflags |= ACTION_USE; } } if (bs->inventory[INVENTORY_HEALTH] < 60) { if (bs->inventory[INVENTORY_MEDKIT] > 0) { //trap_EA_Use(bs->client); bs->botinput.actionflags |= ACTION_USE; } } } /* ================== rvmBot::BotFindEnemy ================== */ int rvmBot::BotFindEnemy(bot_state_t* bs, int curenemy) { if (bs == NULL || !BotAIValidEntityNum(bs->client)) { return false; } 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; } const bool healthdecrease = bs->lasthealth > bs->inventory[INVENTORY_HEALTH]; bs->lasthealth = bs->inventory[INVENTORY_HEALTH]; float cursquaredist = 0.0f; if (curenemy >= 0 && BotAIValidEntityNum(curenemy)) { idPlayer* curenemyinfo = gameLocal.entities[curenemy]->Cast(); if (curenemyinfo != NULL) { idVec3 dir = curenemyinfo->GetPhysics()->GetOrigin() - bs->origin; cursquaredist = dir.LengthSqr(); } } for (int i = 0; i < MAX_CLIENTS && i < gameLocal.num_entities; i++) { if (i == bs->client || i == curenemy || !BotAIValidEntityNum(i)) { continue; } idPlayer* entinfo = gameLocal.entities[i]->Cast(); if (entinfo == NULL) { continue; } if (EntityIsDead(entinfo) || i == bs->entitynum || entinfo->spectating) { continue; } idVec3 potentialTargetOrigin = entinfo->GetPhysics()->GetOrigin(); idVec3 dir = potentialTargetOrigin - bs->origin; float squaredist = dir.LengthSqr(); if (curenemy >= 0 && cursquaredist > 0.0f && squaredist > cursquaredist) { continue; } if (squaredist > Square(900.0f + alertness * 4000.0f)) { continue; } float fov; if (curenemy < 0 && (healthdecrease || entinfo->IsShooting())) { fov = 360.0f; } else { const float clampedDist = squaredist > Square(810.0f) ? Square(810.0f) : squaredist; fov = 180.0f - (90.0f - clampedDist / (810.0f * 9.0f)); } float vis = 1.0f; if (bs->attackerEntity != entinfo) { vis = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, fov, i); if (vis <= 0.0f) { continue; } } // 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()) { if (!entinfo->CheckFOV(clientEnt->GetPhysics()->GetOrigin())) { BotUpdateBattleInventory(bs, i); if (BotWantsToRetreat(bs) && easyfragger < 0.5f) { continue; } } } 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; } /* ================== rvmBot::BotMoveToGoal ================== */ void rvmBot::BotMoveToGoal(bot_state_t* bs, bot_goal_t* goal) { bs->currentGoal = *goal; bs->currentGoal.framenum = gameLocal.framenum; } /* ================== rvmBot::BotAimAtEnemy ================== */ void rvmBot::BotAimAtEnemy(bot_state_t* bs) { 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; botWeaponInfoManager.BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi); const bool selectedMachinegun = bs->weaponnum == WP_MACHINEGUN; const bool selectedShotgun = bs->weaponnum == WP_SHOTGUN; const bool selectedPlasmaGun = bs->weaponnum == WP_PLASMAGUN; const bool selectedRocketLauncher = bs->weaponnum == WP_ROCKET_LAUNCHER; const int selectedWeaponInventory = selectedMachinegun ? INVENTORY_MACHINEGUN : selectedShotgun ? INVENTORY_SHOTGUN : selectedPlasmaGun ? INVENTORY_PLASMAGUN : selectedRocketLauncher ? INVENTORY_ROCKETLAUNCHER : -1; if (selectedWeaponInventory >= 0 && wi.weaponindex != selectedWeaponInventory) { for (int weaponInfoNum = 0; weaponInfoNum < BOT_MAX_WEAPONS; weaponInfoNum++) { weaponinfo_t candidateWi; botWeaponInfoManager.BotGetWeaponInfo(bs->ws, weaponInfoNum, &candidateWi); if (candidateWi.number == weaponInfoNum && candidateWi.weaponindex == selectedWeaponInventory) { wi = candidateWi; break; } } } if (selectedMachinegun) { aim_accuracy = botCharacterStatsManager.Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_MACHINEGUN, 0, 1); } else if (selectedShotgun) { aim_accuracy = botCharacterStatsManager.Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_SHOTGUN, 0, 1); } else if (selectedRocketLauncher) { 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 (selectedPlasmaGun) { 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); } 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) { bs->enemyvisible_time = Bot_Time(); bs->lastenemyorigin = entinfo->GetPhysics()->GetOrigin(); bs->last_enemy_visible_position = bs->lastenemyorigin; // 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; 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); bestorigin = (wi.proj.damagetype & DAMAGETYPE_RADIAL) ? candidates[1] : candidates[0]; for (int i = 0; i < 5; i++) { if (BotAITraceCanHitEntity(bs->entitynum, bs->enemy, start, candidates[i], MASK_SHOT)) { 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)) { bestorigin = trace.endpos; } } } 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 { bestorigin = bs->lastenemyorigin; if (bestorigin.LengthSqr() < Square(1.0f)) { bestorigin = entinfo->GetPhysics()->GetOrigin(); } bestorigin[2] += 16.0f; } 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)) { bs->aimtarget = bestorigin; } else { bs->aimtarget = trace.endpos; } idVec3 dir = bestorigin - bs->eye; if (dir.LengthSqr() < Square(1.0f)) { return; } if (selectedMachinegun || selectedShotgun) { 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(); const float miss = 1.0f - aim_accuracy; for (int i = 0; i < 3; i++) { dir[i] += 0.12f * rvmBotUtil::crandom() * miss; } } 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] += 2.0f * wi.hspread * rvmBotUtil::crandom() * (1.0f - aim_accuracy); bs->viewangles[YAW] = idMath::AngleMod(bs->viewangles[YAW]); bs->botinput.viewangles = bs->viewangles; } /* ================== rvmBot::BotWantsToChase ================== */ bool rvmBot::BotWantsToChase(bot_state_t* bs) { if (BotAggression(bs) > 50) { return true; } return false; } /* ================== rvmBot::BotNearbyGoal ================== */ int rvmBot::BotNearbyGoal(bot_state_t* bs, int tfl, bot_goal_t* ltg, float range) { if (bs == NULL) { return false; } // 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); return botGoalManager.BotChooseNBGItem(bs->gs, bs->origin, bs->inventory, tfl, ltg, range); } /* ======================= rvmBot::BotGetRandomPointNearPosition ======================= */ void rvmBot::BotGetRandomPointNearPosition(const idVec3 point, idVec3& randomPoint, float radius) { randomPoint = point; idNAV* aas = this->aas; if (!aas) { return; } idVec3 basePoint = point; const int baseAreaNum = aas->AdjustPositionAndGetArea(basePoint); if (baseAreaNum <= 0) { return; } if (radius <= 0.0f) { randomPoint = aas->AreaCenter(baseAreaNum); aas->PushPointIntoAreaNum(baseAreaNum, randomPoint); return; } for (int i = 0; i < 24; i++) { const float angle = gameLocal.random.RandomFloat() * idMath::TWO_PI; const float dist = gameLocal.random.RandomFloat() * radius; idVec3 testPoint = point; testPoint.x += idMath::Cos(angle) * dist; testPoint.y += idMath::Sin(angle) * dist; const int testAreaNum = aas->AdjustPositionAndGetArea(testPoint); if (testAreaNum <= 0) { continue; } 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(baseAreaNum); aas->PushPointIntoAreaNum(baseAreaNum, randomPoint); } /* ======================= rvmBot::BotMoveInRandomDirection ======================= */ int rvmBot::BotMoveInRandomDirection(bot_state_t* bs) { if (bs == NULL || !BotAIValidEntityNum(bs->client)) { return 0; } rvmBot* ent = gameLocal.entities[bs->client]->Cast(); if (ent == NULL) { return 0; } float dist = idMath::Distance(ent->GetPhysics()->GetOrigin(), bs->random_move_position); if (bs->random_move_position.LengthSqr() < Square(1.0f) || !ent->PointVisible(bs->random_move_position)) { dist = 0.0f; } if (dist < 25.0f) { BotGetRandomPointNearPosition(ent->GetPhysics()->GetOrigin(), bs->random_move_position, 80.0f); } 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); } bs->botinput.dir = dir; bs->botinput.speed = 400.0f; bs->useRandomPosition = true; return 0; } /* ============ rvmBot::ShowHideArea ============ */ void rvmBot::MoveToCoverPoint(void) { int areaNum, numObstacles; idVec3 target; aasGoal_t goal; aasObstacle_t obstacles[10]; idVec3 origin = GetOrigin(); idNAV* aas = gameLocal.GetBotNAV(); if (aas == NULL) { return; } 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); numObstacles = 1; idNAVCallback_FindCoverArea findCover(target); if (aas->FindNearestGoal(goal, areaNum, origin, target, TFL_WALK | TFL_AIR, obstacles, numObstacles, findCover)) { bs.currentGoal.origin = goal.origin; } } /* ================== rvmBot::BotCheckAttack ================== */ void rvmBot::BotCheckAttack(bot_state_t* bs) { if (bs == NULL || bs->enemy < 0 || !BotAIValidEntityNum(bs->enemy) || !BotAIValidEntityNum(bs->client)) { return; } 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; } if (bs->firethrottlewait_time > Bot_Time()) { return; } 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.0f; } else { bs->firethrottleshoot_time = Bot_Time() + 1.0f - firethrottle; bs->firethrottlewait_time = 0.0f; } } weaponinfo_t wi; botWeaponInfoManager.BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi); const bool selectedMachinegun = bs->weaponnum == WP_MACHINEGUN; const bool selectedShotgun = bs->weaponnum == WP_SHOTGUN; const bool selectedPlasmaGun = bs->weaponnum == WP_PLASMAGUN; const bool selectedRocketLauncher = bs->weaponnum == WP_ROCKET_LAUNCHER; const int selectedWeaponInventory = selectedMachinegun ? INVENTORY_MACHINEGUN : selectedShotgun ? INVENTORY_SHOTGUN : selectedPlasmaGun ? INVENTORY_PLASMAGUN : selectedRocketLauncher ? INVENTORY_ROCKETLAUNCHER : -1; if (selectedWeaponInventory >= 0 && wi.weaponindex != selectedWeaponInventory) { for (int weaponInfoNum = 0; weaponInfoNum < BOT_MAX_WEAPONS; weaponInfoNum++) { weaponinfo_t candidateWi; botWeaponInfoManager.BotGetWeaponInfo(bs->ws, weaponInfoNum, &candidateWi); if (candidateWi.number == weaponInfoNum && candidateWi.weaponindex == selectedWeaponInventory) { wi = candidateWi; break; } } } // Hard visibility gate: movement/chase states may aim at last known position, // but they should not press attack unless the enemy is currently visible. if (BotEntityVisibleTest(bs->entitynum, bs->eye, bs->viewangles, 360.0f, attackentity, false) <= 0.0f) { bs->flags &= ~BFL_ATTACKED; return; } 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 (selectedShotgun) { 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; if (!eyeTraceClear && !(wi.proj.damagetype & DAMAGETYPE_RADIAL)) { 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 = start + forward * 1000.0f; start = start - forward * 12.0f; 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) { return; } if (weaponTrace.c.entityNum != attackentity) { if (wi.proj.damagetype & DAMAGETYPE_RADIAL) { // Splash shots are allowed, but only when the visible impact/aim point is // close enough to the currently visible enemy to plausibly damage them. if (weaponTrace.fraction < 1.0f && weaponTrace.fraction * 1000.0f < wi.proj.radius + 24.0f) { return; } const float splashRadius = wi.proj.radius > 1.0f ? wi.proj.radius : 96.0f; const idVec3 splashPoint = weaponTrace.fraction < 1.0f ? weaponTrace.endpos : bs->aimtarget; const idVec3 enemyCenter = BotAIEntityCenter(entinfo); if ((splashPoint - enemyCenter).LengthSqr() > Square(splashRadius + 96.0f)) { return; } } else if (!eyeTraceClear) { return; } } if (wi.flags & WFL_FIRERELEASED) { if (bs->flags & BFL_ATTACKED) { bs->botinput.actionflags |= ACTION_ATTACK; bs->flags &= ~BFL_ATTACKED; } else { bs->flags |= BFL_ATTACKED; } } else { bs->botinput.actionflags |= ACTION_ATTACK; bs->flags |= BFL_ATTACKED; } }