mirror of
https://github.com/KartKrewDev/RingRacers.git
synced 2025-10-30 08:01:28 +00:00
There were a few remaining cases of bot ticcmd generation editing player structures directly. Fix all of this and make as much of it pass const player pointers so this physically can't be allowed to happen ever again. Appears to improve bot sync in netgames & demos bot support, but I have not tested extensively.
1912 lines
48 KiB
C++
1912 lines
48 KiB
C++
// DR. ROBOTNIK'S RING RACERS
|
|
//-----------------------------------------------------------------------------
|
|
// Copyright (C) by Sally "TehRealSalt" Cochenour
|
|
// Copyright (C) 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 <algorithm>
|
|
|
|
#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 "k_race.h" // finishBeamLine
|
|
#include "m_perfstats.h"
|
|
#include "k_podium.h"
|
|
#include "k_respawn.h"
|
|
#include "m_easing.h"
|
|
#include "d_clisrv.h"
|
|
#include "g_party.h"
|
|
#include "k_grandprix.h" // K_CanChangeRules
|
|
#include "hu_stuff.h" // HU_AddChatText
|
|
#include "discord.h" // DRPC_UpdatePresence
|
|
#include "i_net.h" // doomcom
|
|
|
|
/*--------------------------------------------------
|
|
void K_SetBot(UINT8 playerNum, UINT8 skinnum, UINT8 difficulty, botStyle_e style)
|
|
|
|
See header file for description.
|
|
--------------------------------------------------*/
|
|
void K_SetBot(UINT8 newplayernum, UINT8 skinnum, UINT8 difficulty, botStyle_e style)
|
|
{
|
|
CONS_Debug(DBG_NETPLAY, "addbot: %d\n", newplayernum);
|
|
|
|
G_AddPlayer(newplayernum);
|
|
|
|
if (newplayernum+1 > doomcom->numslots)
|
|
doomcom->numslots = (INT16)(newplayernum+1);
|
|
|
|
playernode[newplayernum] = servernode;
|
|
|
|
// this will permit unlocks
|
|
memcpy(&players[newplayernum].availabilities, R_GetSkinAvailabilities(false, skinnum), MAXAVAILABILITY*sizeof(UINT8));
|
|
|
|
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 && K_BotDefaultSpectator();
|
|
|
|
skincolornum_t color = static_cast<skincolornum_t>(skins[skinnum].prefcolor);
|
|
const char *realname = skins[skinnum].realname;
|
|
if (tutorialchallenge == TUTORIALSKIP_INPROGRESS)
|
|
{
|
|
// The ROYGBIV Rangers
|
|
switch (newplayernum)
|
|
{
|
|
case 1:
|
|
color = SKINCOLOR_RED;
|
|
realname = "Champ";
|
|
break;
|
|
case 2:
|
|
color = SKINCOLOR_ORANGE;
|
|
realname = "Pharaoh";
|
|
break;
|
|
case 3:
|
|
color = SKINCOLOR_YELLOW;
|
|
realname = "Caesar";
|
|
break;
|
|
case 4:
|
|
color = SKINCOLOR_GREEN;
|
|
realname = "General";
|
|
break;
|
|
case 5:
|
|
color = SKINCOLOR_CYAN; // blue (lighter than _BLUE)
|
|
realname = "Shogun";
|
|
break;
|
|
case 6:
|
|
color = SKINCOLOR_BLUEBERRY; // indigo
|
|
realname = "Emperor";
|
|
break;
|
|
case 7:
|
|
color = SKINCOLOR_VIOLET;
|
|
realname = "King";
|
|
break;
|
|
default:
|
|
color = SKINCOLOR_BLACK;
|
|
realname = "Vizier"; // working in the shadows
|
|
break;
|
|
}
|
|
}
|
|
players[newplayernum].skincolor = color;
|
|
sprintf(player_names[newplayernum], "%s", realname);
|
|
|
|
SetPlayerSkinByNum(newplayernum, skinnum);
|
|
|
|
playerconsole[newplayernum] = newplayernum;
|
|
G_BuildLocalSplitscreenParty(newplayernum);
|
|
|
|
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(UINT8 skin, UINT8 difficulty, botStyle_e style, UINT8 *p)
|
|
|
|
See header file for description.
|
|
--------------------------------------------------*/
|
|
boolean K_AddBot(UINT8 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 UINT8 defaultbotskin = R_BotDefaultSkin();
|
|
UINT8 difficulty;
|
|
UINT8 pmax = (dedicated ? MAXPLAYERS-1 : MAXPLAYERS);
|
|
UINT8 numplayers = 0;
|
|
UINT8 numbots = 0;
|
|
UINT8 numwaiting = 0;
|
|
SINT8 wantedbots = 0;
|
|
UINT8 usableskins = 0;
|
|
UINT8 grabskins[MAXSKINS+1];
|
|
UINT8 i;
|
|
|
|
// Init usable bot skins list
|
|
for (i = 0; i < numskins; i++)
|
|
{
|
|
grabskins[usableskins++] = i;
|
|
}
|
|
grabskins[usableskins] = MAXSKINS;
|
|
|
|
if ((gametyperules & GTR_BOTS) == 0)
|
|
{
|
|
difficulty = 0;
|
|
}
|
|
else if (tutorialchallenge == TUTORIALSKIP_INPROGRESS)
|
|
{
|
|
pmax = 8; // can you believe this is a nerf
|
|
difficulty = MAXBOTDIFFICULTY;
|
|
}
|
|
else if (K_CanChangeRules(true) == false)
|
|
{
|
|
difficulty = 0;
|
|
}
|
|
else
|
|
{
|
|
difficulty = cv_kartbot.value;
|
|
pmax = std::min<UINT8>(pmax, static_cast<UINT8>(cv_maxconnections.value));
|
|
if (cv_maxplayers.value > 0)
|
|
{
|
|
pmax = std::min<UINT8>(pmax, static_cast<UINT8>(cv_maxplayers.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;
|
|
}
|
|
}
|
|
|
|
if (numbots < wantedbots)
|
|
{
|
|
// We require MORE bots!
|
|
UINT8 newplayernum = 0;
|
|
|
|
if (dedicated)
|
|
{
|
|
newplayernum = 1;
|
|
}
|
|
|
|
// Rearrange usable bot skins list to prevent gaps for randomised selection
|
|
if (tutorialchallenge == TUTORIALSKIP_INPROGRESS)
|
|
{
|
|
usableskins = 0; // force a crack team of Eggrobo
|
|
}
|
|
else for (i = 0; i < usableskins; i++)
|
|
{
|
|
if (!(grabskins[i] == MAXSKINS || !R_SkinUsable(-1, grabskins[i], true)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
while (usableskins > i && (grabskins[usableskins] == MAXSKINS || !R_SkinUsable(-1, grabskins[usableskins], true)))
|
|
{
|
|
usableskins--;
|
|
}
|
|
|
|
grabskins[i] = grabskins[usableskins];
|
|
grabskins[usableskins] = MAXSKINS;
|
|
}
|
|
|
|
while (numbots < wantedbots)
|
|
{
|
|
UINT8 skinnum = defaultbotskin;
|
|
|
|
if (usableskins > 0)
|
|
{
|
|
UINT8 index = P_RandomKey(PR_BOTS, usableskins);
|
|
skinnum = grabskins[index];
|
|
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)
|
|
{
|
|
i = MAXPLAYERS;
|
|
while (numbots > wantedbots && i > 0)
|
|
{
|
|
i--;
|
|
|
|
if (playeringame[i] && players[i].bot)
|
|
{
|
|
CL_RemovePlayer(i, KR_LEAVE);
|
|
numbots--;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 (K_PodiumSequence() == true)
|
|
return true;
|
|
|
|
if (player->exiting)
|
|
return true;
|
|
|
|
if (player->bot)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/*--------------------------------------------------
|
|
boolean K_BotCanTakeCut(player_t *player)
|
|
|
|
See header file for description.
|
|
--------------------------------------------------*/
|
|
boolean K_BotCanTakeCut(const player_t *player)
|
|
{
|
|
if (
|
|
#if 1
|
|
K_TripwirePassConditions(player) != TRIPWIRE_NONE
|
|
#else
|
|
K_ApplyOffroad(player) == false
|
|
#endif
|
|
|| 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 (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(const 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->trick != 0 || 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;
|
|
constexpr INT32 modifier_max = FRACUNIT * 2;
|
|
|
|
const fixed_t complexity_value = std::clamp<fixed_t>(
|
|
FixedDiv(K_GetTrackComplexity(), complexity_scale),
|
|
-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].bot)
|
|
{
|
|
// 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)
|
|
{
|
|
constexpr fixed_t rubberdeltabase = FRACUNIT / 4; // +/- x0.25
|
|
|
|
// Lv. 1: x0.50 avg
|
|
// Lv. 9: x1.50 avg
|
|
const fixed_t difficultyEase = ((player->botvars.difficulty - 1) * FRACUNIT) / (DIFFICULTBOT - 1);
|
|
const fixed_t rubberavg = Easing_Linear(difficultyEase, FRACUNIT / 2, FRACUNIT * 3 / 2);
|
|
|
|
// Lv. 1: x0.35 min
|
|
// Lv. 9: x1.35 min
|
|
const fixed_t rubberdeltamin = FixedMul(rubberdeltabase, K_BotMapModifier());
|
|
const fixed_t rubbermin = std::max<fixed_t>(rubberavg - rubberdeltamin, FRACUNIT/3);
|
|
|
|
// Lv. 1: x0.65 max
|
|
// Lv. 9: x1.65 max
|
|
const fixed_t rubberdeltamax = FixedMul(rubberdeltabase, K_BotMapModifier());
|
|
const fixed_t rubbermax = std::min<fixed_t>(rubberavg - rubberdeltamax, FRACUNIT*3);
|
|
|
|
fixed_t rubberband = FRACUNIT >> 1;
|
|
player_t *firstplace = nullptr;
|
|
size_t i = SIZE_MAX;
|
|
|
|
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;
|
|
}
|
|
|
|
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 fixed_t spacing = FixedDiv(2560 * 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 (rubberband > FRACUNIT)
|
|
{
|
|
rubberband = FRACUNIT;
|
|
}
|
|
else if (rubberband < 0)
|
|
{
|
|
rubberband = 0;
|
|
}
|
|
}
|
|
|
|
return Easing_Linear(rubberband, rubbermin, rubbermax);
|
|
}
|
|
|
|
/*--------------------------------------------------
|
|
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<angle_t>(delta, AngleDelta(nextAngle, prevAngle));
|
|
}
|
|
}
|
|
|
|
if (delta > maxDelta)
|
|
{
|
|
delta = maxDelta;
|
|
}
|
|
|
|
reduce = FixedDiv(delta, maxDelta);
|
|
reduce = FRACUNIT + FixedMul(reduce, maxReduce - FRACUNIT);
|
|
|
|
*smallestRadius = std::min<fixed_t>(*smallestRadius, radius);
|
|
*smallestScaled = std::min<fixed_t>(*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(const player_t *player)
|
|
{
|
|
const precise_t time = I_GetPreciseTime();
|
|
|
|
// Stair janking makes it harder to steer, so attempt to steer harder.
|
|
const UINT8 jankDiv = (player->stairjank > 0) ? 4 : 1;
|
|
|
|
const INT16 handling = K_GetKartTurnValue(player, KART_FULLTURN) / jankDiv; // Reduce prediction based on how fast you can turn
|
|
|
|
const tic_t futuresight = (TICRATE * KART_FULLTURN) / std::max<INT16>(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<INT32>(((speed / FRACUNIT) * static_cast<INT32>(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<botprediction_t *>(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<fixed_t>(disttonext, distanceleft) * FRACUNIT);
|
|
predict->y += P_ReturnThrustY(nullptr, angletonext, std::min<fixed_t>(disttonext, distanceleft) * FRACUNIT);
|
|
}
|
|
|
|
ps_bots[player - players].prediction += I_GetPreciseTime() - time;
|
|
return predict;
|
|
}
|
|
|
|
/*--------------------------------------------------
|
|
static UINT8 K_TrySpindash(const player_t *player, ticcmd_t *cmd)
|
|
|
|
Determines conditions where the bot should attempt to spindash.
|
|
|
|
Input Arguments:-
|
|
player - Bot player to check.
|
|
|
|
Return:-
|
|
0 to make the bot drive normally, 1 to e-brake, 2 to e-brake & charge spindash.
|
|
(TODO: make this an enum)
|
|
--------------------------------------------------*/
|
|
static UINT8 K_TrySpindash(const player_t *player, ticcmd_t *cmd)
|
|
{
|
|
const tic_t difficultyModifier = (TICRATE/6);
|
|
|
|
const fixed_t oldSpeed = R_PointToDist2(0, 0, player->rmomx, player->rmomy);
|
|
const fixed_t baseAccel = K_GetNewSpeed(player) - oldSpeed;
|
|
const fixed_t speedDiff = player->speed - player->lastspeed;
|
|
|
|
const INT32 angleDiff = AngleDelta(player->mo->angle, K_MomentumAngleReal(player->mo));
|
|
|
|
if (player->spindashboost || player->tiregrease // You just released a spindash, you don't need to try again yet, jeez.
|
|
|| P_IsObjectOnGround(player->mo) == false) // Not in a state where we want 'em to spindash.
|
|
{
|
|
cmd->bot.spindashconfirm = 0;
|
|
return 0;
|
|
}
|
|
|
|
// Try "start boosts" first
|
|
if (leveltime == starttime)
|
|
{
|
|
// Forces them to release, even if they haven't fully charged.
|
|
// Don't want them to keep charging if they didn't have time to.
|
|
return 0;
|
|
}
|
|
|
|
if (leveltime < starttime)
|
|
{
|
|
INT32 boosthold = starttime - K_GetSpindashChargeTime(player);
|
|
|
|
boosthold -= (DIFFICULTBOT - std::min<UINT8>(DIFFICULTBOT, player->botvars.difficulty)) * difficultyModifier;
|
|
|
|
if (leveltime >= (unsigned)boosthold)
|
|
{
|
|
// Start charging...
|
|
return 2;
|
|
}
|
|
else
|
|
{
|
|
// Just hold your ground and e-brake.
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (player->botvars.spindashconfirm >= BOTSPINDASHCONFIRM)
|
|
{
|
|
INT32 chargingPoint = (K_GetSpindashChargeTime(player) + difficultyModifier);
|
|
|
|
// Release quicker the higher the difficulty is.
|
|
// Sounds counter-productive, but that's actually the best strategy after the race has started.
|
|
chargingPoint -= std::min<UINT8>(DIFFICULTBOT, player->botvars.difficulty) * difficultyModifier;
|
|
|
|
if (player->spindash > chargingPoint)
|
|
{
|
|
// Time to release.
|
|
return 0;
|
|
}
|
|
|
|
return 2;
|
|
}
|
|
else
|
|
{
|
|
// Logic for normal racing.
|
|
boolean anyCondition = false;
|
|
boolean uphill = false;
|
|
|
|
#define AddForCondition(x) \
|
|
if (x) \
|
|
{ \
|
|
anyCondition = true;\
|
|
if (player->botvars.spindashconfirm < BOTSPINDASHCONFIRM) \
|
|
{ \
|
|
cmd->bot.spindashconfirm++; \
|
|
} \
|
|
}
|
|
|
|
if (K_SlopeResistance(player) == false && player->mo->standingslope != nullptr)
|
|
{
|
|
const pslope_t *slope = player->mo->standingslope;
|
|
|
|
if ((slope->flags & SL_NOPHYSICS) == 0 && abs(slope->zdelta) >= FRACUNIT/21)
|
|
{
|
|
const fixed_t speedPercent = FixedDiv(player->speed, 20 * player->mo->scale);
|
|
fixed_t slopeDot = 0;
|
|
angle_t angle = K_MomentumAngle(player->mo) - slope->xydirection;
|
|
|
|
if (P_MobjFlip(player->mo) * slope->zdelta < 0)
|
|
{
|
|
angle ^= ANGLE_180;
|
|
}
|
|
|
|
slopeDot = FINECOSINE(angle >> ANGLETOFINESHIFT);
|
|
uphill = ((slopeDot + (speedPercent / 2)) < -FRACUNIT/2);
|
|
}
|
|
}
|
|
|
|
constexpr fixed_t minimum_offroad = (3 << FRACBITS) >> 1; // Do not spindash in weak offroad
|
|
AddForCondition(K_ApplyOffroad(player) == true && player->offroad > minimum_offroad); // Slowed by offroad
|
|
AddForCondition(speedDiff < (baseAccel >> 3)); // Accelerating slower than expected
|
|
AddForCondition(angleDiff > ANG60); // Being pushed backwards
|
|
AddForCondition(uphill == true); // Going up a steep slope without speed
|
|
|
|
if (player->cmomx || player->cmomy)
|
|
{
|
|
angle_t cAngle = R_PointToDist2(0, 0, player->cmomx, player->cmomy);
|
|
angle_t cDelta = AngleDelta(player->mo->angle, cAngle);
|
|
|
|
AddForCondition(cDelta > ANGLE_90); // Conveyor going against you
|
|
}
|
|
|
|
if (anyCondition == false)
|
|
{
|
|
if (player->botvars.spindashconfirm > 0)
|
|
{
|
|
cmd->bot.spindashconfirm--;
|
|
}
|
|
}
|
|
}
|
|
|
|
// We're doing just fine, we don't need to spindash, thanks.
|
|
return 0;
|
|
}
|
|
|
|
/*--------------------------------------------------
|
|
static boolean K_TryRingShooter(const player_t *player)
|
|
|
|
Determines conditions where the bot should attempt to respawn.
|
|
|
|
Input Arguments:-
|
|
player - Bot player to check.
|
|
|
|
Return:-
|
|
true if we want to hold the respawn button, otherwise false.
|
|
--------------------------------------------------*/
|
|
static boolean K_TryRingShooter(const player_t *player)
|
|
{
|
|
if (player->respawn.state != RESPAWNST_NONE)
|
|
{
|
|
// We're already respawning!
|
|
return false;
|
|
}
|
|
|
|
if (player->exiting)
|
|
{
|
|
// Where are you trying to go?
|
|
return false;
|
|
}
|
|
|
|
if ((gametyperules & GTR_CIRCUIT) == 0 || (leveltime <= starttime))
|
|
{
|
|
// Only do this during a Race that has started.
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*--------------------------------------------------
|
|
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;
|
|
}
|
|
}
|
|
|
|
/*--------------------------------------------------
|
|
static void K_BotTrick(const player_t *player, ticcmd_t *cmd, const botcontroller_t *botController)
|
|
|
|
Determines inputs for trick panels.
|
|
|
|
Input Arguments:-
|
|
player - Player to generate the ticcmd for.
|
|
cmd - The player's ticcmd to modify.
|
|
botController - Bot controller struct.
|
|
|
|
Return:-
|
|
None
|
|
--------------------------------------------------*/
|
|
static void K_BotTrick(const player_t *player, ticcmd_t *cmd, const botcontroller_t *botController)
|
|
{
|
|
// Trick panel state -- do nothing until a controller line is found, in which case do a trick.
|
|
if (botController == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (player->trickpanel == TRICKSTATE_READY)
|
|
{
|
|
switch (botController->trick)
|
|
{
|
|
case TMBOTTR_LEFT:
|
|
cmd->turning = KART_FULLTURN;
|
|
break;
|
|
case TMBOTTR_RIGHT:
|
|
cmd->turning = -KART_FULLTURN;
|
|
break;
|
|
case TMBOTTR_UP:
|
|
cmd->throwdir = KART_FULLTURN;
|
|
break;
|
|
case TMBOTTR_DOWN:
|
|
cmd->throwdir = -KART_FULLTURN;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*--------------------------------------------------
|
|
static angle_t K_BotSmoothLanding(const player_t *player, angle_t destangle)
|
|
|
|
Calculates a new destination angle while in the air,
|
|
to be able to successfully smooth land.
|
|
|
|
Input Arguments:-
|
|
player - Bot player to check.
|
|
destangle - Previous destination angle.
|
|
|
|
Return:-
|
|
New destination angle.
|
|
--------------------------------------------------*/
|
|
static angle_t K_BotSmoothLanding(const player_t *player, angle_t destangle)
|
|
{
|
|
angle_t newAngle = destangle;
|
|
boolean air = !P_IsObjectOnGround(player->mo);
|
|
angle_t steepVal = air ? STUMBLE_STEEP_VAL_AIR : STUMBLE_STEEP_VAL;
|
|
angle_t slopeSteep = std::max<angle_t>(AngleDelta(player->mo->pitch, 0), AngleDelta(player->mo->roll, 0));
|
|
|
|
if (slopeSteep > steepVal)
|
|
{
|
|
fixed_t pitchMul = -FINESINE(destangle >> ANGLETOFINESHIFT);
|
|
fixed_t rollMul = FINECOSINE(destangle >> ANGLETOFINESHIFT);
|
|
angle_t testAngles[2];
|
|
angle_t testDeltas[2];
|
|
UINT8 i;
|
|
|
|
testAngles[0] = R_PointToAngle2(0, 0, rollMul, pitchMul);
|
|
testAngles[1] = R_PointToAngle2(0, 0, -rollMul, -pitchMul);
|
|
|
|
for (i = 0; i < 2; i++)
|
|
{
|
|
testDeltas[i] = AngleDelta(testAngles[i], destangle);
|
|
}
|
|
|
|
if (testDeltas[1] < testDeltas[0])
|
|
{
|
|
return testAngles[1];
|
|
}
|
|
else
|
|
{
|
|
return testAngles[0];
|
|
}
|
|
}
|
|
|
|
return newAngle;
|
|
}
|
|
|
|
/*--------------------------------------------------
|
|
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(const player_t *player, ticcmd_t *cmd, botprediction_t *predict, angle_t destangle)
|
|
{
|
|
// Handle steering towards waypoints!
|
|
INT32 turnamt = 0;
|
|
SINT8 turnsign = 0;
|
|
angle_t moveangle;
|
|
INT32 anglediff;
|
|
|
|
I_Assert(predict != nullptr);
|
|
|
|
destangle = K_BotSmoothLanding(player, destangle);
|
|
|
|
moveangle = player->mo->angle;
|
|
anglediff = AngleDeltaSigned(moveangle, destangle);
|
|
|
|
if (anglediff < 0)
|
|
{
|
|
turnsign = 1;
|
|
}
|
|
else
|
|
{
|
|
turnsign = -1;
|
|
}
|
|
|
|
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<angle_t>(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)
|
|
{
|
|
// Going the right way, don't turn at all.
|
|
turnamt = 0;
|
|
}
|
|
}
|
|
|
|
return turnamt;
|
|
}
|
|
|
|
/*--------------------------------------------------
|
|
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(const player_t *player, ticcmd_t *cmd, botprediction_t *predict, angle_t destangle)
|
|
{
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
destangle = K_BotSmoothLanding(player, destangle);
|
|
|
|
// 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_BotPodiumTurning(const player_t *player, ticcmd_t *cmd)
|
|
|
|
Calculates bot turning for the podium cutscene.
|
|
--------------------------------------------------*/
|
|
static void K_BotPodiumTurning(const player_t *player, ticcmd_t *cmd)
|
|
{
|
|
const angle_t destAngle = R_PointToAngle2(
|
|
player->mo->x, player->mo->y,
|
|
player->currentwaypoint->mobj->x, player->currentwaypoint->mobj->y
|
|
);
|
|
const INT32 delta = AngleDeltaSigned(destAngle, player->mo->angle);
|
|
const INT16 handling = K_GetKartTurnValue(player, KART_FULLTURN);
|
|
fixed_t mul = FixedDiv(delta, (angle_t)(handling << TICCMD_REDUCE));
|
|
|
|
if (mul > FRACUNIT)
|
|
{
|
|
mul = FRACUNIT;
|
|
}
|
|
|
|
if (mul < -FRACUNIT)
|
|
{
|
|
mul = -FRACUNIT;
|
|
}
|
|
|
|
cmd->turning = FixedMul(mul, KART_FULLTURN);
|
|
}
|
|
|
|
/*--------------------------------------------------
|
|
static void K_BuildBotPodiumTiccmd(const player_t *player, ticcmd_t *cmd)
|
|
|
|
Calculates all bot movement for the podium cutscene.
|
|
--------------------------------------------------*/
|
|
static void K_BuildBotPodiumTiccmd(const player_t *player, ticcmd_t *cmd)
|
|
{
|
|
if (player->currentwaypoint == nullptr)
|
|
{
|
|
// We've reached the end of our path.
|
|
// Simply stop moving.
|
|
return;
|
|
}
|
|
|
|
if (K_GetWaypointIsSpawnpoint(player->currentwaypoint) == false)
|
|
{
|
|
// Hacky flag reuse: slow down before reaching your podium stand.
|
|
cmd->forwardmove = MAXPLMOVE * 3 / 4;
|
|
}
|
|
else
|
|
{
|
|
cmd->forwardmove = MAXPLMOVE;
|
|
}
|
|
|
|
cmd->buttons |= BT_ACCELERATE;
|
|
|
|
K_BotPodiumTurning(player, cmd);
|
|
}
|
|
|
|
/*--------------------------------------------------
|
|
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(const player_t *player, ticcmd_t *cmd)
|
|
{
|
|
precise_t t = 0;
|
|
|
|
botprediction_t *predict = nullptr;
|
|
auto predict_finally = srb2::finally([&predict]() { Z_Free(predict); });
|
|
|
|
boolean trySpindash = true;
|
|
angle_t destangle = 0;
|
|
UINT8 spindash = 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.
|
|
#ifdef DEVELOP
|
|
if (!cv_botcontrol.value)
|
|
return;
|
|
#endif
|
|
|
|
// Actual gameplay behaviors below this block!
|
|
const botcontroller_t *botController = K_GetBotController(player->mo);
|
|
if (player->trickpanel != TRICKSTATE_NONE)
|
|
{
|
|
K_BotTrick(player, cmd, botController);
|
|
|
|
// Don't do anything else.
|
|
return;
|
|
}
|
|
|
|
if (botController != nullptr && (botController->flags & TMBOT_NOCONTROL) == TMBOT_NOCONTROL)
|
|
{
|
|
// Disable bot controls entirely.
|
|
return;
|
|
}
|
|
|
|
if (K_TryRingShooter(player) == true && player->botvars.respawnconfirm >= BOTRESPAWNCONFIRM)
|
|
{
|
|
// We want to respawn. Simply hold Y and stop here!
|
|
cmd->buttons |= (BT_RESPAWN | BT_EBRAKEMASK);
|
|
return;
|
|
}
|
|
|
|
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<botprediction_t *>(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 (P_IsObjectOnGround(player->mo) == false)
|
|
{
|
|
if (player->fastfall == 0)
|
|
{
|
|
if (botController != nullptr && (botController->flags & TMBOT_FASTFALL) == TMBOT_FASTFALL)
|
|
{
|
|
// Fast fall!
|
|
cmd->buttons |= BT_EBRAKEMASK;
|
|
return;
|
|
}
|
|
}
|
|
|
|
//return; // Don't allow bots to turn in the air.
|
|
}
|
|
|
|
if (forcedDir == true)
|
|
{
|
|
destangle = R_PointToAngle2(player->mo->x, player->mo->y, predict->x, predict->y);
|
|
turnamt = K_HandleBotTrack(player, cmd, predict, destangle);
|
|
trySpindash = false;
|
|
}
|
|
else if (leveltime <= starttime && finishBeamLine != nullptr)
|
|
{
|
|
// Handle POSITION!!
|
|
const fixed_t distBase = 480*mapobjectscale;
|
|
const fixed_t distAdjust = 128*mapobjectscale;
|
|
|
|
const fixed_t closeDist = distBase + (distAdjust * (9 - player->kartweight));
|
|
const fixed_t farDist = closeDist + (distAdjust * 2);
|
|
|
|
const tic_t futureSight = (TICRATE >> 1);
|
|
|
|
fixed_t distToFinish = K_DistanceOfLineFromPoint(
|
|
finishBeamLine->v1->x, finishBeamLine->v1->y,
|
|
finishBeamLine->v2->x, finishBeamLine->v2->y,
|
|
player->mo->x, player->mo->y
|
|
) - (K_BotSpeedScaled(player, player->speed) * futureSight);
|
|
|
|
// Don't run the spindash code at all until we're in the right place
|
|
trySpindash = false;
|
|
|
|
if (distToFinish < closeDist)
|
|
{
|
|
// We're too close, we need to start backing up.
|
|
turnamt = K_HandleBotReverse(player, cmd, predict, destangle);
|
|
}
|
|
else if (distToFinish < farDist)
|
|
{
|
|
INT32 bullyTurn = INT32_MAX;
|
|
|
|
// We're in about the right place, let's do whatever we want to.
|
|
|
|
if (player->kartspeed >= 5)
|
|
{
|
|
// Faster characters want to spindash.
|
|
// Slower characters will use their momentum.
|
|
trySpindash = true;
|
|
}
|
|
|
|
// Look for characters to bully.
|
|
bullyTurn = K_PositionBully(player);
|
|
if (bullyTurn == INT32_MAX)
|
|
{
|
|
// No one to bully, just go for a spindash as anyone.
|
|
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);
|
|
}
|
|
cmd->buttons &= ~(BT_ACCELERATE|BT_BRAKE);
|
|
cmd->forwardmove = 0;
|
|
trySpindash = true;
|
|
}
|
|
else
|
|
{
|
|
turnamt = bullyTurn;
|
|
|
|
// If already spindashing, wait until we get a relatively OK charge first.
|
|
if (player->spindash == 0 || player->spindash > TICRATE)
|
|
{
|
|
trySpindash = false;
|
|
cmd->buttons |= BT_ACCELERATE;
|
|
cmd->forwardmove = MAXPLMOVE;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Too far away, we need to just drive up.
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
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 (trySpindash == true)
|
|
{
|
|
// Spindashing
|
|
spindash = K_TrySpindash(player, cmd);
|
|
|
|
if (spindash > 0)
|
|
{
|
|
cmd->buttons |= BT_EBRAKEMASK;
|
|
cmd->forwardmove = 0;
|
|
|
|
if (spindash == 2 && player->speed < 6*mapobjectscale)
|
|
{
|
|
cmd->buttons |= BT_DRIFT;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (spindash == 0 && player->exiting == 0)
|
|
{
|
|
// Don't pointlessly try to use rings/sneakers while charging a spindash.
|
|
// 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_kartdebugbots.value != 0 && player - players == displayplayers[0])
|
|
{
|
|
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)
|
|
{
|
|
// 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;
|
|
|
|
if (K_PodiumSequence() == true)
|
|
{
|
|
K_BuildBotPodiumTiccmd(player, cmd);
|
|
return;
|
|
}
|
|
|
|
switch (player->botvars.style)
|
|
{
|
|
case BOT_STYLE_STAY:
|
|
{
|
|
// Hey, this one's pretty easy :P
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
K_BuildBotTiccmdNormal(player, cmd);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*--------------------------------------------------
|
|
void K_UpdateBotGameplayVars(player_t *player);
|
|
|
|
See header file for description.
|
|
--------------------------------------------------*/
|
|
void K_UpdateBotGameplayVars(player_t *player)
|
|
{
|
|
player->botvars.rubberband = FRACUNIT;
|
|
|
|
if (gamestate != GS_LEVEL || !player->mo)
|
|
{
|
|
// Not in the level.
|
|
return;
|
|
}
|
|
|
|
player->botvars.rubberband = K_UpdateRubberband(player);
|
|
|
|
player->botvars.turnconfirm += player->cmd.bot.turnconfirm;
|
|
player->botvars.spindashconfirm += player->cmd.bot.spindashconfirm;
|
|
|
|
if (K_TryRingShooter(player) == true)
|
|
{
|
|
// Our anti-grief system is already a perfect system
|
|
// for determining if we're not making progress, so
|
|
// lets reuse it for bot respawning!
|
|
P_IncrementGriefValue(player, &player->botvars.respawnconfirm, BOTRESPAWNCONFIRM);
|
|
}
|
|
|
|
K_UpdateBotGameplayVarsItemUsage(player);
|
|
}
|