/* =========================================================================== Return to Castle Wolfenstein single player GPL Source Code Copyright (C) 1999-2010 id Software LLC, a ZeniMax Media company. This file is part of the Return to Castle Wolfenstein single player GPL Source Code (“RTCW SP Source Code”). RTCW SP Source Code is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. RTCW SP Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with RTCW SP Source Code. If not, see . In addition, the RTCW SP Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the RTCW SP Source Code. If not, please request a copy in writing from id Software at the address below. If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA. =========================================================================== */ /***************************************************************************** * name: ai_dmq3.c * * desc: Quake3 bot AI * * *****************************************************************************/ #include "../game/g_local.h" #include "../game/botlib.h" #include "../game/be_aas.h" #include "../game/be_ea.h" #include "../game/be_ai_char.h" #include "../game/be_ai_chat.h" #include "../game/be_ai_gen.h" #include "../game/be_ai_goal.h" #include "../game/be_ai_move.h" #include "../game/be_ai_weap.h" #include "../botai/botai.h" // #include "ai_main.h" #include "ai_dmq3.h" #include "ai_chat.h" #include "ai_cmd.h" #include "ai_dmnet.h" #include "ai_team.h" // #include "chars.h" //characteristics #include "inv.h" //indexes into the inventory #include "syn.h" //synonyms #include "match.h" //string matching types and vars #define IDEAL_ATTACKDIST 140 #define WEAPONINDEX_MACHINEGUN 2 #define MAX_WAYPOINTS 128 ////////////////// // from aasfile.h #define AREACONTENTS_MOVER 1024 #define AREACONTENTS_MODELNUMSHIFT 24 #define AREACONTENTS_MAXMODELNUM 0xFF #define AREACONTENTS_MODELNUM ( AREACONTENTS_MAXMODELNUM << AREACONTENTS_MODELNUMSHIFT ) ////////////////// // bot_waypoint_t botai_waypoints[MAX_WAYPOINTS]; bot_waypoint_t *botai_freewaypoints; //NOTE: not using a cvar which can be updated because the game should be reloaded anyway int gametype; //game type // Rafael gameskill int gameskill; vmCvar_t bot_grapple; vmCvar_t bot_rocketjump; vmCvar_t bot_fastchat; vmCvar_t bot_nochat; vmCvar_t bot_testrchat; vec3_t lastteleport_origin; float lastteleport_time; //true when the map changed int max_bspmodelindex; //maximum BSP model index //CTF flag goals bot_goal_t ctf_redflag; bot_goal_t ctf_blueflag; #ifdef CTF /* ================== BotCTFCarryingFlag ================== */ int BotCTFCarryingFlag( bot_state_t *bs ) { if ( gametype != GT_CTF ) { return CTF_FLAG_NONE; } if ( bs->inventory[INVENTORY_REDFLAG] > 0 ) { return CTF_FLAG_RED; } else if ( bs->inventory[INVENTORY_BLUEFLAG] > 0 ) { return CTF_FLAG_BLUE; } return CTF_FLAG_NONE; } /* ================== BotCTFTeam ================== */ int BotCTFTeam( bot_state_t *bs ) { char skin[128], *p; if ( gametype != GT_CTF ) { return CTF_TEAM_NONE; } ClientSkin( bs->client, skin, sizeof( skin ) ); p = strchr( skin, '/' ); if ( !p ) { p = skin; } else { p++;} if ( Q_stricmp( p, CTF_SKIN_REDTEAM ) == 0 ) { return CTF_TEAM_RED; } if ( Q_stricmp( p, CTF_SKIN_BLUETEAM ) == 0 ) { return CTF_TEAM_BLUE; } return CTF_TEAM_NONE; } /* ================== BotCTFRetreatGoals ================== */ void BotCTFRetreatGoals( bot_state_t *bs ) { //when carrying a flag in ctf the bot should rush to the base if ( BotCTFCarryingFlag( bs ) ) { //if not already rushing to the base if ( bs->ltgtype != LTG_RUSHBASE ) { bs->ltgtype = LTG_RUSHBASE; bs->teamgoal_time = trap_AAS_Time() + CTF_RUSHBASE_TIME; bs->rushbaseaway_time = 0; } } } /* ================== BotCTFSeekGoals ================== */ void BotCTFSeekGoals( bot_state_t *bs ) { float rnd; //when carrying a flag in ctf the bot should rush to the base if ( BotCTFCarryingFlag( bs ) ) { //if not already rushing to the base if ( bs->ltgtype != LTG_RUSHBASE ) { bs->ltgtype = LTG_RUSHBASE; bs->teamgoal_time = trap_AAS_Time() + CTF_RUSHBASE_TIME; bs->rushbaseaway_time = 0; } return; } //if the bot is roaming if ( bs->ctfroam_time > trap_AAS_Time() ) { return; } //if already a CTF or team goal if ( bs->ltgtype == LTG_TEAMHELP || bs->ltgtype == LTG_TEAMACCOMPANY || bs->ltgtype == LTG_DEFENDKEYAREA || bs->ltgtype == LTG_GETFLAG || bs->ltgtype == LTG_RUSHBASE || bs->ltgtype == LTG_CAMPORDER || bs->ltgtype == LTG_PATROL ) { return; } //if the bot has anough aggression to decide what to do if ( BotAggression( bs ) < 50 ) { return; } //set the time to send a message to the team mates bs->teammessage_time = trap_AAS_Time() + 2 * random(); //get the flag or defend the base rnd = random(); if ( rnd < 0.33 && ctf_redflag.areanum && ctf_blueflag.areanum ) { bs->ltgtype = LTG_GETFLAG; //set the time the bot will stop getting the flag bs->teamgoal_time = trap_AAS_Time() + CTF_GETFLAG_TIME; } else if ( rnd < 0.66 && ctf_redflag.areanum && ctf_blueflag.areanum ) { //FIXME: do not always use the base flag if ( BotCTFTeam( bs ) == CTF_TEAM_RED ) { memcpy( &bs->teamgoal, &ctf_redflag, sizeof( bot_goal_t ) ); } else { memcpy( &bs->teamgoal, &ctf_blueflag, sizeof( bot_goal_t ) );} //set the ltg type bs->ltgtype = LTG_DEFENDKEYAREA; //set the time the bot stop defending the base bs->teamgoal_time = trap_AAS_Time() + TEAM_DEFENDKEYAREA_TIME; bs->defendaway_time = 0; } else { bs->ltgtype = 0; //set the time the bot will stop roaming bs->ctfroam_time = trap_AAS_Time() + CTF_ROAM_TIME; } #ifdef DEBUG BotPrintTeamGoal( bs ); #endif //DEBUG } #endif //CTF /* ================== BotPointAreaNum ================== */ int BotPointAreaNum( vec3_t origin ) { int areanum, numareas, areas[1]; vec3_t end, ofs; #define BOTAREA_JIGGLE_DIST 32 areanum = trap_AAS_PointAreaNum( origin ); if ( areanum ) { return areanum; } VectorCopy( origin, end ); end[2] += 10; numareas = trap_AAS_TraceAreas( origin, end, areas, NULL, 1 ); if ( numareas > 0 ) { return areas[0]; } // Ridah, jiggle them around to look for a fuzzy area, helps LARGE characters reach destinations that are against walls ofs[2] = 10; for ( ofs[0] = -BOTAREA_JIGGLE_DIST; ofs[0] <= BOTAREA_JIGGLE_DIST; ofs[0] += BOTAREA_JIGGLE_DIST * 2 ) { for ( ofs[1] = -BOTAREA_JIGGLE_DIST; ofs[1] <= BOTAREA_JIGGLE_DIST; ofs[1] += BOTAREA_JIGGLE_DIST * 2 ) { VectorAdd( origin, ofs, end ); numareas = trap_AAS_TraceAreas( origin, end, areas, NULL, 1 ); if ( numareas > 0 ) { return areas[0]; } } } return 0; } /* ================== ClientName ================== */ char *ClientName( int client, char *name, int size ) { char buf[MAX_INFO_STRING]; if ( client < 0 || client >= MAX_CLIENTS ) { BotAI_Print( PRT_ERROR, "ClientName: client out of range\n" ); return "[client out of range]"; } trap_GetConfigstring( CS_PLAYERS + client, buf, sizeof( buf ) ); strncpy( name, Info_ValueForKey( buf, "n" ), size - 1 ); name[size - 1] = '\0'; Q_CleanStr( name ); return name; } /* ================== ClientSkin ================== */ char *ClientSkin( int client, char *skin, int size ) { char buf[MAX_INFO_STRING]; if ( client < 0 || client >= MAX_CLIENTS ) { BotAI_Print( PRT_ERROR, "ClientSkin: client out of range\n" ); return "[client out of range]"; } trap_GetConfigstring( CS_PLAYERS + client, buf, sizeof( buf ) ); strncpy( skin, Info_ValueForKey( buf, "model" ), size - 1 ); skin[size - 1] = '\0'; return skin; } /* ================== ClientFromName ================== */ int ClientFromName( char *name ) { int i; char buf[MAX_INFO_STRING]; static int maxclients; if ( !maxclients ) { maxclients = trap_Cvar_VariableIntegerValue( "sv_maxclients" ); } for ( i = 0; i < maxclients && i < MAX_CLIENTS; i++ ) { trap_GetConfigstring( CS_PLAYERS + i, buf, sizeof( buf ) ); Q_CleanStr( buf ); if ( !Q_stricmp( Info_ValueForKey( buf, "n" ), name ) ) { return i; } } return -1; } /* ================== stristr ================== */ char *stristr( char *str, char *charset ) { int i; while ( *str ) { for ( i = 0; charset[i] && str[i]; i++ ) { if ( toupper( charset[i] ) != toupper( str[i] ) ) { break; } } if ( !charset[i] ) { return str; } str++; } return NULL; } /* ================== EasyClientName ================== */ char *EasyClientName( int client, char *buf, int size ) { int i; char *str1, *str2, *ptr, c; char name[128]; strcpy( name, ClientName( client, name, sizeof( name ) ) ); for ( i = 0; name[i]; i++ ) name[i] &= 127; //remove all spaces for ( ptr = strstr( name, " " ); ptr; ptr = strstr( name, " " ) ) { memmove( ptr, ptr + 1, strlen( ptr + 1 ) + 1 ); } //check for [x] and ]x[ clan names str1 = strstr( name, "[" ); str2 = strstr( name, "]" ); if ( str1 && str2 ) { if ( str2 > str1 ) { memmove( str1, str2 + 1, strlen( str2 + 1 ) + 1 ); } else { memmove( str2, str1 + 1, strlen( str1 + 1 ) + 1 );} } //remove Mr prefix if ( ( name[0] == 'm' || name[0] == 'M' ) && ( name[1] == 'r' || name[1] == 'R' ) ) { memmove( name, name + 2, strlen( name + 2 ) + 1 ); } //only allow lower case alphabet characters ptr = name; while ( *ptr ) { c = *ptr; if ( ( c >= 'a' && c <= 'z' ) || ( c >= '0' && c <= '9' ) || c == '_' ) { ptr++; } else if ( c >= 'A' && c <= 'Z' ) { *ptr += 'a' - 'A'; ptr++; } else { memmove( ptr, ptr + 1, strlen( ptr + 1 ) + 1 ); } } strncpy( buf, name, size - 1 ); buf[size - 1] = '\0'; return buf; } /* ================== BotChooseWeapon ================== */ void BotChooseWeapon( bot_state_t *bs ) { int newweaponnum; if ( bs->cur_ps.weaponstate == WEAPON_RAISING || bs->cur_ps.weaponstate == WEAPON_RAISING_TORELOAD || //----(SA) added bs->cur_ps.weaponstate == WEAPON_DROPPING || bs->cur_ps.weaponstate == WEAPON_DROPPING_TORELOAD ) { //----(SA) added trap_EA_SelectWeapon( bs->client, bs->weaponnum ); } else { newweaponnum = trap_BotChooseBestFightWeapon( bs->ws, bs->inventory ); if ( bs->weaponnum != newweaponnum ) { bs->weaponchange_time = trap_AAS_Time(); } bs->weaponnum = newweaponnum; //BotAI_Print(PRT_MESSAGE, "bs->weaponnum = %d\n", bs->weaponnum); trap_EA_SelectWeapon( bs->client, bs->weaponnum ); } } /* ================== BotSetupForMovement ================== */ void BotSetupForMovement( bot_state_t *bs ) { bot_initmove_t initmove; memset( &initmove, 0, sizeof( bot_initmove_t ) ); VectorCopy( bs->cur_ps.origin, initmove.origin ); VectorCopy( bs->cur_ps.velocity, initmove.velocity ); VectorCopy( bs->cur_ps.origin, initmove.viewoffset ); initmove.viewoffset[2] += bs->cur_ps.viewheight; initmove.entitynum = bs->entitynum; initmove.client = bs->client; initmove.thinktime = bs->thinktime; //set the onground flag if ( bs->cur_ps.groundEntityNum != ENTITYNUM_NONE ) { initmove.or_moveflags |= MFL_ONGROUND; } //set the teleported flag if ( ( bs->cur_ps.pm_flags & PMF_TIME_KNOCKBACK ) && ( bs->cur_ps.pm_time > 0 ) ) { initmove.or_moveflags |= MFL_TELEPORTED; } //set the waterjump flag if ( ( bs->cur_ps.pm_flags & PMF_TIME_WATERJUMP ) && ( bs->cur_ps.pm_time > 0 ) ) { initmove.or_moveflags |= MFL_WATERJUMP; } //set presence type if ( bs->cur_ps.pm_flags & PMF_DUCKED ) { initmove.presencetype = PRESENCE_CROUCH; } else { initmove.presencetype = PRESENCE_NORMAL;} // if ( bs->walker > 0.5 ) { initmove.or_moveflags |= MFL_WALK; } // VectorCopy( bs->viewangles, initmove.viewangles ); // trap_BotInitMoveState( bs->ms, &initmove ); } /* ================== BotUpdateInventory ================== */ void BotUpdateInventory( bot_state_t *bs ) { //armor bs->inventory[INVENTORY_ARMOR] = bs->cur_ps.stats[STAT_ARMOR]; //weapons bs->inventory[INVENTORY_LUGER] = COM_BitCheck( bs->cur_ps.weapons, ( WP_LUGER ) ); bs->inventory[INVENTORY_MAUSER] = COM_BitCheck( bs->cur_ps.weapons, ( WP_MAUSER ) ); bs->inventory[INVENTORY_MP40] = COM_BitCheck( bs->cur_ps.weapons, ( WP_MP40 ) ); bs->inventory[INVENTORY_GRENADELAUNCHER] = COM_BitCheck( bs->cur_ps.weapons, ( WP_GRENADE_LAUNCHER ) ); bs->inventory[INVENTORY_VENOM] = COM_BitCheck( bs->cur_ps.weapons, ( WP_VENOM ) ); bs->inventory[INVENTORY_FLAMETHROWER] = COM_BitCheck( bs->cur_ps.weapons, ( WP_FLAMETHROWER ) ); bs->inventory[INVENTORY_GAUNTLET] = COM_BitCheck( bs->cur_ps.weapons, ( WP_GAUNTLET ) ); // ammo bs->inventory[INVENTORY_9MM] = bs->cur_ps.ammo[BG_FindAmmoForWeapon( WP_MP40 )]; bs->inventory[INVENTORY_792MM] = bs->cur_ps.ammo[BG_FindAmmoForWeapon( WP_MAUSER )]; bs->inventory[INVENTORY_GRENADES] = bs->cur_ps.ammo[BG_FindAmmoForWeapon( WP_GRENADE_LAUNCHER )]; bs->inventory[INVENTORY_127MM] = bs->cur_ps.ammo[BG_FindAmmoForWeapon( WP_VENOM )]; bs->inventory[INVENTORY_FUEL] = bs->cur_ps.ammo[BG_FindAmmoForWeapon( WP_FLAMETHROWER )]; //powerups bs->inventory[INVENTORY_HEALTH] = bs->cur_ps.stats[STAT_HEALTH]; bs->inventory[INVENTORY_TELEPORTER] = bs->cur_ps.stats[STAT_HOLDABLE_ITEM] == MODELINDEX_TELEPORTER; bs->inventory[INVENTORY_MEDKIT] = bs->cur_ps.stats[STAT_HOLDABLE_ITEM] == MODELINDEX_MEDKIT; bs->inventory[INVENTORY_QUAD] = bs->cur_ps.powerups[PW_QUAD] != 0; bs->inventory[INVENTORY_ENVIRONMENTSUIT] = bs->cur_ps.powerups[PW_BATTLESUIT] != 0; bs->inventory[INVENTORY_HASTE] = bs->cur_ps.powerups[PW_HASTE] != 0; bs->inventory[INVENTORY_INVISIBILITY] = bs->cur_ps.powerups[PW_INVIS] != 0; bs->inventory[INVENTORY_REGEN] = bs->cur_ps.powerups[PW_REGEN] != 0; bs->inventory[INVENTORY_FLIGHT] = bs->cur_ps.powerups[PW_FLIGHT] != 0; bs->inventory[INVENTORY_REDFLAG] = bs->cur_ps.powerups[PW_REDFLAG] != 0; bs->inventory[INVENTORY_BLUEFLAG] = bs->cur_ps.powerups[PW_BLUEFLAG] != 0; // } /* ================== BotUpdateBattleInventory ================== */ void BotUpdateBattleInventory( bot_state_t *bs, int enemy ) { vec3_t dir; aas_entityinfo_t entinfo; BotEntityInfo( enemy, &entinfo ); VectorSubtract( entinfo.origin, bs->origin, dir ); bs->inventory[ENEMY_HEIGHT] = (int) dir[2]; dir[2] = 0; bs->inventory[ENEMY_HORIZONTAL_DIST] = (int) VectorLength( dir ); //FIXME: add num visible enemies and num visible team mates to the inventory } /* ================== BotBattleUseItems ================== */ void BotBattleUseItems( bot_state_t *bs ) { if ( bs->inventory[INVENTORY_HEALTH] < 40 ) { if ( bs->inventory[INVENTORY_TELEPORTER] > 0 ) { trap_EA_Use( bs->client ); } if ( bs->inventory[INVENTORY_MEDKIT] > 0 ) { trap_EA_Use( bs->client ); } } } /* ================== BotSetTeleportTime ================== */ void BotSetTeleportTime( bot_state_t *bs ) { if ( ( bs->cur_ps.eFlags ^ bs->last_eFlags ) & EF_TELEPORT_BIT ) { bs->teleport_time = trap_AAS_Time(); } bs->last_eFlags = bs->cur_ps.eFlags; } /* ================== BotIsDead ================== */ qboolean BotIsDead( bot_state_t *bs ) { return ( bs->cur_ps.pm_type == PM_DEAD ); } /* ================== BotIsObserver ================== */ qboolean BotIsObserver( bot_state_t *bs ) { char buf[MAX_INFO_STRING]; if ( bs->cur_ps.pm_type == PM_SPECTATOR ) { return qtrue; } trap_GetConfigstring( CS_PLAYERS + bs->client, buf, sizeof( buf ) ); if ( atoi( Info_ValueForKey( buf, "t" ) ) == TEAM_SPECTATOR ) { return qtrue; } return qfalse; } /* ================== BotIntermission ================== */ qboolean BotIntermission( bot_state_t *bs ) { //NOTE: we shouldn't look at the game code... if ( level.intermissiontime ) { return qtrue; } return ( bs->cur_ps.pm_type == PM_FREEZE || bs->cur_ps.pm_type == PM_INTERMISSION ); } /* ============== BotInLava ============== */ qboolean BotInLava( bot_state_t *bs ) { vec3_t feet; VectorCopy( bs->origin, feet ); feet[2] -= 23; return ( trap_AAS_PointContents( feet ) & CONTENTS_LAVA ); } /* ============== BotInSlime ============== */ qboolean BotInSlime( bot_state_t *bs ) { vec3_t feet; VectorCopy( bs->origin, feet ); feet[2] -= 23; return ( trap_AAS_PointContents( feet ) & CONTENTS_SLIME ); } /* ================== EntityIsDead ================== */ qboolean EntityIsDead( aas_entityinfo_t *entinfo ) { playerState_t ps; if ( entinfo->number >= 0 && entinfo->number < MAX_CLIENTS ) { //retrieve the current client state BotAI_GetClientState( entinfo->number, &ps ); if ( ps.pm_type != PM_NORMAL ) { return qtrue; } } return qfalse; } /* ================== EntityIsInvisible ================== */ qboolean EntityIsInvisible( aas_entityinfo_t *entinfo ) { if ( entinfo->powerups & ( 1 << PW_INVIS ) ) { return qtrue; } return qfalse; } /* ================== EntityIsShooting ================== */ qboolean EntityIsShooting( aas_entityinfo_t *entinfo ) { if ( entinfo->flags & EF_FIRING ) { return qtrue; } return qfalse; } /* ================== EntityIsChatting ================== */ qboolean EntityIsChatting( aas_entityinfo_t *entinfo ) { if ( entinfo->flags & EF_TALK ) { return qtrue; } return qfalse; } /* ================== EntityHasQuad ================== */ qboolean EntityHasQuad( aas_entityinfo_t *entinfo ) { if ( entinfo->powerups & ( 1 << PW_QUAD ) ) { return qtrue; } return qfalse; } /* ================== BotCreateWayPoint ================== */ bot_waypoint_t *BotCreateWayPoint( char *name, vec3_t origin, int areanum ) { bot_waypoint_t *wp; vec3_t waypointmins = {-8, -8, -8}, waypointmaxs = {8, 8, 8}; wp = botai_freewaypoints; if ( !wp ) { BotAI_Print( PRT_WARNING, "BotCreateWayPoint: Out of waypoints\n" ); return NULL; } botai_freewaypoints = botai_freewaypoints->next; Q_strncpyz( wp->name, name, sizeof( wp->name ) ); VectorCopy( origin, wp->goal.origin ); VectorCopy( waypointmins, wp->goal.mins ); VectorCopy( waypointmaxs, wp->goal.maxs ); wp->goal.areanum = areanum; wp->next = NULL; wp->prev = NULL; return wp; } /* ================== BotFindWayPoint ================== */ bot_waypoint_t *BotFindWayPoint( bot_waypoint_t *waypoints, char *name ) { bot_waypoint_t *wp; for ( wp = waypoints; wp; wp = wp->next ) { if ( !Q_stricmp( wp->name, name ) ) { return wp; } } return NULL; } /* ================== BotFreeWaypoints ================== */ void BotFreeWaypoints( bot_waypoint_t *wp ) { bot_waypoint_t *nextwp; for (; wp; wp = nextwp ) { nextwp = wp->next; wp->next = botai_freewaypoints; botai_freewaypoints = wp; } } /* ================== BotInitWaypoints ================== */ void BotInitWaypoints( void ) { int i; botai_freewaypoints = NULL; for ( i = 0; i < MAX_WAYPOINTS; i++ ) { botai_waypoints[i].next = botai_freewaypoints; botai_freewaypoints = &botai_waypoints[i]; } } /* ================== TeamPlayIsOn ================== */ int TeamPlayIsOn( void ) { return ( gametype == GT_TEAM || gametype == GT_CTF ); } /* ================== BotAggression FIXME: move this to external fuzzy logic NOTE!!: I made no changes to this code for wolf weapon awareness. (SA) ================== */ float BotAggression( bot_state_t *bs ) { //if the bot has quad if ( bs->inventory[INVENTORY_QUAD] ) { //if the bot is not holding the gauntlet or the enemy is really nearby if ( bs->weaponnum != WP_GAUNTLET || bs->inventory[ENEMY_HORIZONTAL_DIST] < 80 ) { return 70; } } //if the enemy is located way higher than the bot if ( bs->inventory[ENEMY_HEIGHT] > 200 ) { return 0; } //if the bot is very low on health if ( bs->inventory[INVENTORY_HEALTH] < 60 ) { return 0; } //if the bot is low on health if ( bs->inventory[INVENTORY_HEALTH] < 80 ) { //if the bot has insufficient armor if ( bs->inventory[INVENTORY_ARMOR] < 40 ) { return 0; } } // //if the bot can use the bfg // if (bs->inventory[INVENTORY_BFG10K] > 0 && // bs->inventory[INVENTORY_BFGAMMO] > 7) return 100; // //if the bot can use the railgun // if (bs->inventory[INVENTORY_RAILGUN] > 0 && // bs->inventory[INVENTORY_SLUGS] > 5) return 95; //if the bot can use the lightning gun if ( bs->inventory[INVENTORY_FLAMETHROWER] > 0 && bs->inventory[INVENTORY_FUEL] > 50 ) { return 90; } //if the bot can use the rocketlauncher if ( bs->inventory[INVENTORY_ROCKETLAUNCHER] > 0 && bs->inventory[INVENTORY_ROCKETS] > 5 ) { return 90; } //if the bot can use the SP5 if ( bs->inventory[INVENTORY_SP5] > 0 && bs->inventory[INVENTORY_SP5AMMO] > 40 ) { return 85; } //if the bot can use the grenade launcher if ( bs->inventory[INVENTORY_GRENADELAUNCHER] > 0 && bs->inventory[INVENTORY_GRENADES] > 10 ) { return 80; } // //if the bot can use the shotgun // if (bs->inventory[INVENTORY_SHOTGUN] > 0 && // bs->inventory[INVENTORY_SHELLS] > 10) return 50; //otherwise the bot is not feeling too good return 0; } /* ================== BotWantsToRetreat ================== */ int BotWantsToRetreat( bot_state_t *bs ) { #ifdef CTF //always retreat when carrying a CTF flag if ( BotCTFCarryingFlag( bs ) ) { return qtrue; } //if the bot is getting the flag if ( bs->ltgtype == LTG_GETFLAG ) { return qtrue; } #endif //CTF if ( BotAggression( bs ) < 50 ) { return qtrue; } return qfalse; } /* ================== BotWantsToChase ================== */ int BotWantsToChase( bot_state_t *bs ) { #ifdef CTF //always retreat when carrying a CTF flag if ( BotCTFCarryingFlag( bs ) ) { return qfalse; } //if the bot is getting the flag if ( bs->ltgtype == LTG_GETFLAG ) { return qfalse; } #endif //CTF if ( BotAggression( bs ) > 50 ) { return qtrue; } return qfalse; } /* ================== BotWantsToHelp ================== */ int BotWantsToHelp( bot_state_t *bs ) { return qtrue; } /* ================== BotCanAndWantsToRocketJump ================== */ int BotCanAndWantsToRocketJump( bot_state_t *bs ) { float rocketjumper; //if rocket jumping is disabled if ( !bot_rocketjump.integer ) { return qfalse; } //if no rocket launcher if ( bs->inventory[INVENTORY_ROCKETLAUNCHER] <= 0 ) { return qfalse; } //if low on rockets if ( bs->inventory[INVENTORY_ROCKETS] < 3 ) { return qfalse; } //never rocket jump with the Quad if ( bs->inventory[INVENTORY_QUAD] ) { return qfalse; } //if low on health if ( bs->inventory[INVENTORY_HEALTH] < 60 ) { return qfalse; } //if not full health if ( bs->inventory[INVENTORY_HEALTH] < 90 ) { //if the bot has insufficient armor if ( bs->inventory[INVENTORY_ARMOR] < 40 ) { return qfalse; } } rocketjumper = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_WEAPONJUMPING, 0, 1 ); if ( rocketjumper < 0.5 ) { return qfalse; } return qtrue; } /* ================== BotGoCamp ================== */ void BotGoCamp( bot_state_t *bs, bot_goal_t *goal ) { float camper; //set message time to zero so bot will NOT show any message bs->teammessage_time = 0; //set the ltg type bs->ltgtype = LTG_CAMP; //set the team goal memcpy( &bs->teamgoal, goal, sizeof( bot_goal_t ) ); //get the team goal time camper = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_CAMPER, 0, 1 ); if ( camper > 0.99 ) { bs->teamgoal_time = 99999; } else { bs->teamgoal_time = 120 + 180 * camper + random() * 15;} //set the last time the bot started camping bs->camp_time = trap_AAS_Time(); //the teammate that requested the camping bs->teammate = 0; //do NOT type arrive message bs->arrive_time = 1; } /* ================== BotWantsToCamp ================== */ int BotWantsToCamp( bot_state_t *bs ) { float camper; int cs, traveltime, besttraveltime; bot_goal_t goal, bestgoal; camper = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_CAMPER, 0, 1 ); if ( camper < 0.1 ) { return qfalse; } //if the bot has a team goal if ( bs->ltgtype == LTG_TEAMHELP || bs->ltgtype == LTG_TEAMACCOMPANY || bs->ltgtype == LTG_DEFENDKEYAREA || bs->ltgtype == LTG_GETFLAG || bs->ltgtype == LTG_RUSHBASE || bs->ltgtype == LTG_CAMP || bs->ltgtype == LTG_CAMPORDER || bs->ltgtype == LTG_PATROL ) { return qfalse; } //if camped recently if ( bs->camp_time > trap_AAS_Time() - 60 + 300 * ( 1 - camper ) ) { return qfalse; } // if ( random() > camper ) { bs->camp_time = trap_AAS_Time(); return qfalse; } //if the bot isn't healthy anough if ( BotAggression( bs ) < 50 ) { return qfalse; } //the bot should have at least have the rocket launcher, the railgun or the bfg10k with some ammo if ( ( bs->inventory[INVENTORY_ROCKETLAUNCHER] <= 0 || bs->inventory[INVENTORY_ROCKETS < 10] ) // && (bs->inventory[INVENTORY_RAILGUN] <= 0 || bs->inventory[INVENTORY_SLUGS] < 10) // && (bs->inventory[INVENTORY_BFG10K] <= 0 || bs->inventory[INVENTORY_BFGAMMO] < 10) ) { return qfalse; } //find the closest camp spot besttraveltime = 99999; for ( cs = trap_BotGetNextCampSpotGoal( 0, &goal ); cs; cs = trap_BotGetNextCampSpotGoal( cs, &goal ) ) { traveltime = trap_AAS_AreaTravelTimeToGoalArea( bs->areanum, bs->origin, goal.areanum, TFL_DEFAULT ); if ( traveltime && traveltime < besttraveltime ) { besttraveltime = traveltime; memcpy( &bestgoal, &goal, sizeof( bot_goal_t ) ); } } if ( besttraveltime > 150 ) { return qfalse; } //ok found a camp spot, go camp there BotGoCamp( bs, &bestgoal ); // return qtrue; } /* ================== BotDontAvoid ================== */ void BotDontAvoid( bot_state_t *bs, char *itemname ) { bot_goal_t goal; int num; num = trap_BotGetLevelItemGoal( -1, itemname, &goal ); while ( num >= 0 ) { trap_BotRemoveFromAvoidGoals( bs->gs, goal.number ); num = trap_BotGetLevelItemGoal( num, itemname, &goal ); } } /* ================== BotGoForPowerups ================== */ void BotGoForPowerups( bot_state_t *bs ) { //don't avoid any of the powerups anymore BotDontAvoid( bs, "Quad Damage" ); BotDontAvoid( bs, "Regeneration" ); BotDontAvoid( bs, "Battle Suit" ); BotDontAvoid( bs, "Speed" ); BotDontAvoid( bs, "Invisibility" ); //BotDontAvoid(bs, "Flight"); //reset the long term goal time so the bot will go for the powerup //NOTE: the long term goal type doesn't change bs->ltg_time = 0; } /* ================== BotRoamGoal ================== */ void BotRoamGoal( bot_state_t *bs, vec3_t goal ) { float len, r1, r2, sign, n; int pc; vec3_t dir, bestorg, belowbestorg; bsp_trace_t trace; for ( n = 0; n < 10; n++ ) { //start at the bot origin VectorCopy( bs->origin, bestorg ); r1 = random(); if ( r1 < 0.8 ) { //add a random value to the x-coordinate r2 = random(); if ( r2 < 0.5 ) { sign = -1; } else { sign = 1;} bestorg[0] += sign * 700 * random() + 50; } if ( r1 > 0.2 ) { //add a random value to the y-coordinate r2 = random(); if ( r2 < 0.5 ) { sign = -1; } else { sign = 1;} bestorg[1] += sign * 700 * random() + 50; } //add a random value to the z-coordinate (NOTE: 48 = maxjump?) bestorg[2] += 3 * 48 * random() - 2 * 48 - 1; //trace a line from the origin to the roam target BotAI_Trace( &trace, bs->origin, NULL, NULL, bestorg, bs->entitynum, MASK_SOLID ); //direction and length towards the roam target VectorSubtract( bestorg, bs->origin, dir ); len = VectorNormalize( dir ); //if the roam target is far away anough if ( len > 200 ) { //the roam target is in the given direction before walls VectorScale( dir, len * trace.fraction - 40, dir ); VectorAdd( bs->origin, dir, bestorg ); //get the coordinates of the floor below the roam target belowbestorg[0] = bestorg[0]; belowbestorg[1] = bestorg[1]; belowbestorg[2] = bestorg[2] - 800; BotAI_Trace( &trace, bestorg, NULL, NULL, belowbestorg, bs->entitynum, MASK_SOLID ); // if ( !trace.startsolid ) { trace.endpos[2]++; pc = trap_PointContents( trace.endpos,bs->entitynum ); if ( !( pc & CONTENTS_LAVA ) ) { //----(SA) modified since slime is no longer deadly // if (!(pc & (CONTENTS_LAVA | CONTENTS_SLIME))) { VectorCopy( bestorg, goal ); return; } } } } VectorCopy( bestorg, goal ); } /* ================== BotAttackMove ================== */ bot_moveresult_t BotAttackMove( bot_state_t *bs, int tfl ) { int movetype, i; float attack_skill, jumper, croucher, dist, strafechange_time; float attack_dist, attack_range; vec3_t forward, backward, sideward, hordir, up = {0, 0, 1}; aas_entityinfo_t entinfo; bot_moveresult_t moveresult; bot_goal_t goal; if ( bs->attackchase_time > trap_AAS_Time() ) { //create the chase goal goal.entitynum = bs->enemy; goal.areanum = bs->lastenemyareanum; VectorCopy( bs->lastenemyorigin, goal.origin ); VectorSet( goal.mins, -8, -8, -8 ); VectorSet( goal.maxs, 8, 8, 8 ); //initialize the movement state BotSetupForMovement( bs ); //move towards the goal trap_BotMoveToGoal( &moveresult, bs->ms, &goal, tfl ); return moveresult; } // memset( &moveresult, 0, sizeof( bot_moveresult_t ) ); // attack_skill = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_ATTACK_SKILL, 0, 1 ); jumper = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_JUMPER, 0, 1 ); croucher = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_CROUCHER, 0, 1 ); //if the bot is really stupid if ( attack_skill < 0.2 ) { return moveresult; } //initialize the movement state BotSetupForMovement( bs ); //get the enemy entity info BotEntityInfo( bs->enemy, &entinfo ); //direction towards the enemy VectorSubtract( entinfo.origin, bs->origin, forward ); //the distance towards the enemy dist = VectorNormalize( forward ); VectorNegate( forward, backward ); //walk, crouch or jump movetype = MOVE_WALK; // if ( bs->attackcrouch_time < trap_AAS_Time() - 1 ) { if ( random() < jumper ) { movetype = MOVE_JUMP; } //wait at least one second before crouching again else if ( bs->attackcrouch_time < trap_AAS_Time() - 1 && random() < croucher ) { bs->attackcrouch_time = trap_AAS_Time() + croucher * 5; } } if ( bs->attackcrouch_time > trap_AAS_Time() ) { movetype = MOVE_CROUCH; } //if the bot should jump if ( movetype == MOVE_JUMP ) { //if jumped last frame if ( bs->attackjump_time > trap_AAS_Time() ) { movetype = MOVE_WALK; } else { bs->attackjump_time = trap_AAS_Time() + 1; } } if ( bs->cur_ps.weapon == WP_GAUNTLET ) { attack_dist = 0; attack_range = 0; } else { attack_dist = IDEAL_ATTACKDIST; attack_range = 40; } //if the bot is stupid if ( attack_skill <= 0.4 ) { //just walk to or away from the enemy if ( dist > attack_dist + attack_range ) { if ( trap_BotMoveInDirection( bs->ms, forward, 400, movetype ) ) { return moveresult; } } if ( dist < attack_dist - attack_range ) { if ( trap_BotMoveInDirection( bs->ms, backward, 400, movetype ) ) { return moveresult; } } return moveresult; } //increase the strafe time bs->attackstrafe_time += bs->thinktime; //get the strafe change time strafechange_time = 0.4 + ( 1 - attack_skill ) * 0.2; if ( attack_skill > 0.7 ) { strafechange_time += crandom() * 0.2; } //if the strafe direction should be changed if ( bs->attackstrafe_time > strafechange_time ) { //some magic number :) if ( random() > 0.935 ) { //flip the strafe direction bs->flags ^= BFL_STRAFERIGHT; bs->attackstrafe_time = 0; } } // for ( i = 0; i < 2; i++ ) { hordir[0] = forward[0]; hordir[1] = forward[1]; hordir[2] = 0; VectorNormalize( hordir ); //get the sideward vector CrossProduct( hordir, up, sideward ); //reverse the vector depending on the strafe direction if ( bs->flags & BFL_STRAFERIGHT ) { VectorNegate( sideward, sideward ); } //randomly go back a little if ( random() > 0.9 ) { VectorAdd( sideward, backward, sideward ); } else { //walk forward or backward to get at the ideal attack distance if ( dist > attack_dist + attack_range ) { VectorAdd( sideward, forward, sideward ); } else if ( dist < attack_dist - attack_range ) { VectorAdd( sideward, backward, sideward ); } } //perform the movement if ( trap_BotMoveInDirection( bs->ms, sideward, 400, movetype ) ) { return moveresult; } //movement failed, flip the strafe direction bs->flags ^= BFL_STRAFERIGHT; bs->attackstrafe_time = 0; } //bot couldn't do any usefull movement // bs->attackchase_time = AAS_Time() + 6; return moveresult; } /* ================== BotSameTeam ================== */ int BotSameTeam( bot_state_t *bs, int entnum ) { char info1[128], info2[128]; if ( bs->client < 0 || bs->client >= MAX_CLIENTS ) { //BotAI_Print(PRT_ERROR, "BotSameTeam: client out of range\n"); return qfalse; } if ( entnum < 0 || entnum >= MAX_CLIENTS ) { //BotAI_Print(PRT_ERROR, "BotSameTeam: client out of range\n"); return qfalse; } if ( gametype == GT_TEAM || gametype == GT_CTF ) { trap_GetConfigstring( CS_PLAYERS + bs->client, info1, sizeof( info1 ) ); trap_GetConfigstring( CS_PLAYERS + entnum, info2, sizeof( info2 ) ); // if ( atoi( Info_ValueForKey( info1, "t" ) ) == atoi( Info_ValueForKey( info2, "t" ) ) ) { return qtrue; } } return qfalse; } /* ================== InFieldOfVision ================== */ qboolean InFieldOfVision( vec3_t viewangles, float fov, vec3_t angles ) { int i; float diff, angle; for ( i = 0; i < 2; i++ ) { angle = AngleMod( viewangles[i] ); angles[i] = AngleMod( angles[i] ); diff = angles[i] - angle; if ( angles[i] > angle ) { if ( diff > 180.0 ) { diff -= 360.0; } } else { if ( diff < -180.0 ) { diff += 360.0; } } if ( diff > 0 ) { if ( diff > fov * 0.5 ) { return qfalse; } } else { if ( diff < -fov * 0.5 ) { return qfalse; } } } return qtrue; } /* ================== BotEntityVisible returns visibility in the range [0, 1] taking fog and water surfaces into account ================== */ float BotEntityVisible( int viewer, vec3_t eye, vec3_t viewangles, float fov, int ent ) { int i, contents_mask, passent, hitent, infog, inwater, otherinfog, pc; float fogdist, waterfactor, vis, bestvis; bsp_trace_t trace; aas_entityinfo_t entinfo; vec3_t dir, entangles, start, end, middle; //calculate middle of bounding box BotEntityInfo( ent, &entinfo ); VectorAdd( entinfo.mins, entinfo.maxs, middle ); VectorScale( middle, 0.5, middle ); VectorAdd( entinfo.origin, middle, middle ); //check if entity is within field of vision VectorSubtract( middle, eye, dir ); vectoangles( dir, entangles ); if ( !InFieldOfVision( viewangles, fov, entangles ) ) { return 0; } // pc = trap_AAS_PointContents( eye ); infog = ( pc & CONTENTS_SOLID ); inwater = ( pc & ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ) ); // bestvis = 0; for ( i = 0; i < 3; i++ ) { //if the point is not in potential visible sight //if (!AAS_inPVS(eye, middle)) continue; // contents_mask = CONTENTS_SOLID | CONTENTS_PLAYERCLIP; passent = viewer; hitent = ent; VectorCopy( eye, start ); VectorCopy( middle, end ); //if the entity is in water, lava or slime if ( trap_AAS_PointContents( middle ) & ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ) ) { contents_mask |= ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ); } //if eye is in water, lava or slime if ( inwater ) { if ( !( contents_mask & ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ) ) ) { passent = ent; hitent = viewer; VectorCopy( middle, start ); VectorCopy( eye, end ); } contents_mask ^= ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ); } //trace from start to end BotAI_Trace( &trace, start, NULL, NULL, end, passent, contents_mask ); //if water was hit waterfactor = 1.0; if ( trace.contents & ( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ) ) { //if the water surface is translucent if ( 1 ) { //trace through the water contents_mask &= ~( CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER ); BotAI_Trace( &trace, trace.endpos, NULL, NULL, end, passent, contents_mask ); waterfactor = 0.5; } } //if a full trace or the hitent was hit if ( trace.fraction >= 1 || trace.ent == hitent ) { //check for fog, assuming there's only one fog brush where //either the viewer or the entity is in or both are in otherinfog = ( trap_AAS_PointContents( middle ) & CONTENTS_FOG ); if ( infog && otherinfog ) { VectorSubtract( trace.endpos, eye, dir ); fogdist = VectorLength( dir ); } else if ( infog ) { VectorCopy( trace.endpos, start ); BotAI_Trace( &trace, start, NULL, NULL, eye, viewer, CONTENTS_FOG ); VectorSubtract( eye, trace.endpos, dir ); fogdist = VectorLength( dir ); } else if ( otherinfog ) { VectorCopy( trace.endpos, end ); BotAI_Trace( &trace, eye, NULL, NULL, end, viewer, CONTENTS_FOG ); VectorSubtract( end, trace.endpos, dir ); fogdist = VectorLength( dir ); } else { //if the entity and the viewer are not in fog assume there's no fog in between fogdist = 0; } //decrease visibility with the view distance through fog vis = 1 / ( ( fogdist * fogdist * 0.001 ) < 1 ? 1 : ( fogdist * fogdist * 0.001 ) ); //if entering water visibility is reduced vis *= waterfactor; // if ( vis > bestvis ) { bestvis = vis; } //if pretty much no fog if ( bestvis >= 0.95 ) { return bestvis; } } //check bottom and top of bounding box as well if ( i == 0 ) { middle[2] += entinfo.mins[2]; } else if ( i == 1 ) { middle[2] += entinfo.maxs[2] - entinfo.mins[2]; } } return bestvis; } /* ================== BotFindEnemy ================== */ int BotFindEnemy( bot_state_t *bs, int curenemy ) { int i, healthdecrease; float fov, dist, curdist, alertness, easyfragger, vis; aas_entityinfo_t entinfo, curenemyinfo; vec3_t dir, angles; alertness = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_ALERTNESS, 0, 1 ); easyfragger = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_EASY_FRAGGER, 0, 1 ); //check if the health decreased healthdecrease = bs->lasthealth > bs->inventory[INVENTORY_HEALTH]; //remember the current health value bs->lasthealth = bs->inventory[INVENTORY_HEALTH]; // if ( curenemy >= 0 ) { BotEntityInfo( curenemy, &curenemyinfo ); VectorSubtract( curenemyinfo.origin, bs->origin, dir ); curdist = VectorLength( dir ); } else { curdist = 0; } // for ( i = 0; i < MAX_CLIENTS; i++ ) { if ( i == bs->client ) { continue; } //if it's the current enemy if ( i == curenemy ) { continue; } // BotEntityInfo( i, &entinfo ); // if ( !entinfo.valid ) { continue; } //if the enemy isn't dead and the enemy isn't the bot self if ( EntityIsDead( &entinfo ) || entinfo.number == bs->entitynum ) { continue; } //if the enemy is invisible and not shooting if ( EntityIsInvisible( &entinfo ) && !EntityIsShooting( &entinfo ) ) { continue; } //if not an easy fragger don't shoot at chatting players if ( easyfragger < 0.5 && EntityIsChatting( &entinfo ) ) { continue; } // if ( lastteleport_time > trap_AAS_Time() - 3 ) { VectorSubtract( entinfo.origin, lastteleport_origin, dir ); if ( VectorLength( dir ) < 70 ) { continue; } } //calculate the distance towards the enemy VectorSubtract( entinfo.origin, bs->origin, dir ); dist = VectorLength( dir ); //if this enemy is further away than the current one if ( curenemy >= 0 && dist > curdist ) { continue; } //if the bot has no if ( dist > 900 + alertness * 4000 ) { continue; } //if on the same team if ( BotSameTeam( bs, i ) ) { continue; } //if the bot's health decreased or the enemy is shooting if ( curenemy < 0 && ( healthdecrease || EntityIsShooting( &entinfo ) ) ) { fov = 360; } else { fov = 90 + 270 - ( 270 - ( dist > 810 ? 810 : dist ) / 3 );} //check if the enemy visibility vis = BotEntityVisible( bs->entitynum, bs->eye, bs->viewangles, fov, i ); if ( vis <= 0 ) { continue; } //if the enemy is quite far away, not shooting and the bot is not damaged if ( curenemy < 0 && dist > 200 && !healthdecrease && !EntityIsShooting( &entinfo ) ) { //check if we can avoid this enemy VectorSubtract( bs->origin, entinfo.origin, dir ); vectoangles( dir, angles ); //if the bot isn't in the fov of the enemy if ( !InFieldOfVision( entinfo.angles, 120, angles ) ) { //update some stuff for this enemy BotUpdateBattleInventory( bs, i ); //if the bot doesn't really want to fight if ( BotWantsToRetreat( bs ) ) { continue; } } } //found an enemy bs->enemy = entinfo.number; if ( curenemy >= 0 ) { bs->enemysight_time = trap_AAS_Time() - 2; } else { bs->enemysight_time = trap_AAS_Time();} bs->enemysuicide = qfalse; bs->enemydeath_time = 0; return qtrue; } return qfalse; } /* ================== BotAimAtEnemy ================== */ void BotAimAtEnemy( bot_state_t *bs ) { int i, enemyvisible; float dist, f, aim_skill, aim_accuracy, speed, reactiontime; vec3_t dir, bestorigin, end, start, groundtarget, cmdmove, enemyvelocity; vec3_t mins = {-4,-4,-4}, maxs = {4, 4, 4}; weaponinfo_t wi; aas_entityinfo_t entinfo; bot_goal_t goal; bsp_trace_t trace; vec3_t target; //if the bot has no enemy if ( bs->enemy < 0 ) { return; } // //BotAI_Print(PRT_MESSAGE, "client %d: aiming at client %d\n", bs->entitynum, bs->enemy); // aim_skill = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_SKILL, 0, 1 ); aim_accuracy = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_ACCURACY, 0, 1 ); // if ( aim_skill > 0.95 ) { //don't aim too early reactiontime = 0.5 * trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_REACTIONTIME, 0, 1 ); if ( bs->enemysight_time > trap_AAS_Time() - reactiontime ) { return; } if ( bs->teleport_time > trap_AAS_Time() - reactiontime ) { return; } } //get the weapon information trap_BotGetWeaponInfo( bs->ws, bs->weaponnum, &wi ); //get the weapon specific aim accuracy and or aim skill //----(SA) commented out the weapons that aren't ours. //----(SA) if we're not using this routine at all and my changes are irrelivant, please let me know. // if (wi.number == WP_MACHINEGUN) { // aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_MACHINEGUN, 0, 1); // } // if (wi.number == WP_SHOTGUN) { // aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_SHOTGUN, 0, 1); // } if ( wi.number == WP_GRENADE_LAUNCHER ) { aim_accuracy = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_ACCURACY_GRENADELAUNCHER, 0, 1 ); aim_skill = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_SKILL_GRENADELAUNCHER, 0, 1 ); } if ( wi.number == WP_FLAMETHROWER ) { aim_accuracy = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_ACCURACY_LIGHTNING, 0, 1 ); } // if (wi.number == WP_RAILGUN) { // aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_RAILGUN, 0, 1); // } if ( wi.number == WP_SILENCER ) { aim_accuracy = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_ACCURACY_SP5, 0, 1 ); aim_skill = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_SKILL_SP5, 0, 1 ); } // if (wi.number == WP_BFG) { // aim_accuracy = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_BFG10K, 0, 1); // aim_skill = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL_BFG10K, 0, 1); // } // if ( aim_accuracy <= 0 ) { aim_accuracy = 0.0001; } //get the enemy entity information BotEntityInfo( bs->enemy, &entinfo ); //if the enemy is invisible then shoot crappy most of the time if ( EntityIsInvisible( &entinfo ) ) { if ( random() > 0.1 ) { aim_accuracy *= 0.4; } } // VectorSubtract( entinfo.origin, entinfo.lastvisorigin, enemyvelocity ); VectorScale( enemyvelocity, 1 / entinfo.update_time, enemyvelocity ); //enemy origin and velocity is remembered every 0.5 seconds if ( bs->enemyposition_time < trap_AAS_Time() ) { // bs->enemyposition_time = trap_AAS_Time() + 0.5; VectorCopy( enemyvelocity, bs->enemyvelocity ); VectorCopy( entinfo.origin, bs->enemyorigin ); } //if not extremely skilled if ( aim_skill < 0.9 ) { VectorSubtract( entinfo.origin, bs->enemyorigin, dir ); //if the enemy moved a bit if ( VectorLength( dir ) > 48 ) { //if the enemy changed direction if ( DotProduct( bs->enemyvelocity, enemyvelocity ) < 0 ) { //aim accuracy should be worse now aim_accuracy *= 0.7; } } } //check visibility of enemy enemyvisible = BotEntityVisible( bs->entitynum, bs->eye, bs->viewangles, 360, bs->enemy ); //if the enemy is visible if ( enemyvisible ) { // VectorCopy( entinfo.origin, bestorigin ); bestorigin[2] += 8; //get the start point shooting from //NOTE: the x and y projectile start offsets are ignored VectorCopy( bs->origin, start ); start[2] += bs->cur_ps.viewheight; start[2] += wi.offset[2]; // BotAI_Trace( &trace, start, mins, maxs, bestorigin, bs->entitynum, MASK_SHOT ); //if the enemy is NOT hit if ( trace.fraction <= 1 && trace.ent != entinfo.number ) { bestorigin[2] += 16; } //if it is not an instant hit weapon the bot might want to predict the enemy if ( wi.speed ) { // VectorSubtract( bestorigin, bs->origin, dir ); dist = VectorLength( dir ); VectorSubtract( entinfo.origin, bs->enemyorigin, dir ); //if the enemy is NOT pretty far away and strafing just small steps left and right if ( !( dist > 100 && VectorLength( dir ) < 32 ) ) { //if skilled anough do exact prediction if ( aim_skill > 0.8 && //if the weapon is ready to fire bs->cur_ps.weaponstate == WEAPON_READY ) { aas_clientmove_t move; vec3_t origin; VectorSubtract( entinfo.origin, bs->origin, dir ); //distance towards the enemy dist = VectorLength( dir ); //direction the enemy is moving in VectorSubtract( entinfo.origin, entinfo.lastvisorigin, dir ); // VectorScale( dir, 1 / entinfo.update_time, dir ); // VectorCopy( entinfo.origin, origin ); origin[2] += 1; // VectorClear( cmdmove ); //AAS_ClearShownDebugLines(); trap_AAS_PredictClientMovement( &move, bs->enemy, origin, PRESENCE_CROUCH, qfalse, dir, cmdmove, 0, dist * 10 / wi.speed, 0.1, 0, 0, qfalse ); VectorCopy( move.endpos, bestorigin ); //BotAI_Print(PRT_MESSAGE, "%1.1f predicted speed = %f, frames = %f\n", trap_AAS_Time(), VectorLength(dir), dist * 10 / wi.speed); } //if not that skilled do linear prediction else if ( aim_skill > 0.4 ) { VectorSubtract( entinfo.origin, bs->origin, dir ); //distance towards the enemy dist = VectorLength( dir ); //direction the enemy is moving in VectorSubtract( entinfo.origin, entinfo.lastvisorigin, dir ); dir[2] = 0; // speed = VectorNormalize( dir ) / entinfo.update_time; //botimport.Print(PRT_MESSAGE, "speed = %f, wi->speed = %f\n", speed, wi->speed); //best spot to aim at VectorMA( entinfo.origin, ( dist / wi.speed ) * speed, dir, bestorigin ); } } } //if the projectile does radial damage if ( aim_skill > 0.6 && wi.proj.damagetype & DAMAGETYPE_RADIAL ) { //if the enemy isn't standing significantly higher than the bot if ( entinfo.origin[2] < bs->origin[2] + 16 ) { //try to aim at the ground in front of the enemy VectorCopy( entinfo.origin, end ); end[2] -= 64; BotAI_Trace( &trace, entinfo.origin, NULL, NULL, end, entinfo.number, MASK_SHOT ); // VectorCopy( bestorigin, groundtarget ); if ( trace.startsolid ) { groundtarget[2] = entinfo.origin[2] - 16; } else { groundtarget[2] = trace.endpos[2] - 8;} //trace a line from projectile start to ground target BotAI_Trace( &trace, start, NULL, NULL, groundtarget, bs->entitynum, MASK_SHOT ); //if hitpoint is not vertically too far from the ground target if ( fabs( trace.endpos[2] - groundtarget[2] ) < 50 ) { VectorSubtract( trace.endpos, groundtarget, dir ); //if the hitpoint is near anough the ground target if ( VectorLength( dir ) < 60 ) { VectorSubtract( trace.endpos, start, dir ); //if the hitpoint is far anough from the bot if ( VectorLength( dir ) > 100 ) { //check if the bot is visible from the ground target trace.endpos[2] += 1; BotAI_Trace( &trace, trace.endpos, NULL, NULL, entinfo.origin, entinfo.number, MASK_SHOT ); if ( trace.fraction >= 1 ) { //botimport.Print(PRT_MESSAGE, "%1.1f aiming at ground\n", AAS_Time()); VectorCopy( groundtarget, bestorigin ); } } } } } } bestorigin[0] += 20 * crandom() * ( 1 - aim_accuracy ); bestorigin[1] += 20 * crandom() * ( 1 - aim_accuracy ); bestorigin[2] += 10 * crandom() * ( 1 - aim_accuracy ); } else { // VectorCopy( bs->lastenemyorigin, bestorigin ); bestorigin[2] += 8; //if the bot is skilled anough if ( aim_skill > 0.5 ) { //do prediction shots around corners // if (wi.number == WP_BFG || //----(SA) removing old weapon references if ( wi.number == WP_GRENADE_LAUNCHER ) { //create the chase goal goal.entitynum = bs->client; goal.areanum = bs->areanum; VectorCopy( bs->eye, goal.origin ); VectorSet( goal.mins, -8, -8, -8 ); VectorSet( goal.maxs, 8, 8, 8 ); // if ( trap_BotPredictVisiblePosition( bs->lastenemyorigin, bs->lastenemyareanum, &goal, TFL_DEFAULT, target ) ) { VectorCopy( target, bestorigin ); bestorigin[2] -= 20; } aim_accuracy = 1; } } } // if ( enemyvisible ) { BotAI_Trace( &trace, bs->eye, NULL, NULL, bestorigin, bs->entitynum, MASK_SHOT ); VectorCopy( trace.endpos, bs->aimtarget ); } else { VectorCopy( bestorigin, bs->aimtarget ); } //get aim direction VectorSubtract( bestorigin, bs->eye, dir ); // if ( wi.number == WP_FLAMETHROWER ) { // if (wi.number == WP_MACHINEGUN || //----(SA) removing old weapon references // wi.number == WP_SHOTGUN || // wi.number == WP_RAILGUN) { //distance towards the enemy dist = VectorLength( dir ); if ( dist > 150 ) { dist = 150; } f = 0.6 + dist / 150 * 0.4; aim_accuracy *= f; } //add some random stuff to the aim direction depending on the aim accuracy if ( aim_accuracy < 0.8 ) { VectorNormalize( dir ); for ( i = 0; i < 3; i++ ) dir[i] += 0.3 * crandom() * ( 1 - aim_accuracy ); } //set the ideal view angles vectoangles( dir, bs->ideal_viewangles ); //take the weapon spread into account for lower skilled bots bs->ideal_viewangles[PITCH] += 6 * wi.vspread * crandom() * ( 1 - aim_accuracy ); bs->ideal_viewangles[PITCH] = AngleMod( bs->ideal_viewangles[PITCH] ); bs->ideal_viewangles[YAW] += 6 * wi.hspread * crandom() * ( 1 - aim_accuracy ); bs->ideal_viewangles[YAW] = AngleMod( bs->ideal_viewangles[YAW] ); //if the bot is really accurate and has the enemy in view for some time if ( aim_accuracy > 0.9 && bs->enemysight_time < trap_AAS_Time() - 1 ) { //set the view angles directly if ( bs->ideal_viewangles[PITCH] > 180 ) { bs->ideal_viewangles[PITCH] -= 360; } VectorCopy( bs->ideal_viewangles, bs->viewangles ); trap_EA_View( bs->client, bs->viewangles ); } } /* ================== BotCheckAttack ================== */ void BotCheckAttack( bot_state_t *bs ) { float points, reactiontime, fov, firethrottle; bsp_trace_t bsptrace; //float selfpreservation; vec3_t forward, right, start, end, dir, angles; weaponinfo_t wi; bsp_trace_t trace; aas_entityinfo_t entinfo; vec3_t mins = {-8, -8, -8}, maxs = {8, 8, 8}; if ( bs->enemy < 0 ) { return; } // reactiontime = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_REACTIONTIME, 0, 1 ); if ( bs->enemysight_time > trap_AAS_Time() - reactiontime ) { return; } if ( bs->teleport_time > trap_AAS_Time() - reactiontime ) { return; } //if changing weapons if ( bs->weaponchange_time > trap_AAS_Time() - 0.1 ) { return; } //check fire throttle characteristic if ( bs->firethrottlewait_time > trap_AAS_Time() ) { return; } firethrottle = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_FIRETHROTTLE, 0, 1 ); if ( bs->firethrottleshoot_time < trap_AAS_Time() ) { if ( random() > firethrottle ) { bs->firethrottlewait_time = trap_AAS_Time() + firethrottle; bs->firethrottleshoot_time = 0; } else { bs->firethrottleshoot_time = trap_AAS_Time() + 1 - firethrottle; bs->firethrottlewait_time = 0; } } // BotEntityInfo( bs->enemy, &entinfo ); VectorSubtract( entinfo.origin, bs->eye, dir ); // if ( VectorLength( dir ) < 100 ) { fov = 120; } else { fov = 50;} /* //if the enemy isn't visible if (!BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, fov, bs->enemy)) { //botimport.Print(PRT_MESSAGE, "enemy not visible\n"); return; }*/ vectoangles( dir, angles ); if ( !InFieldOfVision( bs->viewangles, fov, angles ) ) { return; } BotAI_Trace( &bsptrace, bs->eye, NULL, NULL, bs->aimtarget, bs->client, CONTENTS_SOLID | CONTENTS_PLAYERCLIP ); if ( bsptrace.fraction < 1 && bsptrace.ent != bs->enemy ) { return; } //get the weapon info trap_BotGetWeaponInfo( bs->ws, bs->weaponnum, &wi ); //get the start point shooting from VectorCopy( bs->origin, start ); start[2] += bs->cur_ps.viewheight; AngleVectors( bs->viewangles, forward, right, NULL ); start[0] += forward[0] * wi.offset[0] + right[0] * wi.offset[1]; start[1] += forward[1] * wi.offset[0] + right[1] * wi.offset[1]; start[2] += forward[2] * wi.offset[0] + right[2] * wi.offset[1] + wi.offset[2]; //end point aiming at VectorMA( start, 1000, forward, end ); //a little back to make sure not inside a very close enemy VectorMA( start, -12, forward, start ); BotAI_Trace( &trace, start, mins, maxs, end, bs->entitynum, MASK_SHOT ); //----(SA) should this maybe check the weapon type and adjust the clipflag? it seems like this is probably fine as-is, but I thought I'd note it. //if won't hit the enemy if ( trace.ent != bs->enemy ) { //if the entity is a client if ( trace.ent > 0 && trace.ent <= MAX_CLIENTS ) { //if a teammate is hit if ( BotSameTeam( bs, trace.ent ) ) { return; } } //if the projectile does a radial damage if ( wi.proj.damagetype & DAMAGETYPE_RADIAL ) { if ( trace.fraction * 1000 < wi.proj.radius ) { points = ( wi.proj.damage - 0.5 * trace.fraction * 1000 ) * 0.5; if ( points > 0 ) { // selfpreservation = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_SELFPRESERVATION, 0, 1); // if (random() < selfpreservation) return; return; } } //FIXME: check if a teammate gets radial damage } } //if fire has to be release to activate weapon if ( wi.flags & WFL_FIRERELEASED ) { if ( bs->flags & BFL_ATTACKED ) { trap_EA_Attack( bs->client ); } } else { trap_EA_Attack( bs->client ); } bs->flags ^= BFL_ATTACKED; } /* ================== BotMapScripts ================== */ void BotMapScripts( bot_state_t *bs ) { char info[1024]; char mapname[128]; int i, shootbutton; float aim_accuracy; aas_entityinfo_t entinfo; vec3_t dir; trap_GetServerinfo( info, sizeof( info ) ); strncpy( mapname, Info_ValueForKey( info, "mapname" ), sizeof( mapname ) - 1 ); mapname[sizeof( mapname ) - 1] = '\0'; if ( !Q_stricmp( mapname, "q3tourney6" ) ) { vec3_t mins = {700, 204, 672}, maxs = {964, 468, 680}; vec3_t buttonorg = {304, 352, 920}; //NOTE: NEVER use the func_bobbing in q3tourney6 bs->tfl &= ~TFL_FUNCBOB; //if the bot is below the bounding box if ( bs->origin[0] > mins[0] && bs->origin[0] < maxs[0] ) { if ( bs->origin[1] > mins[1] && bs->origin[1] < maxs[1] ) { if ( bs->origin[2] < mins[2] ) { return; } } } shootbutton = qfalse; //if an enemy is below this bounding box then shoot the button for ( i = 0; i < MAX_CLIENTS; i++ ) { if ( i == bs->client ) { continue; } // BotEntityInfo( i, &entinfo ); // if ( !entinfo.valid ) { continue; } //if the enemy isn't dead and the enemy isn't the bot self if ( EntityIsDead( &entinfo ) || entinfo.number == bs->entitynum ) { continue; } // if ( entinfo.origin[0] > mins[0] && entinfo.origin[0] < maxs[0] ) { if ( entinfo.origin[1] > mins[1] && entinfo.origin[1] < maxs[1] ) { if ( entinfo.origin[2] < mins[2] ) { //if there's a team mate below the crusher if ( BotSameTeam( bs, i ) ) { shootbutton = qfalse; break; } else { shootbutton = qtrue; } } } } } if ( shootbutton ) { bs->flags |= BFL_IDEALVIEWSET; VectorSubtract( buttonorg, bs->eye, dir ); vectoangles( dir, bs->ideal_viewangles ); aim_accuracy = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_AIM_ACCURACY, 0, 1 ); bs->ideal_viewangles[PITCH] += 8 * crandom() * ( 1 - aim_accuracy ); bs->ideal_viewangles[PITCH] = AngleMod( bs->ideal_viewangles[PITCH] ); bs->ideal_viewangles[YAW] += 8 * crandom() * ( 1 - aim_accuracy ); bs->ideal_viewangles[YAW] = AngleMod( bs->ideal_viewangles[YAW] ); // if ( InFieldOfVision( bs->viewangles, 20, bs->ideal_viewangles ) ) { trap_EA_Attack( bs->client ); } } } } /* ================== BotCheckButtons ================== */ /* void CheckButtons(void) { int modelindex, i, numbuttons = 0; char *classname, *model; float lip, health, dist; bsp_entity_t *ent; vec3_t mins, maxs, size, origin, angles, movedir, goalorigin; vec3_t start, end, bboxmins, bboxmaxs; aas_trace_t trace; for (ent = entities; ent; ent = ent->next) { classname = AAS_ValueForBSPEpairKey(ent, "classname"); if (!strcmp(classname, "func_button")) { //create a bot goal towards the button model = AAS_ValueForBSPEpairKey(ent, "model"); modelindex = AAS_IndexFromModel(model); //if the model is not loaded if (!modelindex) modelindex = atoi(model+1); VectorClear(angles); AAS_BSPModelMinsMaxsOrigin(modelindex - 1, angles, mins, maxs, NULL); //get the lip of the button lip = AAS_FloatForBSPEpairKey(ent, "lip"); if (!lip) lip = 4; //get the move direction from the angle VectorSet(angles, 0, AAS_FloatForBSPEpairKey(ent, "angle"), 0); AAS_SetMovedir(angles, movedir); //button size VectorSubtract(maxs, mins, size); //button origin VectorAdd(mins, maxs, origin); VectorScale(origin, 0.5, origin); //touch distance of the button dist = fabs(movedir[0]) * size[0] + fabs(movedir[1]) * size[1] + fabs(movedir[2]) * size[2];// - lip; dist *= 0.5; // health = AAS_FloatForBSPEpairKey(ent, "health"); //if the button is shootable if (health) { //calculate the goal origin VectorMA(origin, -dist, movedir, goalorigin); AAS_DrawPermanentCross(goalorigin, 4, LINECOLOR_BLUE); } //end if else { //add bounding box size to the dist AAS_PresenceTypeBoundingBox(PRESENCE_CROUCH, bboxmins, bboxmaxs); for (i = 0; i < 3; i++) { if (movedir[i] < 0) dist += fabs(movedir[i]) * fabs(bboxmaxs[i]); else dist += fabs(movedir[i]) * fabs(bboxmins[i]); } //end for //calculate the goal origin VectorMA(origin, -dist, movedir, goalorigin); // VectorCopy(goalorigin, start); start[2] += 24; VectorSet(end, start[0], start[1], start[2] - 100); trace = AAS_TraceClientBBox(start, end, PRESENCE_CROUCH, -1); if (!trace.startsolid) { VectorCopy(trace.endpos, goalorigin); } //end if // AAS_DrawPermanentCross(goalorigin, 4, LINECOLOR_YELLOW); // VectorSubtract(mins, origin, mins); VectorSubtract(maxs, origin, maxs); // VectorAdd(mins, origin, start); AAS_DrawPermanentCross(start, 4, LINECOLOR_BLUE); VectorAdd(maxs, origin, start); AAS_DrawPermanentCross(start, 4, LINECOLOR_BLUE); } //end else if (++numbuttons > 5) return; } //end if } //end for } //end of the function CheckButtons */ /* ================== BotEntityToActivate ================== */ //#define OBSTACLEDEBUG int BotEntityToActivate( int entitynum ) { int i, ent, cur_entities[10]; char model[MAX_INFO_STRING], tmpmodel[128]; char target[128], classname[128]; float health; char targetname[10][128]; aas_entityinfo_t entinfo; BotEntityInfo( entitynum, &entinfo ); Com_sprintf( model, sizeof( model ), "*%d", entinfo.modelindex ); for ( ent = trap_AAS_NextBSPEntity( 0 ); ent; ent = trap_AAS_NextBSPEntity( ent ) ) { if ( !trap_AAS_ValueForBSPEpairKey( ent, "model", tmpmodel, sizeof( tmpmodel ) ) ) { continue; } if ( !strcmp( model, tmpmodel ) ) { break; } } if ( !ent ) { BotAI_Print( PRT_ERROR, "BotEntityToActivate: no entity found with model %s\n", model ); return 0; } trap_AAS_ValueForBSPEpairKey( ent, "classname", classname, sizeof( classname ) ); if ( !classname ) { BotAI_Print( PRT_ERROR, "BotEntityToActivate: entity with model %s has no classname\n", model ); return 0; } //if it is a door if ( !strcmp( classname, "func_door" ) ) { if ( trap_AAS_FloatForBSPEpairKey( ent, "health", &health ) ) { //if health the door must be shot to open if ( health ) { return ent; } } } //get the targetname so we can find an entity with a matching target if ( !trap_AAS_ValueForBSPEpairKey( ent, "targetname", targetname[0], sizeof( targetname[0] ) ) ) { #ifdef OBSTACLEDEBUG BotAI_Print( PRT_ERROR, "BotEntityToActivate: entity with model \"%s\" has no targetname\n", model ); #endif //OBSTACLEDEBUG return 0; } cur_entities[0] = trap_AAS_NextBSPEntity( 0 ); for ( i = 0; i >= 0 && i < 10; ) { for ( ent = cur_entities[i]; ent; ent = trap_AAS_NextBSPEntity( ent ) ) { if ( !trap_AAS_ValueForBSPEpairKey( ent, "target", target, sizeof( target ) ) ) { continue; } if ( !strcmp( targetname[i], target ) ) { cur_entities[i] = trap_AAS_NextBSPEntity( ent ); break; } } if ( !ent ) { BotAI_Print( PRT_ERROR, "BotEntityToActivate: no entity with target \"%s\"\n", targetname[i] ); i--; continue; } if ( !trap_AAS_ValueForBSPEpairKey( ent, "classname", classname, sizeof( classname ) ) ) { BotAI_Print( PRT_ERROR, "BotEntityToActivate: entity with target \"%s\" has no classname\n", targetname[i] ); continue; } if ( !strcmp( classname, "func_button" ) ) { //BSP button model return ent; } else if ( !strcmp( classname, "trigger_multiple" ) ) { //invisible trigger multiple box return ent; } else { i--; } } BotAI_Print( PRT_ERROR, "BotEntityToActivate: unknown activator with classname \"%s\"\n", classname ); return 0; } /* ================== BotSetMovedir ================== */ vec3_t VEC_UP = {0, -1, 0}; vec3_t MOVEDIR_UP = {0, 0, 1}; vec3_t VEC_DOWN = {0, -2, 0}; vec3_t MOVEDIR_DOWN = {0, 0, -1}; void BotSetMovedir( vec3_t angles, vec3_t movedir ) { if ( VectorCompare( angles, VEC_UP ) ) { VectorCopy( MOVEDIR_UP, movedir ); } else if ( VectorCompare( angles, VEC_DOWN ) ) { VectorCopy( MOVEDIR_DOWN, movedir ); } else { AngleVectors( angles, movedir, NULL, NULL ); } } void BotModelMinsMaxs( int modelindex, vec3_t mins, vec3_t maxs ) { gentity_t *ent; int i; ent = &g_entities[0]; for ( i = 0; i < level.num_entities; i++, ent++ ) { if ( !ent->inuse ) { continue; } if ( ent->s.modelindex == modelindex ) { VectorCopy( ent->r.mins, mins ); VectorCopy( ent->r.maxs, maxs ); return; } } VectorClear( mins ); VectorClear( maxs ); } /* ================== BotAIBlocked ================== */ void BotAIBlocked( bot_state_t *bs, bot_moveresult_t *moveresult, int activate ) { int movetype, ent, i, areas[10], numareas, modelindex; char classname[128], model[128]; #ifdef OBSTACLEDEBUG char buf[128]; #endif float lip, dist, health, angle; vec3_t hordir, size, start, end, mins, maxs, sideward, angles; vec3_t movedir, origin, goalorigin, bboxmins, bboxmaxs; vec3_t up = {0, 0, 1}, extramins = {-1, -1, -1}, extramaxs = {1, 1, 1}; aas_entityinfo_t entinfo; /* bsp_trace_t bsptrace; */ #ifdef OBSTACLEDEBUG char netname[MAX_NETNAME]; #endif if ( !moveresult->blocked ) { return; } // BotEntityInfo( moveresult->blockentity, &entinfo ); #ifdef OBSTACLEDEBUG ClientName( bs->client, netname, sizeof( netname ) ); BotAI_Print( PRT_MESSAGE, "%s: I'm blocked by model %d\n", netname, entinfo.modelindex ); #endif //OBSTACLEDEBUG //if blocked by a bsp model and the bot wants to activate it if possible if ( entinfo.modelindex > 0 && entinfo.modelindex <= max_bspmodelindex && activate ) { //find the bsp entity which should be activated in order to remove //the blocking entity ent = BotEntityToActivate( entinfo.number ); if ( !ent ) { strcpy( classname, "" ); #ifdef OBSTACLEDEBUG BotAI_Print( PRT_MESSAGE, "%s: can't find activator for blocking entity\n", ClientName( bs->client, netname, sizeof( netname ) ) ); #endif //OBSTACLEDEBUG } else { trap_AAS_ValueForBSPEpairKey( ent, "classname", classname, sizeof( classname ) ); #ifdef OBSTACLEDEBUG ClientName( bs->client, netname, sizeof( netname ) ); BotAI_Print( PRT_MESSAGE, "%s: I should activate %s\n", netname, classname ); #endif //OBSTACLEDEBUG } #ifdef OBSTACLEDEBUG // ClientName(bs->client, netname, sizeof(netname)); // BotAI_Print(PRT_MESSAGE, "%s: I've got no brain cells for activating entities\n", netname); #endif //OBSTACLEDEBUG /* //the bot should now activate one of the following entities //"func_button", "trigger_multiple", "func_door" //all these activators use BSP models, so it should be a matter of //finding where this model is located using AAS and then activating //by walking against the model it or shooting at it // //if it is a door we should shoot at if (!strcmp(classname, "func_door")) { //get the door model model = AAS_ValueForBSPEpairKey(ent, "model"); modelindex = AAS_IndexFromModel(model); //if the model is not loaded if (!modelindex) return; VectorClear(angles); AAS_BSPModelMinsMaxsOrigin(modelindex - 1, angles, mins, maxs, NULL); //get a goal to shoot at VectorAdd(maxs, mins, goalorigin); VectorScale(goalorigin, 0.5, goalorigin); VectorSubtract(goalorigin, bs->origin, movedir); // vectoangles(movedir, moveresult->ideal_viewangles); moveresult->flags |= MOVERESULT_MOVEMENTVIEW; //select the blaster EA_UseItem(bs->client, "Blaster"); //shoot EA_Attack(bs->client); // return; } //end if*/ if ( !strcmp( classname, "func_button" ) ) { //create a bot goal towards the button trap_AAS_ValueForBSPEpairKey( ent, "model", model, sizeof( model ) ); modelindex = atoi( model + 1 ); //if the model is not loaded if ( !modelindex ) { return; } VectorClear( angles ); BotModelMinsMaxs( modelindex, mins, maxs ); //get the lip of the button trap_AAS_FloatForBSPEpairKey( ent, "lip", &lip ); if ( !lip ) { lip = 4; } //get the move direction from the angle trap_AAS_FloatForBSPEpairKey( ent, "angle", &angle ); VectorSet( angles, 0, angle, 0 ); BotSetMovedir( angles, movedir ); //button size VectorSubtract( maxs, mins, size ); //button origin VectorAdd( mins, maxs, origin ); VectorScale( origin, 0.5, origin ); //touch distance of the button dist = fabs( movedir[0] ) * size[0] + fabs( movedir[1] ) * size[1] + fabs( movedir[2] ) * size[2]; dist *= 0.5; // trap_AAS_FloatForBSPEpairKey( ent, "health", &health ); //if the button is shootable if ( health ) { //calculate the goal origin VectorMA( origin, -dist, movedir, goalorigin ); // //AAS_ClearShownDebugLines(); //AAS_DrawArrow(bs->origin, goalorigin, LINECOLOR_BLUE, LINECOLOR_YELLOW); // VectorSubtract( goalorigin, bs->origin, movedir ); vectoangles( movedir, moveresult->ideal_viewangles ); moveresult->flags |= MOVERESULT_MOVEMENTVIEW; //select the blaster trap_EA_SelectWeapon( bs->client, WEAPONINDEX_MACHINEGUN ); //shoot trap_EA_Attack( bs->client ); return; } //end if else { //add bounding box size to the dist trap_AAS_PresenceTypeBoundingBox( PRESENCE_CROUCH, bboxmins, bboxmaxs ); for ( i = 0; i < 3; i++ ) { if ( movedir[i] < 0 ) { dist += fabs( movedir[i] ) * fabs( bboxmaxs[i] ); } else { dist += fabs( movedir[i] ) * fabs( bboxmins[i] );} } //end for //calculate the goal origin VectorMA( origin, -dist, movedir, goalorigin ); // VectorCopy( goalorigin, start ); start[2] += 24; VectorCopy( start, end ); end[2] -= 100; numareas = trap_AAS_TraceAreas( start, end, areas, NULL, 10 ); // for ( i = 0; i < numareas; i++ ) { if ( trap_AAS_AreaReachability( areas[i] ) ) { break; } } if ( i < numareas ) { // #ifdef OBSTACLEDEBUG if ( bs->activatemessage_time < trap_AAS_Time() ) { Com_sprintf( buf, sizeof( buf ), "I have to activate a button at %1.1f %1.1f %1.1f in area %d\n", goalorigin[0], goalorigin[1], goalorigin[2], areas[i] ); trap_EA_Say( bs->client, buf ); bs->activatemessage_time = trap_AAS_Time() + 5; } //end if #endif //OBSTACLEDEBUG // //VectorMA(origin, -dist, movedir, goalorigin); // VectorCopy( origin, bs->activategoal.origin ); bs->activategoal.areanum = areas[i]; VectorSubtract( mins, origin, bs->activategoal.mins ); VectorSubtract( maxs, origin, bs->activategoal.maxs ); // VectorAdd( bs->activategoal.mins, extramins, bs->activategoal.mins ); VectorAdd( bs->activategoal.maxs, extramaxs, bs->activategoal.maxs ); // bs->activategoal.entitynum = entinfo.number; bs->activategoal.number = 0; bs->activategoal.flags = 0; bs->activate_time = trap_AAS_Time() + 10; AIEnter_Seek_ActivateEntity( bs ); } //end if else { #ifdef OBSTACLEDEBUG BotAI_Print( PRT_MESSAGE, "button area has no reachabilities\n" ); #endif //OBSTACLEDEBUG if ( bs->ainode == AINode_Seek_NBG ) { bs->nbg_time = 0; } else if ( bs->ainode == AINode_Seek_LTG ) { bs->ltg_time = 0; } } //end else } //end else } //end if /* if (!strcmp(classname, "trigger_multiple")) { //create a bot goal towards the trigger model = AAS_ValueForBSPEpairKey(ent, "model"); modelindex = AAS_IndexFromModel(model); //if the model is not precached (bad thing but happens) assume model is "*X" if (!modelindex) modelindex = atoi(model+1); VectorClear(angles); AAS_BSPModelMinsMaxsOrigin(modelindex - 1, angles, mins, maxs, NULL); VectorAdd(mins, maxs, mid); VectorScale(mid, 0.5, mid); VectorCopy(mid, start); start[2] = maxs[2] + 24; VectorSet(end, start[0], start[1], start[2] - 100); trace = AAS_TraceClientBBox(start, end, PRESENCE_CROUCH, -1); if (trace.startsolid) return; //trace.endpos is now the goal origin VectorCopy(trace.endpos, goalorigin); // #ifdef OBSTACLEDEBUG if (bs->activatemessage_time < AAS_Time()) { Com_sprintf(buf, sizeof(buf), "I have to activate a trigger at %1.1f %1.1f %1.1f in area %d\n", goalorigin[0], goalorigin[1], goalorigin[2], AAS_PointAreaNum(goalorigin)); EA_Say(bs->client, buf); bs->activatemessage_time = AAS_Time() + 5; } //end if* / #endif //OBSTACLEDEBUG // VectorCopy(mid, bs->activategoal.origin); bs->activategoal.areanum = AAS_PointAreaNum(goalorigin); VectorSubtract(mins, mid, bs->activategoal.mins); VectorSubtract(maxs, mid, bs->activategoal.maxs); bs->activategoal.entitynum = entinfo.number; bs->activategoal.number = 0; bs->activategoal.flags = 0; bs->activate_time = AAS_Time() + 10; if (!AAS_AreaReachability(bs->activategoal.areanum)) { #ifdef OBSTACLEDEBUG botimport.Print(PRT_MESSAGE, "trigger area has no reachabilities\n"); #endif //OBSTACLEDEBUG if (bs->ainode == AINode_Seek_NBG) bs->nbg_time = 0; else if (bs->ainode == AINode_Seek_LTG) bs->ltg_time = 0; } //end if else { AIEnter_Seek_ActivateEntity(bs); } //end else return; } //end if*/ } //just some basic dynamic obstacle avoidance code hordir[0] = moveresult->movedir[0]; hordir[1] = moveresult->movedir[1]; hordir[2] = 0; //if no direction just take a random direction if ( VectorNormalize( hordir ) < 0.1 ) { VectorSet( angles, 0, 360 * random(), 0 ); AngleVectors( angles, hordir, NULL, NULL ); } // // if (moveresult->flags & MOVERESULT_ONTOPOFOBSTACLE) movetype = MOVE_JUMP; // else movetype = MOVE_WALK; //if there's an obstacle at the bot's feet and head then //the bot might be able to crouch through VectorCopy( bs->origin, start ); start[2] += 18; VectorMA( start, 5, hordir, end ); VectorSet( mins, -16, -16, -24 ); VectorSet( maxs, 16, 16, 4 ); // // bsptrace = AAS_Trace(start, mins, maxs, end, bs->entitynum, MASK_PLAYERSOLID); // if (bsptrace.fraction >= 1) movetype = MOVE_CROUCH; //get the sideward vector CrossProduct( hordir, up, sideward ); // if ( bs->flags & BFL_AVOIDRIGHT ) { VectorNegate( sideward, sideward ); } //try to crouch straight forward? if ( movetype != MOVE_CROUCH || !trap_BotMoveInDirection( bs->ms, hordir, 400, movetype ) ) { //perform the movement if ( !trap_BotMoveInDirection( bs->ms, sideward, 400, movetype ) ) { //flip the avoid direction flag bs->flags ^= BFL_AVOIDRIGHT; //flip the direction VectorNegate( sideward, sideward ); //move in the other direction trap_BotMoveInDirection( bs->ms, sideward, 400, movetype ); } } //just reset goals and hope the bot will go into another direction //still needed?? if ( bs->ainode == AINode_Seek_NBG ) { bs->nbg_time = 0; } else if ( bs->ainode == AINode_Seek_LTG ) { bs->ltg_time = 0; } } /* ================== BotCheckConsoleMessages ================== */ void BotCheckConsoleMessages( bot_state_t *bs ) { char botname[MAX_NETNAME], message[MAX_MESSAGE_SIZE], netname[MAX_NETNAME]; float chat_reply; int context, handle; bot_consolemessage_t m; bot_match_t match; //the name of this bot ClientName( bs->client, botname, sizeof( botname ) ); // while ( ( handle = trap_BotNextConsoleMessage( bs->cs, &m ) ) != 0 ) { //if the chat state is flooded with messages the bot will read them quickly if ( trap_BotNumConsoleMessages( bs->cs ) < 10 ) { //if it is a chat message the bot needs some time to read it if ( m.type == CMS_CHAT && m.time > trap_AAS_Time() - ( 1 + random() ) ) { break; } } //unify the white spaces in the message trap_UnifyWhiteSpaces( m.message ); //replace synonyms in the right context context = CONTEXT_NORMAL | CONTEXT_NEARBYITEM | CONTEXT_NAMES; if ( BotCTFTeam( bs ) == CTF_TEAM_RED ) { context |= CONTEXT_CTFREDTEAM; } else { context |= CONTEXT_CTFBLUETEAM;} trap_BotReplaceSynonyms( m.message, context ); //if there's no match if ( !BotMatchMessage( bs, m.message ) ) { //if it is a chat message if ( m.type == CMS_CHAT && !bot_nochat.integer ) { // if ( !trap_BotFindMatch( m.message, &match, MTCONTEXT_REPLYCHAT ) ) { trap_BotRemoveConsoleMessage( bs->cs, handle ); continue; } //don't use eliza chats with team messages if ( match.subtype & ST_TEAM ) { trap_BotRemoveConsoleMessage( bs->cs, handle ); continue; } // trap_BotMatchVariable( &match, NETNAME, netname, sizeof( netname ) ); trap_BotMatchVariable( &match, MESSAGE, message, sizeof( message ) ); //if this is a message from the bot self if ( !Q_stricmp( netname, botname ) ) { trap_BotRemoveConsoleMessage( bs->cs, handle ); continue; } //unify the message trap_UnifyWhiteSpaces( message ); // trap_Cvar_Update( &bot_testrchat ); if ( bot_testrchat.integer ) { // trap_BotLibVarSet( "bot_testrchat", "1" ); //if bot replies with a chat message if ( trap_BotReplyChat( bs->cs, message, context, CONTEXT_REPLY, NULL, NULL, NULL, NULL, NULL, NULL, botname, netname ) ) { BotAI_Print( PRT_MESSAGE, "------------------------\n" ); } else { BotAI_Print( PRT_MESSAGE, "**** no valid reply ****\n" ); } } //if at a valid chat position and not chatting already else if ( bs->ainode != AINode_Stand && BotValidChatPosition( bs ) ) { chat_reply = trap_Characteristic_BFloat( bs->character, CHARACTERISTIC_CHAT_REPLY, 0, 1 ); if ( random() < 1.5 / ( NumBots() + 1 ) && random() < chat_reply ) { //if bot replies with a chat message if ( trap_BotReplyChat( bs->cs, message, context, CONTEXT_REPLY, NULL, NULL, NULL, NULL, NULL, NULL, botname, netname ) ) { //remove the console message trap_BotRemoveConsoleMessage( bs->cs, handle ); bs->stand_time = trap_AAS_Time() + BotChatTime( bs ); AIEnter_Stand( bs ); //EA_Say(bs->client, bs->cs.chatmessage); break; } } } } } //remove the console message trap_BotRemoveConsoleMessage( bs->cs, handle ); } } /* ================== BotCheckEvents ================== */ void BotCheckEvents( bot_state_t *bs, entityState_t *state ) { int event; char buf[128]; // //this sucks, we're accessing the gentity_t directly but there's no other fast way //to do it right now if ( bs->entityeventTime[state->number] == g_entities[state->number].eventTime ) { return; } bs->entityeventTime[state->number] = g_entities[state->number].eventTime; //if it's an event only entity if ( state->eType > ET_EVENTS ) { event = ( state->eType - ET_EVENTS ) & ~EV_EVENT_BITS; } else { event = state->event & ~EV_EVENT_BITS; } // switch ( event ) { //client obituary event case EV_OBITUARY: { int target, attacker, mod; target = state->otherEntityNum; attacker = state->otherEntityNum2; mod = state->eventParm; // if ( target == bs->client ) { bs->botdeathtype = mod; bs->lastkilledby = attacker; // if ( target == attacker ) { bs->botsuicide = qtrue; } else { bs->botsuicide = qfalse;} // bs->num_deaths++; } //else if this client was killed by the bot else if ( attacker == bs->client ) { bs->enemydeathtype = mod; bs->lastkilledplayer = target; bs->killedenemy_time = trap_AAS_Time(); // bs->num_kills++; } else if ( attacker == bs->enemy && target == attacker ) { bs->enemysuicide = qtrue; } break; } case EV_GLOBAL_SOUND: { if ( state->eventParm < 0 || state->eventParm > MAX_SOUNDS ) { BotAI_Print( PRT_ERROR, "EV_GLOBAL_SOUND: eventParm (%d) out of range\n", state->eventParm ); break; } trap_GetConfigstring( CS_SOUNDS + state->eventParm, buf, sizeof( buf ) ); if ( !strcmp( buf, "sound/teamplay/flagret_red.wav" ) ) { //red flag is returned bs->redflagstatus = 0; bs->flagstatuschanged = qtrue; } else if ( !strcmp( buf, "sound/teamplay/flagret_blu.wav" ) ) { //blue flag is returned bs->blueflagstatus = 0; bs->flagstatuschanged = qtrue; } else if ( !strcmp( buf, "sound/items/poweruprespawn.wav" ) ) { //powerup respawned... go get it BotGoForPowerups( bs ); } break; } case EV_PLAYER_TELEPORT_IN: { VectorCopy( state->origin, lastteleport_origin ); lastteleport_time = trap_AAS_Time(); break; } case EV_GENERAL_SOUND: { //if this sound is played on the bot if ( state->number == bs->client ) { if ( state->eventParm < 0 || state->eventParm > MAX_SOUNDS ) { BotAI_Print( PRT_ERROR, "EV_GENERAL_SOUND: eventParm (%d) out of range\n", state->eventParm ); break; } //check out the sound trap_GetConfigstring( CS_SOUNDS + state->eventParm, buf, sizeof( buf ) ); //if falling into a death pit if ( !strcmp( buf, "*falling1.wav" ) ) { //if the bot has a personal teleporter if ( bs->inventory[INVENTORY_TELEPORTER] > 0 ) { //use the holdable item trap_EA_Use( bs->client ); } } } break; } } } /* ================== BotCheckSnapshot ================== */ void BotCheckSnapshot( bot_state_t *bs ) { int ent; entityState_t state; // ent = 0; while ( ( ent = BotAI_GetSnapshotEntity( bs->client, ent, &state ) ) != -1 ) { //check the entity state for events BotCheckEvents( bs, &state ); } //check the player state for events BotAI_GetEntityState( bs->client, &state ); //copy the player state events to the entity state //state.event = bs->cur_ps.externalEvent; //state.eventParm = bs->cur_ps.externalEventParm; // BotCheckEvents( bs, &state ); } /* ================== BotCheckAir ================== */ void BotCheckAir( bot_state_t *bs ) { if ( bs->inventory[INVENTORY_ENVIRONMENTSUIT] <= 0 ) { if ( trap_AAS_PointContents( bs->eye ) & ( CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA ) ) { return; } } bs->lastair_time = trap_AAS_Time(); } /* ================== BotDeathmatchAI ================== */ void BotDeathmatchAI( bot_state_t *bs, float thinktime ) { char gender[144], name[144], buf[144]; char userinfo[MAX_INFO_STRING]; int i; //if the bot has just been setup if ( bs->setupcount > 0 ) { bs->setupcount--; if ( bs->setupcount > 0 ) { return; } //get the gender characteristic trap_Characteristic_String( bs->character, CHARACTERISTIC_GENDER, gender, sizeof( gender ) ); //set the bot gender trap_GetUserinfo( bs->client, userinfo, sizeof( userinfo ) ); Info_SetValueForKey( userinfo, "sex", gender ); trap_SetUserinfo( bs->client, userinfo ); //set the team if ( g_gametype.integer != GT_TOURNAMENT ) { Com_sprintf( buf, sizeof( buf ), "team %s", bs->settings.team ); trap_EA_Command( bs->client, buf ); } //set the chat gender if ( gender[0] == 'm' ) { trap_BotSetChatGender( bs->cs, CHAT_GENDERMALE ); } else if ( gender[0] == 'f' ) { trap_BotSetChatGender( bs->cs, CHAT_GENDERFEMALE ); } else { trap_BotSetChatGender( bs->cs, CHAT_GENDERLESS );} //set the chat name ClientName( bs->client, name, sizeof( name ) ); trap_BotSetChatName( bs->cs, name ); // bs->lastframe_health = bs->inventory[INVENTORY_HEALTH]; bs->lasthitcount = bs->cur_ps.persistant[PERS_HITS]; // bs->setupcount = 0; } //no ideal view set bs->flags &= ~BFL_IDEALVIEWSET; //set the teleport time BotSetTeleportTime( bs ); //update some inventory values BotUpdateInventory( bs ); //check the console messages BotCheckConsoleMessages( bs ); //check out the snapshot BotCheckSnapshot( bs ); //check for air BotCheckAir( bs ); //if not in the intermission and not in observer mode if ( !BotIntermission( bs ) && !BotIsObserver( bs ) ) { //do team AI BotTeamAI( bs ); } //if the bot has no ai node if ( !bs->ainode ) { AIEnter_Seek_LTG( bs ); } //if the bot entered the game less than 8 seconds ago if ( !bs->entergamechat && bs->entergame_time > trap_AAS_Time() - 8 ) { if ( BotChat_EnterGame( bs ) ) { bs->stand_time = trap_AAS_Time() + BotChatTime( bs ); AIEnter_Stand( bs ); } bs->entergamechat = qtrue; } //reset the node switches from the previous frame BotResetNodeSwitches(); //execute AI nodes for ( i = 0; i < MAX_NODESWITCHES; i++ ) { if ( bs->ainode( bs ) ) { break; } } //if the bot removed itself :) if ( !bs->inuse ) { return; } //if the bot executed too many AI nodes if ( i >= MAX_NODESWITCHES ) { trap_BotDumpGoalStack( bs->gs ); trap_BotDumpAvoidGoals( bs->gs ); BotDumpNodeSwitches( bs ); ClientName( bs->client, name, sizeof( name ) ); BotAI_Print( PRT_ERROR, "%s at %1.1f switched more than %d AI nodes\n", name, trap_AAS_Time(), MAX_NODESWITCHES ); } // bs->lastframe_health = bs->inventory[INVENTORY_HEALTH]; bs->lasthitcount = bs->cur_ps.persistant[PERS_HITS]; } /* ================== BotSetupDeathmatchAI ================== */ void BotSetupDeathmatchAI( void ) { int ent, modelnum; char model[128]; gametype = trap_Cvar_VariableIntegerValue( "g_gametype" ); // Rafael gameskill gameskill = trap_Cvar_VariableIntegerValue( "g_gameskill" ); // done trap_Cvar_Register( &bot_rocketjump, "bot_rocketjump", "1", 0 ); trap_Cvar_Register( &bot_grapple, "bot_grapple", "0", 0 ); trap_Cvar_Register( &bot_fastchat, "bot_fastchat", "0", 0 ); trap_Cvar_Register( &bot_nochat, "bot_nochat", "0", 0 ); trap_Cvar_Register( &bot_testrchat, "bot_testrchat", "0", 0 ); // if ( gametype == GT_CTF ) { if ( trap_BotGetLevelItemGoal( -1, "Red Flag", &ctf_redflag ) < 0 ) { BotAI_Print( PRT_WARNING, "CTF without Red Flag\n" ); } if ( trap_BotGetLevelItemGoal( -1, "Blue Flag", &ctf_blueflag ) < 0 ) { BotAI_Print( PRT_WARNING, "CTF without Blue Flag\n" ); } } max_bspmodelindex = 0; for ( ent = trap_AAS_NextBSPEntity( 0 ); ent; ent = trap_AAS_NextBSPEntity( ent ) ) { if ( !trap_AAS_ValueForBSPEpairKey( ent, "model", model, sizeof( model ) ) ) { continue; } if ( model[0] == '*' ) { modelnum = atoi( model + 1 ); if ( modelnum > max_bspmodelindex ) { max_bspmodelindex = modelnum; } } } //initialize the waypoint heap BotInitWaypoints(); } /* ================== BotShutdownDeathmatchAI ================== */ void BotShutdownDeathmatchAI( void ) { }