// BLANKART //----------------------------------------------------------------------------- // Copyright (C) 2024 by Sally "TehRealSalt" Cochenour // Copyright (C) 2024 by Kart Krew // // This program is free software distributed under the // terms of the GNU General Public License, version 2. // See the 'LICENSE' file for more details. //----------------------------------------------------------------------------- /// \file k_bot.cpp /// \brief Bot logic & ticcmd generation code #include #include #include "cxxutil.hpp" #include "doomdef.h" #include "d_player.h" #include "g_game.h" #include "r_main.h" #include "p_local.h" #include "k_bot.h" #include "lua_hook.h" #include "byteptr.h" #include "d_net.h" // nodetoplayer #include "k_kart.h" #include "z_zone.h" #include "i_system.h" #include "p_maputl.h" #include "d_ticcmd.h" #include "m_random.h" #include "r_things.h" // numskins #include "m_perfstats.h" #include "m_easing.h" #include "d_clisrv.h" #include "k_grandprix.h" // K_CanChangeRules #include "hu_stuff.h" // HU_AddChatText #ifdef HAVE_DISCORDRPC #include "discord.h" // DRPC_UpdatePresence #endif #include "i_net.h" // doomcom #include "blan/b_soc.h" consvar_t cv_forcebots = CVAR_INIT ("kartforcebots", "Off", CV_NETVAR|CV_CHEAT, CV_OnOff, NULL); consvar_t cv_botcontrol = CVAR_INIT ("kartbotcontrol", "On", CV_NETVAR|CV_CHEAT, CV_OnOff, NULL); /*-------------------------------------------------- void K_SetNameForBot(UINT8 playerNum, const char *realname) See header file for description. --------------------------------------------------*/ void K_SetNameForBot(UINT8 newplayernum, const char *realname) { UINT8 ix = MAXPLAYERS; // These names are generally sourced from skins. I_Assert(MAXPLAYERNAME >= SKINNAMESIZE+2); if (netgame == true) { // Check if a player is currently using the name, case-insensitively. // We only do this if online, because it doesn't matter if there are multiple Eggrobo *off*line. // See also EnsurePlayerNameIsGood for (ix = 0; ix < MAXPLAYERS; ix++) { if (ix == newplayernum) continue; if (playeringame[ix] == false) continue; if (strcasecmp(realname, player_names[ix]) != 0) continue; break; } } if (ix == MAXPLAYERS) { // No conflict detected! sprintf(player_names[newplayernum], "%s", realname); return; } // Ok, now we append on the end for duplicates... char namebuffer[MAXPLAYERNAME+1]; sprintf(namebuffer, "%s %c", realname, 'A'+newplayernum); // ...and use the actual function, to handle more devious duplication. if (!EnsurePlayerNameIsGood(namebuffer, newplayernum)) { // we can't bail from adding the bot... // this hopefully uncontroversial pick is all we CAN do sprintf(namebuffer, "Bot %u", newplayernum+1); } // And finally write. sprintf(player_names[newplayernum], "%s", namebuffer); } /*-------------------------------------------------- void K_SetBot(UINT8 playerNum, UINT16 skinnum, UINT8 difficulty, botStyle_e style) See header file for description. --------------------------------------------------*/ void K_SetBot(UINT8 newplayernum, UINT16 skinnum, UINT8 difficulty, botStyle_e style) { CONS_Debug(DBG_NETPLAY, "addbot: %d\n", newplayernum); G_AddPlayer(newplayernum, newplayernum); if (newplayernum+1 > doomcom->numslots) doomcom->numslots = (INT16)(newplayernum+1); playernode[newplayernum] = servernode; players[newplayernum].splitscreenindex = 0; players[newplayernum].bot = true; players[newplayernum].botvars.difficulty = difficulty; players[newplayernum].botvars.style = style; players[newplayernum].lives = 9; // The bot may immediately become a spectator AT THE START of a GP. // For each subsequent round of GP, K_UpdateGrandPrixBots will handle this. players[newplayernum].spectator = grandprixinfo.gp && grandprixinfo.initalize; skincolornum_t color = static_cast(skins[skinnum].prefcolor); const char *realname = skins[skinnum].realname; players[newplayernum].skincolor = color; K_SetNameForBot(newplayernum, realname); SetPlayerSkinByNum(newplayernum, skinnum); for (UINT8 i = 0; i < PWRLV_NUMTYPES; i++) { clientpowerlevels[newplayernum][i] = 0; } if (netgame) { HU_AddChatText(va("\x82*Bot %d has been added to the game", newplayernum+1), false); } LUA_HookInt(newplayernum, HOOK(PlayerJoin)); } /*-------------------------------------------------- boolean K_AddBot(UINT16 skin, UINT8 difficulty, botStyle_e style, UINT8 *p) See header file for description. --------------------------------------------------*/ boolean K_AddBot(UINT16 skin, UINT8 difficulty, botStyle_e style, UINT8 *p) { UINT8 newplayernum = *p; for (; newplayernum < MAXPLAYERS; newplayernum++) { if (playeringame[newplayernum] == false) { // free player slot break; } } if (newplayernum >= MAXPLAYERS) { // nothing is free *p = MAXPLAYERS; return false; } K_SetBot(newplayernum, skin, difficulty, style); DEBFILE(va("Everyone added bot %d\n", newplayernum)); // use the next free slot *p = newplayernum+1; return true; } /*-------------------------------------------------- void K_UpdateMatchRaceBots(void) See header file for description. --------------------------------------------------*/ void K_UpdateMatchRaceBots(void) { const UINT16 defaultbotskin = K_BotDefaultSkin(); UINT8 difficulty; UINT8 pmax = (dedicated ? MAXPLAYERS-1 : MAXPLAYERS); UINT8 numplayers = 0; UINT8 numbots = 0; UINT8 numwaiting = 0; SINT8 wantedbots = 0; UINT16 usableskins = 0, skincount = numskins; UINT16 grabskins[MAXSKINS+1]; UINT16 i; // Init usable bot skins list for (i = 0; i < skincount; i++) { grabskins[usableskins++] = i; } grabskins[usableskins] = MAXSKINS; if (gamestate == GS_TITLESCREEN) { difficulty = 0; } else if ((gametyperules & GTR_BOTS) == 0 && !cv_forcebots.value) { difficulty = 0; } else if (K_CanChangeRules() == false) { difficulty = 0; } else { difficulty = cv_kartbot.value; if (netgame) { pmax = std::min(pmax, static_cast(cv_maxplayers.value)); } if (cv_ingamecap.value > 0) { pmax = std::min(pmax, static_cast(cv_ingamecap.value)); } } for (i = 0; i < MAXPLAYERS; i++) { if (playeringame[i]) { if (!players[i].spectator) { grabskins[players[i].skin] = MAXSKINS; if (players[i].bot) { numbots++; // While we're here, we should update bot difficulty to the proper value. players[i].botvars.difficulty = difficulty; // Enforce normal style for Match Race players[i].botvars.style = BOT_STYLE_NORMAL; } else { numplayers++; } } else if (players[i].pflags & PF_WANTSTOJOIN) { numwaiting++; } } } if (difficulty == 0) { // Remove bots if there are any. wantedbots = 0; } else { // Add bots to fill up MAXPLAYERS wantedbots = pmax - numplayers - numwaiting; if (wantedbots < 0) { wantedbots = 0; } } auto clear_bots = [&numbots](UINT8 max) { UINT8 i = MAXPLAYERS; while (numbots > max && i > 0) { i--; if (playeringame[i] && players[i].bot) { CL_RemovePlayer(i, KR_LEAVE); numbots--; } } }; if (numbots < wantedbots) { // We require MORE bots! UINT8 newplayernum = 0; if (dedicated) { newplayernum = 1; } for (i = 0; i < usableskins; i++) { if (!(grabskins[i] == MAXSKINS || !R_SkinUsable(-1, grabskins[i]))) { continue; } while (usableskins > i && (grabskins[usableskins] == MAXSKINS || !R_SkinUsable(-1, grabskins[usableskins]))) { usableskins--; } grabskins[i] = grabskins[usableskins]; grabskins[usableskins] = MAXSKINS; } while (numbots < wantedbots) { UINT16 skinnum = defaultbotskin; if (usableskins > 0) { UINT16 index = P_RandomKey(usableskins); skinnum = grabskins[index]; if (((cv_ingamecap.value > 0) && (usableskins+1 >= cv_ingamecap.value)) || (usableskins+1 >= cv_maxplayers.value)) { grabskins[index] = grabskins[--usableskins]; } } if (!K_AddBot(skinnum, difficulty, BOT_STYLE_NORMAL, &newplayernum)) { // Not enough player slots to add the bot, break the loop. break; } numbots++; } } else if (numbots > wantedbots) { clear_bots(wantedbots); } // We should have enough bots now :) #ifdef HAVE_DISCORDRPC // Player count change was possible, so update presence DRPC_UpdatePresence(); #endif } /*-------------------------------------------------- boolean K_PlayerUsesBotMovement(const player_t *player) See header file for description. --------------------------------------------------*/ boolean K_PlayerUsesBotMovement(const player_t *player) { if (player->bot) return true; return false; } /*-------------------------------------------------- boolean K_BotCanTakeCut(player_t *player) See header file for description. --------------------------------------------------*/ boolean K_BotCanTakeCut(player_t *player) { if ( (K_TripwirePassConditions(player) != TRIPWIRE_NONE || K_ApplyOffroad(player) == false) || player->itemtype == KITEM_SNEAKER || player->itemtype == KITEM_ROCKETSNEAKER || player->itemtype == KITEM_INVINCIBILITY ) { return true; } return false; } /*-------------------------------------------------- static fixed_t K_BotSpeedScaled(const player_t *player, fixed_t speed) What the bot "thinks" their speed is, for predictions. Mainly to make bots brake earlier when on friction sectors. Input Arguments:- player - The bot player to calculate speed for. speed - Raw speed value. Return:- The bot's speed value for calculations. --------------------------------------------------*/ static fixed_t K_BotSpeedScaled(const player_t *player, fixed_t speed) { fixed_t result = speed; if (!player->pogospring && P_IsObjectOnGround(player->mo) == false) { // You have no air control, so don't predict too far ahead. return 0; } if (player->mo->movefactor != FRACUNIT) { fixed_t moveFactor = player->mo->movefactor; if (moveFactor == 0) { moveFactor = 1; } // Reverse against friction. Allows for bots to // acknowledge they'll be moving faster on ice, // and to steer harder / brake earlier. moveFactor = FixedDiv(FRACUNIT, moveFactor); // The full value is way too strong, reduce it. moveFactor -= (moveFactor - FRACUNIT)*3/4; result = FixedMul(result, moveFactor); } if (player->mo->standingslope != nullptr) { const pslope_t *slope = player->mo->standingslope; if (!(slope->flags & SL_NOPHYSICS) && abs(slope->zdelta) >= FRACUNIT/21) { fixed_t slopeMul = FRACUNIT; angle_t angle = K_MomentumAngle(player->mo) - slope->xydirection; if (P_MobjFlip(player->mo) * slope->zdelta < 0) angle ^= ANGLE_180; // Going uphill: 0 // Going downhill: FRACUNIT*2 slopeMul = FRACUNIT + FINECOSINE(angle >> ANGLETOFINESHIFT); // Range: 0.5 to 1.5 result = FixedMul(result, (FRACUNIT>>1) + (slopeMul >> 1)); } } return result; } /*-------------------------------------------------- const botcontroller_t *K_GetBotController(const mobj_t *mobj) See header file for description. --------------------------------------------------*/ const botcontroller_t *K_GetBotController(mobj_t *mobj) { botcontroller_t *ret = nullptr; if (P_MobjWasRemoved(mobj) == true) { return nullptr; } if (mobj->subsector == nullptr || mobj->subsector->sector == nullptr) { return nullptr; } ret = &mobj->subsector->sector->botController; ffloor_t *rover = nullptr; for (rover = mobj->subsector->sector->ffloors; rover; rover = rover->next) { if ((rover->fofflags & FOF_EXISTS) == 0) { continue; } fixed_t topheight = P_GetFOFTopZ(mobj, mobj->subsector->sector, rover, mobj->x, mobj->y, nullptr); fixed_t bottomheight = P_GetFOFBottomZ(mobj, mobj->subsector->sector, rover, mobj->x, mobj->y, nullptr); if (mobj->z > topheight || mobj->z + mobj->height < bottomheight) { continue; } botcontroller_t *roverController = &rover->master->frontsector->botController; if (roverController->flags != 0) { ret = roverController; } } return ret; } /*-------------------------------------------------- fixed_t K_BotMapModifier(void) See header file for description. --------------------------------------------------*/ fixed_t K_BotMapModifier(void) { constexpr INT32 complexity_scale = 10000; fixed_t modifier_max = K_TrackModifierMax(); if (K_CanChangeRules() == false) { modifier_max = FRACUNIT; } const fixed_t complexity_value = std::clamp( FixedDiv(K_GetTrackComplexity(), complexity_scale), -FixedDiv(FRACUNIT, modifier_max), modifier_max ); return FRACUNIT + complexity_value; } /*-------------------------------------------------- static UINT32 K_BotRubberbandDistance(const player_t *player) Calculates the distance away from 1st place that the bot should rubberband to. Input Arguments:- player - Player to compare. Return:- Distance to add, as an integer. --------------------------------------------------*/ static UINT32 K_BotRubberbandDistance(const player_t *player) { const UINT32 spacing = FixedDiv(640 * mapobjectscale, K_GetKartGameSpeedScalar(gamespeed)) / FRACUNIT; const UINT8 portpriority = player - players; UINT8 pos = 1; UINT8 i; if (player->botvars.rival) { // The rival should always try to be the front runner for the race. return 0; } for (i = 0; i < MAXPLAYERS; i++) { if (i == portpriority) { continue; } if (!playeringame[i] || players[i].spectator) { continue; } if (!players[i].bot) { continue; } // First check difficulty levels, then score, then settle it with port priority! if (player->botvars.difficulty < players[i].botvars.difficulty) { pos += 3; } else if (player->score < players[i].score) { pos += 2; } else if (i < portpriority) { pos += 1; } } return (pos * spacing); } /*-------------------------------------------------- fixed_t K_BotRubberband(const player_t *player) See header file for description. --------------------------------------------------*/ fixed_t K_BotRubberband(const player_t *player) { if (player->exiting) { // You're done, we don't need to rubberband anymore. return FRACUNIT; } const botcontroller_t *botController = K_GetBotController(player->mo); if (botController != nullptr && (botController->flags & TMBOT_NORUBBERBAND) == TMBOT_NORUBBERBAND) // Disable rubberbanding { return FRACUNIT; } fixed_t difficultyEase = ((player->botvars.difficulty - 1) * FRACUNIT) / (MAXBOTDIFFICULTY - 1); // Lv. 1: x0.65 avg // Lv. MAX: x1.05 avg const fixed_t rubberBase = Easing_OutSine( difficultyEase, FRACUNIT * 65 / 100, FRACUNIT * 105 / 100 ); // +/- x0.35 const fixed_t rubberStretchiness = FixedMul( FixedDiv( 35 * FRACUNIT / 100, K_GetKartGameSpeedScalar(gamespeed) ), K_BotMapModifier() ); // Lv. 1: x0.4 min // Lv. MAX: x0.85 min constexpr fixed_t rubberSlowMin = FRACUNIT / 2; const fixed_t rubberSlow = std::max( rubberBase - rubberStretchiness, rubberSlowMin ); // Lv. 1: x0.9 max // Lv. MAX: x1.35 max constexpr fixed_t rubberFastMax = FRACUNIT * 3 / 2; const fixed_t rubberFast = std::min( rubberBase + rubberStretchiness, rubberFastMax ); fixed_t rubberband = FRACUNIT >> 1; player_t *firstplace = nullptr; size_t i = SIZE_MAX; for (i = 0; i < MAXPLAYERS; i++) { if (!playeringame[i] || players[i].spectator) { continue; } // Don't rubberband to ourselves... if (player == &players[i]) { continue; } #if 0 // Only rubberband up to players. if (players[i].bot) { continue; } #endif if (firstplace == nullptr || players[i].distancetofinish < firstplace->distancetofinish) { firstplace = &players[i]; } } if (firstplace != nullptr) { const UINT32 spacing = FixedDiv(10240 * mapobjectscale, K_GetKartGameSpeedScalar(gamespeed)) / FRACUNIT; const UINT32 wanteddist = firstplace->distancetofinish + K_BotRubberbandDistance(player); const INT32 distdiff = player->distancetofinish - wanteddist; rubberband = FixedDiv(distdiff + spacing, spacing * 2); if (player->boostpower < FRACUNIT) { // Do not let bots cheese offroad as much. rubberband = FixedMul(rubberband, player->boostpower); } if (P_MobjWasRemoved(player->mo) == false && player->mo->movefactor < FRACUNIT) { // Do not let bots speed up on ice too much. rubberband = FixedMul(rubberband, player->mo->movefactor); } if (rubberband > FRACUNIT) { rubberband = FRACUNIT; } else if (rubberband < 0) { rubberband = 0; } } UINT32 scaled_dist = player->distancetofinish; if (mapobjectscale != FRACUNIT) { // Bring back to normal scale. scaled_dist = FixedDiv(scaled_dist, mapobjectscale); } return Easing_Linear(rubberband, rubberSlow, rubberFast); } /*-------------------------------------------------- fixed_t K_UpdateRubberband(player_t *player) See header file for description. --------------------------------------------------*/ fixed_t K_UpdateRubberband(player_t *player) { fixed_t dest = K_BotRubberband(player); fixed_t ret = player->botvars.rubberband; // Ease into the new value. ret += (dest - player->botvars.rubberband) / 8; return ret; } /*-------------------------------------------------- fixed_t K_DistanceOfLineFromPoint(fixed_t v1x, fixed_t v1y, fixed_t v2x, fixed_t v2y, fixed_t cx, fixed_t cy) See header file for description. --------------------------------------------------*/ fixed_t K_DistanceOfLineFromPoint(fixed_t v1x, fixed_t v1y, fixed_t v2x, fixed_t v2y, fixed_t px, fixed_t py) { // Copy+paste from P_ClosestPointOnLine :pensive: fixed_t startx = v1x; fixed_t starty = v1y; fixed_t dx = v2x - v1x; fixed_t dy = v2y - v1y; fixed_t cx, cy; fixed_t vx, vy; fixed_t magnitude; fixed_t t; cx = px - startx; cy = py - starty; vx = dx; vy = dy; magnitude = R_PointToDist2(v2x, v2y, startx, starty); vx = FixedDiv(vx, magnitude); vy = FixedDiv(vy, magnitude); t = (FixedMul(vx, cx) + FixedMul(vy, cy)); vx = FixedMul(vx, t); vy = FixedMul(vy, t); return R_PointToDist2(px, py, startx + vx, starty + vy); } /*-------------------------------------------------- static void K_GetBotWaypointRadius(waypoint_t *waypoint, fixed_t *smallestRadius, fixed_t *smallestScaled) Calculates a new waypoint radius size to use, making it thinner depending on how harsh the turn is. Input Arguments:- waypoint - Waypoint to retrieve the radius of. Return:- N/A --------------------------------------------------*/ static void K_GetBotWaypointRadius(waypoint_t *const waypoint, fixed_t *smallestRadius, fixed_t *smallestScaled) { static const fixed_t maxReduce = FRACUNIT/32; static const angle_t maxDelta = ANGLE_22h; fixed_t radius = waypoint->mobj->radius; fixed_t reduce = FRACUNIT; angle_t delta = 0; size_t i, j; for (i = 0; i < waypoint->numnextwaypoints; i++) { const waypoint_t *next = waypoint->nextwaypoints[i]; const angle_t nextAngle = R_PointToAngle2( waypoint->mobj->x, waypoint->mobj->y, next->mobj->x, next->mobj->y ); for (j = 0; j < waypoint->numprevwaypoints; j++) { const waypoint_t *prev = waypoint->prevwaypoints[j]; const angle_t prevAngle = R_PointToAngle2( prev->mobj->x, prev->mobj->y, waypoint->mobj->x, waypoint->mobj->y ); delta = std::max(delta, AngleDelta(nextAngle, prevAngle)); } } if (delta > maxDelta) { delta = maxDelta; } reduce = FixedDiv(delta, maxDelta); reduce = FRACUNIT + FixedMul(reduce, maxReduce - FRACUNIT); *smallestRadius = std::min(*smallestRadius, radius); *smallestScaled = std::min(*smallestScaled, FixedMul(radius, reduce)); } static fixed_t K_ScaleWPDistWithSlope(fixed_t disttonext, angle_t angletonext, const pslope_t *slope, SINT8 flip) { if (slope == nullptr) { return disttonext; } if ((slope->flags & SL_NOPHYSICS) == 0 && abs(slope->zdelta) >= FRACUNIT/21) { // Displace the prediction to go with the slope physics. fixed_t slopeMul = FRACUNIT; angle_t angle = angletonext - slope->xydirection; if (flip * slope->zdelta < 0) { angle ^= ANGLE_180; } // Going uphill: 0 // Going downhill: FRACUNIT*2 slopeMul = FRACUNIT + FINECOSINE(angle >> ANGLETOFINESHIFT); // Range: 0.25 to 1.75 return FixedMul(disttonext, (FRACUNIT >> 2) + ((slopeMul * 3) >> 2)); } return disttonext; } /*-------------------------------------------------- static botprediction_t *K_CreateBotPrediction(const player_t *player) Calculates a point further along the track to attempt to drive towards. Input Arguments:- player - Player to compare. Return:- Bot prediction struct. --------------------------------------------------*/ static botprediction_t *K_CreateBotPrediction(player_t *player) { ZoneScoped; const precise_t time = I_GetPreciseTime(); const INT16 handling = K_GetKartTurnValue(player, KART_FULLTURN); // Reduce prediction based on how fast you can turn const tic_t futuresight = (TICRATE * KART_FULLTURN) / std::max(1, handling); // How far ahead into the future to try and predict const fixed_t speed = K_BotSpeedScaled(player, P_AproxDistance(player->mo->momx, player->mo->momy)); const INT32 startDist = 0; //(DEFAULT_WAYPOINT_RADIUS * mapobjectscale) / FRACUNIT; const INT32 maxDist = (DEFAULT_WAYPOINT_RADIUS * 3 * mapobjectscale) / FRACUNIT; // This function gets very laggy when it goes far distances, and going too far isn't very helpful anyway. const INT32 distance = std::min(((speed / FRACUNIT) * static_cast(futuresight)) + startDist, maxDist); // Halves radius when encountering a wall on your way to your destination. fixed_t radReduce = FRACUNIT; fixed_t radius = INT32_MAX; fixed_t radiusScaled = INT32_MAX; INT32 distanceleft = distance; angle_t angletonext = ANGLE_MAX; INT32 disttonext = INT32_MAX; INT32 distscaled = INT32_MAX; pslope_t *nextslope = player->mo->standingslope; waypoint_t *wp = player->nextwaypoint; mobj_t *prevwpmobj = player->mo; const boolean useshortcuts = K_BotCanTakeCut(player); const boolean huntbackwards = false; boolean pathfindsuccess = false; path_t pathtofinish = {0}; botprediction_t *predict = nullptr; size_t i; if (wp == nullptr || P_MobjWasRemoved(wp->mobj) == true) { // Can't do any of this if we don't have a waypoint. return nullptr; } predict = static_cast(Z_Calloc(sizeof(botprediction_t), PU_LEVEL, nullptr)); // Init defaults in case of pathfind failure angletonext = R_PointToAngle2(prevwpmobj->x, prevwpmobj->y, wp->mobj->x, wp->mobj->y); disttonext = P_AproxDistance(prevwpmobj->x - wp->mobj->x, prevwpmobj->y - wp->mobj->y); nextslope = wp->mobj->standingslope; distscaled = K_ScaleWPDistWithSlope(disttonext, angletonext, nextslope, P_MobjFlip(wp->mobj)) / FRACUNIT; pathfindsuccess = K_PathfindThruCircuit( wp, (unsigned)distanceleft, &pathtofinish, useshortcuts, huntbackwards ); // Go through the waypoints until we've traveled the distance we wanted to predict ahead! if (pathfindsuccess == true) { for (i = 0; i < pathtofinish.numnodes; i++) { wp = (waypoint_t *)pathtofinish.array[i].nodedata; if (i == 0) { prevwpmobj = player->mo; } else { prevwpmobj = ((waypoint_t *)pathtofinish.array[ i - 1 ].nodedata)->mobj; } angletonext = R_PointToAngle2(prevwpmobj->x, prevwpmobj->y, wp->mobj->x, wp->mobj->y); disttonext = P_AproxDistance(prevwpmobj->x - wp->mobj->x, prevwpmobj->y - wp->mobj->y); nextslope = wp->mobj->standingslope; distscaled = K_ScaleWPDistWithSlope(disttonext, angletonext, nextslope, P_MobjFlip(wp->mobj)) / FRACUNIT; if (P_TraceBotTraversal(player->mo, wp->mobj) == false) { // If we can't get a direct path to this waypoint, reduce our prediction drastically. distscaled *= 4; radReduce = FRACUNIT >> 1; } K_GetBotWaypointRadius(wp, &radius, &radiusScaled); distanceleft -= distscaled; if (distanceleft <= 0) { // We're done!! break; } } Z_Free(pathtofinish.array); } // Set our predicted point's coordinates, // and use the smallest radius of all of the waypoints in the chain! predict->x = wp->mobj->x; predict->y = wp->mobj->y; predict->baseRadius = radius; predict->radius = FixedMul(radiusScaled, radReduce); // Set the prediction coordinates between the 2 waypoints if there's still distance left. if (distanceleft > 0) { // Scaled with the leftover anglemul! predict->x += P_ReturnThrustX(nullptr, angletonext, std::min(disttonext, distanceleft) * FRACUNIT); predict->y += P_ReturnThrustY(nullptr, angletonext, std::min(disttonext, distanceleft) * FRACUNIT); } ps_bots[player - players].prediction += I_GetPreciseTime() - time; return predict; } /*-------------------------------------------------- static void K_DrawPredictionDebug(botprediction_t *predict, const player_t *player) Draws objects to show where the viewpoint bot is trying to go. Input Arguments:- predict - The prediction to visualize. player - The bot player this prediction is for. Return:- None --------------------------------------------------*/ static void K_DrawPredictionDebug(botprediction_t *predict, const player_t *player) { mobj_t *debugMobj = nullptr; angle_t sideAngle = ANGLE_MAX; UINT8 i = UINT8_MAX; I_Assert(predict != nullptr); I_Assert(player != nullptr); I_Assert(player->mo != nullptr && P_MobjWasRemoved(player->mo) == false); sideAngle = player->mo->angle + ANGLE_90; debugMobj = P_SpawnMobj(predict->x, predict->y, player->mo->z, MT_SPARK); P_SetMobjState(debugMobj, S_THOK); debugMobj->frame &= ~FF_TRANSMASK; debugMobj->frame |= FF_TRANS20|FF_FULLBRIGHT; debugMobj->color = SKINCOLOR_ORANGE; P_SetScale(debugMobj, debugMobj->destscale * 2); debugMobj->tics = 2; for (i = 0; i < 2; i++) { mobj_t *radiusMobj = nullptr; fixed_t radiusX = predict->x, radiusY = predict->y; if (i & 1) { radiusX -= FixedMul(predict->radius, FINECOSINE(sideAngle >> ANGLETOFINESHIFT)); radiusY -= FixedMul(predict->radius, FINESINE(sideAngle >> ANGLETOFINESHIFT)); } else { radiusX += FixedMul(predict->radius, FINECOSINE(sideAngle >> ANGLETOFINESHIFT)); radiusY += FixedMul(predict->radius, FINESINE(sideAngle >> ANGLETOFINESHIFT)); } radiusMobj = P_SpawnMobj(radiusX, radiusY, player->mo->z, MT_SPARK); P_SetMobjState(radiusMobj, S_THOK); radiusMobj->frame &= ~FF_TRANSMASK; radiusMobj->frame |= FF_TRANS20|FF_FULLBRIGHT; radiusMobj->color = SKINCOLOR_YELLOW; P_SetScale(debugMobj, debugMobj->destscale / 2); radiusMobj->tics = 2; } } /*-------------------------------------------------- fixed_t K_BotDetermineDriftSkill(player_t *player) Calculates drift skill for a player based on stats. Input Arguments:- player - Player to get drift skill for. Return:- Calculated drift skill. --------------------------------------------------*/ fixed_t K_BotDetermineDriftSkill(player_t *player) { return ((FRACUNIT * (player->kartspeed + player->kartweight)) / 18); } static void K_WaypointGetDirectionVector(waypoint_t *wp1, waypoint_t *wp2, vector3_t *a_o) { vector3_t v1, v2; v1.x = wp1->mobj->x; v1.y = wp1->mobj->y; v1.x = wp1->mobj->z; v2.x = wp2->mobj->x; v2.y = wp2->mobj->y; v2.x = wp2->mobj->z; FV3_SubEx(&v1,&v2,a_o); FV3_Normalize(a_o); } /*-------------------------------------------------- void K_BotSetDriftState(player_t *player, botdrift_t newstate, tic_t lockout) See header file for description. --------------------------------------------------*/ void K_BotSetDriftState(player_t *player, botdrift_t newstate, tic_t lockout) { if (newstate != player->botvars.driftstate) { player->botvars.driftstate = newstate; player->botvars.drifttime = 0; } if (lockout) player->botvars.driftlockout = lockout; } #define MINBOTDRIFT (KART_FULLTURN * 2) / 3 // 0.66 /*-------------------------------------------------- static INT32 K_BotStartDrift(player_t* player) Begins and ends "forced" drifts on a per-waypoint basis. Input Arguments:- player - Player to begin the drift for. Return:- Override value for turn amount. --------------------------------------------------*/ static void K_BotStartDrift(player_t* player) { // Handle DRIFTING towards waypoints! boolean shouldDrift; fixed_t botDriftSpeed; driftSetting_e driftsetting = DRIFT_NONE; fixed_t speedfactor = FixedDiv(player->speed, K_GetKartSpeed(player, false, false)); if (speedfactor < FRACUNIT/2) { // don't bother if we're going too slow K_BotSetDriftState(player, DRIFTSTATE_AUTO, BOTDRIFTLOCKOUT); return; } if (speedfactor > (6-gamespeed)*FRACUNIT/3) { // likewise, don't bother if we're going too fast K_BotSetDriftState(player, DRIFTSTATE_AUTO, BOTDRIFTLOCKOUT/2); return; } if (player->botvars.driftlockout) { // things are not working out in our favor player->botvars.driftlockout--; return; } // check for waypoints ahead of us with drift settings, based on our current speed path_t path = {0}; INT32 maxdist = FixedInt(cv_test1.value) + gamespeed*50; maxdist = FixedMul(maxdist, speedfactor * (player->botvars.driftstate == DRIFTSTATE_ACTIVE ? 1 : 2)); if (maxdist >= 0 && K_PathfindThruCircuit(player->currentwaypoint, maxdist, &path, false, false)) { for (size_t i = 0; i < path.numnodes; i++) { waypoint_t *wp = static_cast(path.array[i].nodedata); if (wp->driftsettings) driftsetting = static_cast(wp->driftsettings); // don't break on DRIFT_END waypoints, // we could miss a drift waypoint right in front of it! if (driftsetting != DRIFT_NONE && driftsetting != DRIFT_END) break; } Z_Free(path.array); } if (driftsetting == DRIFT_NONE) { // No waypoints, nothing we can do here. return; } shouldDrift = false; botDriftSpeed = FixedMul(K_GetKartSpeed(player, false, false), FixedPercentage(BOTDRIFTPERCENT)); if (driftsetting == DRIFT_END) { if (player->botvars.driftstate != DRIFTSTATE_AUTO) K_BotSetDriftState(player, DRIFTSTATE_ENDING, 0); } else if (driftsetting > DRIFT_NONE && driftsetting < DRIFT_END && player->botvars.driftstate == DRIFTSTATE_AUTO) { // Randomly decide to drift based on our skill at drifting, // and how fast we're moving. fixed_t driftpotential = P_RandomKey(MAXDRIFTSKILL); if ((driftpotential <= player->botvars.driftskill) && (botDriftSpeed <= player->speed)) { shouldDrift = true; } if (shouldDrift) { // Start our drift based on the waypoint's drift settings. SINT8 driftturn = 0; switch (driftsetting) { case DRIFT_PWRSLIDE_L: driftturn = -2; break; case DRIFT_LEFT: driftturn = -1; break; case DRIFT_PWRSLIDE_R: driftturn = 2; break; case DRIFT_RIGHT: driftturn = 1; break; default: break; } player->botvars.driftturn = driftturn; K_BotSetDriftState(player, DRIFTSTATE_STARTING, 0); } } } /*-------------------------------------------------- static INT32 K_HandleBotTrack(const player_t *player, ticcmd_t *cmd, botprediction_t *predict) Determines inputs for standard track driving. Input Arguments:- player - Player to generate the ticcmd for. cmd - The player's ticcmd to modify. predict - Pointer to the bot's prediction. Return:- New value for turn amount. --------------------------------------------------*/ static INT32 K_HandleBotTrack(player_t *player, ticcmd_t *cmd, botprediction_t *predict, angle_t destangle) { ZoneScoped; // Handle steering towards waypoints! INT32 turnamt = 0; SINT8 turnsign = 0; angle_t moveangle; INT32 anglediff, anglediff2; fixed_t speedfactor = FixedDiv(player->speed, K_GetKartSpeed(player, false, false)); I_Assert(predict != nullptr); moveangle = player->mo->angle; anglediff = AngleDeltaSigned(moveangle, destangle); // line up for an incoming drift if (player->botvars.driftstate == DRIFTSTATE_STARTING) { anglediff += FixedMul(ANG10-ANG2, speedfactor) * player->botvars.driftturn; } if (anglediff < 0) { turnsign = 1; } else { turnsign = -1; } anglediff2 = anglediff; anglediff = abs(anglediff); turnamt = KART_FULLTURN * turnsign; if (anglediff > ANGLE_67h) { // Wrong way! cmd->forwardmove = -MAXPLMOVE; cmd->buttons |= BT_BRAKE; } else { const fixed_t playerwidth = (player->mo->radius * 2); fixed_t realrad = predict->radius*3/4; // Remove a "safe" distance away from the edges of the road fixed_t rad = realrad; fixed_t dirdist = K_DistanceOfLineFromPoint( player->mo->x, player->mo->y, player->mo->x + FINECOSINE(moveangle >> ANGLETOFINESHIFT), player->mo->y + FINESINE(moveangle >> ANGLETOFINESHIFT), predict->x, predict->y ); if (realrad < playerwidth) { realrad = playerwidth; } // Become more precise based on how hard you need to turn // This makes predictions into turns a little nicer // Facing 90 degrees away from the predicted point gives you 0 radius rad = FixedMul(rad, FixedDiv(std::max(0, ANGLE_90 - anglediff), ANGLE_90) ); // Become more precise the slower you're moving // Also helps with turns // Full speed uses full radius rad = FixedMul(rad, FixedDiv(K_BotSpeedScaled(player, player->speed), K_GetKartSpeed(player, false, false)) ); // Cap the radius to reasonable bounds if (rad > realrad) { rad = realrad; } else if (rad < playerwidth) { rad = playerwidth; } // Full speed ahead! cmd->buttons |= BT_ACCELERATE; cmd->forwardmove = MAXPLMOVE; if (dirdist <= rad && player->botvars.driftstate != DRIFTSTATE_STARTING) // steer towards waypoints when starting drift { // Going the right way, don't turn at all. turnamt = 0; } // 0.5 on Easy, 1.0 on Normal, 1.5 on Hard. //INT32 mindriftamt = FixedMul(MINBOTDRIFT * (cv_kartspeed.value + 1), 2 * FRACUNIT); // Start or continue a drift. if (player->botvars.driftstate == DRIFTSTATE_ACTIVE || player->botvars.driftstate == DRIFTSTATE_ENDING) { cmd->buttons |= BT_DRIFT; fixed_t angofs = K_GetKartSpeedFromStat(5 - (player->kartspeed - 5), false) * -player->botvars.driftturn; // adjust for speed angofs = FixedMul(angofs, speedfactor - (2-gamespeed)*FRACUNIT/4); fixed_t driftpower = angofs - FixedDiv(anglediff2, ANG1); // arbitrary divider on the final driftpower driftpower /= FixedInt(cv_test2.value); // brakedrift if we're steering too hard if (abs(driftpower) >= FRACUNIT) cmd->buttons |= BT_BRAKE; // get the raw turn value and "invert" it (higher weight needs harder steering!) INT16 turnvalue = abs(K_GetKartTurnValue(player, KART_FULLTURN * (player->botvars.driftturn < 0 ? 1 : -1))); turnvalue = 541 - (turnvalue - 541); // weight 5 = 541 turnamt = std::clamp(FixedMul(driftpower, turnvalue), -KART_FULLTURN, KART_FULLTURN); } /* else if ((turnamt) && (player->botvars.driftstate == DRIFTSTATE_AUTO) && (turnpower > FixedPercentage(DRIFTSTARTPCT))) { // TODO: Figure out a drift prediction system. } */ } return turnamt; } #undef MINBOTDRIFT /*-------------------------------------------------- static INT32 K_HandleBotReverse(const player_t *player, ticcmd_t *cmd, botprediction_t *predict) Determines inputs for reversing. Input Arguments:- player - Player to generate the ticcmd for. cmd - The player's ticcmd to modify. predict - Pointer to the bot's prediction. Return:- New value for turn amount. --------------------------------------------------*/ /*static INT32 K_HandleBotReverse(player_t *player, ticcmd_t *cmd, botprediction_t *predict, angle_t destangle) { ZoneScoped; // Handle steering towards waypoints! INT32 turnamt = 0; SINT8 turnsign = 0; angle_t moveangle, angle; INT16 anglediff, momdiff; if (predict != nullptr) { // TODO: Should we reverse through bot controllers? return K_HandleBotTrack(player, cmd, predict, destangle); } if (player->nextwaypoint == nullptr || player->nextwaypoint->mobj == nullptr || P_MobjWasRemoved(player->nextwaypoint->mobj)) { // No data available... return 0; } if ((player->nextwaypoint->prevwaypoints != nullptr) && (player->nextwaypoint->numprevwaypoints > 0U)) { size_t i; for (i = 0U; i < player->nextwaypoint->numprevwaypoints; i++) { if (!K_GetWaypointIsEnabled(player->nextwaypoint->prevwaypoints[i])) { continue; } destangle = R_PointToAngle2( player->nextwaypoint->prevwaypoints[i]->mobj->x, player->nextwaypoint->prevwaypoints[i]->mobj->y, player->nextwaypoint->mobj->x, player->nextwaypoint->mobj->y ); break; } } // Calculate turn direction first. moveangle = player->mo->angle; angle = (moveangle - destangle); if (angle < ANGLE_180) { turnsign = -1; // Turn right anglediff = AngleFixed(angle)>>FRACBITS; } else { turnsign = 1; // Turn left anglediff = 360-(AngleFixed(angle)>>FRACBITS); } anglediff = abs(anglediff); turnamt = KART_FULLTURN * turnsign; // Now calculate momentum momdiff = 180; if (player->speed > player->mo->scale) { momdiff = 0; moveangle = K_MomentumAngle(player->mo); angle = (moveangle - destangle); if (angle < ANGLE_180) { momdiff = AngleFixed(angle)>>FRACBITS; } else { momdiff = 360-(AngleFixed(angle)>>FRACBITS); } momdiff = abs(momdiff); } if (anglediff > 90 || momdiff < 90) { // We're not facing the track, // or we're going too fast. // Let's E-Brake. cmd->forwardmove = 0; cmd->buttons |= BT_ACCELERATE|BT_BRAKE; } else { fixed_t slopeMul = FRACUNIT; if (player->mo->standingslope != nullptr) { const pslope_t *slope = player->mo->standingslope; if (!(slope->flags & SL_NOPHYSICS) && abs(slope->zdelta) >= FRACUNIT/21) { angle_t sangle = player->mo->angle - slope->xydirection; if (P_MobjFlip(player->mo) * slope->zdelta < 0) sangle ^= ANGLE_180; slopeMul = FRACUNIT - FINECOSINE(sangle >> ANGLETOFINESHIFT); } } #define STEEP_SLOPE (FRACUNIT*11/10) if (slopeMul > STEEP_SLOPE) { // Slope is too steep to reverse -- EBrake. cmd->forwardmove = 0; cmd->buttons |= BT_ACCELERATE|BT_BRAKE; } else { cmd->forwardmove = -MAXPLMOVE; cmd->buttons |= BT_BRAKE; //|BT_LOOKBACK } #undef STEEP_SLOPE if (anglediff < 10) { turnamt = 0; } } return turnamt; }*/ /*-------------------------------------------------- static void K_BuildBotTiccmdNormal(const player_t *player, ticcmd_t *cmd) Build ticcmd for bots with a style of BOT_STYLE_NORMAL --------------------------------------------------*/ static void K_BuildBotTiccmdNormal(player_t *player, ticcmd_t *cmd) { precise_t t = 0; botprediction_t *predict = nullptr; auto predict_finally = srb2::finally([&predict]() { Z_Free(predict); }); angle_t destangle = 0; INT32 turnamt = 0; if (!(gametyperules & GTR_BOTS) // No bot behaviors || K_GetNumWaypoints() == 0 // No waypoints || leveltime <= introtime // During intro camera || player->playerstate == PST_DEAD // Dead, respawning. || player->mo->scale <= 1) // Post-finish "death" animation { // No need to do anything else. return; } if (player->exiting && player->nextwaypoint == K_GetFinishLineWaypoint() && ((mapheaderinfo[gamemap - 1]->levelflags & LF_SECTIONRACE) == LF_SECTIONRACE)) { // Sprint map finish, don't give Sal's children migraines trying to pathfind out return; } // Defanging bots for testing. if (!cv_botcontrol.value) return; // Actual gameplay behaviors below this block! const botcontroller_t *botController = K_GetBotController(player->mo); if (botController != nullptr && (botController->flags & TMBOT_NOCONTROL) == TMBOT_NOCONTROL) { // Disable bot controls entirely. return; } if (player->exiting) { //Bot finish // TODO: Make bots spin around like a player would based on random chance return; } if (player->botvars.respawnconfirm >= BOTRESPAWNCONFIRM) { // We want to respawn. Simply hold brake and stop here! cmd->buttons &= ~BT_ACCELERATE|BT_DRIFT|BT_ATTACK; if (player->speed > 0) { cmd->buttons |= (BT_BRAKE); cmd->bot.respawnconfirm++; } if ((player->speed < 10*FRACUNIT)) { cmd->bot.respawnconfirm = TICRATE; } return; } else { cmd->bot.respawnconfirm = 0; } destangle = player->mo->angle; boolean forcedDir = false; if (botController != nullptr && (botController->flags & TMBOT_FORCEDIR) == TMBOT_FORCEDIR) { const fixed_t dist = DEFAULT_WAYPOINT_RADIUS * player->mo->scale; // Overwritten prediction predict = static_cast(Z_Calloc(sizeof(botprediction_t), PU_STATIC, nullptr)); predict->x = player->mo->x + FixedMul(dist, FINECOSINE(botController->forceAngle >> ANGLETOFINESHIFT)); predict->y = player->mo->y + FixedMul(dist, FINESINE(botController->forceAngle >> ANGLETOFINESHIFT)); predict->radius = (DEFAULT_WAYPOINT_RADIUS / 4) * mapobjectscale; forcedDir = true; } if (forcedDir == true) { destangle = R_PointToAngle2(player->mo->x, player->mo->y, predict->x, predict->y); turnamt = K_HandleBotTrack(player, cmd, predict, destangle); } else if (leveltime <= starttime) { UINT8 timing = P_RandomRange(0, 5); UINT8 finaltiming = (MAXBOTDIFFICULTY/2)-(player->botvars.difficulty/2)+timing; if (player->botvars.difficulty > 4) { if (leveltime >= starttime-TICRATE-TICRATE/7+finaltiming) { cmd->buttons |= BT_ACCELERATE; cmd->forwardmove = MAXPLMOVE; } } } else { // Handle steering towards waypoints! if (predict == nullptr) { // Create a prediction. predict = K_CreateBotPrediction(player); } if (predict != nullptr) { K_NudgePredictionTowardsObjects(predict, player); destangle = R_PointToAngle2(player->mo->x, player->mo->y, predict->x, predict->y); turnamt = K_HandleBotTrack(player, cmd, predict, destangle); } } if (player->exiting == 0) { // TODO: Allowing projectile items like orbinaut while e-braking would be nice, maybe just pass in the spindash variable? t = I_GetPreciseTime(); K_BotItemUsage(player, cmd, turnamt); ps_bots[player - players].item = I_GetPreciseTime() - t; } if (turnamt != 0) { if (turnamt > KART_FULLTURN) { turnamt = KART_FULLTURN; } else if (turnamt < -KART_FULLTURN) { turnamt = -KART_FULLTURN; } if (turnamt > 0) { // Count up if (player->botvars.turnconfirm < BOTTURNCONFIRM) { cmd->bot.turnconfirm++; } } else if (turnamt < 0) { // Count down if (player->botvars.turnconfirm > -BOTTURNCONFIRM) { cmd->bot.turnconfirm--; } } else { // Back to neutral if (player->botvars.turnconfirm < 0) { cmd->bot.turnconfirm++; } else if (player->botvars.turnconfirm > 0) { cmd->bot.turnconfirm--; } } if (abs(player->botvars.turnconfirm) >= BOTTURNCONFIRM) { // You're commiting to your turn, you're allowed! cmd->turning = turnamt; } } // Free the prediction we made earlier if (predict != nullptr) { if (cv_kartdebugbot.value != 0 && player - players == displayplayers[0] && !(paused || P_AutoPause())) { K_DrawPredictionDebug(predict, player); } } } /*-------------------------------------------------- void K_BuildBotTiccmd(player_t *player, ticcmd_t *cmd) See header file for description. --------------------------------------------------*/ void K_BuildBotTiccmd( player_t *player, // annoyingly NOT const because of LUA_HookTiccmd... grumble grumble ticcmd_t *cmd) { ZoneScoped; // Remove any existing controls memset(cmd, 0, sizeof(ticcmd_t)); if (player->mo == nullptr || player->spectator == true || G_GamestateUsesLevel() == false) { // Not in the level. return; } // Complete override of all ticcmd functionality. // May add more hooks to individual pieces of bot ticcmd, // but this should always be here so anyone can roll // their own :) if (LUA_HookTiccmd(player, cmd, HOOK(BotTiccmd)) == true) { cmd->flags |= TICCMD_BOT; return; } cmd->flags |= TICCMD_BOT; switch (player->botvars.style) { case BOT_STYLE_STAY: { // Hey, this one's pretty easy :P break; } default: { K_BuildBotTiccmdNormal(player, cmd); break; } } } static void K_IncrementBotRespawn(player_t *player, UINT32 *respawn, const UINT32 respawnmax) { const fixed_t requireDist = (12*player->mo->scale) / FRACUNIT; INT32 progress = player->distancetofinishprev - player->distancetofinish; boolean exceptions = ( (leveltime < starttime) || player->flashing != 0 || player->spinouttimer != 0 || player->airtime > 3*TICRATE/2 || (player->justbumped > 0 && player->justbumped < bumptime-1) ); if (!exceptions && (progress < requireDist)) { if (*respawn < respawnmax) { // Making no progress, start counting against you. *respawn = *respawn + 1; if (progress < -requireDist && *respawn < respawnmax) { // Making NEGATIVE progress? Start counting even harder. *respawn = *respawn + 1; } } } else if (*respawn > 0) { // Playing normally. *respawn = *respawn - 1; } } /*-------------------------------------------------- void K_UpdateBotGameplayVars(player_t *player); See header file for description. --------------------------------------------------*/ void K_UpdateBotGameplayVars(player_t *player) { if (gamestate != GS_LEVEL || !player->mo) { // Not in the level. return; } player->botvars.rubberband = K_UpdateRubberband(player); player->botvars.turnconfirm += player->cmd.bot.turnconfirm; // Is a bot not making any progress? Kill it and respawn at next waypoint. K_IncrementBotRespawn(player, &player->botvars.respawnconfirm, BOTRESPAWNCONFIRM); if ((player->cmd.bot.respawnconfirm >= TICRATE) && (player->botvars.respawnconfirm >= BOTRESPAWNCONFIRM)) { // Now a clean function! Neat, eh? K_SetRespawnAtNextWaypoint(player); // WHAT ARE YOU DOING??? RACE ALREADY! P_DamageMobj(player->mo, NULL, NULL, 1, DMG_INSTAKILL); player->botvars.respawnconfirm = 0; } else if (player->cmd.forwardmove < 0) { // stop drifting if we're reversing K_BotSetDriftState(player, DRIFTSTATE_AUTO, BOTDRIFTLOCKOUT); } else { // Figure out if we need to drift. // Drift-ending waypoints will kill the drift timer, // so no need to worry about doing that ourselves. K_BotStartDrift(player); INT32 limit = FixedInt(cv_test3.value) - gamespeed*5; INT32 dtime = ++player->botvars.drifttime; // the faster we are going, the sooner we need to drift fixed_t speedfactor = FixedDiv(player->speed, K_GetKartSpeed(player, false, false)); switch (player->botvars.driftstate) { case DRIFTSTATE_STARTING: limit = std::max(0, limit - FixedMul(TICRATE/5, speedfactor)); if (dtime > limit) { K_BotSetDriftState(player, DRIFTSTATE_ACTIVE, 0); } break; case DRIFTSTATE_ENDING: limit = std::max(0, limit - FixedMul(TICRATE/5, speedfactor)); if (dtime > limit) { K_BotSetDriftState(player, DRIFTSTATE_AUTO, 0); } break; default: break; } } K_UpdateBotGameplayVarsItemUsage(player); }