/* =========================================================================== Copyright (C) 2024 the OpenMoHAA team This file is part of OpenMoHAA source code. OpenMoHAA source code is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. OpenMoHAA source code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenMoHAA source code; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =========================================================================== */ // playerbot.cpp: Multiplayer bot system. #include "g_local.h" #include "actor.h" #include "playerbot.h" #include "consoleevent.h" #include "debuglines.h" #include "scriptexception.h" #include "vehicleturret.h" #include "weaputils.h" #include "g_bot.h" // We assume that we have limited access to the server-side // and that most logic come from the playerstate_s structure cvar_t *bot_manualmove; CLASS_DECLARATION(Listener, BotController, NULL) { {NULL, NULL} }; BotController::botfunc_t BotController::botfuncs[MAX_BOT_FUNCTIONS]; BotController::BotController() { if (LoadingSavegame) { return; } m_botCmd.serverTime = 0; m_botCmd.msec = 0; m_botCmd.buttons = 0; m_botCmd.angles[0] = ANGLE2SHORT(0); m_botCmd.angles[1] = ANGLE2SHORT(0); m_botCmd.angles[2] = ANGLE2SHORT(0); m_botCmd.forwardmove = 0; m_botCmd.rightmove = 0; m_botCmd.upmove = 0; m_botEyes.angles[0] = 0; m_botEyes.angles[1] = 0; m_botEyes.ofs[0] = 0; m_botEyes.ofs[1] = 0; m_botEyes.ofs[2] = DEFAULT_VIEWHEIGHT; m_iCuriousTime = 0; m_iAttackTime = 0; m_iConfirmTime = 0; m_iEnemyEyesTag = -1; m_iNextTauntTime = 0; m_StateFlags = 0; m_RunLabel.TrySetScript("global/bot_run.scr"); } BotController::~BotController() { if (controlledEnt) { controlledEnt->delegate_gotKill.Remove(delegateHandle_gotKill); controlledEnt->delegate_killed.Remove(delegateHandle_killed); controlledEnt->delegate_stufftext.Remove(delegateHandle_stufftext); } } BotMovement& BotController::GetMovement() { return movement; } void BotController::Init(void) { bot_manualmove = gi.Cvar_Get("bot_manualmove", "0", 0); for (int i = 0; i < MAX_BOT_FUNCTIONS; i++) { botfuncs[i].BeginState = &BotController::State_DefaultBegin; botfuncs[i].EndState = &BotController::State_DefaultEnd; } InitState_Attack(&botfuncs[0]); InitState_Curious(&botfuncs[1]); InitState_Grenade(&botfuncs[2]); InitState_Idle(&botfuncs[3]); //InitState_Weapon(&botfuncs[4]); } void BotController::GetUsercmd(usercmd_t *ucmd) { *ucmd = m_botCmd; } void BotController::GetEyeInfo(usereyes_t *eyeinfo) { *eyeinfo = m_botEyes; } void BotController::UpdateBotStates(void) { if (bot_manualmove->integer) { memset(&m_botCmd, 0, sizeof(usercmd_t)); return; } if (!controlledEnt->client->pers.dm_primary[0]) { Event *event; // // Primary weapon // event = new Event(EV_Player_PrimaryDMWeapon); event->AddString("auto"); controlledEnt->ProcessEvent(event); } if (controlledEnt->GetTeam() == TEAM_NONE || controlledEnt->GetTeam() == TEAM_SPECTATOR) { float time; // Add some delay to avoid telefragging time = controlledEnt->entnum / 20.0; if (controlledEnt->EventPending(EV_Player_AutoJoinDMTeam)) { return; } // // Team // controlledEnt->PostEvent(EV_Player_AutoJoinDMTeam, time); return; } if (controlledEnt->IsDead() || controlledEnt->IsSpectator()) { // The bot should respawn m_botCmd.buttons ^= BUTTON_ATTACKLEFT; return; } m_botCmd.buttons |= BUTTON_RUN; m_botCmd.serverTime = level.svsTime; m_botEyes.ofs[0] = 0; m_botEyes.ofs[1] = 0; m_botEyes.ofs[2] = controlledEnt->viewheight; m_botEyes.angles[0] = 0; m_botEyes.angles[1] = 0; CheckStates(); movement.MoveThink(m_botCmd); rotation.TurnThink(m_botCmd, m_botEyes); CheckUse(); CheckValidWeapon(); } void BotController::CheckUse(void) { Vector dir; Vector start; Vector end; trace_t trace; controlledEnt->angles.AngleVectorsLeft(&dir); start = controlledEnt->origin + Vector(0, 0, controlledEnt->viewheight); end = controlledEnt->origin + Vector(0, 0, controlledEnt->viewheight) + dir * 32; trace = G_Trace(start, vec_zero, vec_zero, end, controlledEnt, MASK_USABLE, false, "BotController::CheckUse"); // It may be a door if ((trace.allsolid || trace.startsolid || trace.fraction != 1.0f) && trace.ent) { if (trace.ent->entity->IsSubclassOfDoor()) { Door *door = static_cast(trace.ent->entity); if (door->isOpen()) { m_botCmd.buttons &= ~BUTTON_USE; return; } } // // Toggle the use button // m_botCmd.buttons ^= BUTTON_USE; } else { m_botCmd.buttons &= ~BUTTON_USE; } } void BotController::CheckValidWeapon() { Weapon* weapon = controlledEnt->GetActiveWeapon(WEAPON_MAIN); if (!weapon) { // If holstered, use the best weapon available UseWeaponWithAmmo(); } else if (!weapon->HasAmmo(FIRE_PRIMARY) && !controlledEnt->GetNewActiveWeapon()) { // In case the current weapon has no ammo, use the best available weapon UseWeaponWithAmmo(); } } void BotController::SendCommand(const char *text) { char *buffer; char *data; size_t len; ConsoleEvent ev; len = strlen(text) + 1; buffer = (char *)gi.Malloc(len); data = buffer; Q_strncpyz(data, text, len); const char *com_token = COM_Parse(&data); if (!com_token) { return; } controlledEnt->m_lastcommand = com_token; if (!Event::GetEvent(com_token)) { return; } ev = ConsoleEvent(com_token); if (!(ev.GetEventFlags(ev.eventnum) & EV_CONSOLE)) { gi.Free(buffer); return; } ev.SetConsoleEdict(controlledEnt->edict); while (1) { com_token = COM_Parse(&data); if (!com_token || !*com_token) { break; } ev.AddString(com_token); } gi.Free(buffer); try { controlledEnt->ProcessEvent(ev); } catch (ScriptException& exc) { gi.DPrintf("*** Bot Command Exception *** %s\n", exc.string.c_str()); } } /* ==================== AimAtAimNode Make the bot face toward the current path ==================== */ void BotController::AimAtAimNode(void) { Vector goal; if (!movement.IsMoving()) { return; } goal = movement.GetCurrentGoal(); if (goal != controlledEnt->origin) { rotation.AimAt(goal); } Vector targetAngles = rotation.GetTargetAngles(); targetAngles.x = 0; rotation.SetTargetAngles(targetAngles); } /* ==================== CheckReload Make the bot reload if necessary ==================== */ void BotController::CheckReload(void) { Weapon *weap = controlledEnt->GetActiveWeapon(WEAPON_MAIN); if (weap && weap->CheckReload(FIRE_PRIMARY)) { SendCommand("reload"); } } /* ==================== NoticeEvent Warn the bot of an event ==================== */ void BotController::NoticeEvent(Vector vPos, int iType, Entity *pEnt, float fDistanceSquared, float fRadiusSquared) { Sentient *pSentOwner; float fRangeFactor; fRangeFactor = 1.0 - (fDistanceSquared / fRadiusSquared); if (fRangeFactor < random()) { return; } if (pEnt->IsSubclassOfSentient()) { pSentOwner = static_cast(pEnt); } else if (pEnt->IsSubclassOfVehicleTurretGun()) { VehicleTurretGun *pVTG = static_cast(pEnt); pSentOwner = pVTG->GetSentientOwner(); } else if (pEnt->IsSubclassOfItem()) { Item *pItem = static_cast(pEnt); pSentOwner = pItem->GetOwner(); } else if (pEnt->IsSubclassOfProjectile()) { Projectile *pProj = static_cast(pEnt); pSentOwner = pProj->GetOwner(); } else { pSentOwner = NULL; } if (pSentOwner) { if (pSentOwner == controlledEnt) { // Ignore self return; } if ((pSentOwner->flags & FL_NOTARGET) || pSentOwner->getSolidType() == SOLID_NOT) { return; } // Ignore teammates if (pSentOwner->IsSubclassOfPlayer()) { Player *p = static_cast(pSentOwner); if (g_gametype->integer >= GT_TEAM && p->GetTeam() == controlledEnt->GetTeam()) { return; } } } switch (iType) { case AI_EVENT_MISC: case AI_EVENT_MISC_LOUD: break; case AI_EVENT_WEAPON_FIRE: case AI_EVENT_WEAPON_IMPACT: case AI_EVENT_EXPLOSION: case AI_EVENT_AMERICAN_VOICE: case AI_EVENT_GERMAN_VOICE: case AI_EVENT_AMERICAN_URGENT: case AI_EVENT_GERMAN_URGENT: case AI_EVENT_FOOTSTEP: case AI_EVENT_GRENADE: default: m_iCuriousTime = level.inttime + 20000; m_vNewCuriousPos = vPos; break; } } /* ==================== ClearEnemy Clear the bot's enemy ==================== */ void BotController::ClearEnemy(void) { m_iAttackTime = 0; m_iConfirmTime = 0; m_pEnemy = NULL; m_iEnemyEyesTag = -1; m_vOldEnemyPos = vec_zero; m_vLastEnemyPos = vec_zero; } /* ==================== Bot states -------------------- ____________________ -------------------- ____________________ -------------------- ____________________ -------------------- ____________________ ==================== */ void BotController::CheckStates(void) { m_StateCount = 0; for (int i = 0; i < MAX_BOT_FUNCTIONS; i++) { botfunc_t *func = &botfuncs[i]; if (func->CheckCondition) { if ((this->*func->CheckCondition)()) { if (!(m_StateFlags & (1 << i))) { m_StateFlags |= 1 << i; if (func->BeginState) { (this->*func->BeginState)(); } } if (func->ThinkState) { m_StateCount++; (this->*func->ThinkState)(); } } else { if ((m_StateFlags & (1 << i))) { m_StateFlags &= ~(1 << i); if (func->EndState) { (this->*func->EndState)(); } } } } else { if (func->ThinkState) { m_StateCount++; (this->*func->ThinkState)(); } } } assert(m_StateCount); if (!m_StateCount) { gi.DPrintf("*** WARNING *** %s was stuck with no states !!!", controlledEnt->client->pers.netname); State_Reset(); } } /* ==================== Default state ==================== */ void BotController::State_DefaultBegin(void) { movement.ClearMove(); } void BotController::State_DefaultEnd(void) {} void BotController::State_Reset(void) { m_iCuriousTime = 0; m_iAttackTime = 0; m_vLastCuriousPos = vec_zero; m_vOldEnemyPos = vec_zero; m_vLastEnemyPos = vec_zero; m_vLastDeathPos = vec_zero; m_pEnemy = NULL; m_iEnemyEyesTag = -1; } /* ==================== Idle state Make the bot move to random directions ==================== */ void BotController::InitState_Idle(botfunc_t *func) { func->CheckCondition = &BotController::CheckCondition_Idle; func->ThinkState = &BotController::State_Idle; } bool BotController::CheckCondition_Idle(void) { if (m_iCuriousTime) { return false; } if (m_iAttackTime) { return false; } return true; } void BotController::State_Idle(void) { AimAtAimNode(); CheckReload(); if (!movement.MoveToBestAttractivePoint() && !movement.IsMoving()) { if (m_vLastDeathPos != vec_zero) { movement.MoveTo(m_vLastDeathPos); if (movement.MoveDone()) { m_vLastDeathPos = vec_zero; } } else { Vector randomDir(G_CRandom(16), G_CRandom(16), G_CRandom(16)); Vector preferredDir = Vector(controlledEnt->orientation[0]) * (rand() % 5 ? 1024 : -1024); float radius = 512 + G_Random(2048); movement.AvoidPath(controlledEnt->origin + randomDir, radius, preferredDir); } } } /* ==================== Curious state Forward to the last event position ==================== */ void BotController::InitState_Curious(botfunc_t *func) { func->CheckCondition = &BotController::CheckCondition_Curious; func->ThinkState = &BotController::State_Curious; } bool BotController::CheckCondition_Curious(void) { if (m_iAttackTime) { m_iCuriousTime = 0; return false; } if (level.inttime > m_iCuriousTime) { if (m_iCuriousTime) { movement.ClearMove(); m_iCuriousTime = 0; } return false; } return true; } void BotController::State_Curious(void) { AimAtAimNode(); if (!movement.MoveToBestAttractivePoint(3) && (!movement.IsMoving() || m_vLastCuriousPos != m_vNewCuriousPos)) { movement.MoveTo(m_vNewCuriousPos); m_vLastCuriousPos = m_vNewCuriousPos; } if (movement.MoveDone()) { m_iCuriousTime = 0; } } /* ==================== Attack state Attack the enemy ==================== */ void BotController::InitState_Attack(botfunc_t *func) { func->CheckCondition = &BotController::CheckCondition_Attack; func->EndState = &BotController::State_EndAttack; func->ThinkState = &BotController::State_Attack; } static Vector bot_origin; static int sentients_compare(const void *elem1, const void *elem2) { Entity *e1, *e2; float delta[3]; float d1, d2; e1 = *(Entity **)elem1; e2 = *(Entity **)elem2; VectorSubtract(bot_origin, e1->origin, delta); d1 = VectorLengthSquared(delta); VectorSubtract(bot_origin, e2->origin, delta); d2 = VectorLengthSquared(delta); if (d2 <= d1) { return d1 > d2; } else { return -1; } } bool BotController::IsValidEnemy(Sentient *sent) const { if (sent == controlledEnt) { return false; } if (sent->hidden() || (sent->flags & FL_NOTARGET)) { // Ignore hidden / non-target enemies return false; } if (sent->IsDead()) { // Ignore dead enemies return false; } if (sent->getSolidType() == SOLID_NOT) { // Ignore non-solid, like spectators return false; } if (sent->IsSubclassOfPlayer()) { Player *player = static_cast(sent); if (g_gametype->integer >= GT_TEAM && player->GetTeam() == controlledEnt->GetTeam()) { return false; } } else { if (sent->m_Team == controlledEnt->m_Team) { return false; } } return true; } bool BotController::CheckCondition_Attack(void) { Container sents = SentientList; float maxDistance = 0; bot_origin = controlledEnt->origin; sents.Sort(sentients_compare); for (int i = 1; i <= sents.NumObjects(); i++) { Sentient *sent = sents.ObjectAt(i); if (!IsValidEnemy(sent)) { continue; } maxDistance = Q_min(world->m_fAIVisionDistance, world->farplane_distance * 0.828); if (controlledEnt->CanSee(sent, 80, maxDistance, false)) { if (m_pEnemy != sent) { m_iEnemyEyesTag = -1; } if (!m_pEnemy) { // Slight reaction time m_iConfirmTime = level.inttime + (200 + G_Random(200)); m_iAttackTime = 0; } m_pEnemy = sent; m_vLastEnemyPos = m_pEnemy->origin; if (level.inttime < m_iConfirmTime) { return false; } } if (m_pEnemy && level.inttime >= m_iConfirmTime) { m_iAttackTime = level.inttime + 1000; return true; } } if (level.inttime > m_iAttackTime) { if (m_iAttackTime) { movement.ClearMove(); m_iAttackTime = 0; } return false; } return true; } void BotController::State_EndAttack(void) { m_botCmd.buttons &= ~(BUTTON_ATTACKLEFT | BUTTON_ATTACKRIGHT); controlledEnt->ZoomOff(); } void BotController::State_Attack(void) { bool bMelee = false; bool bCanSee = false; float fMinDistance = 128; float fMinDistanceSquared = fMinDistance * fMinDistance; float fEnemyDistanceSquared; Weapon *pWeap = controlledEnt->GetActiveWeapon(WEAPON_MAIN); bool bNoMove = false; if (!m_pEnemy || !IsValidEnemy(m_pEnemy)) { // Ignore dead enemies m_iAttackTime = 0; return; } float fDistanceSquared = (m_pEnemy->origin - controlledEnt->origin).lengthSquared(); m_vOldEnemyPos = m_vLastEnemyPos; bCanSee = controlledEnt->CanSee(m_pEnemy, 20, Q_min(world->m_fAIVisionDistance, world->farplane_distance * 0.828), false); if (bCanSee) { if (!pWeap) { return; } float fPrimaryBulletRange = pWeap->GetBulletRange(FIRE_PRIMARY) / 1.25f; float fPrimaryBulletRangeSquared = fPrimaryBulletRange * fPrimaryBulletRange; float fSecondaryBulletRange = pWeap->GetBulletRange(FIRE_SECONDARY); float fSecondaryBulletRangeSquared = fSecondaryBulletRange * fSecondaryBulletRange; float fSpreadFactor = pWeap->GetSpreadFactor(FIRE_PRIMARY); // // check the fire movement speed if the weapon has a max fire movement // if (pWeap->GetMaxFireMovement() < 1 && pWeap->HasAmmoInClip(FIRE_PRIMARY)) { float length; length = controlledEnt->velocity.length(); if ((length / sv_runspeed->value) > (pWeap->GetMaxFireMovementMult())) { bNoMove = true; movement.ClearMove(); } } fMinDistance = fPrimaryBulletRange; if (fMinDistance > 256) { fMinDistance = 256; } fMinDistanceSquared = fMinDistance * fMinDistance; if (controlledEnt->client->ps.stats[STAT_AMMO] > 0 || controlledEnt->client->ps.stats[STAT_CLIPAMMO] > 0) { if (fDistanceSquared <= fPrimaryBulletRangeSquared) { if (pWeap->IsSemiAuto()) { if (controlledEnt->client->ps.iViewModelAnim == VM_ANIM_IDLE || controlledEnt->client->ps.iViewModelAnim >= VM_ANIM_IDLE_0 && controlledEnt->client->ps.iViewModelAnim <= VM_ANIM_IDLE_2) { if (fSpreadFactor < 0.25) { m_botCmd.buttons ^= BUTTON_ATTACKLEFT; if (pWeap->GetZoom()) { if (!controlledEnt->IsZoomed()) { m_botCmd.buttons |= BUTTON_ATTACKRIGHT; } else { m_botCmd.buttons &= ~BUTTON_ATTACKRIGHT; } } } else { bNoMove = true; movement.ClearMove(); } } else { m_botCmd.buttons &= ~(BUTTON_ATTACKLEFT | BUTTON_ATTACKRIGHT); controlledEnt->ZoomOff(); } } else { m_botCmd.buttons |= BUTTON_ATTACKLEFT; } } else { m_botCmd.buttons &= ~(BUTTON_ATTACKLEFT | BUTTON_ATTACKRIGHT); controlledEnt->ZoomOff(); } } else { m_botCmd.buttons &= ~(BUTTON_ATTACKLEFT | BUTTON_ATTACKRIGHT); controlledEnt->ZoomOff(); } if (pWeap->GetFireType(FIRE_SECONDARY) == FT_MELEE) { if (controlledEnt->client->ps.stats[STAT_AMMO] <= 0 && controlledEnt->client->ps.stats[STAT_CLIPAMMO] <= 0) { bMelee = true; } else if (fDistanceSquared <= fSecondaryBulletRangeSquared) { bMelee = true; } } if (bMelee) { m_botCmd.buttons &= ~BUTTON_ATTACKLEFT; if (fDistanceSquared <= fSecondaryBulletRangeSquared) { m_botCmd.buttons ^= BUTTON_ATTACKRIGHT; } else { m_botCmd.buttons &= ~BUTTON_ATTACKRIGHT; } } m_iAttackTime = level.inttime + 1000; m_iAttackStopAimTime = level.inttime + 3000; m_vLastEnemyPos = m_pEnemy->centroid; } else { m_botCmd.buttons &= ~(BUTTON_ATTACKLEFT | BUTTON_ATTACKRIGHT); fMinDistanceSquared = 0; } if (bCanSee || level.inttime < m_iAttackStopAimTime) { Vector vRandomOffset; Vector vTarget; orientation_t eyes_or; if (m_iEnemyEyesTag == -1) { // Cache the tag m_iEnemyEyesTag = gi.Tag_NumForName(m_pEnemy->edict->tiki, "eyes bone"); } if (m_iEnemyEyesTag != -1) { // Use the enemy's eyes bone m_pEnemy->GetTag(m_iEnemyEyesTag, &eyes_or); vRandomOffset = Vector(G_CRandom(8), G_CRandom(8), -G_Random(32)); vTarget = eyes_or.origin + vRandomOffset; rotation.AimAt(eyes_or.origin + vRandomOffset); } else { vRandomOffset = Vector(G_CRandom(8), G_CRandom(8), 16 + G_Random(m_pEnemy->viewheight - 16)); vTarget = m_pEnemy->origin + vRandomOffset; } rotation.AimAt(vTarget); } else { AimAtAimNode(); } if (bNoMove) { return; } fEnemyDistanceSquared = (controlledEnt->origin - m_vLastEnemyPos).lengthSquared(); if ((!movement.MoveToBestAttractivePoint(5) && !movement.IsMoving()) || (m_vOldEnemyPos != m_vLastEnemyPos && !movement.MoveDone()) || fEnemyDistanceSquared < fMinDistanceSquared) { if (!bMelee || !bCanSee) { if (fEnemyDistanceSquared < fMinDistanceSquared) { Vector vDir = controlledEnt->origin - m_vLastEnemyPos; VectorNormalizeFast(vDir); movement.AvoidPath(m_vLastEnemyPos, fMinDistance, Vector(controlledEnt->orientation[1]) * 512); } else { movement.MoveTo(m_vLastEnemyPos); } if (!bCanSee && movement.MoveDone()) { // Lost track of the enemy ClearEnemy(); return; } } else { movement.MoveTo(m_vLastEnemyPos); } } if (movement.IsMoving()) { m_iAttackTime = level.inttime + 1000; } } /* ==================== Grenade state Avoid any grenades ==================== */ void BotController::InitState_Grenade(botfunc_t *func) { func->CheckCondition = &BotController::CheckCondition_Grenade; func->ThinkState = &BotController::State_Grenade; } bool BotController::CheckCondition_Grenade(void) { // FIXME: TODO return false; } void BotController::State_Grenade(void) { // FIXME: TODO } /* ==================== Weapon state Change weapon when necessary ==================== */ void BotController::InitState_Weapon(botfunc_t *func) { func->CheckCondition = &BotController::CheckCondition_Weapon; func->BeginState = &BotController::State_BeginWeapon; } bool BotController::CheckCondition_Weapon(void) { return controlledEnt->GetActiveWeapon(WEAPON_MAIN) != controlledEnt->BestWeapon(NULL, false, WEAPON_CLASS_THROWABLE); } void BotController::State_BeginWeapon(void) { Weapon *weap = controlledEnt->BestWeapon(NULL, false, WEAPON_CLASS_THROWABLE); if (weap == NULL) { SendCommand("safeholster 1"); return; } SendCommand(va("use \"%s\"", weap->model.c_str())); } Weapon* BotController::FindWeaponWithAmmo() { Weapon *next; int n; int j; int bestrank; Weapon *bestweapon; const Container& inventory = controlledEnt->getInventory(); n = inventory.NumObjects(); // Search until we find the best weapon with ammo bestweapon = NULL; bestrank = -999999; for (j = 1; j <= n; j++) { next = (Weapon *)G_GetEntity(inventory.ObjectAt(j)); assert(next); if (!next->IsSubclassOfWeapon() || next->IsSubclassOfInventoryItem()) { continue; } if (next->GetWeaponClass() & WEAPON_CLASS_THROWABLE) { continue; } if (next->GetRank() < bestrank) { continue; } if (!next->HasAmmo(FIRE_PRIMARY)) { continue; } bestweapon = (Weapon*)next; bestrank = bestweapon->GetRank(); } return bestweapon; } Weapon* BotController::FindMeleeWeapon() { Weapon *next; int n; int j; int bestrank; Weapon *bestweapon; const Container& inventory = controlledEnt->getInventory(); n = inventory.NumObjects(); // Search until we find the best weapon with ammo bestweapon = NULL; bestrank = -999999; for (j = 1; j <= n; j++) { next = (Weapon *)G_GetEntity(inventory.ObjectAt(j)); assert(next); if (!next->IsSubclassOfWeapon() || next->IsSubclassOfInventoryItem()) { continue; } if (next->GetRank() < bestrank) { continue; } if (next->GetFireType(FIRE_SECONDARY) != FT_MELEE) { continue; } bestweapon = (Weapon*)next; bestrank = bestweapon->GetRank(); } return bestweapon; } void BotController::UseWeaponWithAmmo() { Weapon* bestWeapon = FindWeaponWithAmmo(); if (!bestWeapon) { // // If there is no weapon with ammo, fallback to a weapon that can melee // bestWeapon = FindMeleeWeapon(); } if (!bestWeapon || bestWeapon == controlledEnt->GetActiveWeapon(WEAPON_MAIN)) { return; } controlledEnt->useWeapon(bestWeapon, WEAPON_MAIN); } void BotController::Spawned(void) { ClearEnemy(); m_iCuriousTime = 0; m_botCmd.buttons = 0; } void BotController::Think() { usercmd_t ucmd; usereyes_t eyeinfo; UpdateBotStates(); GetUsercmd(&ucmd); GetEyeInfo(&eyeinfo); G_ClientThink(controlledEnt->edict, &ucmd, &eyeinfo); } void BotController::Killed(const Event& ev) { Entity *attacker; // send the respawn buttons if (!(m_botCmd.buttons & BUTTON_ATTACKLEFT)) { m_botCmd.buttons |= BUTTON_ATTACKLEFT; } else { m_botCmd.buttons &= ~BUTTON_ATTACKLEFT; } m_botEyes.ofs[0] = 0; m_botEyes.ofs[1] = 0; m_botEyes.ofs[2] = 0; m_botEyes.angles[0] = 0; m_botEyes.angles[1] = 0; attacker = ev.GetEntity(1); if (attacker && rand() % 5 == 0) { // 1/5 chance to go back to the attacker position m_vLastDeathPos = attacker->origin; } else { m_vLastDeathPos = vec_zero; } // Choose a new random primary weapon Event event(EV_Player_PrimaryDMWeapon); event.AddString("auto"); controlledEnt->ProcessEvent(event); // // This is useful to change nationality in Spearhead and Breakthrough // this allows the AI to use more weapons // Info_SetValueForKey(controlledEnt->client->pers.userinfo, "dm_playermodel", G_GetRandomAlliedPlayerModel()); Info_SetValueForKey(controlledEnt->client->pers.userinfo, "dm_playergermanmodel", G_GetRandomGermanPlayerModel()); G_ClientUserinfoChanged(controlledEnt->edict, controlledEnt->client->pers.userinfo); } void BotController::GotKill(const Event& ev) { ClearEnemy(); m_iCuriousTime = 0; if (level.inttime >= m_iNextTauntTime && (rand() % 5) == 0) { // // Randomly play a taunt // Event event("dmmessage"); event.AddInteger(0); if (g_protocol >= protocol_e::PROTOCOL_MOHTA_MIN) { event.AddString("*5" + str(1 + (rand() % 8))); } else { event.AddString("*4" + str(1 + (rand() % 9))); } controlledEnt->ProcessEvent(event); m_iNextTauntTime = level.inttime + 5000; } } void BotController::EventStuffText(const str& text) { SendCommand(text); } void BotController::setControlledEntity(Player *player) { controlledEnt = player; movement.SetControlledEntity(player); rotation.SetControlledEntity(player); delegateHandle_gotKill = player->delegate_gotKill.Add(std::bind(&BotController::GotKill, this, std::placeholders::_1)); delegateHandle_killed = player->delegate_killed.Add(std::bind(&BotController::Killed, this, std::placeholders::_1)); delegateHandle_stufftext = player->delegate_stufftext.Add(std::bind(&BotController::EventStuffText, this, std::placeholders::_1)); } Player *BotController::getControlledEntity() const { return controlledEnt; } BotController *BotControllerManager::createController(Player *player) { BotController *controller = new BotController(); controller->setControlledEntity(player); controllers.AddObject(controller); return controller; } void BotControllerManager::removeController(BotController *controller) { controllers.RemoveObject(controller); delete controller; } BotController *BotControllerManager::findController(Entity *ent) { int i; for (i = 1; i <= controllers.NumObjects(); i++) { BotController *controller = controllers.ObjectAt(i); if (controller->getControlledEntity() == ent) { return controller; } } return nullptr; } const Container& BotControllerManager::getControllers() const { return controllers; } BotControllerManager::~BotControllerManager() { Cleanup(); } void BotControllerManager::Init() { BotController::Init(); } void BotControllerManager::Cleanup() { int i; BotController::Init(); for (i = 1; i <= controllers.NumObjects(); i++) { BotController *controller = controllers.ObjectAt(i); delete controller; } controllers.FreeObjectList(); } void BotControllerManager::ThinkControllers() { int i; // Delete controllers that don't have associated player entity // This cannot happen unless some mods remove them for (i = controllers.NumObjects(); i > 0; i--) { BotController* controller = controllers.ObjectAt(i); if (!controller->getControlledEntity()) { gi.DPrintf("Bot %d has no associated player entity. This shouldn't happen unless the entity has been removed by a script. The controller will be removed, please fix.\n", i); // Remove the controller, it will be recreated later to match `sv_numbots` delete controller; controllers.RemoveObjectAt(i); } } for (i = 1; i <= controllers.NumObjects(); i++) { BotController *controller = controllers.ObjectAt(i); controller->Think(); } }