/* 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 //========================================== // BOT_DMclass_Move // DMClass is generic bot class //========================================== void BOT_DMclass_Move(edict_t *self, usercmd_t *ucmd) { int current_node_flags = 0; int next_node_flags = 0; int current_link_type = 0; int i; current_node_flags = nodes[self->ai.current_node].flags; next_node_flags = nodes[self->ai.next_node].flags; if( AI_PlinkExists( self->ai.current_node, self->ai.next_node )) { current_link_type = AI_PlinkMoveType( self->ai.current_node, self->ai.next_node ); //Com_Printf("%s\n", AI_LinkString( current_link_type )); } // Platforms if( current_link_type == LINK_PLATFORM ) { // Move to the center self->ai.move_vector[2] = 0; // kill z movement if(VectorLengthFast(self->ai.move_vector) > 10) ucmd->forwardmove = 200; // walk to center AI_ChangeAngle(self); return; // No move, riding elevator } else if( next_node_flags & NODEFLAGS_PLATFORM ) { // is lift down? for(i=0;iai.next_node ) { //testing line //vec3_t tPoint; //int j; //for(j=0; j<3; j++)//center of the ent // tPoint[j] = nav.ents[i].ent->s.origin[j] + 0.5*(nav.ents[i].ent->r.mins[j] + nav.ents[i].ent->r.maxs[j]); //tPoint[2] = nav.ents[i].ent->s.origin[2] + nav.ents[i].ent->r.maxs[2]; //tPoint[2] += 8; //AITools_DrawLine( self->s.origin, tPoint ); //if not reachable, wait for it (only height matters) if( (nav.ents[i].ent->s.origin[2] + nav.ents[i].ent->r.maxs[2]) > (self->s.origin[2] + self->r.mins[2] + AI_JUMPABLE_HEIGHT) && nav.ents[i].ent->moveinfo.state != STATE_BOTTOM) //jabot092(2) return; //wait for elevator } } } // Ladder movement if( self->is_ladder ) { ucmd->forwardmove = 70; ucmd->upmove = 200; ucmd->sidemove = 0; return; } // Falling off ledge if(!self->groundentity && !self->is_step && !self->is_swim ) { AI_ChangeAngle(self); if (current_link_type == LINK_JUMPPAD ) { ucmd->forwardmove = 120; } else if( current_link_type == LINK_JUMP ) { self->velocity[0] = self->ai.move_vector[0] * 480; self->velocity[1] = self->ai.move_vector[1] * 480; } else { self->velocity[0] = self->ai.move_vector[0] * 220; self->velocity[1] = self->ai.move_vector[1] * 220; } return; } // jumping over (keep fall before this) if( current_link_type == LINK_JUMP && self->groundentity) { trace_t trace; vec3_t v1, v2; //check floor in front, if there's none... Jump! VectorCopy( self->s.origin, v1 ); VectorCopy( self->ai.move_vector, v2 ); VectorNormalize( v2 ); VectorMA( v1, 16, v2, v1 ); v1[2] += self->r.mins[2]; //trap_Trace( &trace, v1, tv(-2, -2, -AI_JUMPABLE_HEIGHT), tv(2, 2, 0), v1, self, MASK_AISOLID ); trap_Trace( &trace, v1, tv(-2, -2, -AI_JUMPABLE_HEIGHT), tv(2, 2, 0), v1, self, MASK_AISOLID ); if( !trace.startsolid && trace.fraction == 1.0 ) { //jump! ucmd->forwardmove = 400; //prevent double jumping on crates VectorCopy( self->s.origin, v1 ); v1[2] += self->r.mins[2]; trap_Trace( &trace, v1, tv(-12, -12, -8), tv(12, 12, 0), v1, self, MASK_AISOLID ); if( trace.startsolid ) ucmd->upmove = 400; return; } } // Move To Short Range goal (not following paths) // plats, grapple, etc have higher priority than SR Goals, cause the bot will // drop from them and have to repeat the process from the beginning if (AI_MoveToGoalEntity(self,ucmd)) return; // swimming if( self->is_swim ) { // We need to be pointed up/down AI_ChangeAngle(self); if( !(trap_PointContents(nodes[self->ai.next_node].origin) & MASK_WATER) ) // Exit water ucmd->upmove = 400; ucmd->forwardmove = 300; return; } // Check to see if stuck, and if so try to free us if(VectorLengthFast(self->velocity) < 37) { // Keep a random factor just in case.... if( random() > 0.1 && AI_SpecialMove(self, ucmd) ) //jumps, crouches, turns... return; self->s.angles[YAW] += random() * 180 - 90; AI_ChangeAngle(self); ucmd->forwardmove = 400; return; } AI_ChangeAngle(self); // Otherwise move as fast as we can... ucmd->forwardmove = 400; } //========================================== // BOT_DMclass_Wander // Wandering code (based on old ACE movement code) //========================================== void BOT_DMclass_Wander(edict_t *self, usercmd_t *ucmd) { vec3_t temp; // Do not move if(self->ai.next_move_time > level.time) return; if (self->deadflag) return; // Special check for elevators, stand still until the ride comes to a complete stop. if(self->groundentity != NULL && self->groundentity->use == Use_Plat) { if(self->groundentity->moveinfo.state == STATE_UP || self->groundentity->moveinfo.state == STATE_DOWN) { self->velocity[0] = 0; self->velocity[1] = 0; self->velocity[2] = 0; self->ai.next_move_time = level.time + 0.5; return; } } // Move To Goal (Short Range Goal, not following paths) if (AI_MoveToGoalEntity(self,ucmd)) return; // Swimming? VectorCopy(self->s.origin,temp); temp[2]+=24; // if(trap_PointContents (temp) & MASK_WATER) if( trap_PointContents (temp) & MASK_WATER) { // If drowning and no node, move up if( self->r.client && self->r.client->next_drown_time > 0 ) //jalfixme: client references must pass into botStatus { ucmd->upmove = 100; self->s.angles[PITCH] = -45; } else ucmd->upmove = 15; ucmd->forwardmove = 300; } // else self->r.client->next_drown_time = 0; // probably shound not be messing with this, but // Lava? temp[2]-=48; //if(trap_PointContents(temp) & (CONTENTS_LAVA|CONTENTS_SLIME)) if( trap_PointContents(temp) & (CONTENTS_LAVA|CONTENTS_SLIME) ) { self->s.angles[YAW] += random() * 360 - 180; ucmd->forwardmove = 400; if(self->groundentity) ucmd->upmove = 400; else ucmd->upmove = 0; return; } // Check for special movement if(VectorLengthFast(self->velocity) < 37) { if(random() > 0.1 && AI_SpecialMove(self,ucmd)) //jumps, crouches, turns... return; self->s.angles[YAW] += random() * 180 - 90; if (!self->is_step)// if there is ground continue otherwise wait for next move ucmd->forwardmove = 0; //0 else if( AI_CanMove( self, BOT_MOVE_FORWARD)) ucmd->forwardmove = 100; return; } // Otherwise move slowly, walking wondering what's going on if( AI_CanMove( self, BOT_MOVE_FORWARD)) ucmd->forwardmove = 100; else ucmd->forwardmove = -100; } //========================================== // BOT_DMclass_CombatMovement // // NOTE: Very simple for now, just a basic move about avoidance. // Change this routine for more advanced attack movement. //========================================== void BOT_DMclass_CombatMovement( edict_t *self, usercmd_t *ucmd ) { float c; float dist; if(!self->enemy) { //do whatever (tmp move wander) if( AI_FollowPath(self) ) { BOT_DMclass_Move(self, ucmd); } return; } c = random(); dist = DistanceFast( self->s.origin, self->enemy->s.origin ); if( dist < 150 ) { //range = AIWEAP_MELEE_RANGE; if( self->s.weapon == WEAP_GUNBLADE ) { // go into him! ucmd->buttons &= ~BUTTON_ATTACK; // remove pressing fire if( AI_CanMove(self,BOT_MOVE_FORWARD) ) // move to your enemy ucmd->forwardmove += 400; else if( c <= 0.5 && AI_CanMove(self,BOT_MOVE_LEFT) ) ucmd->sidemove -= 400; else if( AI_CanMove(self,BOT_MOVE_RIGHT) ) ucmd->sidemove += 400; } else { //priorize sides if( c < 0.4 && AI_CanMove(self,BOT_MOVE_LEFT) ) ucmd->sidemove -= 400; else if( c < 0.8 && AI_CanMove(self,BOT_MOVE_RIGHT) ) ucmd->sidemove += 400; else if( game.gametype != GAMETYPE_MIDAIR && AI_CanMove(self,BOT_MOVE_BACK) ) ucmd->forwardmove -= 400; else if( game.gametype == GAMETYPE_MIDAIR && (dist > 75) && AI_CanMove(self,BOT_MOVE_FORWARD) ) // move to your enemy ucmd->forwardmove += 400; } } else if( dist < 500 ) { //Medium range limit is Grenade Laucher range //range = AIWEAP_SHORT_RANGE; // Priorize forward for going into MELEE c = random(); if( c < 0.30 && AI_CanMove(self,BOT_MOVE_LEFT) ) ucmd->sidemove -= 400; else if( c < 0.60 && AI_CanMove(self,BOT_MOVE_RIGHT) ) ucmd->sidemove += 400; else if( (game.gametype == GAMETYPE_MIDAIR || dist > 250) && AI_CanMove(self,BOT_MOVE_FORWARD) ) // move to your enemy ucmd->forwardmove += 400; else if( AI_CanMove(self,BOT_MOVE_FORWARD) ) ucmd->forwardmove += 100; } else if(dist < 900) { // all dirs //range = AIWEAP_MEDIUM_RANGE; if( c < 0.2 && AI_CanMove(self,BOT_MOVE_LEFT) ) ucmd->sidemove -= 400; else if( c < 0.4 && AI_CanMove(self,BOT_MOVE_RIGHT) ) ucmd->sidemove += 400; //else if( c < 0.6 && AI_CanMove(self,BOT_MOVE_BACK) ) // ucmd->forwardmove -= 400; else if( c < 0.8 && AI_CanMove(self,BOT_MOVE_FORWARD) ) ucmd->forwardmove += 400; } else { //range = AIWEAP_LONG_RANGE; // only move to the sides, and not always if( c < 0.2 && AI_CanMove(self,BOT_MOVE_LEFT) ) ucmd->sidemove -= 400; else if( c < 0.4 && AI_CanMove(self,BOT_MOVE_RIGHT) ) ucmd->sidemove += 400; } } //========================================== // BOT_DMclass_FindEnemy // Scan for enemy (simplifed for now to just pick any visible enemy) //========================================== edict_t *BOT_DMclass_FindEnemy(edict_t *self) { int i; edict_t *bestenemy = NULL; float bestweight = 99999; float weight; // we already set up an enemy this frame (reacting to attacks) if(self->enemy != NULL) return self->enemy; // Find Enemy for(i=0;ir.solid == SOLID_NOT) continue; //Ignore players with 0 weight (was set at botstatus) if(self->ai.status.playersWeights[i] == 0) continue; if( !AIEnemies[i]->deadflag && G_Visible(self, AIEnemies[i]) && trap_inPVS (self->s.origin, AIEnemies[i]->s.origin)) //gi.inPVS(self->s.origin, AIEnemies[i]->s.origin)) { //(weight enemies from fusionbot) Is enemy visible, or is it too close to ignore weight = DistanceFast( self->s.origin, AIEnemies[i]->s.origin ); // if weight is lesser than 0.1 only react to enemies very, very close to you if( self->ai.status.playersWeights[i] < 0.1f && weight > 300 ) continue; //modify weight based on precomputed player weights weight *= (1.0 - self->ai.status.playersWeights[i]); if( G_InFront( self, AIEnemies[i] ) || (weight < 300 ) ) { // Check if best target, or better than current target if (weight < bestweight) { bestweight = weight; bestenemy = AIEnemies[i]; } } } } // If best enemy, set up if(bestenemy) { // if (AIDevel.debugChased && bot_showcombat->value && bestenemy->ai.is_bot) // G_PrintMsg ( AIDevel.chaseguy, "%s: selected %s as enemy.\n", // self->ai.pers.netname, // bestenemy->ai.pers.netname ); self->enemy = bestenemy; return self->enemy; } return NULL; // NO enemy } //========================================== // BOT_DMClass_ChangeWeapon //========================================== qboolean BOT_DMClass_ChangeWeapon (edict_t *ent, gitem_t *item) { int ammocount, weakammocount; // see if we're already using it if (!item || item->tag == ent->s.weapon) return qtrue; // Has not picked up weapon yet if(!ent->r.client->inventory[item->tag]) return qfalse; // Do we have ammo for it? if( item->ammo_tag ) ammocount = ent->r.client->inventory[item->ammo_tag]; else ammocount = 0; if( item->weakammo_tag ) weakammocount = ent->r.client->inventory[item->weakammo_tag]; else weakammocount = 0; if ( !ammocount && !weakammocount ) return qfalse; // Change to this weapon ent->r.client->latched_weapon = item->tag; ent->ai.changeweapon_timeout = level.time + 6.0; //if the bot has no weapon, don't wait for put down if( !ent->s.weapon ) ChangeWeapon(ent); return qtrue; } //========================================== // BOT_DMclass_ChooseWeapon // Choose weapon based on range & weights //========================================== float BOT_DMclass_ChooseWeapon( edict_t *self ) { float dist; int i; float best_weight = 0.0; gitem_t *best_weapon = NULL; int weapon_range = 0; // if no enemy, then what are we doing here? if(!self->enemy) return qfalse; // Base weapon selection on distance: dist = DistanceFast(self->s.origin, self->enemy->s.origin); if(dist < 150) weapon_range = AIWEAP_MELEE_RANGE; else if(dist < 500) //Medium range limit is Grenade Laucher range weapon_range = AIWEAP_SHORT_RANGE; else if(dist < 900) weapon_range = AIWEAP_MEDIUM_RANGE; else weapon_range = AIWEAP_LONG_RANGE; if( self->ai.changeweapon_timeout > level.time ) return AIWeapons[self->s.weapon].RangeWeight[weapon_range]; //return current for( i=0; ir.client->inventory[AIWeapons[i].weaponItem->tag] ) continue; //ignore those we don't have ammo for if (AIWeapons[i].ammoItem != NULL //excepting for those not using ammo && !self->r.client->inventory[AIWeapons[i].ammoItem->tag] && !self->r.client->inventory[AIWeapons[i].ammoWeakItem->tag]) continue; //compare range weights if (AIWeapons[i].RangeWeight[weapon_range] > best_weight) { best_weight = AIWeapons[i].RangeWeight[weapon_range]; best_weapon = AIWeapons[i].weaponItem; } //jal: enable randomnes later //else if (AIWeapons[i].RangeWeight[weapon_range] == best_weight && random() > 0.2) { //allow some random for equal weights // best_weight = AIWeapons[i].RangeWeight[weapon_range]; // best_weapon = AIWeapons[i].weaponItem; //} } //do the change (same weapon, or null best_weapon is covered at ChangeWeapon) BOT_DMClass_ChangeWeapon( self, best_weapon ); return AIWeapons[self->s.weapon].RangeWeight[weapon_range]; //return current } //========================================== // BOT_DMclass_CheckShot // Checks if shot is blocked (doesn't verify it would hit) //========================================== qboolean BOT_DMclass_CheckShot( edict_t *ent, vec3_t point ) { trace_t tr; vec3_t start, forward, right, offset; AngleVectors( ent->r.client->v_angle, forward, right, NULL ); VectorSet( offset, 0, 0, ent->viewheight ); P_ProjectSource( ent->r.client, ent->s.origin, offset, forward, right, start ); //bloqued, don't shoot //tr = gi.trace( start, vec3_origin, vec3_origin, point, ent, MASK_AISOLID); trap_Trace( &tr, start, vec3_origin, vec3_origin, point, ent, MASK_AISOLID ); if( tr.fraction < 0.8f ) { if( tr.ent < 1 || !game.edicts[tr.ent].takedamage || game.edicts[tr.ent].movetype == MOVETYPE_PUSH ) return qfalse; // check if the player we found is at our team if( game.edicts[tr.ent].s.team == ent->s.team && GS_Gametype_IsTeamBased(game.gametype) ) return qfalse; } return qtrue; } firedef_t *Player_GetCurrentWeaponFiredef( edict_t *ent ); //========================================== // BOT_DMclass_PredictProjectileShot // predict target movement //========================================== void BOT_DMclass_PredictProjectileShot( edict_t *self, vec3_t fire_origin, float projectile_speed, vec3_t target, vec3_t target_velocity ) { vec3_t predictedTarget; vec3_t targetMovedir; float targetSpeed; float predictionTime; float distance; trace_t trace; int contents; if( projectile_speed <= 0.0f ) return; targetSpeed = VectorNormalize2( target_velocity, targetMovedir ); // ok, this is not going to be 100% precise, since we will find the // time our projectile will take to travel to enemy's CURRENT position, // and them find enemy's position given his CURRENT velocity and his CURRENT dir // after prediction time. The result will be much better if the player // is moving to the sides (relative to us) than in depth (relative to us). // And, of course, when the player moves in a curve upwards it will totally miss (ie, jumping). distance = DistanceFast( fire_origin, target ); predictionTime = distance/projectile_speed; VectorMA( target, predictionTime*targetSpeed, targetMovedir, predictedTarget ); // if this position is inside solid, try finding a position at half of the prediction time contents = trap_PointContents( predictedTarget ); if( contents & CONTENTS_SOLID && !(contents & CONTENTS_PLAYERCLIP) ) { VectorMA( target, (predictionTime * 0.5f)*targetSpeed, targetMovedir, predictedTarget ); contents = trap_PointContents( predictedTarget ); if( contents & CONTENTS_SOLID && !(contents & CONTENTS_PLAYERCLIP) ) return; // INVALID } // if we can see this point, we use it, otherwise we keep the current position trap_Trace( &trace, fire_origin, vec3_origin, vec3_origin, predictedTarget, self, MASK_SHOT ); if( trace.fraction == 1.0f || (trace.ent && game.edicts[trace.ent].takedamage) ) VectorCopy( predictedTarget, target ); } //========================================== // BOT_DMclass_FireWeapon // Fire if needed //========================================== qboolean BOT_DMclass_FireWeapon( edict_t *self, usercmd_t *ucmd ) { float firedelay; vec3_t target; vec3_t angles; int weapon; float wfac; vec3_t fire_origin; vec3_t dir; trace_t trace; firedef_t *firedef = Player_GetCurrentWeaponFiredef(self); if( !self->enemy ) return qfalse; weapon = self->s.weapon; if( weapon < 0 || weapon >= WEAP_TOTAL ) weapon = 0; if( !firedef ) return qfalse; // Aim VectorCopy( self->enemy->s.origin, target ); fire_origin[0] = self->s.origin[0]; fire_origin[1] = self->s.origin[1]; fire_origin[2] = self->s.origin[2] + self->viewheight; if( !BOT_DMclass_CheckShot( self, target ) ) return qfalse; // find out our weapon AIM style if( AIWeapons[weapon].aimType == AI_AIMSTYLE_PREDICTION_EXPLOSIVE ) { BOT_DMclass_PredictProjectileShot( self, fire_origin, firedef->speed, target, self->enemy->velocity ); if( game.gametype == GAMETYPE_MIDAIR ) wfac = 40.0f; // highest precission in midair else wfac = 200.0f; //aim to the feets when enemy isn't higher if( fire_origin[2] > (target[2] + (self->enemy->r.mins[2] * 0.8)) ) { vec3_t checktarget; VectorSet( checktarget, self->enemy->s.origin[0], self->enemy->s.origin[1], self->enemy->s.origin[2] + self->enemy->r.mins[2] + 4 ); trap_Trace( &trace, fire_origin, vec3_origin, vec3_origin, checktarget, self, MASK_SHOT ); if( trace.fraction == 1.0f || (trace.ent > 0 && game.edicts[trace.ent].takedamage) ) VectorCopy( checktarget, target ); } else if( game.gametype != GAMETYPE_MIDAIR && !AI_IsStep(self->enemy) ) wfac = 260.0f; // more imprecise for air rockets unless it's midair } else if ( AIWeapons[weapon].aimType == AI_AIMSTYLE_PREDICTION ) { wfac = 180.0f; BOT_DMclass_PredictProjectileShot( self, fire_origin, firedef->speed, target, self->enemy->velocity ); } else if ( AIWeapons[weapon].aimType == AI_AIMSTYLE_DROP ) { //jalToDo wfac = 160.0f; BOT_DMclass_PredictProjectileShot( self, fire_origin, firedef->speed, target, self->enemy->velocity ); } else { // AI_AIMSTYLE_INSTANTHIT if( self->s.weapon == WEAP_ELECTROBOLT ) wfac = 250.0f; else if( self->s.weapon == WEAP_LASERGUN ) wfac = 200.0f; else wfac = 160.0f; } wfac *= ( 1.0f - self->ai.pers.skillLevel ); // modify attack angles based on accuracy //target[0] += (random()-0.5f) * wfac; //target[1] += (random()-0.5f) * wfac; // look to target VectorSubtract( target, fire_origin, self->ai.move_vector ); // Set the attack (fix this ramdomness) firedelay = ( 100.0f * (random()-0.25f) ) + ( 10.0f * self->ai.pers.skillLevel ); if( firedelay > 0.0f ) { ucmd->buttons = BUTTON_ATTACK;; // could fire, but wants to? // mess up angles { target[0] += (random()-0.5f) * wfac; target[1] += (random()-0.5f) * wfac; } } //update angles VectorSubtract( target, fire_origin, dir ); VecToAngles( dir, angles ); VectorCopy( angles, self->s.angles ); VectorCopy( angles, self->r.client->v_angle ); if( AIDevel.debugChased && bot_showcombat->integer ) G_PrintMsg( AIDevel.chaseguy, "%s: attacking %s\n", self->ai.pers.netname ,self->enemy->r.client ? self->enemy->r.client->pers.netname : self->classname ); return qtrue; } //========================================== // BOT_DMclass_WeightPlayers // weight players based on game state //========================================== void BOT_DMclass_WeightPlayers( edict_t *self ) { int i, team; //clear memset( self->ai.status.playersWeights, 0, sizeof(self->ai.status.playersWeights) ); for( i = 0; i < num_AIEnemies; i++ ) { if( !AIEnemies[i] ) continue; if( AIEnemies[i] == self ) continue; //ignore spectators and dead players if( AIEnemies[i]->r.svflags & SVF_NOCLIENT || AIEnemies[i]->deadflag ) { self->ai.status.playersWeights[i] = 0.0f; continue; } //if not team based give some weight to every one if( !GS_Gametype_IsTeamBased(game.gametype) ) { self->ai.status.playersWeights[i] = 0.3f; continue; } //team based. Ignore team mates if( AIEnemies[i]->s.team == self->s.team ) continue; if( game.gametype == GAMETYPE_CTF ) { qboolean has_enemy_flag = qfalse; //if the bot has a enemy flag we don't want it going after enemies for( team = TEAM_RED; team < TEAM_RED + g_maxteams->integer; team++ ) { if( team == self->s.team ) continue; if( G_Gametype_CTF_HasFlag(self, team) ) { has_enemy_flag = qtrue; break; } } if( has_enemy_flag ) { //everyone has ultra-low weight self->ai.status.playersWeights[i] = 0.01f; } else { //being at enemy team gives a small weight self->ai.status.playersWeights[i] = 0.2f; //if enemy has our flag, big weight on enemy if( G_Gametype_CTF_HasFlag(AIEnemies[i], self->s.team) ) self->ai.status.playersWeights[i] = 0.9f; } continue; } //else is some other team based gametype self->ai.status.playersWeights[i] = 0.3f; } } //========================================== // BOT_DMclass_WantedFlag // find needed flag //========================================== gitem_t *BOT_DMclass_WantedFlag( edict_t *self ) { qboolean has_enemy_flag = qfalse; edict_t *ent; int team; if( !self->r.client ) return NULL; if( !self->s.team ) G_Printf( "ERROR: BOT_DMclass_WantedFlag: Player without a defined team\n" ); //basically, if we have any other's team flag, we want ours, and viceversa //see if we have any others flag for( team = TEAM_RED; team < TEAM_RED + g_maxteams->integer; team++ ) { if(team == self->s.team) continue; if( G_Gametype_CTF_HasFlag(self, team) ) { has_enemy_flag = qtrue; break; } } //we have it, so we want ours if( has_enemy_flag ) return G_Gametype_CTF_FlagItem( self->s.team ); //TO DO: right now it just returns the first it finds, we should set up all them for( team = TEAM_RED; team < TEAM_RED + g_maxteams->integer; team++ ) { gitem_t *flag = NULL; if(team == self->s.team) continue; flag = G_Gametype_CTF_FlagItem( team ); if( !flag ) continue; ent = NULL; while( (ent = G_Find (ent, FOFS(classname), flag->classname)) != NULL ) { if( !(ent->spawnflags & DROPPED_ITEM) ) { if( ent->r.svflags & SVF_NOCLIENT ) //flag is not at base break; else return flag; } } } return NULL; } //========================================== // BOT_DMclass_WeightInventory // weight items up or down based on bot needs //========================================== void BOT_DMclass_WeightInventory( edict_t *self ) { float LowNeedFactor = 0.5; gclient_t *client; int i; client = self->r.client; //reset with persistant values memcpy( self->ai.status.inventoryWeights, self->ai.pers.inventoryWeights, sizeof(self->ai.pers.inventoryWeights) ); //AMMO: for( i = WEAP_GUNBLADE; i < WEAP_TOTAL; i++ ) { //find out if it's packed up if( !AI_CanPick_Ammo (self, AIWeapons[i].ammoItem) ) self->ai.status.inventoryWeights[AIWeapons[i].ammoItem->tag] = 0.0; //find out if it has a weapon for this amno else if( !client->inventory[AIWeapons[i].weaponItem->tag] ) self->ai.status.inventoryWeights[AIWeapons[i].ammoItem->tag] *= LowNeedFactor; } //WEAPONS //weight weapon down if bot already has it for( i = WEAP_GUNBLADE; i < WEAP_TOTAL; i++ ) { if( AIWeapons[i].weaponItem && client->inventory[AIWeapons[i].weaponItem->tag] ) self->ai.status.inventoryWeights[AIWeapons[i].weaponItem->tag] *= LowNeedFactor; } // ARMOR for( i = ARMOR_GA; i < ARMOR_TOTAL; i++ ) self->ai.status.inventoryWeights[i] = self->ai.pers.inventoryWeights[i] * AI_CanUseArmor( game.items[i], self ); //CTF: if( game.gametype == GAMETYPE_CTF ) { gitem_t *wantedFlag, *otherflag; int team; wantedFlag = BOT_DMclass_WantedFlag(self); //Returns the flag gitem_t // all flags have weights defined inside persistant inventory. Remove weight from the unwanted one/s. for( team = TEAM_RED; team < TEAM_RED + g_maxteams->integer; team++ ) { otherflag = G_Gametype_CTF_FlagItem(team); if( otherflag && otherflag != wantedFlag ) self->ai.status.inventoryWeights[otherflag->tag] = 0.0; } } } //========================================== // BOT_DMclass_UpdateStatus // update ai.status values based on bot state, // so ai can decide based on these settings //========================================== void BOT_DMclass_UpdateStatus( edict_t *self ) { self->enemy = NULL; self->movetarget = NULL; //if the bot is at spectator team, try to join if( self->s.team == TEAM_SPECTATOR ) { //extend the suicide timout self->ai.bloqued_timeout = level.time + 20.0; //try to join if( !self->r.client->pers.queueTimeStamp ) G_Teams_JoinAnyTeam( self, qfalse ); if( self->s.team == TEAM_SPECTATOR ) //couldn't join, delay the next think { self->nextthink = level.timemsec + 1000 + (int)(6000 * random()); return; } else { self->timestamp = level.time + 1 + (8 * random()); } } else { //get ready if in the game if( level.time > self->timestamp ) G_Match_Ready( self ); } // Set up for new client movement: jalfixme VectorCopy(self->r.client->ps.viewangles,self->s.angles); VectorSet (self->r.client->ps.pmove.delta_angles, 0, 0, 0); if (self->r.client->jumppad_time) self->ai.status.jumpadReached = qtrue; //jumpad time from client to botStatus else self->ai.status.jumpadReached = qfalse; if (self->r.client->ps.pmove.pm_flags & PMF_TIME_TELEPORT) self->ai.status.TeleportReached = qtrue; else self->ai.status.TeleportReached = qfalse; //set up AI status for the upcoming AI_frame BOT_DMclass_WeightInventory( self ); //weight items BOT_DMclass_WeightPlayers( self ); //weight players } #ifdef VSAYS qboolean G_BOTvsay_f( edict_t *ent, char *msg, qboolean team ); void BOT_DMclass_VSAYmessages( edict_t *self ) { gitem_t *itemgoal = NULL; if( match.state != MATCH_STATE_PLAYTIME ) return; if( self->r.client->damageteam_given > 25 ) { if( rand() & 1 ) { G_BOTvsay_f( self, "oops", qtrue ); } else { G_BOTvsay_f( self, "sorry", qtrue ); } return; } if( self->ai.vsay_timeout > level.timemsec ) return; if( match.endtime && level.time > match.endtime - 4.0f ) { self->ai.vsay_timeout = level.timemsec + 1000*(1 + match.endtime - level.time); G_BOTvsay_f( self, "goodgame", qfalse ); return; } self->ai.vsay_timeout = level.timemsec + ((3+random()*12) * 1000); if( GS_Gametype_IsTeamBased(game.gametype) ) { if( self->ai.vsay_goalent && self->ai.vsay_goalent->item ) { itemgoal = self->ai.vsay_goalent->item; if( game.gametype == GAMETYPE_CTF ) { gitem_t *wantedFlag = BOT_DMclass_WantedFlag( self ); gitem_t *myFlag = G_Gametype_CTF_FlagItem( self->s.team ); if( wantedFlag == myFlag && itemgoal == wantedFlag && random() > 0.7 ) { G_BOTvsay_f( self, "needbackup", qtrue ); return; } if( itemgoal == wantedFlag && random() > 0.7 ) { G_BOTvsay_f( self, "onoffense", qtrue ); return; } } } if( self->health < 20 && random() > 0.3 ) { G_BOTvsay_f( self, "needhealth", qtrue ); return; } if( (self->s.weapon == 0 || self->s.weapon == 1) && random() > 0.7 ) { G_BOTvsay_f( self, "needweapon", qtrue ); return; } if( self->r.client->armor < 10 && random() > 0.8 ) { G_BOTvsay_f( self, "needarmor", qtrue ); return; } } else if( random() > 1.0f / game.numBots ) { // when not team based we only have these options, so reduce the chances of being played return; } if( random() > 0.8 ) { G_BOTvsay_f( self, "yeehaa", GS_Gametype_IsTeamBased(game.gametype) ); return; } if( random() > 0.8 ) { G_BOTvsay_f( self, "attack", GS_Gametype_IsTeamBased(game.gametype) ); return; } if( random() > 0.8 ) { G_BOTvsay_f( self, "noproblem", GS_Gametype_IsTeamBased(game.gametype) ); return; } } #endif // VSAYS //========================================== // BOT_DMClass_BloquedTimeout // the bot has been bloqued for too long //========================================== void BOT_DMClass_BloquedTimeout( edict_t *self ) { self->health = 0; self->ai.bloqued_timeout = level.time + 15.0; self->die( self, self, self, 100000, vec3_origin ); self->nextthink = level.timemsec + game.framemsec; } //========================================== // BOT_DMclass_DeadFrame // ent is dead = run this think func //========================================== void BOT_DMclass_DeadFrame( edict_t *self ) { usercmd_t ucmd; ucmd.buttons = 0; // set approximate ping and show values ucmd.msec = 75 + floor (random () * 25) + 1; self->r.client->r.ping = ucmd.msec; // ask for respawn if the minimum bot respawning time passed if( level.timemsec > self->deathtimestamp + 3000 ) { self->r.client->buttons = self->r.client->latched_buttons = 0; ucmd.buttons = BUTTON_ATTACK; } ClientThink( self, &ucmd ); self->nextthink = level.timemsec + game.framemsec; } //========================================== // BOT_DMclass_RunFrame // States Machine & call client movement //========================================== void BOT_DMclass_RunFrame( edict_t *self ) { usercmd_t ucmd; memset( &ucmd, 0, sizeof(ucmd) ); #ifndef WSW_RELEASE if( !bot_dummy->integer ) { #endif // Look for enemies self->enemy = BOT_DMclass_FindEnemy(self); if( self->enemy && BOT_DMclass_ChooseWeapon(self) >= 0.3 ) // don't fight with bad weapons { if( BOT_DMclass_FireWeapon( self, &ucmd ) ) { self->ai.state = BOT_STATE_ATTACK; self->ai.state_combat_timeout = level.time + 1.0; } } else if( self->ai.state == BOT_STATE_ATTACK && level.time > self->ai.state_combat_timeout) { //Jalfixme: change to: AI_SetUpStateMove(self); self->ai.state = BOT_STATE_MOVE; } // Execute the move, or wander if( self->ai.state == BOT_STATE_MOVE ) BOT_DMclass_Move( self, &ucmd ); else if(self->ai.state == BOT_STATE_ATTACK) BOT_DMclass_CombatMovement( self, &ucmd ); else if ( self->ai.state == BOT_STATE_WANDER ) BOT_DMclass_Wander( self, &ucmd ); //set up for pmove ucmd.angles[PITCH] = ANGLE2SHORT(self->s.angles[PITCH]); ucmd.angles[YAW] = ANGLE2SHORT(self->s.angles[YAW]); ucmd.angles[ROLL] = ANGLE2SHORT(self->s.angles[ROLL]); // press button walk if( abs(ucmd.forwardmove) <= 200 && abs(ucmd.sidemove) <= 200 ) ucmd.buttons |= BUTTON_WALK; #ifndef WSW_RELEASE } #endif // set approximate ping and show values ucmd.msec = game.framemsec; self->r.client->r.ping = ucmd.msec - floor(random()*25); // send command through id's code ClientThink( self, &ucmd ); self->nextthink = level.timemsec + game.framemsec; // vsay #ifdef VSAYS BOT_DMclass_VSAYmessages( self ); #endif } //========================================== // BOT_DMclass_InitPersistant // Persistant after respawns. //========================================== void BOT_DMclass_InitPersistant(edict_t *self) { self->classname = "dmbot"; //copy name if( self->r.client->pers.netname ) self->ai.pers.netname = self->r.client->pers.netname; else self->ai.pers.netname = "dmBot"; //set 'class' functions self->ai.pers.RunFrame = BOT_DMclass_RunFrame; self->ai.pers.UpdateStatus = BOT_DMclass_UpdateStatus; self->ai.pers.bloquedTimeout = BOT_DMClass_BloquedTimeout; self->ai.pers.deadFrame = BOT_DMclass_DeadFrame; //available moveTypes for this class self->ai.pers.moveTypesMask = (LINK_MOVE|LINK_STAIRS|LINK_FALL|LINK_WATER|LINK_WATERJUMP|LINK_JUMPPAD|LINK_PLATFORM|LINK_TELEPORT|LINK_LADDER|LINK_JUMP|LINK_CROUCH); //Persistant Inventory Weights (0 = can not pick) memset( self->ai.pers.inventoryWeights, 0, sizeof (self->ai.pers.inventoryWeights) ); // weapons self->ai.pers.inventoryWeights[WEAP_GUNBLADE] = 0.0f; self->ai.pers.inventoryWeights[WEAP_SHOCKWAVE] = 0.0f; self->ai.pers.inventoryWeights[WEAP_RIOTGUN] = 0.5f; self->ai.pers.inventoryWeights[WEAP_GRENADELAUNCHER] = 0.6f; self->ai.pers.inventoryWeights[WEAP_ROCKETLAUNCHER] = 0.8f; self->ai.pers.inventoryWeights[WEAP_PLASMAGUN] = 0.7f; self->ai.pers.inventoryWeights[WEAP_ELECTROBOLT] = 0.8f; self->ai.pers.inventoryWeights[WEAP_LASERGUN] = 0.8f; //ammo //self->ai.pers.inventoryWeights[AMMO_WEAK_GUNBLADE] = 0.4f; self->ai.pers.inventoryWeights[AMMO_WAVES] = 0.1f; self->ai.pers.inventoryWeights[AMMO_SHELLS] = 0.5f; self->ai.pers.inventoryWeights[AMMO_GRENADES] = 0.5f; self->ai.pers.inventoryWeights[AMMO_ROCKETS] = 0.6f; self->ai.pers.inventoryWeights[AMMO_PLASMA] = 0.5f; self->ai.pers.inventoryWeights[AMMO_BOLTS] = 0.6f; self->ai.pers.inventoryWeights[AMMO_LASERS] = 0.6f; //armor self->ai.pers.inventoryWeights[ARMOR_RA] = 0.9f; self->ai.pers.inventoryWeights[ARMOR_YA] = 0.8f; self->ai.pers.inventoryWeights[ARMOR_GA] = 0.5f; self->ai.pers.inventoryWeights[ARMOR_SHARD] = 0.2f; //backpack self->ai.pers.inventoryWeights[AMMO_PACK] = 0.4f; if( game.gametype == GAMETYPE_CTF ) { self->ai.pers.inventoryWeights[FLAG_BLUE] = 4.0f; self->ai.pers.inventoryWeights[FLAG_RED] = 4.0f; self->ai.pers.inventoryWeights[FLAG_YELLOW] = 4.0f; self->ai.pers.inventoryWeights[FLAG_GREEN] = 4.0f; } }