From 1c3fc2c32dec266f544065efebf8cec3f5e35af3 Mon Sep 17 00:00:00 2001 From: Marco Cawthorne Date: Thu, 4 Jan 2024 14:23:39 -0800 Subject: [PATCH] Bot improvements contributed by CYBERDEViL: - Bots will choose a random Bomb Site / Escape Zone / VIP Safety Zone. - T-bots: fetch bomb backpack when it has been droppen. - T-bots: plant the bomb. - T-bots: roam the planted bomb. - CT-bots: random buy defusal kit. - CT-bots: defuse bomb. - CT-bots: roam the defuser. - Small waypoint adjustments for `de_dust2` which enables bots to successfully jump on the big boxes to B, and they jump more easy over the ridge from T spawn to B. --- src/server/bot.h | 1 + src/server/bot.qc | 384 +++++++++++++++++++++++----- src/server/defs.h | 1 + src/server/gamerules_multiplayer.qc | 10 + src/shared/item_c4bomb.qc | 4 + zpak001.pk3dir/data/de_dust2.way | 18 +- 6 files changed, 352 insertions(+), 66 deletions(-) diff --git a/src/server/bot.h b/src/server/bot.h index ab65d47..0a16ec0 100644 --- a/src/server/bot.h +++ b/src/server/bot.h @@ -17,3 +17,4 @@ void CSBot_BombPlantedNotify(void); void CSBot_HostageRescueNotify(void); void CSBot_RoundStart(void); +void CSBot_RestartRound(void); diff --git a/src/server/bot.qc b/src/server/bot.qc index a1b3186..720b9a7 100644 --- a/src/server/bot.qc +++ b/src/server/bot.qc @@ -14,6 +14,15 @@ * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +/** @brief Get the absolute center pos of a entity */ +vector getEntityCenterPos(entity e) { + vector newVec; + newVec[0] = e.absmin[0] + (0.5 * (e.absmax[0] - e.absmin[0])); + newVec[1] = e.absmin[1] + (0.5 * (e.absmax[1] - e.absmin[1])); + newVec[2] = e.absmin[2] + (0.5 * (e.absmax[2] - e.absmin[2])); + return newVec; +} + class csbot:bot { void(void) csbot; @@ -21,15 +30,29 @@ class csbot:bot /* some objectives */ virtual void(void) RunToConfront; virtual void(void) RunToBomb; - virtual void(void) RunToBombsite; - virtual void(void) RunToEscapeZone; - virtual void(void) RunToVIPSafetyZone; + virtual void(int) RunToBombsite; + virtual void(void) RunToRandomBombsite; + virtual void(int) RunToEscapeZone; + virtual void(void) RunToRandomEscapeZone; + virtual void(int) RunToVIPSafetyZone; + virtual void(void) RunToRandomVIPSafetyZone; virtual void(void) RunToHostages; + virtual void(vector, int) Roam; virtual void(void) CreateObjective; virtual void(void) PostFrame; virtual void(void) WeaponThink; + /* helpers */ + virtual entity(string, int) GetEntityByNameAndIndex; + virtual entity(int) GetBombsiteByIndex; + virtual entity(int) GetEscapeZoneByIndex; + virtual entity(int) GetVIPSafetyZoneByIndex; + virtual void(vector, float) AimLerp; + + int m_actionIsPlanting; + int m_actionIsDefusing; + /* Workaround: * gflags is not yet set when CSBot_BuyStart_Shop() or CreateObjective() * are called, so we back it up on PostFrame() and use that instead. @@ -59,7 +82,7 @@ void csbot::RunToBomb(void) { entity e = world; - e = find(e, ::model, "models/w_c4bomb.mdl"); + e = find(e, ::model, "models/w_c4.mdl"); if (e) { RouteToPosition(e.origin); @@ -67,61 +90,52 @@ csbot::RunToBomb(void) } } -/* go to a random bombsite */ +/* go to given bombsite */ void -csbot::RunToBombsite(void) +csbot::RunToBombsite(int bombsiteIndex) { - entity e = world; - vector vecTarget; - - /* FIXME: make this random and error checked */ - while (e == world) - e = find(e, ::classname, "func_bomb_target"); - - vecTarget[0] = e.absmin[0] + (0.5 * (e.absmax[0] - e.absmin[0])); - vecTarget[1] = e.absmin[1] + (0.5 * (e.absmax[1] - e.absmin[1])); - vecTarget[2] = e.absmin[2] + (0.5 * (e.absmax[2] - e.absmin[2])); - - RouteToPosition(vecTarget); - ChatSayTeam("Going to run to a Bomb Site!"); + entity e = GetBombsiteByIndex(bombsiteIndex); + RouteToPosition(getEntityCenterPos(e)); + ChatSayTeam(strcat("Going to run to Bomb Site ", itos(bombsiteIndex), "!")); } -/* go to a random escape zone */ +/* go to random bombsite */ void -csbot::RunToEscapeZone(void) +csbot::RunToRandomBombsite(void) { - entity e = world; - vector vecTarget; - - /* FIXME: make this random and error checked */ - while (e == world) - e = find(e, ::classname, "func_escapezone"); - - vecTarget[0] = e.absmin[0] + (0.5 * (e.absmax[0] - e.absmin[0])); - vecTarget[1] = e.absmin[1] + (0.5 * (e.absmax[1] - e.absmin[1])); - vecTarget[2] = e.absmin[2] + (0.5 * (e.absmax[2] - e.absmin[2])); + RunToBombsite(random(0, g_cs_bombzones)); +} - RouteToPosition(vecTarget); - ChatSayTeam("Going to run to an Escape Zone!"); +/* go to given escape zone */ +void +csbot::RunToEscapeZone(int index) +{ + entity e = GetEscapeZoneByIndex(index); + RouteToPosition(getEntityCenterPos(e)); + ChatSayTeam(strcat("Going to run to Escape Zone ", itos(index), "!")); } /* go to a random escape zone */ void -csbot::RunToVIPSafetyZone(void) +csbot::RunToRandomEscapeZone(void) { - entity e = world; - vector vecTarget; - - /* FIXME: make this random and error checked */ - while (e == world) - e = find(e, ::classname, "func_vip_safetyzone"); + RunToEscapeZone(random(0, g_cs_escapezones)); +} - vecTarget[0] = e.absmin[0] + (0.5 * (e.absmax[0] - e.absmin[0])); - vecTarget[1] = e.absmin[1] + (0.5 * (e.absmax[1] - e.absmin[1])); - vecTarget[2] = e.absmin[2] + (0.5 * (e.absmax[2] - e.absmin[2])); +/* go to given VIP Safety Zone */ +void +csbot::RunToVIPSafetyZone(int index) +{ + entity e = GetVIPSafetyZoneByIndex(index); + RouteToPosition(getEntityCenterPos(e)); + ChatSayTeam(strcat("Going to run to VIP Safety Zone ", itos(index), "!")); +} - RouteToPosition(vecTarget); - ChatSayTeam("Going to run to a VIP Safety Zone!"); +/* go to a random VIP Safety Zone */ +void +csbot::RunToRandomVIPSafetyZone(void) +{ + RunToVIPSafetyZone(random(0, g_cs_vipzones)); } void @@ -135,45 +149,226 @@ csbot::RunToHostages(void) ChatSayTeam("Going to run to the hostages!"); } +/** @brief Let the bot roam within a maximum distance from a given origin. */ +void csbot::Roam(vector roamOrigin, int maxDistance) { + /* Get random point whitin a radius from the given origin */ + int angle = random(0, 360); /* random angle. */ + int distance = random(0, maxDistance); /* random distance */ + float radian = angle * 3.145238095238 / 180; + vector randLoc = roamOrigin; + randLoc.x += sin(radian) * distance; + randLoc.y += cos(radian) * distance; + + /* Find closest waypoint to our random location. */ + float flBestDist = COST_INFINITE; + int iBestNodeIndex = -1; + for (int i = 0; i < g_iNodes; i++) { + float fDist = vlen(g_pNodes[i].origin - randLoc) - g_pNodes[i].radius; + + if (fDist > (float)maxDistance) { + /* Distance is greater then our maxDistance */ + continue; + } + + if (fDist < flBestDist) { + flBestDist = fDist; + iBestNodeIndex = i; + } + } + + if (iBestNodeIndex == -1) { + /* TODO No waypoint in range found */ + print("WARNING!: Roaming failed, could not find waypoint in range.\n"); + return; + } + + /* Go to the random waypoint. */ + RouteToPosition(g_pNodes[iBestNodeIndex].origin); +} + void csbot::CreateObjective(void) { + /* Bomb defuse map */ + if (g_cs_bombzones > 0) { + /* Bomb is planted */ + if (g_cs_bombplanted) { + entity eBomb = find(world, ::model, "models/w_c4.mdl"); + if (eBomb == world) { + /* No bomb model found, but it is/was planted */ + + /* RoundOver: Bomb is defused */ + if (g_cs_gamestate == GAME_END) { + RunToRandomBombsite(); + return; + } + + /* Error */ + print("WARNING! g_cs_bombplanted == TRUE, but bomb model " + "cannot be found in the world.\n"); + return; + } - if (g_cs_bombzones > 0 && g_cs_bombplanted) - RunToBombsite(); + if (team == TEAM_CT) { + if (g_cs_bombbeingdefused && m_actionIsDefusing == FALSE) { + /* Bomb is being defused but not by this bot */ + /* Go and roam the defuser */ + Roam(eBomb.origin, 300); + return; + } + + if (m_actionIsDefusing) { + if (!g_cs_bombbeingdefused) { + /* Defusing complete or somehow failed. */ + m_actionIsDefusing = FALSE; + } else { + /* Continue defusing. */ + input_buttons |= (INPUT_BUTTON5 | INPUT_BUTTON8); + input_movevalues = [0,0,0]; + button5 = input_buttons & INPUT_BUTTON5; // don't release button5 + } + } + else { + int distToBomb = floor(vlen(eBomb.origin - origin)); + if (distToBomb > 60) { + /* To far away from the bomb to defuse it, run to it! */ + RunToBomb(); + } else { + /* Aim at the bomb. */ + input_buttons |= INPUT_BUTTON8; // duck + if ((flags & FL_ONUSABLE)) { + // Aimed at the bomb, ready to defuse! + ChatSayTeam("Defusing!"); + input_buttons |= INPUT_BUTTON5; + input_movevalues = [0,0,0]; + button5 = input_buttons & INPUT_BUTTON5; // don't release button5 + m_actionIsDefusing = TRUE; + } else { + // Do the real aiming + float flLerp = bound(0.0f, frametime * 45, 1.0f); // aim speed + AimLerp(eBomb.origin + [0, 0, -6], flLerp); + } + } + } + } + /* team == TEAM_T */ + else { + /* Let T bots roam around the planted bomb */ + Roam(eBomb.origin, 500); + } + return; + } + /* Bomb is NOT planted */ + else { + if (team == TEAM_T) { + /* T-bot: plant bomb */ + if ((g_items & ITEM_C4BOMB)) { + /* We carry the bomb */ + if (m_gflagsBackup & GF_BOMBZONE) { + /* We are at a bombsite and ready to plant the bomb */ + if (activeweapon != WEAPON_C4BOMB) { + activeweapon = WEAPON_C4BOMB; + Weapons_Draw((player)self); + } + + if (!m_actionIsPlanting) { + ChatSayTeam("Going to plant the bomb!"); + m_actionIsPlanting = TRUE; + } + + /* Workaround */ + gflags = m_gflagsBackup; + + /* Duck and plant bomb. */ + input_buttons = (INPUT_BUTTON0 | INPUT_BUTTON8); + input_movevalues = [0,0,0]; + } + else { + /* Go to a bombsite first */ + RunToRandomBombsite(); + } + return; + } + else { + /* T-bot: check if the bomb has been dropped */ + entity e = find(world, ::model, "models/w_backpack.mdl"); + if (e != world) { + /* The bomb backpack has been dropped */ + /* Go fetch dropped bomb! */ + ChatSayTeam("Arrr! Bomb on the ground, going to fetch it!"); + RouteToPosition(getEntityCenterPos(e)); + return; + } + } + } + } + } if (g_cs_escapezones && team == TEAM_T) { - RunToEscapeZone(); + RunToRandomEscapeZone(); return; } if (random() < 0.5 && g_cs_escapezones > 0 && team == TEAM_CT) { - RunToEscapeZone(); + RunToRandomEscapeZone(); return; } - if (g_cs_vipzones > 0 && team == TEAM_CT) { - RunToVIPSafetyZone(); + RunToRandomVIPSafetyZone(); return; } if (random() < 0.5 && g_cs_vipzones > 0 && team == TEAM_T) { - RunToVIPSafetyZone(); + RunToRandomVIPSafetyZone(); return; } - if (random() < 0.5) { if (g_cs_hostagestotal > 0) RunToHostages(); if (g_cs_bombzones > 0) - RunToBombsite(); + RunToRandomBombsite(); } else { RunToConfront(); } } +/** @brief Aim towards a given (vector)aimpos with a given (float)lerp speed. + * + * @note + * Copied code from nuclide botlib (inside bot::RunAI), maybe make this a + * method there, could be usefull for other stuff? + **/ +void csbot::AimLerp(vector aimpos, float flLerp) { + vector aimdir, vecNewAngles; + + vector oldAngle = v_angle; + + /* that's the old angle */ + makevectors(v_angle); + vecNewAngles = v_forward; + + /* aimdir = new final angle */ + aimdir = vectoangles(aimpos - origin); + makevectors(aimdir); + + /* slowly lerp towards the final angle */ + vecNewAngles[0] = Math_Lerp(vecNewAngles[0], v_forward[0], flLerp); + vecNewAngles[1] = Math_Lerp(vecNewAngles[1], v_forward[1], flLerp); + vecNewAngles[2] = Math_Lerp(vecNewAngles[2], v_forward[2], flLerp); + + /* make sure we're aiming tight */ + v_angle = vectoangles(vecNewAngles); + v_angle[0] = Math_FixDelta(v_angle[0]); + v_angle[1] = Math_FixDelta(v_angle[1]); + v_angle[2] = Math_FixDelta(v_angle[2]); + angles[0] = Math_FixDelta(v_angle[0]); + angles[1] = Math_FixDelta(v_angle[1]); + angles[2] = Math_FixDelta(v_angle[2]); + input_angles = v_angle; +} + void csbot::PostFrame(void) { @@ -199,12 +394,56 @@ csbot::WeaponThink(void) } }; +/** @brief Get entity by class name and index **/ +entity +csbot::GetEntityByNameAndIndex(const string name, int index) +{ + int curIndex = 0; + for (entity a = world; (a = find(a, ::classname, name));) { + if (curIndex == index) { + return a; + } + ++curIndex; + } + print("WARNING: cstrike/server/bot.qc GetEntityByNameAndIndex: no entity '", + name, "' with index ", itos(index), "!\n"); + return world; +} + +/** @brief Get bombsite entity by bombsite index + * + * @note + * When there are for example 2 bombsites (g_cs_bombzones == 2) then valid + * indexes would be 0 and 1. + * */ +entity +csbot::GetBombsiteByIndex(int index) +{ + return GetEntityByNameAndIndex("func_bomb_target", index); +} + +/** @brief Get Escape Zone entity by index **/ +entity +csbot::GetEscapeZoneByIndex(int index) +{ + return GetEntityByNameAndIndex("func_escapezone", index); +} + +/** @brief Get VIP Safety Zone entity by index **/ +entity +csbot::GetVIPSafetyZoneByIndex(int index) +{ + return GetEntityByNameAndIndex("func_vip_safetyzone", index); +} + void csbot::csbot(void) { bot::bot(); targetname = "_csbot_"; team = infokeyf(this, "*team"); + m_actionIsPlanting = FALSE; + m_actionIsDefusing = FALSE; m_gflagsBackup = 0; } @@ -220,8 +459,8 @@ CSBot_BombPlantedNotify(void) continue; if (targ.health <= 0) continue; - - targ.RunToBombsite(); + + targ.RunToRandomBombsite(); } } } @@ -238,7 +477,7 @@ CSBot_HostageRescueNotify(void) continue; if (targ.health <= 0) continue; - + targ.RunToHostages(); } } @@ -273,7 +512,7 @@ CSBot_BuyStart_Shop(void) if (r == WEAPON_ELITES) { continue; } if (r == WEAPON_MAC10) { continue; } } - + if (g_cstrikeWeaponPrice[r] <= pl.money) { CSEv_BuyWeapon_f((float)r); done = 1; @@ -285,6 +524,14 @@ CSBot_BuyStart_Shop(void) done = 1; } + /* CT: Random buy bomb defuse kit when enough money left */ + if (pl.team == TEAM_CT && g_cs_bombzones > 0 && + g_cstrikeUtilPrice[(float)5] <= pl.money && + random() < 0.5) + { + CSEv_BuyEquipment_f((float)5); // ITEM_DEFUSAL + } + /* need armor */ if (pl.armor < 100) { if (pl.money >= g_cstrikeUtilPrice[1]) /* kevlar and helmet */ @@ -339,8 +586,27 @@ CSBot_RoundStart(void) if (targ.health <= 0) continue; - targ.RunToBombsite(); + targ.RunToRandomBombsite(); } } } */ } + +void +CSBot_RestartRound(void) +{ + // Reset some variables for all bots + for (entity a = world; (a = find(a, classname, "player"));) { + if (clienttype(a) != CLIENTTYPE_REAL) { + csbot targ; + targ = (csbot)a; + + if (targ.team == TEAM_T) { + targ.m_actionIsPlanting = FALSE; + } + else { + targ.m_actionIsDefusing = FALSE; + } + } + } +} diff --git a/src/server/defs.h b/src/server/defs.h index e44da05..64107b3 100644 --- a/src/server/defs.h +++ b/src/server/defs.h @@ -30,6 +30,7 @@ var int g_cs_vipzones; var int g_cs_escapezones; var int g_cs_bombzones; +var int g_cs_bombbeingdefused; var int g_cs_bombplanted; var int g_cs_roundswon_ct; var int g_cs_roundswon_t; diff --git a/src/server/gamerules_multiplayer.qc b/src/server/gamerules_multiplayer.qc index c59367f..4ff4e4e 100644 --- a/src/server/gamerules_multiplayer.qc +++ b/src/server/gamerules_multiplayer.qc @@ -479,6 +479,9 @@ CSMultiplayerRules::RestartRound(int iWipe) } } + // Reset CSBot vars + CSBot_RestartRound(); + for (entity eFind = world; (eFind = findfloat(eFind, ::team, TEAM_T));) { if (!(eFind.flags & FL_CLIENT)) continue; @@ -558,6 +561,12 @@ CSMultiplayerRules::RestartRound(int iWipe) NSEntity e = (NSEntity)eFind; e.Destroy(); } + // Remove potential bomb backpack model from the world, else bots will go + // chase a ghost. + entity e = find(world, ::model, "models/w_backpack.mdl"); + if (e != world) { + remove(e); + } WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET); WriteByte(MSG_MULTICAST, EV_CLEARDECALS); @@ -650,6 +659,7 @@ CSMultiplayerRules::RoundOver(int iTeamWon, int iMoneyReward, int fSilent) TimerBegin(5, GAME_END); // Round is over, 5 seconds til a new round starts g_cs_hostagesrescued = 0; + g_cs_bombbeingdefused = 0; g_cs_bombplanted = 0; g_cs_roundsplayed++; diff --git a/src/shared/item_c4bomb.qc b/src/shared/item_c4bomb.qc index 0bfa170..e02b859 100644 --- a/src/shared/item_c4bomb.qc +++ b/src/shared/item_c4bomb.qc @@ -110,6 +110,7 @@ item_c4::Logic(void) /* clear user */ m_eUser = world; m_flDefusalState = 0.0f; + g_cs_bombbeingdefused = FALSE; } else { /* defusal kit always cuts the time in half */ @@ -121,6 +122,8 @@ item_c4::Logic(void) /* tracked stat */ pl.progress = m_flDefusalState; pl.flags |= FL_FROZEN; + + g_cs_bombbeingdefused = TRUE; } } @@ -185,6 +188,7 @@ item_c4::OnRemoveEntity(void) ClearProgress(); m_flBeepTime = 0.0f; m_flDefusalState = 0; + g_cs_bombbeingdefused = FALSE; } void diff --git a/zpak001.pk3dir/data/de_dust2.way b/zpak001.pk3dir/data/de_dust2.way index 7d1bbaf..9b691e1 100644 --- a/zpak001.pk3dir/data/de_dust2.way +++ b/zpak001.pk3dir/data/de_dust2.way @@ -115,18 +115,19 @@ 129 404.310150 0 -1236.87 2106.78 36.0312 8.000000 1 31 129.404831 0 --1112.67 2558.34 54.5411 8.000000 2 - 35 46.630283 0 - 34 148.579971 0 --1066.34 2420.98 21.9286 32.000000 3 - 33 148.579971 0 +-1115.89 2556.04 54.2432 8.000000 2 + 35 46.630283 6 + 150 90.005127 0 +-1060.06 2414.18 18.9855 32.000000 3 31 229.015961 0 129 368.347656 0 --1122.41 2561.52 100.031 8.000000 1 + 150 99.204529 0 +-1153.41 2561.52 100.031 8.000000 2 36 46.012051 0 + 33 53.782150 2 -1167.97 2555.04 100.031 8.000000 1 37 58.178169 6 --1168.83 2550.57 158.031 8.000000 1 +-1186.83 2550.57 158.031 8.000000 1 38 49.115257 0 -1216.33 2543.05 148.031 8.000000 1 39 93.324020 0 @@ -522,3 +523,6 @@ 22 431.499512 0 24 322.543915 0 25 444.441376 0 +-1043.02 2511.69 25.5536 8.000000 2 + 33 90.005127 0 + 34 99.204529 0