/* Copyright (C) 1997-2001 Id Software, Inc. This program 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 2 of the License, or (at your option) any later version. This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "../g_local.h" #include "ai_local.h" //ACE //========================================== // AI_Init // Inits global parameters //========================================== void AI_Init(void) { //Init developer mode bot_showpath = trap_Cvar_Get("bot_showpath", "0", 0); bot_showcombat = trap_Cvar_Get("bot_showcombat", "0", 0); bot_showsrgoal = trap_Cvar_Get("bot_showsrgoal", "0", 0); bot_showlrgoal = trap_Cvar_Get("bot_showlrgoal", "0", 0); bot_debugmonster = trap_Cvar_Get("bot_debugmonster", "0", 0); #ifndef WSW_RELEASE bot_dummy = trap_Cvar_Get("bot_dummy", "0", 0); #endif AIDevel.debugMode = qfalse; AIDevel.debugChased = qfalse; AIDevel.chaseguy = NULL; AIDevel.showPLinks = qfalse; AIDevel.plinkguy = NULL; } //========================================== // AI_NewMap // Inits Map local parameters //========================================== void AI_NewMap(void) { edict_t *ent; //Load nodes AI_InitNavigationData(); AI_InitAIWeapons (); // count bots game.numBots = 0; for( ent = game.edicts + 1; PLAYERNUM(ent) < game.maxclients; ent++ ) { if( !ent->r.inuse || !ent->ai.type ) continue; if( ent->r.svflags & SVF_FAKECLIENT && ent->ai.type == AI_ISBOT ) game.numBots++; } } //========================================== // G_FreeAI // removes the AI handle from memory //========================================== void G_FreeAI( edict_t *ent ) { if( !ent->ai.type ) return; if( ent->ai.type == AI_ISBOT ) { game.numBots--; } ent->ai.type = AI_INACTIVE; //memset( &ent->ai, 0, sizeof( ai_handle_t ) ); //G_Free( ent->ai ); //ent->ai = NULL; } //========================================== // G_SpawnAI // allocate ai_handle_t for this entity //========================================== void G_SpawnAI( edict_t *ent ) { //if( !ent->ai ) // ent->ai = G_Malloc(sizeof(ai_handle_t)); memset( &ent->ai, 0, sizeof(ai_handle_t) ); if( ent->r.svflags & SVF_FAKECLIENT ) ent->ai.type = AI_ISBOT; else ent->ai.type = AI_ISMONSTER; } //========================================== // AI_SetUpMoveWander //========================================== void AI_SetUpMoveWander( edict_t *ent ) { ent->ai.state = BOT_STATE_WANDER; ent->ai.wander_timeout = level.time + 1.0; ent->ai.nearest_node_tries = 0; ent->ai.next_move_time = level.time; ent->ai.bloqued_timeout = level.time + 15.0; ent->ai.goal_node = INVALID; ent->ai.current_node = INVALID; ent->ai.next_node = INVALID; } //========================================== // AI_ResetWeights // Init bot weights from bot-class weights. //========================================== void AI_ResetWeights(edict_t *ent) { //restore defaults from bot persistant memset(ent->ai.status.inventoryWeights, 0, sizeof (ent->ai.status.inventoryWeights)); memcpy(ent->ai.status.inventoryWeights, ent->ai.pers.inventoryWeights, sizeof(ent->ai.pers.inventoryWeights)); } //========================================== // AI_ResetNavigation // Init bot navigation. Called at first spawn & each respawn //========================================== void AI_ResetNavigation(edict_t *ent) { int i; ent->enemy = NULL; ent->movetarget = NULL; ent->ai.state_combat_timeout = 0.0; ent->ai.state = BOT_STATE_WANDER; ent->ai.wander_timeout = level.time; ent->ai.nearest_node_tries = 0; ent->ai.next_move_time = level.time; ent->ai.bloqued_timeout = level.time + 15.0; ent->ai.goal_node = INVALID; ent->ai.current_node = INVALID; ent->ai.next_node = INVALID; VectorSet( ent->ai.move_vector, 0, 0, 0 ); //reset bot_roams timeouts for( i=0; iai.status.broam_timeouts[i] = 0.0; } //========================================== // AI_BotRoamForLRGoal // // Try assigning a bot roam node as LR Goal //========================================== qboolean AI_BotRoamForLRGoal(edict_t *self, int current_node) { int i; float cost; float weight, best_weight = 0.0; int goal_node = INVALID; int best_broam = INVALID; float dist; if (!nav.num_broams) return qfalse; for( i=0; iai.status.broam_timeouts[i] > level.time) continue; //limit cost finding by distance dist = DistanceFast( self->s.origin, nodes[nav.broams[i].node].origin ); if( dist > 10000 ) continue; //find cost cost = AI_FindCost(current_node, nav.broams[i].node, self->ai.pers.moveTypesMask); if(cost == INVALID || cost < 3) // ignore invalid and very short hops continue; cost *= random(); // Allow random variations for broams weight = nav.broams[i].weight / cost; // Check against cost of getting there if(weight > best_weight) { best_weight = weight; goal_node = nav.broams[i].node; best_broam = i; } } if(best_weight == 0.0 || goal_node == INVALID) return qfalse; //set up the goal self->ai.state = BOT_STATE_MOVE; self->ai.tries = 0; // Reset the count of how many times we tried this goal //if(AIDevel.debugChased && bot_showlrgoal->integer) // G_PrintMsg (AIDevel.chaseguy, "%s: selected a bot roam of weight %f at node %d for LR goal.\n",self->ai.pers.netname, nav.broams[best_broam].weight, goal_node); AI_SetGoal(self,goal_node); return qtrue; } //========================================== // AI_PickLongRangeGoal // // Evaluate the best long range goal and send the bot on // its way. This is a good time waster, so use it sparingly. // Do not call it for every think cycle. // // jal: I don't think there is any problem by calling it, // now that we have stored the costs at the nav.costs table (I don't do it anyway) //========================================== void AI_PickLongRangeGoal(edict_t *self) { int i; int node; float weight,best_weight=0.0; int current_node, goal_node = INVALID; edict_t *goal_ent = NULL; float cost; float dist; #ifdef VSAYS self->ai.vsay_goalent = NULL; #endif // look for a target current_node = AI_FindClosestReachableNode(self->s.origin, self,((1+self->ai.nearest_node_tries)*NODE_DENSITY),NODE_ALL); self->ai.current_node = current_node; if(current_node == -1) //failed. Go wandering :( { if (AIDevel.debugChased && bot_showlrgoal->integer) G_PrintMsg (AIDevel.chaseguy, "%s: LRGOAL: Closest node not found. Tries:%i\n", self->ai.pers.netname, self->ai.nearest_node_tries); if( self->ai.state != BOT_STATE_WANDER ) AI_SetUpMoveWander( self ); self->ai.wander_timeout = level.time + 1.0; self->ai.nearest_node_tries++; //extend search radius with each try return; } self->ai.nearest_node_tries = 0; // Items for( i=0; ir.solid == SOLID_NOT) continue; //ignore items wich can't be weighted (must have a valid item flag) if( !nav.items[i].ent->item || !(nav.items[i].ent->item->type & (IT_AMMO|IT_HEALTH|IT_ARMOR|IT_WEAPON|IT_POWERUP|IT_FLAG)) ) continue; weight = AI_ItemWeight( self, nav.items[i].ent ); if( weight == 0.0f ) //ignore zero weighted items continue; //limit cost finding distance dist = DistanceFast( self->s.origin, nav.items[i].ent->s.origin ); //different distance limits for different types if( nav.items[i].ent->item->type & (IT_AMMO) && dist > 2000 ) continue; if( nav.items[i].ent->item->type & (IT_HEALTH|IT_ARMOR|IT_POWERUP) && dist > 5000 ) continue; if( nav.items[i].ent->item->type & IT_WEAPON && dist > 10000 ) continue; if( nav.items[i].ent->item->type & IT_FLAG && dist > 15000 ) continue; cost = AI_FindCost( current_node, nav.items[i].node, self->ai.pers.moveTypesMask ); if( cost == INVALID || cost < 3 ) // ignore invalid and very short hops continue; //weight *= random(); // Allow random variations weight /= cost; // Check against cost of getting there if( weight > best_weight ) { best_weight = weight; goal_node = nav.items[i].node; goal_ent = nav.items[i].ent; } } // Players: This should be its own function and is for now just finds a player to set as the goal. for( i=0; ir.svflags & SVF_NOCLIENT) continue; //ignore zero weighted players if( self->ai.status.playersWeights[i] == 0.0f ) continue; node = AI_FindClosestReachableNode( AIEnemies[i]->s.origin, AIEnemies[i], NODE_DENSITY, NODE_ALL); cost = AI_FindCost(current_node, node, self->ai.pers.moveTypesMask); if(cost == INVALID || cost < 4) // ignore invalid and very short hops continue; //precomputed player weights weight = self->ai.status.playersWeights[i]; //weight *= random(); // Allow random variations weight /= cost; // Check against cost of getting there if(weight > best_weight) { best_weight = weight; goal_node = node; goal_ent = AIEnemies[i]; } } // If do not find a goal, go wandering.... if(best_weight == 0.0 || goal_node == INVALID) { //BOT_ROAMS if (!AI_BotRoamForLRGoal(self, current_node)) { self->ai.goal_node = INVALID; self->ai.state = BOT_STATE_WANDER; self->ai.wander_timeout = level.time + 1.0; if(AIDevel.debugChased && bot_showlrgoal->integer) G_PrintMsg (AIDevel.chaseguy, "%s: did not find a LR goal, wandering.\n",self->ai.pers.netname); } return; // no path? } // OK, everything valid, let's start moving to our goal. self->ai.state = BOT_STATE_MOVE; self->ai.tries = 0; // Reset the count of how many times we tried this goal if(goal_ent != NULL && AIDevel.debugChased && bot_showlrgoal->integer) G_PrintMsg (AIDevel.chaseguy, "%s: selected a %s at node %d for LR goal.\n",self->ai.pers.netname, goal_ent->classname, goal_node); #ifdef VSAYS self->ai.vsay_goalent = goal_ent; #endif AI_SetGoal(self,goal_node); } //========================================== // AI_PickShortRangeGoal // Pick best goal based on importance and range. This function // overrides the long range goal selection for items that // are very close to the bot and are reachable. //========================================== void AI_PickShortRangeGoal(edict_t *self) { edict_t *target; float weight,best_weight=0.0; edict_t *best = NULL; if( !self->r.client ) return; // look for a target (should make more efficent later) target = findradius(NULL, self->s.origin, AI_GOAL_SR_RADIUS); while(target) { if(target->classname == NULL) return; // Missile detection code if(strcmp(target->classname,"rocket")==0 || strcmp(target->classname,"grenade")==0) { //if player who shoot is a potential enemy if (self->ai.status.playersWeights[target->r.owner->s.number-1]) { if(AIDevel.debugChased && bot_showcombat->integer) G_PrintMsg (AIDevel.chaseguy, "%s: ROCKET ALERT!\n", self->ai.pers.netname); self->enemy = target->r.owner; // set who fired the rocket as enemy return; } } if (AI_ItemIsReachable(self,target->s.origin)) { if (G_InFront(self, target)) { weight = AI_ItemWeight(self, target); if(weight > best_weight) { best_weight = weight; best = target; } } } // next target target = findradius(target, self->s.origin, AI_GOAL_SR_RADIUS); } //jalfixme (what's goalentity doing here?) if(best_weight) { self->movetarget = best; self->goalentity = best; if(AIDevel.debugChased && bot_showsrgoal->integer && (self->goalentity != self->movetarget)) G_PrintMsg (AIDevel.chaseguy, "%s: selected a %s for SR goal.\n",self->ai.pers.netname, self->movetarget->classname); } } //=================== // AI_CategorizePosition // Categorize waterlevel and groundentity/stepping //=================== void AI_CategorizePosition (edict_t *ent) { qboolean stepping = AI_IsStep(ent); ent->was_swim = ent->is_swim; ent->was_step = ent->is_step; ent->is_ladder = AI_IsLadder( ent->s.origin, ent->s.angles, ent->r.mins, ent->r.maxs, ent ); G_CategorizePosition(ent); if (ent->waterlevel > 2 || ( ent->waterlevel && !stepping )) { ent->is_swim = qtrue; ent->is_step = qfalse; return; } ent->is_swim = qfalse; ent->is_step = stepping; } //========================================== // AI_Think // think funtion for AIs //========================================== void AI_Think (edict_t *self) { if( !self->ai.type ) return; AIDebug_SetChased(self); //jal:debug shit AI_CategorizePosition(self); //freeze AI when dead if( self->deadflag || match.state >= MATCH_STATE_POSTMATCH ) { self->ai.pers.deadFrame(self); return; } //if completely stuck somewhere if(VectorLengthFast(self->velocity) > 37) self->ai.bloqued_timeout = level.time + 10.0; if( self->ai.bloqued_timeout < level.time ) { self->ai.pers.bloquedTimeout(self); return; } //update status information to feed up ai self->ai.pers.UpdateStatus(self); //if it's at spectator team, return if( self->s.team == TEAM_SPECTATOR || self->r.svflags & SVF_NOCLIENT ) return; //update position in path, set up move vector if( self->ai.state == BOT_STATE_MOVE ) { if( !AI_FollowPath(self) ) { AI_SetUpMoveWander( self ); self->ai.wander_timeout = level.time + game.frametime; // do it next frame } } //pick a new long range goal if( self->ai.state == BOT_STATE_WANDER && self->ai.wander_timeout < level.time) AI_PickLongRangeGoal(self); //Find any short range goal AI_PickShortRangeGoal(self); //run class based states machine self->ai.pers.RunFrame(self); }