Compare commits

...

3 Commits

Author SHA1 Message Date
Marco Cawthorne 4a8f4a6082
Add a whole bunch of new bot features, including bot profiles.
Developers: the internal class `bot` is now `NSBot`, and an entityDef
of the old name will be instantiated instead.

Override defs/bot.def in your mod and make it use any custom spawnclass
you wish. Now games don't have to override `addbot` or `bot_add` inside
the multiplayer game rules.

There's also more console commands. Clients now have access to:
addBot, killAllBots, killClass [classname], killMovables, trigger [targetname], input [entnum] [input] [data], listBotProfiles, listTargets, teleport [targetname], teleportToClass [classname], respawnEntities, spawn
2024-01-26 19:27:50 -08:00
Marco Cawthorne ba4ddbd3f6
Math_FixDelta: Make recursive. Rename pending... 2024-01-26 19:10:57 -08:00
Marco Cawthorne 2332fdd45f
Add Documentation/Bots/BotOverview.md 2024-01-26 19:10:16 -08:00
21 changed files with 572 additions and 67 deletions

View File

@ -0,0 +1,55 @@
# Bots
Bots are handled by BotLib, located under `src/botlib` in the directory tree.
Nuclide's BotLib takes some inspiration from **Quake III Arena** its bots, but shares no code or specific ideas or implementations. We do not use **AAS** for navigation, we leverage the route/pathfinding system **FTEQW** provides. Bots also share some code with regular NPC/Monster type entities through the use of the NSNavAI class.
Games are allowed to handle how they want to integrate bots themselves, but for development purposes there are ways to force bots to spawn.
# Bot profiles
Nuclide has support for bot profiles, like in **Quake III Arena**. They work differently although they appear compatible by design. You can define them in a script that looks something like this and is located at `scripts/bots.txt` in your game directory:
```
{
name Albert
model zombie01
topcolor 0xeff
bottomcolor 0xff0020
}
{
name Susie
model police02
topcolor 0xff6b00
bottomcolor 0xff0b00
}
{
name Dog
funname ^4D^2o^1g
model dog01
topcolor 0x9200ff
bottomcolor 0xc800ff
}
```
Only the `name` key is required. The only other special key is `funname` which sets the nickname to be different from the internal name. The other keys are set as user info (setinfo) keys on the bot client.
This will allow games to add whatever extra keys they wish that they can then recognize anywhere in the client/server codebase. No additional networking or APIs necessary. Simply query bot properties via their userinfo.
# Bot Console Commands
## addBot [profile name]
Adds a bot to the current game.
## killAllBots
Force kills and respawns bots in the current game.
## resetAllBotsGoals
Force reset bots current trajectory and goals.
# Bot Console Variables
See `platform/cvars.cfg` under the `// bots` section.

View File

@ -881,6 +881,7 @@ INPUT = src/ \
Documentation/DedicatedServer.md \
Documentation/Filesystem.md \
Documentation/Networking.md \
Documentation/Bots/BotOverview.md \
Documentation/EntityGuide.md \
Documentation/Classes.md \
Documentation/Materials/MatOverview.md \

View File

@ -109,9 +109,6 @@ MultiplayerRules::ConsoleCommand(NSClientPlayer pp, string cmd)
tokenize(cmd);
switch (argv(0)) {
case "bot_add":
Bot_AddQuick();
break;
default:
return (0);
}
@ -129,4 +126,4 @@ Game_InitRules(void)
} else {
g_grMode = spawn(MultiplayerRules);
}
}
}

View File

@ -0,0 +1,4 @@
entityDef bot
{
spawnclass NSBot
}

View File

@ -1,5 +1,14 @@
// common
// bots
seta bot_enable 1 // Enable (1) or disable (0) usage of bots in the game.
seta bot_pause 0 // Enable (1) or disable (0) an interrupt for the Bot AIs thinking.
seta bot_noChat 0 // Enable (1) or disable (0) a suppression of any bot chatter.
seta bot_fastChat 0 // Enable (1) or disable (0) bot chatter that does not stop other inputs.
seta bot_debug 0 // Enable (1) or disable (0) bot debug features that otherwise won't work.
seta bot_developer 0 // Enable (1) or disable (0) bot debug text in console.
seta bot_minClients -1 // When set, ensures to fill the server with this many players/bots.
// common
seta com_showFPS 0 // Draws the Frames Per Second counter. Has two values: 1 - Simple 2 - Detailed
seta com_showTracers 0 // Debug display for tracelines, networking intensive.

View File

@ -41,7 +41,7 @@ typedef enum
/** Base class for the Bot AI.
*/
class bot:player
class NSBot:player
{
/* routing */
int m_iNodes;
@ -74,7 +74,7 @@ class bot:player
float m_flForceWeaponAttack;
vector m_vecForceWeaponAttackPos;
void(void) bot;
void(void) NSBot;
virtual void(botstate_t) SetState;
virtual botstate_t(void) GetState;
@ -107,7 +107,7 @@ entity Bot_AddQuick(void);
/** Applies random custom colors to the given bot entity. */
void
Bot_RandomColormap(bot target)
Bot_RandomColormap(NSBot target)
{
vector x = hsv2rgb(random() * 360, 100, 100);
float top = x[2] + (x[1] << 8) + (x[0] << 16);

View File

@ -15,44 +15,44 @@
*/
botstate_t
bot::GetState(void)
NSBot::GetState(void)
{
return m_bsState;
}
void
bot::SetState(botstate_t state)
NSBot::SetState(botstate_t state)
{
m_bsState = state;
}
botpersonality_t
bot::GetPersonality(void)
NSBot::GetPersonality(void)
{
return m_bpPersonality;
}
float
bot::GetWalkSpeed(void)
NSBot::GetWalkSpeed(void)
{
return 120;
}
float
bot::GetRunSpeed(void)
NSBot::GetRunSpeed(void)
{
return 240;
}
void
bot::RouteClear(void)
NSBot::RouteClear(void)
{
super::RouteClear();
m_flNodeGiveup = 0.0f;
}
void
bot::BrainThink(int enemyvisible, int enemydistant)
NSBot::BrainThink(int enemyvisible, int enemydistant)
{
/* we had a target and it's now dead. now what? */
if (m_eTarget) {
@ -67,7 +67,7 @@ bot::BrainThink(int enemyvisible, int enemydistant)
}
void
bot::UseButton(void)
NSBot::UseButton(void)
{
float bestDist;
func_button bestButton = __NULL__;
@ -96,7 +96,7 @@ bot::UseButton(void)
}
void
bot::SeeThink(void)
NSBot::SeeThink(void)
{
NSGameRules rules = (NSGameRules)g_grMode;
@ -163,7 +163,7 @@ bot::SeeThink(void)
}
void
bot::CheckRoute(void)
NSBot::CheckRoute(void)
{
float flDist;
vector vecEndPos;
@ -193,7 +193,7 @@ bot::CheckRoute(void)
/* we're inside the radius */
if (flDist <= flRadius) {
NSLog("^2bot::^3CheckRoute^7: " \
NSLog("^2NSBot::^3CheckRoute^7: " \
"%s reached node\n", this.targetname);
m_iCurNode--;
@ -218,7 +218,7 @@ bot::CheckRoute(void)
/* can we walk directly to our target destination? */
if (trace_fraction == 1.0) {
print("^2bot::^3CheckRoute^7: " \
print("^2NSBot::^3CheckRoute^7: " \
"Walking directly to last node\n");
m_iCurNode = -1;
}
@ -261,13 +261,13 @@ bot::CheckRoute(void)
}
void
bot::CreateObjective(void)
NSBot::CreateObjective(void)
{
RouteToPosition(Route_SelectDestination(this));
}
void
bot::RunAI(void)
NSBot::RunAI(void)
{
vector aimDir, aimPos;
bool enemyVisible, enemyDistant;
@ -294,7 +294,7 @@ bot::RunAI(void)
if (!m_iNodes && autocvar_bot_aimless == 0) {
CreateObjective();
NSLog("bot::RunAI: %s is calculating first bot route",
NSLog("NSBot::RunAI: %s is calculating first bot route",
this.netname);
/* our route probably has not been processed yet */
@ -500,12 +500,12 @@ bot::RunAI(void)
}
void
bot::PreFrame(void)
NSBot::PreFrame(void)
{
}
void
bot::PostFrame(void)
NSBot::PostFrame(void)
{
#ifndef NEW_INVENTORY
/* we've picked something new up */
@ -518,7 +518,7 @@ bot::PostFrame(void)
}
void
bot::SetName(string nickname)
NSBot::SetName(string nickname)
{
if (autocvar_bot_prefix)
forceinfokey(this, "name", sprintf("%s %s", autocvar_bot_prefix, nickname));
@ -527,9 +527,66 @@ bot::SetName(string nickname)
}
void
bot::bot(void)
NSBot::NSBot(void)
{
classname = "player";
targetname = "_nuclide_bot_";
forceinfokey(this, "*bot", "1");
}
void
Bot_KickRandom(void)
{
for (entity e = world;(e = find(e, ::classname, "player"));) {
if (clienttype(e) == CLIENTTYPE_BOT) {
dropclient(e);
return;
}
}
}
void
bot_spawner_think(void)
{
int clientCount = 0i;
int botCount = 0i;
int minClientsCvar = (int)cvar("bot_minClients");
/* if -1, we are not managing _anything_ */
if (minClientsCvar == -1) {
self.nextthink = time + 5.0f;
return;
}
/* count total clients + bots */
for (entity e = world;(e = find(e, ::classname, "player"));) {
if (clienttype(e) == CLIENTTYPE_BOT) {
clientCount++;
botCount++;
}else if (clienttype(e) == CLIENTTYPE_REAL) {
clientCount++;
}
}
/* add remove as necessary */
if (clientCount < cvar("bot_minClients")) {
BotProfile_AddRandom();
} else if (clientCount > cvar("bot_minClients")) {
if (botCount > 0i) {
Bot_KickRandom();
}
}
self.nextthink = time + 1.0f;
}
void
BotLib_Init(void)
{
BotProfile_Init();
/* this spawner manages the active bots */
entity bot_spawner = spawn();
bot_spawner.think = bot_spawner_think;
bot_spawner.nextthink = time + 1.0f;
}

View File

@ -15,13 +15,13 @@
*/
void
bot::ChatSay(string msg)
NSBot::ChatSay(string msg)
{
g_grMode.ChatMessageAll(this, msg);
}
void
bot::ChatSayTeam(string msg)
NSBot::ChatSayTeam(string msg)
{
g_grMode.ChatMessageTeam(this, msg);
}

View File

@ -15,7 +15,7 @@
*/
void
bot::Pain(void)
NSBot::Pain(void)
{
NSGameRules rules = g_grMode;
@ -44,7 +44,7 @@ bot::Pain(void)
}
void
bot::SetEnemy(entity en)
NSBot::SetEnemy(entity en)
{
m_eTarget = en;
@ -56,7 +56,7 @@ bot::SetEnemy(entity en)
}
void
bot::WeaponThink(void)
NSBot::WeaponThink(void)
{
int r = Weapons_IsEmpty(this, activeweapon);
@ -74,7 +74,7 @@ bot::WeaponThink(void)
}
void
bot::WeaponAttack(void)
NSBot::WeaponAttack(void)
{
bool shouldAttack = false;
@ -130,7 +130,7 @@ bot::WeaponAttack(void)
}
void
bot::ForceWeaponAttack(vector attackPos, float attackTime)
NSBot::ForceWeaponAttack(vector attackPos, float attackTime)
{
m_vecForceWeaponAttackPos = attackPos;
m_flForceWeaponAttack = attackTime + time;
@ -153,7 +153,7 @@ BotLib_Alert(vector pos, float radius, float t)
if (vlen(pos - w.origin) > radius)
continue;
bot f = (bot) w;
NSBot f = (NSBot) w;
/* they already got a target of some kind */
if (f.m_eTarget)

View File

@ -61,3 +61,30 @@ Bot_AddQuick(void)
self = oself;
return (newbot);
}
void
Bot_KillAllBots(void)
{
entity oldSelf = self;
for ( other = world; ( other = find( other, classname, "player" ) ); ) {
if ( clienttype( other ) == CLIENTTYPE_BOT ) {
self = other;
ClientKill();
}
}
self = oldSelf;
}
void
Bot_ResetAllBotsGoals(void)
{
for ( other = world; ( other = find( other, classname, "player" ) ); ) {
if ( clienttype( other ) == CLIENTTYPE_BOT ) {
NSBot theBot = (NSBot)other;
theBot.SetEnemy(__NULL__);
theBot.RouteClear();
}
}
}

View File

@ -17,6 +17,7 @@
#include "bot.h"
#include "botinfo.h"
#include "cvar.h"
#include "profiles.h"
vector Route_SelectDestination( bot target );
vector Route_SelectDestination( NSBot target );

View File

@ -2,6 +2,7 @@
#includelist
defs.h
profiles.qc
bot.qc
bot_chat.qc
bot_combat.qc

42
src/botlib/profiles.h Normal file
View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2023 Vera Visions LLC.
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/* BotScript
script/bots.txt
Listing of various bot profiles
where infokeys can be set and interpreted
by the game-logic at will.
The `name` keys has to _always_ be present.
The `funname` key is optional.
Name acts as both an identifier as well
as a nickname when `funname` is not present.
Anything else is considered to be extra.
*/
typedef struct
{
string m_strName;
string m_strNetName;
string m_strExtra;
} botScript_t;
#define BOTSCRIPT_MAX 32
botScript_t g_bots[BOTSCRIPT_MAX];
var int g_botScriptCount;

211
src/botlib/profiles.qc Normal file
View File

@ -0,0 +1,211 @@
/*
* Copyright (c) 2023 Vera Visions LLC.
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
bool
Bot_ExistsInServer(string botName)
{
for (entity e = world;(e = find(e, ::classname, "player"));) {
if (clienttype(e) == CLIENTTYPE_BOT) {
if (e.netname == botName) {
return (true);
}
}
}
return (false);
}
bool
Bot_AddBot_f(string botName)
{
int extraCount = 0i;
int foundID = -1i;
entity oldSelf;
NSBot newBot;
int i = 0i;
if (!botName) {
print("Usage: Addbot <botname> [skill 1-5] [team] [msec delay] [altname]\n");
return (false);
}
if (!g_nodes_present) {
print("^1BotScript_Add^7: Can't add bot. No waypoints.\n");
return (false);
}
/* grab the right profile id */
for (i = 0i; i < g_botScriptCount; i++) {
if (g_bots[i].m_strName == botName) {
foundID = i;
}
}
if (foundID == -1i) {
print("^1BotScript_Add^7: Named profile not found.\n");
return (false);
}
oldSelf = self;
self = spawnclient();
if (!self) {
print("^1BotScript_Add^7: Can't add bot. Server is full\n");
self = oldSelf;
return (false);
}
newBot = (NSBot)self;
newBot.SetInfoKey("name", g_bots[foundID].m_strNetName);
extraCount = tokenize(g_bots[foundID].m_strExtra);
for (i = 0i; i < extraCount; i+=2) {
newBot.SetInfoKey(argv(i), argv(i+1));
}
ClientConnect();
PutClientInServer();
self = oldSelf;
return (true);
}
bool
BotProfile_AddRandom(void)
{
int startValue = (int)floor(random(0, g_botScriptCount));
int spawnBot = -1i;
/* start at a random index */
for (int i = startValue; i < g_botScriptCount; i++) {
if (Bot_ExistsInServer(g_bots[i].m_strNetName) == false) {
spawnBot = i;
break;
}
}
/* still haven't found it. count down. */
if (spawnBot == -1i) {
for (int i = startValue - 1i; i > 0i; i--) {
if (Bot_ExistsInServer(g_bots[i].m_strNetName) == false) {
spawnBot = i;
break;
}
}
}
/* every bot exists already */
if (spawnBot == -1i) {
print("^1BotProfile_AddRandom^7: Not enough profiles available.\n");
return (false);
}
Bot_AddBot_f(g_bots[spawnBot].m_strName);
return (true);
}
void
BotProfile_Init(void)
{
filestream botScript;
string tempString;
botScript_t currentDef;
int braceDepth = 0i;
g_botScriptCount = 0i;
if (autocvar(bot_enable, 1) == 0) {
return;
}
botScript = fopen("scripts/bots.txt", FILE_READ);
if (botScript < 0) {
return;
}
/* line by line */
while ((tempString = fgets(botScript))) {
int lineSegments = tokenize_console(tempString);
/* word for word */
for (int i = 0i; i < lineSegments; i++) {
string word = argv(i);
switch (word) {
case "{":
braceDepth++;
break;
case "}":
braceDepth--;
/* we've reached the end of a definition */
if (braceDepth == 0) {
/* we have something somewhat valid I guess */
if (currentDef.m_strName != "") {
g_bots[g_botScriptCount].m_strNetName = currentDef.m_strNetName;
g_bots[g_botScriptCount].m_strExtra = currentDef.m_strExtra;
if (g_bots[g_botScriptCount].m_strNetName == "") {
g_bots[g_botScriptCount].m_strNetName = currentDef.m_strName;
}
g_bots[g_botScriptCount].m_strName = strtolower(currentDef.m_strName);
/* increment the def count */
if (g_botScriptCount < BOTSCRIPT_MAX)
g_botScriptCount++;
}
currentDef.m_strName = "";
currentDef.m_strNetName = "";
currentDef.m_strExtra = "";
}
break;
default:
if (braceDepth == 1) {
if (word == "name") {
currentDef.m_strName = argv(i+1);
i++;
} else if (word == "funname") {
currentDef.m_strNetName = argv(i+1);
i++;
} else { /* rest gets dumped into extra */
currentDef.m_strExtra = strcat(currentDef.m_strExtra, "\"", word, "\"", " ");
}
}
}
}
}
fclose(botScript);
print(sprintf("%i bots parsed\n", g_botScriptCount));
}
void
Bot_ListBotProfiles_f(void)
{
if (!g_botScriptCount) {
print("no bot profiles found.\n");
return;
}
for (int i = 0; i < g_botScriptCount; i++) {
print(sprintf("%i: %s\n", i, g_bots[i].m_strName));
print(sprintf("\t%S\n", g_bots[i].m_strNetName));
print(sprintf("\t%s\n", g_bots[i].m_strExtra));
}
}

View File

@ -159,7 +159,7 @@ Route_SelectRandomSpot(void)
}
vector
Route_SelectDestination(bot target)
Route_SelectDestination(NSBot target)
{
CGameRules rules;
rules = (CGameRules)g_grMode;

View File

@ -479,8 +479,8 @@ Way_GoToPoint(entity pl)
for (entity a = world; (a = find(a, classname, "player"));) {
if (clienttype(a) != CLIENTTYPE_REAL) {
bot targ;
targ = (bot)a;
NSBot targ;
targ = (NSBot)a;
targ.RouteClear();
targ.RouteToPosition(pl.origin);
print(sprintf("Told bot to go to %v\n", trace_endpos));

View File

@ -432,6 +432,47 @@ Cmd_Parse(string sCMD)
case "-menu_right":
pSeat->m_iInputReload = FALSE;
break;
/* client aliases for server commands */
case "addBot":
localcmd(sprintf("sv addBot %s\n", argv(1)));
break;
case "killAllBots":
localcmd(sprintf("sv killAllBots %s\n", argv(1)));
break;
case "resetAllBotsGoals":
localcmd(sprintf("sv resetAllBotsGoals %s\n", argv(1)));
break;
case "killClass":
localcmd(sprintf("sv killClass %s\n", argv(1)));
break;
case "killMovables":
localcmd(sprintf("sv killMovables %s\n", argv(1)));
break;
case "trigger":
localcmd(sprintf("sv trigger %s\n", argv(1)));
break;
case "input":
localcmd(sprintf("sv input %s\n", argv(1)));
break;
case "listBotProfiles":
localcmd(sprintf("sv listBotProfiles %s\n", argv(1)));
break;
case "listTargets":
localcmd(sprintf("sv listTargets %s\n", argv(1)));
break;
case "teleport":
localcmd(sprintf("sv teleport %s\n", argv(1)));
break;
case "teleportToClass":
localcmd(sprintf("sv teleportToClass %s\n", argv(1)));
break;
case "respawnEntities":
localcmd(sprintf("sv respawnEntities %s\n", argv(1)));
break;
case "spawn":
localcmd(sprintf("sv spawn %s\n", argv(1)));
break;
default:
return (false);
}
@ -459,6 +500,21 @@ Cmd_Init(void)
registercommand("listClientSoundDef");
registercommand("listServerSoundDef");
/* server commands */
registercommand("addBot");
registercommand("killAllBots");
registercommand("resetAllBotsGoals");
registercommand("killClass");
registercommand("killMovables");
registercommand("trigger");
registercommand("input");
registercommand("listTargets");
registercommand("teleport");
registercommand("teleportToClass");
registercommand("respawnEntities");
registercommand("spawn");
registercommand("listBotProfiles");
registercommand("cleardecals");
registercommand("testLight");
registercommand("testPointLight");

View File

@ -493,10 +493,12 @@ EntityDef_SpawnClassname(string className)
for (int i = 0i; i < g_entDefCount; i++) {
if (className == g_entDefTable[i].entClass) {
EntityDef_Precaches(i);
NSLog("Spawning eDef %S", className);
return EntityDef_PrepareEntity(self, i);
}
}
NSLog("^1Failed spawning eDef %S", className);
return __NULL__;
}

View File

@ -54,7 +54,8 @@ ClientConnect(void)
/* bot needs special init */
#ifdef BOT_INCLUDED
if (clienttype(self) == CLIENTTYPE_BOT) {
spawnfunc_bot();
/* from now on we're of type NSBot */
EntityDef_SpawnClassname("bot");
} else
#endif
@ -182,7 +183,7 @@ PlayerPreThink(void)
#ifdef BOT_INCLUDED
if (clienttype(self) == CLIENTTYPE_BOT) {
((bot)self).PreFrame();
((NSBot)self).PreFrame();
}
#endif
@ -208,7 +209,7 @@ PlayerPostThink(void)
#ifdef BOT_INCLUDED
if (clienttype(self) == CLIENTTYPE_BOT) {
((bot)self).PostFrame();
((NSBot)self).PostFrame();
}
#endif
@ -458,10 +459,14 @@ initents(void)
g_ents_initialized = TRUE;
/* engine hacks for dedicated servers */
cvar_set("s_nominaldistance", "1000");
cvar_set("s_nominaldistance", "2048");
/* other engine hacks */
cvar_set("sv_nqplayerphysics", "0");
#ifdef BOT_INCLUDED
BotLib_Init();
#endif
}
/** Any command executed on the server (either tty, rcon or `sv`) gets
@ -507,12 +512,40 @@ ConsoleCmd(string cmd)
/* time to handle commands that apply to all games */
tokenize(cmd);
switch (argv(0)) {
case "trigger_ent":
case "addBot":
Bot_AddBot_f(strtolower(argv(1)));
break;
case "killAllBots":
Bot_KillAllBots();
break;
case "resetAllBotsGoals":
Bot_ResetAllBotsGoals();
break;
case "listBotProfiles":
Bot_ListBotProfiles_f();
break;
case "killClass":
string targetClass;
targetClass = argv(1);
if (targetClass)
for (entity a = world; (a = find(a, ::classname, targetClass));) {
NSEntity t = (NSEntity)a;
t.Destroy();
}
break;
case "killMovables":
for (entity a = world; (a = findfloat(a, ::movetype, MOVETYPE_PHYSICS));) {
NSEntity t = (NSEntity)a;
t.Destroy();
}
break;
case "trigger":
string targ;
targ = argv(1);
if (targ)
for (entity a = world; (a = find(a, ::targetname, argv(1)));) {
for (entity a = world; (a = find(a, ::targetname, targ));) {
NSEntity t = (NSEntity)a;
if (t.Trigger)
@ -530,34 +563,42 @@ ConsoleCmd(string cmd)
print(sprintf("Sending input to %d, %S: %S\n", entNum, inputName, inputData));
}
break;
case "goto_ent":
case "listTargets":
for (entity a = world; (a = findfloat(a, ::identity, 1));) {
if (a.targetname) {
print(sprintf("%d: %s (%s)\n", num_for_edict(a), a.targetname, a.classname));
}
}
break;
case "teleport":
static entity targetFinder;
targetFinder = find(targetFinder, ::targetname, argv(1));
/* try at least one more time to skip world */
if (!targetFinder)
targetFinder = find(targetFinder, ::targetname, argv(1));
if (targetFinder)
setorigin(pl, targetFinder.origin);
break;
case "teleportToClass":
static entity finder;
finder = find(finder, ::classname, argv(1));
/* try at least one more time to skip world */
if (!finder)
finder = find(finder, ::classname, argv(1));
if (finder)
setorigin(pl, finder.origin);
break;
case "respawn_ents":
case "respawnEntities":
for (entity a = world; (a = findfloat(a, ::identity, 1));) {
NSEntity ent = (NSEntity)a;
ent.Respawn();
}
break;
case "spawn":
entity eDef = spawn();
eDef.classname = strcat("spawnfunc_", argv(1));
self = eDef;
callfunction(self.classname);
self = pl;
makevectors(pl.v_angle);
if (eDef.identity == 1) {
NSEntity ent = (NSEntity)eDef;
}
traceline(pl.origin, pl.origin + (v_forward * 1024), MOVE_NORMAL, pl);
setorigin(eDef, trace_endpos);
break;
case "spawndef":
NSEntity unit = EntityDef_CreateClassname(argv(1));
makevectors(pl.v_angle);
traceline(pl.origin, pl.origin + (v_forward * 1024), MOVE_NORMAL, pl);

View File

@ -666,7 +666,7 @@ NSClientPlayer::ServerInputFrame(void)
#ifdef BOT_INCLUDED
/* wait a few seconds, as we may not have been spawned yet */
if (clienttype(this) == CLIENTTYPE_BOT) {
((bot)this).RunAI();
((NSBot)this).RunAI();
}
#endif

View File

@ -29,18 +29,19 @@ Math_Lerp(float fA, float fB, float fPercent)
return (fA * (1 - fPercent)) + (fB * fPercent);
}
/* tries to make sure an angle value stays within certain constraints...
* however it doesn't account for much larger discrepancies */
/* recursive function that fixes an euler angle */
float
Math_FixDelta(float fDelta)
{
if (fDelta >= 180) {
if (fDelta > 180) {
fDelta -= 360;
} else if (fDelta <= -180) {
} else if (fDelta < -180) {
fDelta += 360;
} else {
return fDelta;
}
return fDelta;
return Math_FixDelta(fDelta);
}
vector