/* =========================================================================== Return to Castle Wolfenstein multiplayer GPL Source Code Copyright (C) 1999-2010 id Software LLC, a ZeniMax Media company. This file is part of the Return to Castle Wolfenstein multiplayer GPL Source Code (“RTCW MP Source Code”). RTCW MP 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 MP 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 MP Source Code. If not, see . In addition, the RTCW MP 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 MP 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_cast_funcs.c * * desc: Wolfenstein AI Character Decision Making * */ #include "../game/g_local.h" #include "../game/q_shared.h" #include "../game/botlib.h" //bot lib interface #include "../game/be_aas.h" #include "../game/be_ea.h" #include "../game/be_ai_gen.h" #include "../game/be_ai_goal.h" #include "../game/be_ai_move.h" #include "../botai/botai.h" //bot ai interface #include "ai_cast.h" /* This file contains the generic thinking states for the characters. Different types of movement or behaviour will be represented by a seperate thinking function, which may or may not pass control over to a new behaviour function. If control is passed onto a new function, the string name of the current function is returned, mostly for debugging purposes. !!! NOTE: control must not be passed to a new AI func from outside of this file. A new AI func must only be called from within another AI func. This gives us the ability to keep all code related to sections of AI self-contained, so adding new features to the AI will be less likely to step on other areas of AI. */ static int enemies[MAX_CLIENTS], numEnemies; // this is used to prevent try/abort/try/abort/etc grenade flush behaviour static int lastGrenadeFlush = 0; #define AICAST_LEADERDIST_MAX 240 // try and stay at least this close to them when nothing else to do #define AICAST_LEADERDIST_MIN 64 // get this close if we have a clear line of sight to them char *AIFunc_BattleChase( cast_state_t *cs ); char *AIFunc_Battle( cast_state_t *cs ); static bot_moveresult_t *moveresult; /* ============ AIFunc_Restore() restores the last aifunc that was backed up ============ */ char *AIFunc_Restore( cast_state_t *cs ) { // if the old aifunc was BattleChase, set it back to Battle, in case we have found a good position if ( cs->oldAifunc == AIFunc_BattleChase ) { cs->oldAifunc = AIFunc_Battle; } cs->aifunc = cs->oldAifunc; return cs->aifunc( cs ); } /* ============ AICast_GetRandomViewAngle() ============ */ float AICast_GetRandomViewAngle( cast_state_t *cs, float tracedist ) { int cnt, passent, contents_mask; vec3_t vec, dir, start, end; trace_t trace; float bestdist, bestyaw; cnt = 0; VectorClear( vec ); // VectorCopy( cs->bs->origin, start ); start[2] += cs->bs->cur_ps.viewheight; // passent = cs->bs->entitynum; contents_mask = CONTENTS_SOLID | CONTENTS_PLAYERCLIP | CONTENTS_WATER | CONTENTS_SLIME; // contents_mask = CONTENTS_SOLID|CONTENTS_PLAYERCLIP|CONTENTS_WATER; bestdist = 0; bestyaw = 0; // while ( cnt++ < 4 ) { vec[YAW] = random() * 360.0; // AngleVectors( vec, dir, NULL, NULL ); VectorMA( start, tracedist, dir, end ); // trap_Trace( &trace, start, NULL, NULL, end, passent, contents_mask ); // if ( trace.fraction >= 1 ) { return vec[YAW]; } else if ( !bestdist || bestdist < trace.fraction ) { bestdist = trace.fraction; bestyaw = vec[YAW]; } } // if ( bestdist ) { return bestyaw; } // just return their current direction return cs->bs->ideal_viewangles[YAW]; } /* ============ AICast_MoveToPos() returns a pointer to the moveresult it used to make the move, so we can investigate it outside of this function ============ */ bot_moveresult_t *AICast_MoveToPos( cast_state_t *cs, vec3_t pos, int entnum ) { bot_goal_t goal; vec3_t /*target,*/ dir; static bot_moveresult_t lmoveresult; int tfl; bot_state_t *bs; //int pretime = Sys_MilliSeconds(); moveresult = NULL; if ( cs->castScriptStatus.scriptNoMoveTime > level.time ) { return NULL; } if ( cs->pauseTime > level.time ) { return NULL; } // bs = cs->bs; tfl = cs->travelflags; //if in lava or slime the bot should be able to get out if ( BotInLava( bs ) ) { tfl |= TFL_LAVA; } if ( BotInSlime( bs ) ) { tfl |= TFL_SLIME; } // //create the chase goal memset( &goal, 0, sizeof( goal ) ); goal.entitynum = entnum; goal.areanum = BotPointAreaNum( pos ); VectorCopy( pos, goal.origin ); VectorSet( goal.mins, -8, -8, -8 ); VectorSet( goal.maxs, 8, 8, 8 ); if ( entnum == cs->followEntity && !cs->followSlowApproach ) { goal.flags |= GFL_NOSLOWAPPROACH; // just speed right passed it } // // debugging, show the route if ( aicast_debug.integer == 2 && ( g_entities[cs->entityNum].aiName && !strcmp( aicast_debugname.string, g_entities[cs->entityNum].aiName ) ) ) { trap_AAS_RT_ShowRoute( cs->bs->origin, cs->bs->areanum, goal.areanum ); } // //initialize the movement state BotSetupForMovement( bs ); //if this is a slow moving creature, don't use avoidreach if ( cs->attributes[RUNNING_SPEED] < 100 ) { //reset the avoid reach, otherwise bot is stuck in current area trap_BotResetAvoidReach( bs->ms ); } else if ( !VectorCompare( cs->lastMoveToPosGoalOrg, pos ) ) { //reset the avoid reach, otherwise bot is stuck in current area trap_BotResetAvoidReach( bs->ms ); VectorCopy( pos, cs->lastMoveToPosGoalOrg ); } //move towards the goal trap_BotMoveToGoal( &lmoveresult, bs->ms, &goal, tfl ); //if the movement failed if ( lmoveresult.failure ) { //reset the avoid reach, otherwise bot is stuck in current area trap_BotResetAvoidReach( bs->ms ); //BotAI_Print(PRT_MESSAGE, "movement failure %d\n", lmoveresult.traveltype); // clear all movement trap_EA_Move( cs->entityNum, vec3_origin, 0 ); } // if ( lmoveresult.flags & ( MOVERESULT_MOVEMENTVIEW | MOVERESULT_SWIMVIEW ) ) { VectorCopy( lmoveresult.ideal_viewangles, bs->ideal_viewangles ); VectorCopy( bs->ideal_viewangles, cs->viewlock_viewangles ); cs->aiFlags |= AIFL_VIEWLOCKED; } else if ( !( cs->bs->flags & BFL_ATTACKED ) ) { // if we are attacking, don't change angles bot_input_t bi; trap_EA_GetInput( bs->client, 0.1, &bi ); if ( VectorLength( lmoveresult.movedir ) < 0.5 ) { VectorSubtract( goal.origin, bs->origin, dir ); vectoangles( dir, bs->ideal_viewangles ); } else { // use our velocity if we are moving if ( VectorNormalize2( cs->bs->cur_ps.velocity, dir ) > 1 ) { vectoangles( dir, bs->ideal_viewangles ); } else { vectoangles( lmoveresult.movedir, bs->ideal_viewangles ); } } bs->ideal_viewangles[2] *= 0.5; // disabled, needs work /* if (lmoveresult.flags & MOVERESULT_FUTUREVIEW) { if (AngleDifference(bs->ideal_viewangles[1], lmoveresult.ideal_viewangles[1]) > 45) bs->ideal_viewangles[1] += 45; else if (AngleDifference(bs->ideal_viewangles[1], lmoveresult.ideal_viewangles[1]) < -45) bs->ideal_viewangles[1] -= 45; else bs->ideal_viewangles[1] = lmoveresult.ideal_viewangles[1]; bs->ideal_viewangles[1] = AngleNormalize360( bs->ideal_viewangles[1] ); bs->ideal_viewangles[0] = lmoveresult.ideal_viewangles[0]; bs->ideal_viewangles[0] = 0.5*AngleNormalize180( bs->ideal_viewangles[0] ); } */ } // this must go last so we face the direction we avoid move AICast_Blocked( cs, &lmoveresult, qfalse, &goal ); //G_Printf("MoveToPos: %i ms\n", -pretime + Sys_MilliSeconds() ); // debug, print movement info if ( 0 ) { // (SA) added to hide the print bot_input_t bi; trap_EA_GetInput( cs->bs->client, (float) level.time / 1000, &bi ); G_Printf( "spd: %i\n", (int)bi.speed ); } return &lmoveresult; } /* ============ AICast_SpeedScaleForDistance() ============ */ float AICast_SpeedScaleForDistance( cast_state_t *cs, float startdist, float idealDist ) { #define PREDICT_TIME_WALK 0.2 #define PREDICT_TIME_CROUCH 0.2 #define PREDICT_TIME_RUN 0.3 float speed, dist; dist = startdist - idealDist; if ( dist < 1 ) { dist = 1; } // if walking if ( cs->movestate == MS_WALK ) { speed = cs->attributes[WALKING_SPEED]; if ( speed * PREDICT_TIME_WALK > dist ) { return 0.2 + 0.8 * ( dist / ( speed * PREDICT_TIME_WALK ) ); } else { return 1.0; } } else // if crouching if ( cs->movestate == MS_CROUCH || cs->bs->attackcrouch_time > trap_AAS_Time() ) { speed = cs->attributes[RUNNING_SPEED] * cs->bs->cur_ps.crouchSpeedScale; if ( speed * PREDICT_TIME_CROUCH > dist ) { return 0.3 + 0.7 * ( dist / ( speed * PREDICT_TIME_CROUCH ) ); } else { return 1.0; } } else // running { speed = cs->attributes[RUNNING_SPEED]; if ( speed * PREDICT_TIME_RUN > dist ) { return 0.2 + 0.8 * ( dist / ( speed * PREDICT_TIME_RUN ) ); } else { return 1.0; } } } /* ============ AIFunc_Idle() The cast AI is standing around, contemplating the meaning of life ============ */ char *AIFunc_Idle( cast_state_t *cs ) { // we are in an idle state, looking for something to do // // do we need to avoid a danger? if ( cs->dangerEntityValidTime >= level.time ) { if ( !AICast_GetTakeCoverPos( cs, cs->dangerEntity, cs->dangerEntityPos, cs->takeCoverPos ) ) { // shit?? } // go to a position that cannot be seen from the dangerPos cs->takeCoverTime = cs->dangerEntityValidTime + 1000; cs->bs->attackcrouch_time = 0; return AIFunc_AvoidDangerStart( cs ); } // // are we waiting for a door? if ( cs->doorMarkerTime > level.time - 100 ) { return AIFunc_DoorMarkerStart( cs, cs->doorMarkerDoor, cs->doorMarkerNum ); } // // do we need to go to our leader? if ( cs->leaderNum >= 0 && Distance( cs->bs->origin, g_entities[cs->leaderNum].r.currentOrigin ) > MAX_LEADER_DIST ) { return AIFunc_ChaseGoalStart( cs, cs->leaderNum, AICAST_LEADERDIST_MAX, qtrue ); } // // look for things we should attack numEnemies = AICast_ScanForEnemies( cs, enemies ); if ( numEnemies == -1 ) { // query mode return NULL; } else if ( numEnemies == -2 ) { // inspection may be required char *retval; // TTimo gcc: suggest parentheses around assignment used as truth value if ( ( retval = AIFunc_InspectFriendlyStart( cs, enemies[0] ) ) ) { return retval; } } else if ( numEnemies == -3 ) { // bullet impact if ( cs->aiState < AISTATE_COMBAT ) { return AIFunc_InspectBulletImpactStart( cs ); } } else if ( numEnemies == -4 ) { // audible event if ( cs->aiState < AISTATE_COMBAT ) { return AIFunc_InspectAudibleEventStart( cs, cs->audibleEventEnt ); } } else if ( numEnemies > 0 ) { int i; cs->bs->enemy = -1; // choose an enemy for ( i = 0; i < numEnemies; i++ ) { if ( Distance( cs->bs->origin, cs->vislist[enemies[i]].visible_pos ) > 16 ) { // if we are really close to the last place we saw them, no point trying to attack, since we'll just end up back here if ( cs->bs->enemy < 0 ) { cs->bs->enemy = enemies[i]; } else if ( AICast_CheckAttack( cs, enemies[i], qfalse ) ) { cs->bs->enemy = enemies[i]; return AIFunc_BattleStart( cs ); } } } if ( cs->bs->enemy >= 0 ) { if ( ( ( cs->leaderNum < 0 ) || ( cs->thinkFuncChangeTime < level.time - 3000 ) ) && AICast_WantsToChase( cs ) ) { // don't leave our leader as soon as we get to them return AIFunc_BattleStart( cs ); } else if ( AICast_EntityVisible( AICast_GetCastState( cs->bs->enemy ), cs->entityNum, qtrue ) || AICast_CheckAttack( AICast_GetCastState( cs->bs->enemy ), cs->entityNum, qfalse ) ) { // if we are tactical enough, look for a hiding spot if ( !( cs->leaderNum >= 0 ) && cs->attributes[TACTICAL] > 0.4 && cs->attributes[AGGRESSION] < 1.0 ) { // they can see us, and we want to hide from them if ( !AICast_GetTakeCoverPos( cs, cs->bs->enemy, cs->vislist[cs->bs->enemy].visible_pos, cs->takeCoverPos ) ) { // go to a position that cannot be seen from the last place we saw the enemy, and wait there for some time cs->takeCoverTime = level.time + 2000 + rand() % 3000; return AIFunc_BattleTakeCoverStart( cs ); } } // attack them if nothing else to do, since they can attack us here return AIFunc_BattleStart( cs ); } else if ( cs->leaderNum < 0 ) { // we should pursue if no leader, and not wanting to hide return AIFunc_BattleStart( cs ); } else { // they can't see us anyway, so ignore them cs->lastEnemy = cs->bs->enemy; // at least face them if they come to get us cs->bs->enemy = -1; // crouching makes us look like we are hiding, which is what we are doing if ( cs->attributes[ATTACK_CROUCH] > 0.5 ) { cs->bs->attackcrouch_time = trap_AAS_Time() + 1; } } } } // // if we are in combat mode, then we should relax, since we dont have an enemy if ( cs->aiState >= AISTATE_COMBAT ) { AICast_StateChange( cs, AISTATE_ALERT ); } // // if we couldn't find anything, see if our previous enemy is still around, if so, go find them // this is an attempt to prevent guys from running away to hide from something, never to // be seen again. They shouldn't really "forget" that they are indeed soldiers. if ( !( cs->leaderNum >= 0 ) && cs->lastEnemy >= 0 && g_entities[cs->lastEnemy].health > 0 && cs->vislist[cs->lastEnemy].real_visible_timestamp < level.time - 5000 && cs->takeCoverTime < level.time - 5000 ) { cs->bs->enemy = cs->lastEnemy; // just go to the place we last saw them return AIFunc_BattleStart( cs ); } // // if we've recently been in a fight, keep looking around, so we don't look stupid if ( cs->lastEnemy >= 0 ) { // // if we like to crouch, then do so, since we are in a battle situation /* if (cs->lastEnemy > 0) { // not the player if (cs->attributes[ATTACK_CROUCH] > 0.3) { cs->bs->attackcrouch_time = trap_AAS_Time() + 2; } } else { // if we were attacking the player, relax a bit more if (cs->attributes[ATTACK_CROUCH] > 0.6) { cs->bs->attackcrouch_time = trap_AAS_Time() + 2; } } */ // if we only just recently saw them, face that direction if ( ( g_entities[cs->lastEnemy].health > 0 ) && cs->vislist[cs->lastEnemy].visible_timestamp > ( level.time - 20000 ) && AICast_VisibleFromPos( cs->bs->origin, cs->entityNum, cs->vislist[cs->lastEnemy].visible_pos, cs->lastEnemy, qfalse ) ) { vec3_t dir; // VectorSubtract( cs->vislist[cs->lastEnemy].visible_pos, cs->bs->origin, dir ); if ( VectorLength( dir ) < 1 ) { cs->bs->ideal_viewangles[PITCH] = 0; } else { VectorNormalize( dir ); vectoangles( dir, cs->bs->ideal_viewangles ); cs->bs->ideal_viewangles[PITCH] = AngleNormalize180( cs->bs->ideal_viewangles[PITCH] ) * 0.5; } // if we like to crouch, then do so, since we are in a battle situation // if (cs->attributes[ATTACK_CROUCH] > 0.1) { // cs->bs->attackcrouch_time = trap_AAS_Time() + 2; // } } else if ( /*cs->castScriptStatus.castScriptEventIndex < 0 &&*/ cs->attributes[TACTICAL] && cs->nextIdleAngleChange < level.time ) { // wait a second before changing again if ( ( cs->nextIdleAngleChange + 3000 ) < level.time ) { // FIXME: This could be changed to use some AAS sampling, which would: // // Given a src area, pick a random dest area which is visible from that area // and return it's position, which we'd then use to set the next view angles // // This would result in more efficient, more realistic behaviour, since they'd // also use PITCH angles to look at areas above/below them cs->idleYaw = AICast_GetRandomViewAngle( cs, 512 ); if ( abs( AngleDelta( cs->idleYaw, cs->bs->ideal_viewangles[YAW] ) ) < 45 ) { cs->nextIdleAngleChange = level.time + 1000 + rand() % 2500; } else { // do really fast cs->nextIdleAngleChange = level.time + 500; } // adjust with time cs->idleYawChange = AngleDelta( cs->idleYaw, cs->bs->ideal_viewangles[YAW] ); /// ((float)(cs->nextIdleAngleChange - level.time) / 1000.0); cs->bs->ideal_viewangles[PITCH] = 0; } } else if ( cs->idleYawChange ) { cs->idleYawChange = AngleDelta( cs->idleYaw, cs->bs->ideal_viewangles[YAW] ); cs->bs->ideal_viewangles[YAW] = AngleMod( cs->bs->ideal_viewangles[YAW] + ( cs->idleYawChange * cs->bs->thinktime ) ); } } // check for a movement we should be making if ( cs->obstructingTime > level.time ) { AICast_MoveToPos( cs, cs->obstructingPos, -1 ); if ( cs->movestate != MS_CROUCH ) { cs->movestate = MS_WALK; } cs->movestateType = MSTYPE_TEMPORARY; } // set head look flag if no enemy if ( cs->bs->enemy < 0 && cs->attributes[TACTICAL] >= 0.5 && !( cs->aiFlags & AIFL_NO_HEADLOOK ) ) { g_entities[cs->entityNum].client->ps.eFlags |= EF_HEADLOOK; } // reload? if ( ( cs->bs->cur_ps.ammoclip[BG_FindClipForWeapon( cs->bs->cur_ps.weapon )] < (int)( 0.75 * ammoTable[cs->bs->cur_ps.weapon].maxclip ) ) && cs->bs->cur_ps.ammo[BG_FindAmmoForWeapon( cs->bs->cur_ps.weapon )] ) { trap_EA_Reload( cs->entityNum ); } return NULL; } /* ============ AIFunc_IdleStart() ============ */ char *AIFunc_IdleStart( cast_state_t *cs ) { g_entities[cs->entityNum].flags &= ~FL_AI_GRENADE_KICK; // stop following cs->followEntity = -1; // if our enemy has just died, inspect the body if ( cs->bs->enemy >= 0 ) { if ( g_entities[cs->entityNum].aiTeam == AITEAM_NAZI && g_entities[cs->bs->enemy].health <= 0 ) { return AIFunc_InspectBodyStart( cs ); } else { cs->bs->enemy = -1; } } // make sure we don't avoid any areas when we start again trap_BotInitAvoidReach( cs->bs->ms ); // randomly choose idle animation //----(SA) try always using the 'casual' stand on spawn and change to crouching one when 'alerted' if ( cs->aiFlags & AIFL_STAND_IDLE2 ) { // if (rand()%2 || (cs->lastEnemy < 0 && cs->aiFlags & AIFL_TALKING)) g_entities[cs->entityNum].client->ps.eFlags |= EF_STAND_IDLE2; // else // g_entities[cs->entityNum].client->ps.eFlags &= ~EF_STAND_IDLE2; } cs->aifunc = AIFunc_Idle; return "AIFunc_Idle"; } /* ============ AIFunc_InspectFriendly() ============ */ char *AIFunc_InspectFriendly( cast_state_t *cs ) { gentity_t *followent, *ent; bot_state_t *bs; vec3_t destorg; float dist; qboolean moved = qfalse; ent = &g_entities[cs->entityNum]; // if we have an enemy, attack now! if ( cs->bs->enemy >= 0 ) { return AIFunc_BattleStart( cs ); } cs->followEntity = cs->inspectNum; cs->followDist = 64; cs->scriptPauseTime = level.time + 4000; // wait for at least this long before resuming any scripted walking, etc // do we need to avoid a danger? if ( cs->dangerEntityValidTime >= level.time ) { if ( !AICast_GetTakeCoverPos( cs, cs->dangerEntity, cs->dangerEntityPos, cs->takeCoverPos ) ) { // go to a position that cannot be seen from the dangerPos cs->takeCoverTime = cs->dangerEntityValidTime + 1000; cs->bs->attackcrouch_time = 0; cs->castScriptStatus.scriptGotoId = -1; cs->movestate = MS_DEFAULT; cs->movestateType = MSTYPE_NONE; return AIFunc_AvoidDangerStart( cs ); } } // // are we waiting for a door? if ( cs->doorMarkerTime > level.time - 100 ) { return AIFunc_DoorMarkerStart( cs, cs->doorMarkerDoor, cs->doorMarkerNum ); } followent = &g_entities[cs->followEntity]; // if the entity is not ready yet if ( !followent->inuse ) { // if it's a connecting client, wait if ( cs->followEntity < MAX_CLIENTS && ( ( followent->client && followent->client->pers.connected == CON_CONNECTING ) || ( level.time < 3000 ) ) ) { return AIFunc_ChaseGoalIdleStart( cs, cs->followEntity, cs->followDist ); } else // stop following it { AICast_EndChase( cs ); return AIFunc_IdleStart( cs ); } } if ( followent->client ) { VectorCopy( followent->client->ps.origin, destorg ); } else { VectorCopy( followent->r.currentOrigin, destorg ); } // they are ready, are they inside range? FIXME: make configurable dist = Distance( destorg, cs->bs->origin ); if ( !( dist < cs->followDist && ( ent->waterlevel || ( cs->bs->cur_ps.groundEntityNum != ENTITYNUM_NONE ) ) ) ) { // // go to them // bs = cs->bs; // set this flag so we know when we;ve just reached them cs->aiFlags |= AIFL_MISCFLAG1; // move straight to them if we can if ( !moved && ( cs->bs->cur_ps.groundEntityNum != ENTITYNUM_NONE || g_entities[cs->entityNum].waterlevel > 1 ) ) { aicast_predictmove_t move; vec3_t dir; bot_input_t bi; usercmd_t ucmd; trace_t tr; qboolean simTest = qfalse; if ( VectorLength( cs->bs->cur_ps.velocity ) < 120 ) { simTest = qtrue; } if ( !simTest ) { // trace will eliminate most unsuccessful paths trap_Trace( &tr, cs->bs->origin, NULL /*g_entities[cs->entityNum].r.mins*/, NULL /*g_entities[cs->entityNum].r.maxs*/, followent->r.currentOrigin, cs->entityNum, g_entities[cs->entityNum].clipmask ); if ( tr.entityNum == cs->followEntity || tr.fraction == 1 ) { simTest = qtrue; } } if ( simTest ) { // try walking straight to them VectorSubtract( followent->r.currentOrigin, cs->bs->origin, dir ); VectorNormalize( dir ); if ( !ent->waterlevel ) { dir[2] = 0; } trap_EA_Move( cs->entityNum, dir, 400 ); trap_EA_GetInput( cs->entityNum, (float) level.time / 1000, &bi ); AICast_InputToUserCommand( cs, &bi, &ucmd, bs->cur_ps.delta_angles ); AICast_PredictMovement( cs, 10, 0.8, &move, &ucmd, cs->followEntity ); if ( move.stopevent == PREDICTSTOP_HITENT ) { // success! vectoangles( dir, cs->bs->ideal_viewangles ); cs->bs->ideal_viewangles[2] *= 0.5; moved = qtrue; } } } // if ( !moved ) { // use AAS routing moveresult = AICast_MoveToPos( cs, followent->r.currentOrigin, cs->followEntity ); // if we cant get there, forget it if ( moveresult && moveresult->failure ) { return AIFunc_DefaultStart( cs ); } } // should we slow down? if ( cs->followDist && cs->followSlowApproach ) { cs->speedScale = AICast_SpeedScaleForDistance( cs, dist, cs->followDist ); } /* // check for a movement we should be making if (cs->obstructingTime > level.time) { AICast_MoveToPos( cs, cs->obstructingPos, -1 ); if (cs->movestate != MS_CROUCH) { cs->movestate = MS_WALK; } cs->movestateType = MSTYPE_TEMPORARY; } */ } else if ( cs->aiFlags & AIFL_MISCFLAG1 ) { cs->aiFlags &= ~AIFL_MISCFLAG1; if ( g_entities[cs->inspectNum].health <= 0 ) { // call a script event cs->aiFlags &= ~AIFL_DENYACTION; AICast_ForceScriptEvent( cs, "inspectbodyend", g_entities[cs->inspectNum].aiName ); if ( cs->aiFlags & AIFL_DENYACTION ) { // relinguish control back to scripting return AIFunc_DefaultStart( cs ); } } } { int numEnemies; // // look for things we should attack numEnemies = AICast_ScanForEnemies( cs, enemies ); if ( numEnemies == -1 ) { // query mode return NULL; } else if ( numEnemies == -2 ) { // inspection // only override current objective if we are inspecting a dead guy, and the new inspect target is fighting someone if ( ( g_entities[cs->inspectNum].health <= 0 ) && ( g_entities[enemies[0]].health > 0 ) ) { return AIFunc_InspectFriendlyStart( cs, enemies[0] ); } } // RF, disabled this, if we are interrupted, scripts might not work right, and anyway, this is only a bullet, not as if it's a dead guy or anything //else if (numEnemies == -3) // bullet impact //{ // if (cs->aiState < AISTATE_COMBAT) { // return AIFunc_InspectBulletImpactStart( cs ); // } //} else if ( numEnemies > 0 ) { int i; cs->bs->enemy = enemies[0]; // just attack the first one // override with a visible enemy for ( i = 1; i < numEnemies; i++ ) { if ( AICast_CheckAttack( cs, enemies[i], qfalse ) ) { cs->bs->enemy = enemies[i]; break; } else if ( cs->bs->enemy < 0 ) { cs->lastEnemy = enemies[i]; } } return AIFunc_BattleStart( cs ); } } if ( cs->nextIdleAngleChange < level.time ) { // wait a second before changing again if ( ( cs->nextIdleAngleChange + 3000 ) < level.time ) { // FIXME: This could be changed to use some AAS sampling, which would: // // Given a src area, pick a random dest area which is visible from that area // and return it's position, which we'd then use to set the next view angles // // This would result in more efficient, more realistic behaviour, since they'd // also use PITCH angles to look at areas above/below them cs->idleYaw = AICast_GetRandomViewAngle( cs, 512 ); if ( abs( AngleDelta( cs->idleYaw, cs->bs->ideal_viewangles[YAW] ) ) < 45 ) { cs->nextIdleAngleChange = level.time + 1000 + rand() % 2500; } else { // do really fast cs->nextIdleAngleChange = level.time + 500; } // adjust with time cs->idleYawChange = AngleDelta( cs->idleYaw, cs->bs->ideal_viewangles[YAW] ); /// ((float)(cs->nextIdleAngleChange - level.time) / 1000.0); cs->bs->ideal_viewangles[PITCH] = 0; } } else if ( cs->idleYawChange ) { cs->idleYawChange = AngleDelta( cs->idleYaw, cs->bs->ideal_viewangles[YAW] ); cs->bs->ideal_viewangles[YAW] = AngleMod( cs->bs->ideal_viewangles[YAW] + ( cs->idleYawChange * cs->bs->thinktime ) ); } // set head look flag if no enemy if ( cs->bs->enemy < 0 && cs->attributes[TACTICAL] >= 0.5 && !( cs->aiFlags & AIFL_NO_HEADLOOK ) ) { g_entities[cs->entityNum].client->ps.eFlags |= EF_HEADLOOK; } // reload? if ( ( cs->bs->cur_ps.ammoclip[cs->bs->cur_ps.weapon] < (int)( 0.75 * ammoTable[cs->bs->cur_ps.weapon].maxclip ) ) && cs->bs->cur_ps.ammo[BG_FindAmmoForWeapon( cs->bs->cur_ps.weapon )] ) { trap_EA_Reload( cs->entityNum ); } return NULL; } /* ============ AIFunc_InspectFriendlyStart ============ */ char *AIFunc_InspectFriendlyStart( cast_state_t *cs, int entnum ) { cast_state_t *ocs; ocs = AICast_GetCastState( entnum ); // we are about to deal with the request for inspection cs->vislist[entnum].flags &= ~AIVIS_INSPECT; cs->scriptPauseTime = level.time + 4000; // wait for at least this long before resuming any scripted walking, etc if ( ocs->aiState >= AISTATE_COMBAT || g_entities[entnum].health <= 0 ) { // mark this character as having been inspected cs->vislist[entnum].flags |= AIVIS_INSPECTED; } // what should we do? wait here? hide? go see them? // if dead, go see them if ( g_entities[entnum].health <= 0 ) { cs->inspectNum = entnum; cs->aifunc = AIFunc_InspectFriendly; return "AIFunc_InspectFriendlyStart"; } // not dead, so call scripting event AICast_ForceScriptEvent( cs, "inspectfriendlycombatstart", g_entities[entnum].aiName ); if ( cs->aiFlags & AIFL_DENYACTION ) { // ignore this friendly forever and ever amen cs->vislist[entnum].flags |= AIVIS_INSPECTED; return NULL; } // if they are in combat, then act according to aggressiveness if ( ocs->aiState >= AISTATE_COMBAT ) { if ( cs->attributes[AGGRESSION] < 0.3 ) { if ( !AICast_GetTakeCoverPos( cs, entnum, g_entities[entnum].client->ps.origin, cs->takeCoverPos ) ) { cs->takeCoverTime = level.time + 10000; // hide for 10 seconds cs->scriptPauseTime = cs->takeCoverTime; // crouch there if possible if ( cs->attributes[ATTACK_CROUCH] > 0.1 ) { cs->bs->attackcrouch_time = trap_AAS_Time() + 3.0; } return AIFunc_BattleTakeCoverStart( cs ); } } } // if still around, then we need to go to them cs->inspectNum = entnum; cs->aifunc = AIFunc_InspectFriendly; return "AIFunc_InspectFriendly"; } /* ============ AIFunc_InspectBulletImpact() ============ */ char *AIFunc_InspectBulletImpact( cast_state_t *cs ) { gentity_t *ent; vec3_t v1; gclient_t *client; // client = &level.clients[cs->entityNum]; // ent = &g_entities[cs->entityNum]; // cs->bulletImpactIgnoreTime = level.time + 800; // // wait until we are looking at the impact if ( cs->aiFlags & AIFL_MISCFLAG2 ) { // pause any scripting cs->scriptPauseTime = level.time + 1000; // look at bullet impact VectorSubtract( cs->bulletImpactEnd, cs->bs->origin, v1 ); VectorNormalize( v1 ); vectoangles( v1, cs->bs->ideal_viewangles ); // // if we are facing that direction, we've looked at the impact point if ( fabs( cs->bs->ideal_viewangles[YAW] - cs->bs->viewangles[YAW] ) < 1 ) { cs->aiFlags &= ~AIFL_MISCFLAG2; } return NULL; } else if ( cs->aiFlags & AIFL_MISCFLAG1 ) { // clear the flag now cs->aiFlags &= ~AIFL_MISCFLAG1; // start looking back at bullet VectorSubtract( cs->bulletImpactStart, cs->bs->origin, v1 ); VectorNormalize( v1 ); vectoangles( v1, cs->bs->ideal_viewangles ); if ( cs->aiState < AISTATE_ALERT ) { // change to alert state if ( !AICast_StateChange( cs, AISTATE_ALERT ) ) { // stop doing whatever we are doing, and return control to scripting cs->scriptPauseTime = 0; return AIFunc_IdleStart( cs ); } // make sure we didnt change thinkfunc if ( cs->aifunc != AIFunc_InspectBulletImpact ) { //G_Error( "scripting passed control out of AIFunc_InspectBulletImpact(), this is bad" ); return NULL; } } // pause any scripting if ( ent->client->ps.legsTimer ) { cs->scriptPauseTime = level.time + ent->client->ps.legsTimer; } else { // just wait for a few seconds looking at the source cs->scriptPauseTime = level.time + 3500; } } // are we done? if ( cs->scriptPauseTime < level.time ) { return AIFunc_IdleStart( cs ); } // // reload? if ( ( cs->bs->cur_ps.ammoclip[BG_FindClipForWeapon( cs->bs->cur_ps.weapon )] < (int)( 0.75 * ammoTable[cs->bs->cur_ps.weapon].maxclip ) ) && cs->bs->cur_ps.ammo[BG_FindAmmoForWeapon( cs->bs->cur_ps.weapon )] ) { trap_EA_Reload( cs->entityNum ); } // // check for enemies { int numEnemies; // // look for things we should attack numEnemies = AICast_ScanForEnemies( cs, enemies ); if ( numEnemies == -2 ) { // inspection // only override current objective if we are inspecting a dead guy, and the new inspect target is fighting someone if ( ( g_entities[cs->inspectNum].health <= 0 ) && ( g_entities[enemies[0]].health > 0 ) ) { return AIFunc_InspectFriendlyStart( cs, enemies[0] ); } } else if ( numEnemies > 0 ) { int i; cs->bs->enemy = enemies[0]; // just attack the first one // override with a visible enemy for ( i = 1; i < numEnemies; i++ ) { if ( AICast_CheckAttack( cs, enemies[i], qfalse ) ) { cs->bs->enemy = enemies[i]; break; } else if ( cs->bs->enemy < 0 ) { cs->lastEnemy = enemies[i]; } } return AIFunc_BattleStart( cs ); } } // return NULL; } /* ============ AIFunc_InspectBulletImpactStart() ============ */ char *AIFunc_InspectBulletImpactStart( cast_state_t *cs ) { int oldScriptIndex; // set the impact timer so we ignore bullets while inspecting this one cs->bulletImpactIgnoreTime = level.time + 5000; // pause any scripting cs->scriptPauseTime = level.time + 1000; // set this so we know if we've started the trace back to the bullet origin cs->aiFlags |= AIFL_MISCFLAG1; cs->aiFlags |= AIFL_MISCFLAG2; // // call the script event oldScriptIndex = cs->scriptCallIndex; AICast_ScriptEvent( cs, "bulletimpactsound", "" ); if ( oldScriptIndex == cs->scriptCallIndex ) { // no script event, so call the animation script BG_AnimScriptEvent( &g_entities[cs->entityNum].client->ps, ANIM_ET_BULLETIMPACT, qfalse, qtrue ); } // cs->aifunc = AIFunc_InspectBulletImpact; return "AIFunc_InspectBulletImpact"; } /* ============ AIFunc_InspectAudibleEvent() ============ */ char *AIFunc_InspectAudibleEvent( cast_state_t *cs ) { gentity_t *ent; bot_state_t *bs; vec3_t destorg; float dist; qboolean moved = qfalse; ent = &g_entities[cs->entityNum]; // if we have an enemy, attack now! if ( cs->bs->enemy >= 0 ) { return AIFunc_BattleStart( cs ); } cs->followDist = 64; // do we need to avoid a danger? if ( cs->dangerEntityValidTime >= level.time ) { if ( !AICast_GetTakeCoverPos( cs, cs->dangerEntity, cs->dangerEntityPos, cs->takeCoverPos ) ) { // go to a position that cannot be seen from the dangerPos cs->takeCoverTime = cs->dangerEntityValidTime + 1000; cs->bs->attackcrouch_time = 0; cs->castScriptStatus.scriptGotoId = -1; cs->movestate = MS_DEFAULT; cs->movestateType = MSTYPE_NONE; return AIFunc_AvoidDangerStart( cs ); } } // // are we waiting for a door? if ( cs->doorMarkerTime > level.time - 100 ) { return AIFunc_DoorMarkerStart( cs, cs->doorMarkerDoor, cs->doorMarkerNum ); } VectorCopy( cs->audibleEventOrg, destorg ); // they are ready, are they inside range? FIXME: make configurable dist = Distance( destorg, cs->bs->origin ); if ( !( dist < cs->followDist && ( ent->waterlevel || ( cs->bs->cur_ps.groundEntityNum != ENTITYNUM_NONE ) ) ) ) { // // go to them // bs = cs->bs; // set this flag so we know when we;ve just reached them cs->aiFlags |= AIFL_MISCFLAG1; // if not overly aggressive, pursue with caution if ( cs->attributes[AGGRESSION] <= 0.8 ) { cs->movestate = MS_CROUCH; cs->movestateType = MSTYPE_TEMPORARY; } // move straight to them if we can if ( !moved && ( cs->bs->cur_ps.groundEntityNum != ENTITYNUM_NONE || g_entities[cs->entityNum].waterlevel > 1 ) ) { aicast_predictmove_t move; vec3_t dir; bot_input_t bi; usercmd_t ucmd; trace_t tr; qboolean simTest = qfalse; if ( VectorLength( cs->bs->cur_ps.velocity ) < 120 ) { simTest = qtrue; } if ( !simTest ) { // trace will eliminate most unsuccessful paths trap_Trace( &tr, cs->bs->origin, NULL /*g_entities[cs->entityNum].r.mins*/, NULL /*g_entities[cs->entityNum].r.maxs*/, destorg, cs->entityNum, g_entities[cs->entityNum].clipmask ); if ( tr.fraction == 1 ) { simTest = qtrue; } } if ( simTest ) { // try walking straight to them gentity_t *gent; // gent = G_Spawn(); VectorCopy( destorg, gent->r.currentOrigin ); // VectorSubtract( destorg, cs->bs->origin, dir ); VectorNormalize( dir ); if ( !ent->waterlevel ) { dir[2] = 0; } trap_EA_Move( cs->entityNum, dir, 400 ); trap_EA_GetInput( cs->entityNum, (float) level.time / 1000, &bi ); AICast_InputToUserCommand( cs, &bi, &ucmd, bs->cur_ps.delta_angles ); AICast_PredictMovement( cs, 10, 0.8, &move, &ucmd, gent->s.number ); // if ( move.stopevent == PREDICTSTOP_HITENT ) { // success! vectoangles( dir, cs->bs->ideal_viewangles ); cs->bs->ideal_viewangles[2] *= 0.5; moved = qtrue; } // G_FreeEntity( gent ); } } // if ( !moved ) { // use AAS routing AICast_MoveToPos( cs, destorg, -1 ); // if we cant get there, forget it if ( moveresult && moveresult->failure ) { return AIFunc_DefaultStart( cs ); } } // should we slow down? if ( cs->followDist && cs->followSlowApproach ) { cs->speedScale = AICast_SpeedScaleForDistance( cs, dist, cs->followDist ); } /* // check for a movement we should be making if (cs->obstructingTime > level.time) { AICast_MoveToPos( cs, cs->obstructingPos, -1 ); if (cs->movestate != MS_CROUCH) { cs->movestate = MS_WALK; } cs->movestateType = MSTYPE_TEMPORARY; } */ } else if ( cs->aiFlags & AIFL_MISCFLAG1 ) { cs->aiFlags &= ~AIFL_MISCFLAG1; // call a script event cs->aiFlags &= ~AIFL_DENYACTION; AICast_ForceScriptEvent( cs, "inspectsoundend", g_entities[cs->audibleEventEnt].aiName ); if ( cs->aiFlags & AIFL_DENYACTION ) { // relinguish control back to scripting return AIFunc_DefaultStart( cs ); } } else { // look around randomly if ( cs->battleHuntViewTime < level.time ) { cs->battleHuntViewTime = level.time + 700 + rand() % 1000; // set a random viewangle cs->bs->ideal_viewangles[YAW] = AngleMod( cs->bs->ideal_viewangles[YAW] + ( 45.0 + random() * 45.0 ) * ( 2 * ( rand() % 2 ) - 1 ) ); } // if ( cs->scriptPauseTime < level.time ) { // we're done waiting around here return AIFunc_DefaultStart( cs ); } } { int numEnemies; // // look for things we should attack numEnemies = AICast_ScanForEnemies( cs, enemies ); if ( numEnemies == -1 ) { // query mode return NULL; } else if ( numEnemies == -2 ) { // inspection // only override current objective if we are inspecting a dead guy, and the new inspect target is fighting someone if ( ( g_entities[cs->inspectNum].health <= 0 ) && ( g_entities[enemies[0]].health > 0 ) ) { return AIFunc_InspectFriendlyStart( cs, enemies[0] ); } } else if ( numEnemies > 0 ) { int i; cs->bs->enemy = enemies[0]; // just attack the first one // override with a visible enemy for ( i = 1; i < numEnemies; i++ ) { if ( AICast_CheckAttack( cs, enemies[i], qfalse ) ) { cs->bs->enemy = enemies[i]; break; } else if ( cs->bs->enemy < 0 ) { cs->lastEnemy = enemies[i]; } } return AIFunc_BattleStart( cs ); } } // set head look flag if no enemy if ( cs->bs->enemy < 0 && cs->attributes[TACTICAL] >= 0.5 && !( cs->aiFlags & AIFL_NO_HEADLOOK ) ) { g_entities[cs->entityNum].client->ps.eFlags |= EF_HEADLOOK; } // reload? if ( ( cs->bs->cur_ps.ammoclip[cs->bs->cur_ps.weapon] < (int)( 0.75 * ammoTable[cs->bs->cur_ps.weapon].maxclip ) ) && cs->bs->cur_ps.ammo[BG_FindAmmoForWeapon( cs->bs->cur_ps.weapon )] ) { trap_EA_Reload( cs->entityNum ); } return NULL; } /* ============ AIFunc_InspectAudibleEventStart ============ */ char *AIFunc_InspectAudibleEventStart( cast_state_t *cs, int entnum ) { cast_state_t *ocs; int oldScriptIndex; ocs = AICast_GetCastState( entnum ); // we have now processed the audible event (whether we act on it or not) cs->audibleEventTime = -9999; // trigger a script event, which has the ability to deny the request oldScriptIndex = cs->scriptCallIndex; AICast_ForceScriptEvent( cs, "inspectsoundstart", g_entities[cs->audibleEventEnt].aiName ); if ( cs->aiFlags & AIFL_DENYACTION ) { return NULL; } // if not in alert mode, go there now if ( cs->aiState < AISTATE_ALERT ) { AICast_StateChange( cs, AISTATE_ALERT ); } if ( oldScriptIndex == cs->scriptCallIndex ) { BG_AnimScriptEvent( &g_entities[cs->entityNum].client->ps, ANIM_ET_INSPECTSOUND, qfalse, qtrue ); } // pause the scripting for now cs->scriptPauseTime = level.time + 4000; // wait for at least this long before resuming any scripted walking, etc // what should we do? wait here? hide? go see them? // if dead, go see them if ( g_entities[entnum].health <= 0 ) { cs->inspectNum = entnum; cs->aifunc = AIFunc_InspectFriendly; return "AIFunc_InspectFriendlyStart"; } // if they are in combat, then act according to aggressiveness if ( ocs->aiState >= AISTATE_COMBAT ) { if ( cs->attributes[AGGRESSION] < 0.3 ) { if ( !AICast_GetTakeCoverPos( cs, entnum, g_entities[entnum].client->ps.origin, cs->takeCoverPos ) ) { cs->takeCoverTime = level.time + 10000; // hide for 10 seconds cs->scriptPauseTime = cs->takeCoverTime; // crouch there if possible if ( cs->attributes[ATTACK_CROUCH] > 0.1 ) { cs->bs->attackcrouch_time = trap_AAS_Time() + 3.0; } return AIFunc_BattleTakeCoverStart( cs ); } } } cs->aifunc = AIFunc_InspectAudibleEvent; return "AIFunc_InspectAudibleEvent"; } /* ============ AIFunc_ChaseGoalIdle() ============ */ char *AIFunc_ChaseGoalIdle( cast_state_t *cs ) { gentity_t *followent; vec3_t dir; if ( cs->followEntity < 0 ) { AICast_EndChase( cs ); return AIFunc_IdleStart( cs ); } followent = &g_entities[cs->followEntity]; // CHECK: will this interfere with scripting? // // do we need to avoid a danger? if ( cs->dangerEntityValidTime >= level.time ) { if ( !AICast_GetTakeCoverPos( cs, cs->dangerEntity, cs->dangerEntityPos, cs->takeCoverPos ) ) { // go to a position that cannot be seen from the dangerPos cs->takeCoverTime = cs->dangerEntityValidTime + 1000; cs->bs->attackcrouch_time = 0; return AIFunc_AvoidDangerStart( cs ); } } // // are we waiting for a door? if ( cs->doorMarkerTime > level.time - 100 ) { return AIFunc_DoorMarkerStart( cs, cs->doorMarkerDoor, cs->doorMarkerNum ); } // // if the player is not ready yet, wait if ( !followent->inuse ) { return NULL; } // has the scripting stopped asking us to pursue this goal? if ( cs->followIsGoto && ( cs->followTime < level.time ) ) { return AIFunc_Idle( cs ); } // they are ready, are they outside range? if ( Distance( followent->r.currentOrigin, cs->bs->origin ) > cs->followDist ) { return AIFunc_ChaseGoalStart( cs, cs->followEntity, cs->followDist, qtrue ); } // check for a movement we should be making if ( cs->obstructingTime > level.time ) { AICast_MoveToPos( cs, cs->obstructingPos, -1 ); cs->speedScale = cs->attributes[WALKING_SPEED] / cs->attributes[RUNNING_SPEED]; } // if we have an enemy, fire if they're visible else if ( cs->bs->enemy >= 0 ) { //attack the enemy if possible AICast_ProcessAttack( cs ); } // if we had an enemy recently, face them else if ( cs->lastEnemy >= 0 ) { vec3_t dir; // VectorSubtract( cs->vislist[cs->lastEnemy].visible_pos, cs->bs->origin, dir ); if ( VectorLength( dir ) < 1 ) { cs->bs->ideal_viewangles[PITCH] = 0; } else { VectorNormalize( dir ); vectoangles( dir, cs->bs->ideal_viewangles ); } // reload? if ( ( cs->bs->cur_ps.ammoclip[BG_FindClipForWeapon( cs->bs->cur_ps.weapon )] < (int)( 0.75 * ammoTable[cs->bs->cur_ps.weapon].maxclip ) ) && cs->bs->cur_ps.ammo[BG_FindAmmoForWeapon( cs->bs->cur_ps.weapon )] ) { trap_EA_Reload( cs->entityNum ); } } else if ( followent->client ) { // face them VectorSubtract( followent->r.currentOrigin, cs->bs->origin, dir ); dir[2] += followent->client->ps.viewheight - g_entities[cs->bs->entitynum].client->ps.viewheight; VectorNormalize( dir ); vectoangles( dir, cs->bs->ideal_viewangles ); } // look for things we should attack numEnemies = AICast_ScanForEnemies( cs, enemies ); if ( numEnemies == -1 ) { // query mode return NULL; } else if ( numEnemies == -2 ) { // inspection may be required char *retval; // TTimo gcc: suggest parentheses around assignment used as truth value if ( ( retval = AIFunc_InspectFriendlyStart( cs, enemies[0] ) ) ) { return retval; } } else if ( numEnemies == -3 ) { // bullet impact if ( cs->aiState < AISTATE_COMBAT ) { return AIFunc_InspectBulletImpactStart( cs ); } } else if ( numEnemies == -4 ) { // audible event if ( cs->aiState < AISTATE_COMBAT ) { return AIFunc_InspectAudibleEventStart( cs, cs->audibleEventEnt ); } } else if ( numEnemies > 0 ) { cs->bs->enemy = enemies[0]; // just attack the first one } // set head look flag if no enemy if ( cs->bs->enemy < 0 && cs->attributes[TACTICAL] >= 0.5 && !( cs->aiFlags & AIFL_NO_HEADLOOK ) ) { g_entities[cs->entityNum].client->ps.eFlags |= EF_HEADLOOK; } return NULL; } /* ============ AIFunc_ChaseGoalIdleStart() ============ */ char *AIFunc_ChaseGoalIdleStart( cast_state_t *cs, int entitynum, float reachdist ) { // make sure we don't avoid any areas when we start again trap_BotInitAvoidReach( cs->bs->ms ); // if we are following someone, always use the default (ready for action) anim if ( entitynum < MAX_CLIENTS ) { g_entities[cs->entityNum].client->ps.eFlags &= ~EF_STAND_IDLE2; } else { // randomly choose idle animation //----(SA) try always using the 'casual' stand on spawn and change to crouching one when 'alerted' if ( cs->aiFlags & AIFL_STAND_IDLE2 ) { // if (cs->lastEnemy < 0) g_entities[cs->entityNum].client->ps.eFlags |= EF_STAND_IDLE2; // else // g_entities[cs->entityNum].client->ps.eFlags &= ~EF_STAND_IDLE2; } } cs->followEntity = entitynum; cs->followDist = reachdist; cs->aifunc = AIFunc_ChaseGoalIdle; return "AIFunc_ChaseGoalIdle"; } /* ============ AIFunc_ChaseGoal() ============ */ char *AIFunc_ChaseGoal( cast_state_t *cs ) { gentity_t *followent, *ent; bot_state_t *bs; vec3_t destorg; float dist; qboolean moved = qfalse; ent = &g_entities[cs->entityNum]; if ( cs->followEntity < 0 ) { AICast_EndChase( cs ); return AIFunc_IdleStart( cs ); } // CHECK: will this mess with scripting? // // do we need to avoid a danger? if ( cs->dangerEntityValidTime >= level.time ) { if ( !AICast_GetTakeCoverPos( cs, cs->dangerEntity, cs->dangerEntityPos, cs->takeCoverPos ) ) { // go to a position that cannot be seen from the dangerPos cs->takeCoverTime = cs->dangerEntityValidTime + 1000; cs->bs->attackcrouch_time = 0; cs->castScriptStatus.scriptGotoId = -1; cs->movestate = MS_DEFAULT; cs->movestateType = MSTYPE_NONE; return AIFunc_AvoidDangerStart( cs ); } } // // are we waiting for a door? if ( cs->doorMarkerTime > level.time - 100 ) { return AIFunc_DoorMarkerStart( cs, cs->doorMarkerDoor, cs->doorMarkerNum ); } followent = &g_entities[cs->followEntity]; // if the entity is not ready yet if ( !followent->inuse ) { // if it's a connecting client, wait if ( cs->followEntity < MAX_CLIENTS && ( ( followent->client && followent->client->pers.connected == CON_CONNECTING ) || ( level.time < 3000 ) ) ) { return AIFunc_ChaseGoalIdleStart( cs, cs->followEntity, cs->followDist ); } else // stop following it { AICast_EndChase( cs ); return AIFunc_IdleStart( cs ); } } // has the scripting stopped asking us to pursue this goal? if ( cs->followIsGoto && ( cs->followTime < level.time ) ) { return AIFunc_Idle( cs ); } if ( followent->client ) { VectorCopy( followent->client->ps.origin, destorg ); } else { VectorCopy( followent->r.currentOrigin, destorg ); } // they are ready, are they inside range? FIXME: make configurable dist = Distance( destorg, cs->bs->origin ); if ( cs->followSlowApproach && dist < cs->followDist && ( ent->waterlevel || ( cs->bs->cur_ps.groundEntityNum != ENTITYNUM_NONE ) ) ) { // if this is a scripted GOTO, stop following now if ( cs->followEntity == cs->castScriptStatus.scriptGotoEnt ) { AICast_EndChase( cs ); return AIFunc_IdleStart( cs ); } // if we have reached our leader else { if ( cs->followEntity == cs->leaderNum ) { if ( dist < AICAST_LEADERDIST_MIN ) { AICast_EndChase( cs ); return AIFunc_IdleStart( cs ); } else { trace_t tr; // if we have a clear line to our leader, move closer, since there may be others following also trap_Trace( &tr, cs->bs->origin, cs->bs->cur_ps.mins, cs->bs->cur_ps.maxs, g_entities[cs->followEntity].r.currentOrigin, cs->entityNum, g_entities[cs->entityNum].clipmask ); if ( tr.entityNum != cs->followEntity ) { AICast_EndChase( cs ); return AIFunc_IdleStart( cs ); } // if we have crouching ability, then use it while we are just moving closer if ( cs->attributes[ATTACK_CROUCH] > 0.1 ) { cs->bs->attackcrouch_time = trap_AAS_Time() + 1.0; } } } else { return AIFunc_ChaseGoalIdleStart( cs, cs->followEntity, cs->followDist ); } } } // // go to them // bs = cs->bs; // // RF, disabled this, MIKE sees dead people //if (followent->client && followent->health <= 0) { // AICast_EndChase( cs ); // return AIFunc_IdleStart(cs); //} // move straight to them if we can if ( !moved && ( cs->bs->cur_ps.groundEntityNum != ENTITYNUM_NONE || g_entities[cs->entityNum].waterlevel > 1 ) ) { aicast_predictmove_t move; vec3_t dir; bot_input_t bi; usercmd_t ucmd; trace_t tr; qboolean simTest = qfalse; if ( VectorLength( cs->bs->cur_ps.velocity ) < 120 ) { simTest = qtrue; } if ( !simTest ) { // trace will eliminate most unsuccessful paths trap_Trace( &tr, cs->bs->origin, NULL /*g_entities[cs->entityNum].r.mins*/, NULL /*g_entities[cs->entityNum].r.maxs*/, followent->r.currentOrigin, cs->entityNum, g_entities[cs->entityNum].clipmask ); if ( tr.entityNum == cs->followEntity || tr.fraction == 1 ) { simTest = qtrue; } } if ( simTest ) { // try walking straight to them VectorSubtract( followent->r.currentOrigin, cs->bs->origin, dir ); VectorNormalize( dir ); if ( !ent->waterlevel ) { dir[2] = 0; } trap_EA_Move( cs->entityNum, dir, 400 ); trap_EA_GetInput( cs->entityNum, (float) level.time / 1000, &bi ); AICast_InputToUserCommand( cs, &bi, &ucmd, bs->cur_ps.delta_angles ); AICast_PredictMovement( cs, 10, 0.8, &move, &ucmd, cs->followEntity ); if ( move.stopevent == PREDICTSTOP_HITENT ) { // success! vectoangles( dir, cs->bs->ideal_viewangles ); cs->bs->ideal_viewangles[2] *= 0.5; moved = qtrue; } } } // if ( !moved ) { // use AAS routing moveresult = AICast_MoveToPos( cs, followent->r.currentOrigin, cs->followEntity ); if ( moveresult && moveresult->failure ) { // shit? } } // should we slow down? if ( cs->followDist && cs->followSlowApproach && cs->followDist < 48 ) { cs->speedScale = AICast_SpeedScaleForDistance( cs, dist, cs->followDist ); } // check for a movement we should be making if ( cs->obstructingTime > level.time ) { AICast_MoveToPos( cs, cs->obstructingPos, -1 ); if ( cs->movestate != MS_CROUCH ) { cs->movestate = MS_WALK; } cs->movestateType = MSTYPE_TEMPORARY; } // if we have an enemy, fire if they're visible if ( cs->bs->enemy >= 0 ) { //attack the enemy if possible AICast_ProcessAttack( cs ); } else { int numEnemies; // // look for things we should attack numEnemies = AICast_ScanForEnemies( cs, enemies ); if ( numEnemies == -1 ) { // query mode return NULL; } else if ( numEnemies == -2 ) { // inspection may be required char *retval; // TTimo gcc: suggest parentheses around assignment used as truth value if ( ( retval = AIFunc_InspectFriendlyStart( cs, enemies[0] ) ) ) { return retval; } } else if ( numEnemies == -3 ) { // bullet impact if ( cs->aiState < AISTATE_COMBAT ) { return AIFunc_InspectBulletImpactStart( cs ); } } else if ( numEnemies == -4 ) { // audible event if ( cs->aiState < AISTATE_COMBAT ) { return AIFunc_InspectAudibleEventStart( cs, cs->audibleEventEnt ); } } else if ( numEnemies > 0 ) { int i; cs->bs->enemy = enemies[0]; // just attack the first one // override with a visible enemy for ( i = 1; i < numEnemies; i++ ) { if ( AICast_CheckAttack( cs, enemies[i], qfalse ) ) { cs->bs->enemy = enemies[i]; break; } else if ( cs->bs->enemy < 0 ) { cs->lastEnemy = enemies[i]; } } } // reload? if ( ( cs->bs->cur_ps.ammoclip[BG_FindClipForWeapon( cs->bs->cur_ps.weapon )] < (int)( 0.75 * ammoTable[cs->bs->cur_ps.weapon].maxclip ) ) && cs->bs->cur_ps.ammo[BG_FindAmmoForWeapon( cs->bs->cur_ps.weapon )] ) { trap_EA_Reload( cs->entityNum ); } } // set head look flag if no enemy if ( cs->bs->enemy < 0 && cs->attributes[TACTICAL] >= 0.5 && !( cs->aiFlags & AIFL_NO_HEADLOOK ) ) { g_entities[cs->entityNum].client->ps.eFlags |= EF_HEADLOOK; } return NULL; } /* ============ AIFunc_ChaseGoalStart() ============ */ char *AIFunc_ChaseGoalStart( cast_state_t *cs, int entitynum, float reachdist, qboolean slowApproach ) { cs->followEntity = entitynum; cs->followDist = reachdist; cs->followIsGoto = qfalse; cs->followSlowApproach = slowApproach; cs->aifunc = AIFunc_ChaseGoal; return "AIFunc_ChaseGoal"; } /* ============ AIFunc_DoorMarker() ============ */ char *AIFunc_DoorMarker( cast_state_t *cs ) { gentity_t *followent, *door; bot_state_t *bs; vec3_t destorg; float dist; // // do we need to avoid a danger? if ( cs->dangerEntityValidTime >= level.time ) { if ( !AICast_GetTakeCoverPos( cs, cs->dangerEntity, cs->dangerEntityPos, cs->takeCoverPos ) ) { // shit?? } // go to a position that cannot be seen from the dangerPos cs->takeCoverTime = cs->dangerEntityValidTime + 1000; cs->bs->attackcrouch_time = 0; return AIFunc_AvoidDangerStart( cs ); } followent = &g_entities[cs->doorMarker]; // if the entity is not ready yet if ( !followent->inuse ) { cs->doorMarkerTime = 0; //return AIFunc_DefaultStart( cs ); return AIFunc_Restore( cs ); } // if the door is open or idle door = &g_entities[cs->doorEntNum]; if ( ( !door->key ) && ( door->s.apos.trType == TR_STATIONARY && door->s.pos.trType == TR_STATIONARY ) ) { cs->doorMarkerTime = 0; //return AIFunc_DefaultStart( cs ); return AIFunc_Restore( cs ); } // if we have an enemy, fire if they're visible if ( cs->bs->enemy >= 0 ) { //attack the enemy if possible AICast_ProcessAttack( cs ); } // they are ready, are they inside range? FIXME: make configurable dist = Distance( destorg, cs->bs->origin ); if ( dist < 12 ) { // check for a movement we should be making if ( cs->obstructingTime > level.time ) { AICast_MoveToPos( cs, cs->obstructingPos, -1 ); } // if the door is locked, resume if ( followent->key ) { return AIFunc_Restore( cs ); } return NULL; } // go to it // bs = cs->bs; // moveresult = AICast_MoveToPos( cs, followent->r.currentOrigin, followent->s.number ); // if we cant get there, forget it if ( moveresult && moveresult->failure ) { return AIFunc_Restore( cs ); } // should we slow down? if ( cs->followDist ) { cs->speedScale = AICast_SpeedScaleForDistance( cs, dist, cs->followDist ); } // reload? if ( ( cs->bs->cur_ps.ammoclip[BG_FindClipForWeapon( cs->bs->cur_ps.weapon )] < (int)( 0.75 * ammoTable[cs->bs->cur_ps.weapon].maxclip ) ) && cs->bs->cur_ps.ammo[BG_FindAmmoForWeapon( cs->bs->cur_ps.weapon )] ) { trap_EA_Reload( cs->entityNum ); } return NULL; } /* ============ AIFunc_DoorMarkerStart() ============ */ char *AIFunc_DoorMarkerStart( cast_state_t *cs, int doornum, int markernum ) { cs->doorEntNum = doornum; cs->doorMarker = markernum; cs->oldAifunc = cs->aifunc; cs->aifunc = AIFunc_DoorMarker; return "AIFunc_DoorMarker"; } /* ============= AIFunc_BattleRoll() ============= */ char *AIFunc_BattleRoll( cast_state_t *cs ) { gclient_t *client = &level.clients[cs->entityNum]; vec3_t dir; // // record the time cs->lastRollMove = level.time; client->ps.eFlags |= EF_NOSWINGANGLES; // if ( !client->ps.torsoTimer ) { if ( cs->battleRollTime < level.time ) { return AIFunc_Restore( cs ); } else { // attack? if ( cs->bs->enemy >= 0 ) { AICast_ProcessAttack( cs ); } } } if ( g_entities[cs->entityNum].health <= 0 ) { return AIFunc_DefaultStart( cs ); } if ( ( cs->bs->enemy >= 0 ) && ( client->ps.torsoTimer < 400 ) && ( cs->takeCoverTime < level.time ) ) { // start turning towards our enemy AICast_AimAtEnemy( cs ); if ( client->ps.torsoTimer ) { AICast_ProcessAttack( cs ); } } // // all characters so far only move during the first second of animation if ( cs->thinkFuncChangeTime > level.time - 800 ) { // just move in the direction of our ideal_viewangles AngleVectors( cs->bs->ideal_viewangles, dir, NULL, NULL ); trap_EA_Move( cs->entityNum, dir, 300 ); // if we are crouching, move a little faster than normal if ( cs->bs->attackcrouch_time > trap_AAS_Time() ) { cs->speedScale = 1.5; } } else if ( cs->takeCoverTime > level.time ) { // // if we are taking Cover, use this position, if it's bad, we'll just look for a better spot once we're done here VectorCopy( cs->bs->origin, cs->takeCoverPos ); } // return NULL; } /* ============= AIFunc_BattleRollStart() ============= */ char *AIFunc_BattleRollStart( cast_state_t *cs, vec3_t vec ) { int duration; // TTimo unused //gclient_t *client = &level.clients[cs->entityNum]; // // backup the current thinkfunc, so we can return to it when done cs->oldAifunc = cs->aifunc; // // face the direction of movement vectoangles( vec, cs->bs->ideal_viewangles ); // do the roll duration = BG_AnimScriptEvent( &g_entities[cs->entityNum].client->ps, ANIM_ET_ROLL, qfalse, qfalse ); // if ( duration < 0 ) { // it failed return NULL; } // add some duration to make sure it fully plays out duration += 100; g_entities[cs->entityNum].client->ps.legsTimer = duration; g_entities[cs->entityNum].client->ps.torsoTimer = duration; // cs->noAttackTime = level.time + duration - 200; // set the duration cs->battleRollTime = level.time + duration; // move into crouch position cs->bs->attackcrouch_time = trap_AAS_Time() + ( 0.001 * duration ) + 1.0; // record the time cs->lastRollMove = level.time; // // make sure we move this frame AIFunc_BattleRoll( cs ); // cs->aifunc = AIFunc_BattleRoll; return "AIFunc_BattleRoll"; } /* ============= AIFunc_BattleDiveStart() ============= */ char *AIFunc_BattleDiveStart( cast_state_t *cs, vec3_t vec ) { int duration; // TTimo unused //gclient_t *client = &level.clients[cs->entityNum]; // // backup the current thinkfunc, so we can return to it when done cs->oldAifunc = cs->aifunc; // // face the direction of movement vectoangles( vec, cs->bs->ideal_viewangles ); // do the roll duration = BG_AnimScriptEvent( &g_entities[cs->entityNum].client->ps, ANIM_ET_DIVE, qfalse, qfalse ); // if ( duration < 0 ) { // it failed return NULL; } // cs->noAttackTime = level.time + duration - 200; // set the duration cs->battleRollTime = level.time + duration; // move into crouch position cs->bs->attackcrouch_time = trap_AAS_Time() + ( 0.001 * duration ) + 1.0; // record the time cs->lastRollMove = level.time; // // make sure we move this frame AIFunc_BattleRoll( cs ); // cs->aifunc = AIFunc_BattleRoll; return "AIFunc_BattleRoll"; } /* ============= AIFunc_FlipMove() ============= */ char *AIFunc_FlipMove( cast_state_t *cs ) { gclient_t *client = &level.clients[cs->entityNum]; vec3_t dir; // if ( !client->ps.torsoTimer ) { cs->bs->attackcrouch_time = 0; return AIFunc_Restore( cs ); } if ( g_entities[cs->entityNum].health <= 0 ) { return AIFunc_DefaultStart( cs ); } // // just move in the direction of our ideal_viewangles AngleVectors( cs->bs->ideal_viewangles, dir, NULL, NULL ); trap_EA_Move( cs->entityNum, dir, 400 ); // if we are crouching, move a little faster than normal if ( cs->bs->attackcrouch_time > trap_AAS_Time() ) { cs->speedScale = 1.5; } // return NULL; } /* ============= AIFunc_FlipMoveStart() ============= */ char *AIFunc_FlipMoveStart( cast_state_t *cs, vec3_t vec ) { int duration; // TTimo unused //gclient_t *client = &level.clients[cs->entityNum]; // // backup the current thinkfunc, so we can return to it when done cs->oldAifunc = cs->aifunc; // // record the time cs->lastRollMove = level.time; // face the direction of movement vectoangles( vec, cs->bs->ideal_viewangles ); cs->noAttackTime = level.time + 1200; // do the roll duration = BG_AnimScriptEvent( &g_entities[cs->entityNum].client->ps, ANIM_ET_ROLL, qfalse, qfalse ); // if ( duration < 0 ) { // it failed return NULL; } // move into crouch position cs->bs->attackcrouch_time = trap_AAS_Time() + 0.8; // // make sure we move this frame AIFunc_FlipMove( cs ); // cs->aifunc = AIFunc_FlipMove; return "AIFunc_FlipMove"; } /* ============= AIFunc_BattleHunt() ============= */ char *AIFunc_BattleHunt( cast_state_t *cs ) { const float chaseDist = 32; gentity_t *followent, *ent; bot_state_t *bs; vec3_t destorg; qboolean moved = qfalse; // TTimo unused //gclient_t *client = &level.clients[cs->entityNum]; char *rval; // TTimo might be used uninitialized float dist = 0; cast_state_t *ocs; int *ammo, i; ent = &g_entities[cs->entityNum]; // // do we need to avoid a danger? if ( cs->dangerEntityValidTime >= level.time ) { if ( !AICast_GetTakeCoverPos( cs, cs->dangerEntity, cs->dangerEntityPos, cs->takeCoverPos ) ) { // shit?? } // go to a position that cannot be seen from the dangerPos cs->takeCoverTime = cs->dangerEntityValidTime + 1000; cs->bs->attackcrouch_time = 0; return AIFunc_AvoidDangerStart( cs ); } // // are we waiting for a door? if ( cs->doorMarkerTime > level.time - 100 ) { return AIFunc_DoorMarkerStart( cs, cs->doorMarkerDoor, cs->doorMarkerNum ); } // bs = cs->bs; // if ( bs->enemy < 0 ) { return AIFunc_IdleStart( cs ); } // ocs = AICast_GetCastState( cs->bs->enemy ); // if ( cs->aiFlags & AIFL_ATTACK_CROUCH ) { cs->bs->attackcrouch_time = trap_AAS_Time() + 1; } // followent = &g_entities[bs->enemy]; // // if the entity is not ready yet if ( !followent->inuse ) { // if it's a connecting client, wait if ( !( ( bs->enemy < MAX_CLIENTS ) && ( ( followent->client && followent->client->pers.connected == CON_CONNECTING ) || ( level.time < 3000 ) ) ) ) { // they don't exist anymore, stop attacking bs->enemy = -1; } return AIFunc_IdleStart( cs ); } // // if we can see them, go back to an attack state AICast_ChooseWeapon( cs, qtrue ); // enable special weapons, if we cant get them, change back if ( AICast_EntityVisible( cs, bs->enemy, qtrue ) // take into account reaction time && AICast_CheckAttack( cs, bs->enemy, qfalse ) && cs->obstructingTime < level.time ) { if ( AICast_StopAndAttack( cs ) ) { // TTimo gcc: suggest parentheses around assignment used as truth value if ( ( rval = AIFunc_BattleStart( cs ) ) ) { return rval; } } else { // just attack them now and keep chasing AICast_ProcessAttack( cs ); } AICast_ChooseWeapon( cs, qfalse ); } else { int numEnemies, shouldAttack; AICast_ChooseWeapon( cs, qfalse ); ammo = cs->bs->cur_ps.ammo; shouldAttack = qfalse; numEnemies = AICast_ScanForEnemies( cs, enemies ); if ( numEnemies == -1 ) { // query mode return NULL; } else if ( numEnemies == -2 ) { // inspection may be required char *retval; if ( cs->aiState < AISTATE_COMBAT ) { // TTimo gcc: suggest parentheses around assignment used as truth value if ( ( retval = AIFunc_InspectFriendlyStart( cs, enemies[0] ) ) ) { return retval; } } } else if ( numEnemies == -3 ) { // bullet impact if ( cs->aiState < AISTATE_COMBAT ) { return AIFunc_InspectBulletImpactStart( cs ); } } else if ( numEnemies == -4 ) { // audible event if ( cs->aiState < AISTATE_COMBAT ) { return AIFunc_InspectAudibleEventStart( cs, cs->audibleEventEnt ); } } else if ( AICast_GotEnoughAmmoForWeapon( cs, cs->bs->weaponnum ) ) { if ( numEnemies > 0 ) { // default to the first known enemy, overwrite if we find a clearer shot cs->bs->enemy = enemies[0]; // for ( i = 0; i < numEnemies; i++ ) { if ( AICast_CheckAttack( cs, enemies[i], qfalse ) || AICast_CheckAttack( AICast_GetCastState( enemies[i] ), cs->entityNum, qfalse ) ) { cs->bs->enemy = enemies[i]; shouldAttack = qtrue; break; } else if ( cs->bs->enemy < 0 ) { cs->lastEnemy = enemies[i]; } } // note: next frame we'll process this new enemy an begin an attack if necessary } } AICast_ChooseWeapon( cs, qfalse ); } // have we spent enough time in combat mode? if ( cs->aiState == AISTATE_COMBAT ) { if ( cs->vislist[bs->enemy].visible_timestamp < level.time - COMBAT_TIMEOUT ) { AICast_StateChange( cs, AISTATE_ALERT ); } } // while hunting, use crouch mode if possible if ( cs->attributes[ATTACK_CROUCH] >= 0.1 ) { cs->bs->attackcrouch_time = trap_AAS_Time() + 1; } if ( cs->battleHuntPauseTime ) { if ( cs->battleHuntPauseTime < level.time ) { // pausetime has expired, so go into ambush mode if ( AICast_GetTakeCoverPos( cs, cs->bs->enemy, cs->vislist[bs->enemy].chase_marker[cs->battleChaseMarker], cs->takeCoverPos ) ) { // wait in ambush, for them to return VectorCopy( cs->vislist[bs->enemy].chase_marker[cs->battleChaseMarker], cs->combatGoalOrigin ); return AIFunc_BattleAmbushStart( cs ); } // couldn't find a spot, so just stay here? VectorCopy( cs->bs->origin, cs->combatGoalOrigin ); VectorCopy( cs->bs->origin, cs->takeCoverPos ); return AIFunc_BattleAmbushStart( cs ); } else { // stay here, looking around if ( cs->battleHuntViewTime < level.time ) { cs->battleHuntViewTime = level.time + 700 + rand() % 1000; // set a random viewangle cs->bs->ideal_viewangles[YAW] = AngleMod( cs->bs->ideal_viewangles[YAW] + ( 45.0 + random() * 45.0 ) * ( 2 * ( rand() % 2 ) - 1 ) ); cs->bs->ideal_viewangles[PITCH] = 0; } } } else { // cycle through markers VectorCopy( cs->vislist[bs->enemy].chase_marker[cs->battleChaseMarker], destorg ); if ( ( dist = Distance( destorg, cs->bs->origin ) ) < chaseDist ) { if ( cs->battleChaseMarker == ( cs->vislist[bs->enemy].chase_marker_count - 1 ) ) { cs->battleHuntPauseTime = level.time + 4000; cs->battleHuntViewTime = level.time + 1000; } else { cs->battleChaseMarker += cs->battleChaseMarkerDir; if ( cs->battleChaseMarker > cs->vislist[bs->enemy].chase_marker_count ) { cs->battleChaseMarkerDir *= -1; cs->battleChaseMarker = cs->vislist[bs->enemy].chase_marker_count - 1; } else if ( cs->battleChaseMarker < 0 ) { cs->battleChaseMarkerDir *= -1; cs->battleChaseMarker = 0; } } } // if ( cs->battleHuntPauseTime < level.time ) { // just go to them if ( !moved && cs->leaderNum < 0 ) { moveresult = AICast_MoveToPos( cs, destorg, bs->enemy ); if ( moveresult && moveresult->failure ) { // no path, so go back to idle behaviour // try to go to ambush mode cs->bs->enemy = -1; return AIFunc_DefaultStart( cs ); } else { moved = qtrue; } } } } // // slow down real close to the goal, so we don't go passed it cs->speedScale = AICast_SpeedScaleForDistance( cs, dist, chaseDist ); // reload? if ( ( cs->bs->cur_ps.ammoclip[BG_FindClipForWeapon( cs->bs->cur_ps.weapon )] < (int)( 0.75 * ammoTable[cs->bs->cur_ps.weapon].maxclip ) ) && cs->bs->cur_ps.ammo[BG_FindAmmoForWeapon( cs->bs->cur_ps.weapon )] ) { trap_EA_Reload( cs->entityNum ); } return NULL; } /* ============= AIFunc_BattleHuntStart() ============= */ char *AIFunc_BattleHuntStart( cast_state_t *cs ) { cs->combatGoalTime = 0; cs->battleChaseMarker = 0; cs->battleHuntPauseTime = 0; // cs->aifunc = AIFunc_BattleHunt; return "AIFunc_BattleHunt"; } /* ============= AIFunc_BattleAmbush() ============= */ char *AIFunc_BattleAmbush( cast_state_t *cs ) { bot_state_t *bs; vec3_t destorg, vec; float dist, moveDist; int enemies[MAX_CLIENTS], numEnemies, i; qboolean shouldAttack, idleYaw; aicast_predictmove_t move; int *ammo; vec3_t dir; // TTimo unused //gclient_t *client = &level.clients[cs->entityNum]; // // do we need to avoid a danger? if ( cs->dangerEntityValidTime >= level.time ) { if ( !AICast_GetTakeCoverPos( cs, cs->dangerEntity, cs->dangerEntityPos, cs->takeCoverPos ) ) { // shit?? } // go to a position that cannot be seen from the dangerPos cs->takeCoverTime = cs->dangerEntityValidTime + 1000; cs->bs->attackcrouch_time = 0; return AIFunc_AvoidDangerStart( cs ); } // // are we waiting for a door? if ( cs->doorMarkerTime > level.time - 100 ) { return AIFunc_DoorMarkerStart( cs, cs->doorMarkerDoor, cs->doorMarkerNum ); } // we need to move towards it bs = cs->bs; // // note: removing this will cause problems down below! if ( bs->enemy < 0 ) { return AIFunc_IdleStart( cs ); } // // have we spent enough time in combat mode? if ( cs->aiState == AISTATE_COMBAT ) { if ( cs->vislist[bs->enemy].visible_timestamp < level.time - COMBAT_TIMEOUT ) { AICast_StateChange( cs, AISTATE_ALERT ); } } // while hunting, use crouch mode if possible if ( cs->attributes[ATTACK_CROUCH] >= 0.1 ) { cs->bs->attackcrouch_time = trap_AAS_Time() + 1; } // VectorCopy( cs->takeCoverPos, destorg ); VectorSubtract( destorg, cs->bs->origin, vec ); vec[2] *= 0.2; dist = VectorLength( vec ); // // update the chase marker if ( cs->vislist[cs->bs->enemy].chase_marker_count > 0 ) { VectorCopy( cs->vislist[cs->bs->enemy].chase_marker[cs->vislist[cs->bs->enemy].chase_marker_count - 1], cs->combatGoalOrigin ); } // // look for things we should attack // if we are out of ammo, we shouldn't bother trying to attack (and we should keep hiding) ammo = cs->bs->cur_ps.ammo; shouldAttack = qfalse; numEnemies = AICast_ScanForEnemies( cs, enemies ); if ( numEnemies == -1 ) { // query mode return NULL; } else if ( numEnemies == -2 ) { // inspection may be required char *retval; // TTimo gcc: suggest parentheses around assignment used as truth value if ( ( retval = AIFunc_InspectFriendlyStart( cs, enemies[0] ) ) ) { return retval; } } else if ( numEnemies == -3 ) { // bullet impact if ( cs->aiState < AISTATE_COMBAT ) { return AIFunc_InspectBulletImpactStart( cs ); } } else if ( numEnemies == -4 ) { // audible event if ( cs->aiState < AISTATE_COMBAT ) { return AIFunc_InspectAudibleEventStart( cs, cs->audibleEventEnt ); } } else if ( AICast_GotEnoughAmmoForWeapon( cs, cs->bs->weaponnum ) ) { if ( numEnemies > 0 ) { // default to the first known enemy, overwrite if we find a clearer shot cs->bs->enemy = enemies[0]; // for ( i = 0; i < numEnemies; i++ ) { if ( AICast_CheckAttack( cs, enemies[i], qfalse ) || AICast_CheckAttack( AICast_GetCastState( enemies[i] ), cs->entityNum, qfalse ) ) { cs->bs->enemy = enemies[i]; shouldAttack = qtrue; break; } else if ( cs->bs->enemy < 0 ) { cs->lastEnemy = enemies[i]; } } } } else { // keep hiding forever cs->takeCoverTime = level.time + 1000; } // memset( &move, 0, sizeof( move ) ); // // are we close enough to the goal? if ( VectorLength( cs->takeCoverPos ) > 1 && dist > 8 && ( cs->obstructingTime < level.time ) && !shouldAttack ) { const float simTime = 0.8; float enemyDist; // // we haven't reached it yet, make sure we at least wait there for a few seconds after arriving cs->takeCoverTime = level.time + 2000 + rand() % 2000; // moveresult = AICast_MoveToPos( cs, destorg, -1 ); if ( moveresult ) { //if the movement failed if ( moveresult->failure ) { //reset the avoid reach, otherwise bot is stuck in current area trap_BotResetAvoidReach( bs->ms ); // couldn't get there, so stop trying to get there VectorClear( cs->takeCoverPos ); dist = 0; } // if ( moveresult->blocked ) { // abort the TakeCover VectorClear( cs->takeCoverPos ); dist = 0; } } // // NOTE: this is also used by hidepos prediction (below) // if we are going to bump into something soon, abort it AICast_PredictMovement( cs, 1, simTime, &move, &cs->bs->lastucmd, -1 ); enemyDist = Distance( cs->bs->origin, g_entities[cs->bs->enemy].s.pos.trBase ); VectorSubtract( move.endpos, cs->bs->origin, vec ); moveDist = VectorNormalize( vec ); // if ( ( move.numtouch && move.touchents[0] < aicast_maxclients ) // hit something // or moved closer to the enemy || ( ( enemyDist < 128 ) && ( ( enemyDist - 1 ) > ( Distance( move.endpos, g_entities[cs->bs->enemy].s.pos.trBase ) ) ) ) ) { // abort the manouver VectorClear( cs->takeCoverPos ); dist = 0; } // // we should slow down on approaching the destination point else if ( dist < 64 ) { cs->speedScale = AICast_SpeedScaleForDistance( cs, dist, 0 ); } // don't actually hide, check if we are about to, so we can hide right here if ( !( cs->aiFlags & AIFL_MISCFLAG1 ) ) { if ( move.numtouch || !AICast_VisibleFromPos( move.endpos, cs->entityNum, cs->combatGoalOrigin, cs->bs->enemy, qfalse ) ) { // abort the manouver, reached a good spot cs->aiFlags |= AIFL_MISCFLAG1; VectorCopy( cs->bs->origin, cs->takeCoverPos ); } } } else { // // check for a movement we should be making if ( cs->obstructingTime > level.time ) { AICast_MoveToPos( cs, cs->obstructingPos, -1 ); } // if we have some enemies that we can attack immediately (without going anywhere to chase them) if ( shouldAttack ) { return AIFunc_BattleStart( cs ); } // do we need to go to our leader? else if ( cs->leaderNum >= 0 && Distance( cs->bs->origin, g_entities[cs->leaderNum].r.currentOrigin ) > MAX_LEADER_DIST ) { // wait until we've been hiding for long enough if ( level.time > cs->takeCoverTime ) { return AIFunc_ChaseGoalStart( cs, cs->leaderNum, AICAST_LEADERDIST_MAX, qtrue ); } } // else, crouch while we hide if ( cs->attributes[ATTACK_CROUCH] > 0.1 || cs->crouchHideFlag ) { cs->bs->attackcrouch_time = trap_AAS_Time() + 1; } } // idleYaw = qtrue; // if we can see the place we are hiding from, look at it if ( AICast_VisibleFromPos( cs->bs->origin, cs->entityNum, cs->combatGoalOrigin, cs->lastEnemy, qfalse ) ) { // face the position we are retreating from VectorSubtract( cs->combatGoalOrigin, cs->bs->origin, dir ); dir[2] = 0; if ( VectorNormalize( dir ) > 4 ) { idleYaw = qfalse; vectoangles( dir, cs->bs->ideal_viewangles ); } } // if ( idleYaw ) { // look around randomly (but not straight into walls) if ( cs->nextIdleAngleChange < level.time ) { // wait a second before changing again if ( ( cs->nextIdleAngleChange + 3000 ) < level.time ) { // FIXME: This could be changed to use some AAS sampling, which would: // // Given a src area, pick a random dest area which is visible from that area // and return it's position, which we'd then use to set the next view angles // // This would result in more efficient, more realistic behaviour, since they'd // also use PITCH angles to look at areas above/below them cs->idleYaw = AICast_GetRandomViewAngle( cs, 512 ); if ( abs( AngleDelta( cs->idleYaw, cs->bs->ideal_viewangles[YAW] ) ) < 45 ) { cs->nextIdleAngleChange = level.time + 1000 + rand() % 2500; } else { // do really fast cs->nextIdleAngleChange = level.time + 500; } // adjust with time cs->idleYawChange = AngleDelta( cs->idleYaw, cs->bs->ideal_viewangles[YAW] ); /// ((float)(cs->nextIdleAngleChange - level.time) / 1000.0); cs->bs->ideal_viewangles[PITCH] = 0; } } else if ( cs->idleYawChange ) { cs->idleYawChange = AngleDelta( cs->idleYaw, cs->bs->ideal_viewangles[YAW] ); cs->bs->ideal_viewangles[YAW] = AngleMod( cs->bs->ideal_viewangles[YAW] + ( cs->idleYawChange * cs->bs->thinktime ) ); } } // if ( !cs->crouchHideFlag ) { // no enemy, and no need to crouch, so stop crouching if ( cs->bs->attackcrouch_time > trap_AAS_Time() + 1 ) { cs->bs->attackcrouch_time = trap_AAS_Time() + 1; } } // reload? if ( ( cs->bs->cur_ps.ammoclip[BG_FindClipForWeapon( cs->bs->cur_ps.weapon )] < (int)( 0.75 * ammoTable[cs->bs->cur_ps.weapon].maxclip ) ) && cs->bs->cur_ps.ammo[BG_FindAmmoForWeapon( cs->bs->cur_ps.weapon )] ) { trap_EA_Reload( cs->entityNum ); } return NULL; } /* ============= AIFunc_BattleAmbushStart() ============= */ char *AIFunc_BattleAmbushStart( cast_state_t *cs ) { if ( !AICast_CanMoveWhileFiringWeapon( cs->bs->weaponnum ) ) { // always run to the cover point cs->bs->attackcrouch_time = 0; } else if ( cs->bs->attackcrouch_time > trap_AAS_Time() + 1.0 ) { cs->bs->attackcrouch_time = trap_AAS_Time() + 1.0; } // // start a crouch attack? if ( cs->attributes[ATTACK_CROUCH] > 0.1 && cs->bs->attackcrouch_time >= trap_AAS_Time() ) { // continue cs->bs->attackcrouch_time = trap_AAS_Time() + 1.0; } // if we arent crouching, start crouching soon after we start retreating if ( cs->attributes[ATTACK_CROUCH] > 0.1 ) { cs->aiFlags |= AIFL_ATTACK_CROUCH; } else { cs->aiFlags &= ~AIFL_ATTACK_CROUCH; } // miscflag1 used to set predicted point as our goal, so we dont keep setting this over and over cs->aiFlags &= ~AIFL_MISCFLAG1; cs->aifunc = AIFunc_BattleAmbush; return "AIFunc_BattleAmbush"; } /* ============ AIFunc_BattleChase() ============ */ char *AIFunc_BattleChase( cast_state_t *cs ) { const float chaseDist = 32; gentity_t *followent, *ent; bot_state_t *bs; vec3_t destorg; qboolean moved = qfalse; gclient_t *client = &level.clients[cs->entityNum]; char *rval; float dist; cast_state_t *ocs; ent = &g_entities[cs->entityNum]; // // do we need to avoid a danger? if ( cs->dangerEntityValidTime >= level.time ) { if ( !AICast_GetTakeCoverPos( cs, cs->dangerEntity, cs->dangerEntityPos, cs->takeCoverPos ) ) { // shit?? } // go to a position that cannot be seen from the dangerPos cs->takeCoverTime = cs->dangerEntityValidTime + 1000; cs->bs->attackcrouch_time = 0; return AIFunc_AvoidDangerStart( cs ); } // // are we waiting for a door? if ( cs->doorMarkerTime > level.time - 100 ) { return AIFunc_DoorMarkerStart( cs, cs->doorMarkerDoor, cs->doorMarkerNum ); } bs = cs->bs; // if ( bs->enemy < 0 ) { return AIFunc_IdleStart( cs ); } // // Retreat? if ( AICast_WantToRetreat( cs ) ) { if ( AICast_GetTakeCoverPos( cs, cs->bs->enemy, cs->vislist[cs->bs->enemy].visible_pos, cs->takeCoverPos ) ) { // go to a position that cannot be seen from the last place we saw the enemy, and wait there for some time cs->takeCoverTime = level.time + 2000 + rand() % 3000; return AIFunc_BattleTakeCoverStart( cs ); } } // ocs = AICast_GetCastState( cs->bs->enemy ); // if ( cs->aiFlags & AIFL_ATTACK_CROUCH ) { cs->bs->attackcrouch_time = trap_AAS_Time() + 1; } // followent = &g_entities[bs->enemy]; // // if the entity is not ready yet if ( !followent->inuse ) { // if it's a connecting client, wait if ( !( ( bs->enemy < MAX_CLIENTS ) && ( ( followent->client && followent->client->pers.connected == CON_CONNECTING ) || ( level.time < 3000 ) ) ) ) { // they don't exist anymore, stop attacking bs->enemy = -1; } return AIFunc_IdleStart( cs ); } // // if we can see them, go back to an attack state AICast_ChooseWeapon( cs, qtrue ); // enable special weapons, if we cant get them, change back if ( AICast_EntityVisible( cs, bs->enemy, qtrue ) // take into account reaction time && AICast_CheckAttack( cs, bs->enemy, qfalse ) && cs->obstructingTime < level.time ) { if ( AICast_StopAndAttack( cs ) ) { // TTimo gcc: suggest parentheses around assignment used as truth value if ( ( rval = AIFunc_BattleStart( cs ) ) ) { return rval; } } else { // just attack them now and keep chasing AICast_ProcessAttack( cs ); } AICast_ChooseWeapon( cs, qfalse ); } else { AICast_ChooseWeapon( cs, qfalse ); // not visible, go to their previously visible position /* if (!cs->vislist[bs->enemy].visible_timestamp || Distance( bs->origin, cs->vislist[bs->enemy].visible_pos ) < 16) { // we're done attacking, go back to default state, which in turn will recall previous state // return AIFunc_DefaultStart( cs ); } */ } // // find the chase position if ( followent->client ) { // go to the last visible position VectorCopy( cs->vislist[bs->enemy].visible_pos, destorg ); // if we have reached it, go into hunt mode if ( ( dist = Distance( destorg, cs->bs->origin ) ) < chaseDist ) { // if we haven't been hunted for a while, do so if ( ocs->lastBattleHunted < level.time - 5000 ) { ocs->lastBattleHunted = level.time; return AIFunc_BattleHuntStart( cs ); } // otherwise, go into ambush mode if ( AICast_GetTakeCoverPos( cs, cs->bs->enemy, cs->vislist[cs->bs->enemy].real_visible_pos, cs->takeCoverPos ) ) { VectorCopy( cs->vislist[cs->bs->enemy].real_visible_pos, cs->combatGoalOrigin ); return AIFunc_BattleAmbushStart( cs ); } // couldn't find a spot, so just stay here? VectorCopy( cs->bs->origin, cs->combatGoalOrigin ); VectorCopy( cs->bs->origin, cs->takeCoverPos ); return AIFunc_BattleAmbushStart( cs ); } } else // assume we know where other entities are { VectorCopy( followent->s.pos.trBase, destorg ); dist = Distance( cs->bs->origin, destorg ); } // // if the enemy is inside a CONTENTS_DONOTENTER brush, and we are close enough, stop chasing them if ( AICast_EntityVisible( cs, cs->bs->enemy, qtrue ) && VectorDistance( cs->bs->origin, destorg ) < 384 ) { if ( trap_PointContents( destorg, cs->bs->enemy ) & ( CONTENTS_DONOTENTER | CONTENTS_DONOTENTER_LARGE ) ) { // just stay here, and hope they move out of the brush without finding a spot where they can hit us but we can't hit them return NULL; } } // // is there someone else we can go for instead? numEnemies = AICast_ScanForEnemies( cs, enemies ); if ( numEnemies == -1 ) { // query mode return NULL; } else if ( numEnemies == -2 ) { // inspection may be required char *retval; // TTimo gcc: suggest parentheses around assignment used as truth value if ( ( retval = AIFunc_InspectFriendlyStart( cs, enemies[0] ) ) ) { return retval; } } AICast_ChooseWeapon( cs, qtrue ); // enable special weapons, if we cant get them, change back if ( numEnemies > 0 ) { int i; for ( i = 0; i < numEnemies; i++ ) { if ( enemies[i] != cs->bs->enemy && AICast_CheckAttack( cs, enemies[i], qfalse ) ) { cs->bs->enemy = enemies[i]; return AIFunc_BattleStart( cs ); } } } AICast_ChooseWeapon( cs, qfalse ); // // if we only recently saw them, face them // if ( cs->vislist[cs->bs->enemy].visible_timestamp > level.time - 3000 ) { AICast_AimAtEnemy( cs ); // be ready for an attack if they become visible again //if (cs->attributes[ATTACK_CROUCH] > 0.1) { // crouching for combat // cs->bs->attackcrouch_time = trap_AAS_Time() + 1.0; //} } // // Lob a Grenade? // if we haven't thrown a grenade in a bit, go into "grenade flush mode" if ( ( lastGrenadeFlush > level.time || lastGrenadeFlush < level.time - 7000 ) && ( cs->castScriptStatus.castScriptEventIndex < 0 ) && ( cs->startGrenadeFlushTime < level.time - 3000 ) && ( COM_BitCheck( cs->bs->cur_ps.weapons, WP_GRENADE_LAUNCHER ) ) && ( AICast_GotEnoughAmmoForWeapon( cs, WP_GRENADE_LAUNCHER ) ) && ( cs->weaponFireTimes[WP_GRENADE_LAUNCHER] < level.time - (int)( aicast_skillscale * 3000 ) ) && ( ( cs->bs->weaponnum == WP_GRENADE_LAUNCHER ) || !( cs->castScriptStatus.scriptFlags & SFL_NOCHANGEWEAPON ) ) && ( Distance( cs->bs->origin, cs->vislist[cs->bs->enemy].visible_pos ) > 100 ) && ( Distance( cs->bs->origin, cs->vislist[cs->bs->enemy].visible_pos ) < 1400 ) ) { // try and flush them out with a grenade //G_Printf("pineapple?\n"); return AIFunc_GrenadeFlushStart( cs ); } else if ( ( lastGrenadeFlush > level.time || lastGrenadeFlush < level.time - 7000 ) && ( cs->castScriptStatus.castScriptEventIndex < 0 ) && ( cs->startGrenadeFlushTime < level.time - 2000 ) && ( COM_BitCheck( cs->bs->cur_ps.weapons, WP_GRENADE_PINEAPPLE ) ) && ( AICast_GotEnoughAmmoForWeapon( cs, WP_GRENADE_PINEAPPLE ) ) && ( cs->weaponFireTimes[WP_GRENADE_PINEAPPLE] < level.time - (int)( aicast_skillscale * 3000 ) ) && ( ( cs->bs->weaponnum == WP_GRENADE_PINEAPPLE ) || !( cs->castScriptStatus.scriptFlags & SFL_NOCHANGEWEAPON ) ) && ( Distance( cs->bs->origin, cs->vislist[cs->bs->enemy].visible_pos ) > 100 ) && ( Distance( cs->bs->origin, cs->vislist[cs->bs->enemy].visible_pos ) < 1400 ) ) { // try and flush them out with a grenade //G_Printf("pineapple?\n"); return AIFunc_GrenadeFlushStart( cs ); } // // Flaming Zombie? Shoot flames while running if ( ( cs->aiCharacter == AICHAR_ZOMBIE ) && ( IS_FLAMING_ZOMBIE( ent->s ) ) && ( fabs( cs->bs->ideal_viewangles[YAW] - cs->bs->viewangles[YAW] ) < 5 ) ) { if ( fabs( sin( ( level.time + cs->entityNum * 314 ) / 1000 ) * cos( ( level.time + cs->entityNum * 267 ) / 979 ) ) < 0.5 ) { ent->s.time = level.time + 800; } } // reload? if ( ( cs->bs->cur_ps.ammoclip[BG_FindClipForWeapon( cs->bs->cur_ps.weapon )] < (int)( 0.75 * ammoTable[cs->bs->cur_ps.weapon].maxclip ) ) && cs->bs->cur_ps.ammo[BG_FindAmmoForWeapon( cs->bs->cur_ps.weapon )] ) { trap_EA_Reload( cs->entityNum ); } if ( dist < chaseDist ) { return NULL; } // // go to them // // ........................................................... // Do the movement.. // // move straight to them if we can if ( !moved && cs->leaderNum < 0 && ( cs->bs->cur_ps.groundEntityNum != ENTITYNUM_NONE || g_entities[cs->entityNum].waterlevel > 1 ) && AICast_EntityVisible( cs, cs->bs->enemy, qtrue ) ) { aicast_predictmove_t move; vec3_t dir; bot_input_t bi; usercmd_t ucmd; trace_t tr; // trace will eliminate most unsuccessful paths trap_Trace( &tr, cs->bs->origin, g_entities[cs->entityNum].r.mins, g_entities[cs->entityNum].r.maxs, followent->r.currentOrigin, cs->entityNum, g_entities[cs->entityNum].clipmask ); if ( tr.entityNum == followent->s.number ) { // try walking straight to them VectorSubtract( followent->r.currentOrigin, cs->bs->origin, dir ); VectorNormalize( dir ); if ( !ent->waterlevel ) { dir[2] = 0; } trap_EA_Move( cs->entityNum, dir, 400 ); trap_EA_GetInput( cs->entityNum, (float) level.time / 1000, &bi ); AICast_InputToUserCommand( cs, &bi, &ucmd, bs->cur_ps.delta_angles ); AICast_PredictMovement( cs, 5, 2.0, &move, &ucmd, bs->enemy ); if ( move.stopevent == PREDICTSTOP_HITENT ) { // success! vectoangles( dir, cs->bs->ideal_viewangles ); cs->bs->ideal_viewangles[2] *= 0.5; moved = qtrue; } } } // // if they are visible, but not attackable, look for a spot where we can attack them, and head // for there. This should prevent AI's getting stuck in a bunch. if ( !moved && cs->bs->weaponnum >= WP_LUGER && cs->bs->weaponnum <= WP_SPEARGUN_CO2 && cs->attributes[TACTICAL] >= 0.1 ) { // // check for another movement we should be making if ( cs->obstructingTime > level.time ) { AICast_MoveToPos( cs, cs->obstructingPos, -1 ); moved = qtrue; } // if ( cs->leaderNum >= 0 ) { if ( cs->combatGoalTime < level.time ) { if ( cs->attackSpotTime < level.time ) { cs->attackSpotTime = level.time + 500 + rand() % 500; if ( trap_AAS_FindAttackSpotWithinRange( cs->entityNum, cs->leaderNum, cs->bs->enemy, MAX_LEADER_DIST, AICAST_TFL_DEFAULT, cs->combatGoalOrigin ) ) { cs->combatGoalTime = level.time + 2000; } } } if ( cs->combatGoalTime > level.time ) { if ( Distance( cs->combatGoalOrigin, g_entities[cs->leaderNum].r.currentOrigin ) > MAX_LEADER_DIST ) { // go find a new combatSpot cs->combatGoalTime = 0; } else { // go to the combat spot moveresult = AICast_MoveToPos( cs, cs->combatGoalOrigin, -1 ); if ( moveresult && moveresult->failure ) { // no path, so go back to idle behaviour cs->combatGoalTime = 0; } else { moved = qtrue; if ( Distance( cs->bs->origin, cs->combatGoalOrigin ) < 32 ) { cs->combatGoalTime = 0; } } } } else { // we can't find a way to get to our enemy, so go back to our leader if outside range // do we need to go to our leader? if ( Distance( cs->bs->origin, g_entities[cs->leaderNum].r.currentOrigin ) > MAX_LEADER_DIST ) { return AIFunc_ChaseGoalStart( cs, cs->leaderNum, AICAST_LEADERDIST_MAX, qtrue ); } } } else { if ( cs->combatGoalTime < level.time ) { if ( cs->attackSpotTime < level.time ) { cs->attackSpotTime = level.time + 500 + rand() % 500; if ( trap_AAS_FindAttackSpotWithinRange( cs->entityNum, cs->entityNum, cs->bs->enemy, 512, AICAST_TFL_DEFAULT, cs->combatGoalOrigin ) ) { cs->combatGoalTime = level.time + 2000; } } } if ( cs->combatGoalTime > level.time ) { // go to the combat spot moveresult = AICast_MoveToPos( cs, cs->combatGoalOrigin, -1 ); if ( moveresult && moveresult->failure ) { // no path, so go back to idle behaviour cs->combatGoalTime = 0; } else { moved = qtrue; if ( Distance( cs->bs->origin, cs->combatGoalOrigin ) < 32 ) { cs->combatGoalTime = 0; } } } } } // just go to them if ( !moved && cs->leaderNum < 0 ) { moveresult = AICast_MoveToPos( cs, destorg, bs->enemy ); if ( moveresult && moveresult->failure ) { // no path, so try and hude from them // pausetime has expired, so go into ambush mode if ( AICast_GetTakeCoverPos( cs, cs->bs->enemy, cs->vislist[cs->bs->enemy].real_visible_pos, cs->takeCoverPos ) ) { // wait in ambush, for them to return VectorCopy( cs->bs->origin, cs->combatGoalOrigin ); return AIFunc_BattleAmbushStart( cs ); } // couldn't find a spot, so just stay here? VectorCopy( cs->bs->origin, cs->combatGoalOrigin ); VectorCopy( cs->bs->origin, cs->takeCoverPos ); return AIFunc_BattleAmbushStart( cs ); } else { moved = qtrue; } } // // slow down real close to the goal, so we don't go passed it cs->speedScale = AICast_SpeedScaleForDistance( cs, dist, chaseDist ); // // ........................................................... // speed up over some time #define BATTLE_CHASE_ACCEL_TIME 300 if ( ( cs->attributes[RUNNING_SPEED] > 170 ) && ( cs->bs->weaponnum != WP_GAUNTLET ) && ( level.time < ( cs->startBattleChaseTime + BATTLE_CHASE_ACCEL_TIME ) ) ) { float ideal; ideal = 0.5 + 0.5 * ( 1.0 - ( (float)( ( cs->startBattleChaseTime + BATTLE_CHASE_ACCEL_TIME ) - level.time ) / BATTLE_CHASE_ACCEL_TIME ) ); if ( ideal < cs->speedScale ) { cs->speedScale = ideal; } } // // if we are going to reach them soon, predict the attack { float simTime = 1.0; aicast_predictmove_t move; float moveDist; vec3_t vec; // if ( cs->bs->weaponnum == WP_GAUNTLET ) { simTime = 0.5; } // AICast_PredictMovement( cs, 1, simTime, &move, &cs->bs->lastucmd, cs->bs->enemy ); VectorSubtract( move.endpos, cs->bs->origin, vec ); moveDist = VectorNormalize( vec ); // if ( cs->bs->weaponnum == WP_GAUNTLET ) { if ( move.stopevent == PREDICTSTOP_HITENT ) { AICast_AimAtEnemy( cs ); trap_EA_Attack( bs->client ); bs->flags |= BFL_ATTACKED; } } // // do we went to play a diving animation into a cover position? else if ( ( cs->attributes[TACTICAL] > 0.85 && cs->aiFlags & AIFL_ROLL_ANIM && !client->ps.torsoTimer && !client->ps.legsTimer && cs->lastRollMove < level.time - 800 && move.numtouch == 0 && moveDist > simTime * cs->attributes[RUNNING_SPEED] * 0.8 && move.groundEntityNum == ENTITYNUM_WORLD ) && ( AICast_CheckAttackAtPos( cs->entityNum, cs->bs->enemy, move.endpos, cs->bs->attackcrouch_time > trap_AAS_Time(), qfalse ) ) ) { return AIFunc_BattleRollStart( cs, vec ); } // else if ( cs->aiFlags & AIFL_FLIP_ANIM && cs->lastRollMove < level.time - 800 && !client->ps.torsoTimer && cs->castScriptStatus.castScriptEventIndex < 0 && move.numtouch == 0 && moveDist > simTime * cs->attributes[RUNNING_SPEED] * 0.9 && move.groundEntityNum == ENTITYNUM_WORLD && cs->bs->attackcrouch_time < trap_AAS_Time() ) { int destarea, simarea, starttravel, simtravel; // if we'll be closer after the move, proceed destarea = BotPointAreaNum( destorg ); simarea = BotPointAreaNum( move.endpos ); starttravel = trap_AAS_AreaTravelTimeToGoalArea( cs->bs->areanum, cs->bs->origin, destarea, cs->travelflags ); simtravel = trap_AAS_AreaTravelTimeToGoalArea( simarea, move.endpos, destarea, cs->travelflags ); if ( simtravel < starttravel ) { return AIFunc_FlipMoveStart( cs, vec ); } } // slow down? so we don't go too far from behind the obstruction which is protecting us else if ( ( cs->obstructingTime < level.time ) && ( cs->attributes[TACTICAL] > 0.1 ) && ( AICast_VisibleFromPos( cs->vislist[cs->bs->enemy].visible_pos, cs->bs->enemy, move.endpos, cs->entityNum, qfalse ) ) ) { // start a crouch attack? //if (cs->attributes[ATTACK_CROUCH] > 0.1) { // cs->bs->attackcrouch_time = trap_AAS_Time() + 3.0; //else cs->bs->attackcrouch_time = 0; if ( cs->bs->cur_ps.viewheight > cs->bs->cur_ps.crouchViewHeight && cs->attributes[RUNNING_SPEED] * cs->speedScale > 120 ) { cs->speedScale = 120.0 * cs->attributes[RUNNING_SPEED]; } // also face them, ready for the attack AICast_AimAtEnemy( cs ); /* disabled, causes them to use up ammo in the clip before they get visible if ((cs->castScriptStatus.scriptNoAttackTime < level.time) && (cs->noAttackTime < level.time)) { // if we are using a bullet weapon, start firing now switch (cs->bs->weaponnum) { case WP_MP40: case WP_VENOM: case WP_THOMPSON: case WP_STEN: //----(SA) added trap_EA_Attack(cs->entityNum); } } */ } } // reload? if ( ( cs->bs->cur_ps.ammoclip[BG_FindClipForWeapon( cs->bs->cur_ps.weapon )] < (int)( 0.75 * ammoTable[cs->bs->cur_ps.weapon].maxclip ) ) && cs->bs->cur_ps.ammo[BG_FindAmmoForWeapon( cs->bs->cur_ps.weapon )] ) { trap_EA_Reload( cs->entityNum ); } return NULL; } /* ============ AIFunc_BattleChaseStart() ============ */ char *AIFunc_BattleChaseStart( cast_state_t *cs ) { cs->startBattleChaseTime = level.time; cs->combatGoalTime = 0; cs->battleChaseMarker = -99; cs->battleChaseMarkerDir = 1; // don't wait too long before taking cover, if we just aborted one if ( cs->takeCoverTime > level.time ) { cs->takeCoverTime = level.time + 1500 + rand() % 500; } // // start a crouch attack? if ( cs->attributes[ATTACK_CROUCH] > 0.1 ) { cs->aiFlags |= AIFL_ATTACK_CROUCH; } else { cs->aiFlags &= ~AIFL_ATTACK_CROUCH; } // cs->aifunc = AIFunc_BattleChase; return "AIFunc_BattleChase"; } /* ============ AIFunc_AvoidDanger() ============ */ char *AIFunc_AvoidDanger( cast_state_t *cs ) { bot_state_t *bs; vec3_t destorg, vec; float dist; int enemies[MAX_CLIENTS], numEnemies, i; qboolean shouldAttack; gentity_t *ent; trace_t tr; vec3_t end; gentity_t *danger; // we need to move towards it bs = cs->bs; ent = g_entities + cs->entityNum; // // TODO: if we are on fire, play the correct torso animation if ( ent->s.onFireEnd > level.time ) { // set the animation, and a short timer, but long enough to last until the next frame //if (g_cheats.integer) G_Printf( "TODO: torso onfire animation\n" ); } // // is the danger gone? if ( cs->dangerEntityValidTime < level.time ) { return AIFunc_DefaultStart( cs ); } // // special case: if it's a grenade, and it's going to land near us with some time left before it // explodes, try and kick it back // danger = &g_entities[cs->dangerEntity]; if ( ent->s.onFireEnd < level.time ) { if ( ( danger->s.weapon == WP_GRENADE_LAUNCHER || danger->s.weapon == WP_GRENADE_PINEAPPLE ) && ( danger->nextthink - level.time > 1500 ) && ( level.lastGrenadeKick < level.time - 3000 ) && ( cs->aiFlags & AIFL_CATCH_GRENADE ) && !( danger->flags & FL_AI_GRENADE_KICK ) ) { // if it was thrown by a friend of ours, leave it alone if ( !AICast_SameTeam( cs, danger->r.ownerNum ) ) { if ( G_PredictMissile( danger, danger->nextthink - level.time, cs->takeCoverPos, qfalse ) ) { // make sure it's a valid position, and drop it down to the ground cs->takeCoverPos[2] += -ent->r.mins[2] + 12; VectorCopy( cs->takeCoverPos, end ); end[2] -= 90; trap_Trace( &tr, cs->takeCoverPos, ent->r.mins, ent->r.maxs, end, cs->entityNum, MASK_SOLID ); VectorCopy( tr.endpos, cs->takeCoverPos ); if ( !tr.startsolid && ( tr.fraction < 1.0 ) && VectorDistance( cs->bs->origin, cs->takeCoverPos ) < cs->attributes[RUNNING_SPEED] * 0.0004 * ( danger->nextthink - level.time - 2000 ) ) { // check for a clear path to the grenade trap_Trace( &tr, cs->bs->origin, ent->r.mins, ent->r.maxs, cs->takeCoverPos, cs->entityNum, MASK_SOLID ); if ( VectorDistance( tr.endpos, cs->takeCoverPos ) < 8 ) { danger->flags |= FL_AI_GRENADE_KICK; ent->flags |= FL_AI_GRENADE_KICK; level.lastGrenadeKick = level.time; return AIFunc_GrenadeKickStart( cs ); // we should decide our course of action within this start function (dive or return grenade) } } } } // if it's really close to us, and we're heading for it, may as well pick it up if ( VectorLength( danger->s.pos.trDelta ) < 10 && VectorDistance( danger->r.currentOrigin, cs->bs->origin ) < 128 && ( level.lastGrenadeKick < level.time - 3000 ) && ( cs->aiFlags & AIFL_CATCH_GRENADE ) ) { vec3_t vec; VectorSubtract( danger->r.currentOrigin, cs->bs->origin, vec ); if ( DotProduct( vec, cs->bs->velocity ) > 0 ) { danger->flags |= FL_AI_GRENADE_KICK; ent->flags |= FL_AI_GRENADE_KICK; level.lastGrenadeKick = level.time; return AIFunc_GrenadeKickStart( cs ); // we should decide our course of action within this start function (dive or return grenade) } } } } // if ( g_entities[cs->dangerEntity].inuse ) { // is our current destination still safe? if ( Distance( cs->dangerEntityPos, cs->takeCoverPos ) < cs->dangerDist && AICast_VisibleFromPos( cs->dangerEntityPos, cs->dangerEntity, cs->takeCoverPos, cs->entityNum, qfalse ) ) { //G_Printf("current coverPos is dangerous, looking for a better place..\n" ); if ( !AICast_GetTakeCoverPos( cs, cs->dangerEntity, cs->dangerEntityPos, cs->takeCoverPos ) ) { // just run away from it ??? } } } else { // the entity isn't here anymore, so stop hiding cs->dangerEntityValidTime = -1; return AIFunc_DefaultStart( cs ); } // VectorCopy( cs->takeCoverPos, destorg ); VectorSubtract( destorg, cs->bs->origin, vec ); vec[2] *= 0.2; dist = VectorLength( vec ); // shouldAttack = qfalse; if ( ent->s.onFireEnd < level.time ) { // look for things we should attack numEnemies = AICast_ScanForEnemies( cs, enemies ); if ( numEnemies > 0 ) { // default to the first known enemy, overwrite if we find a clearer shot cs->bs->enemy = enemies[0]; // for ( i = 0; i < numEnemies; i++ ) { if ( AICast_CheckAttack( cs, enemies[i], qfalse ) || AICast_CheckAttack( AICast_GetCastState( enemies[i] ), cs->entityNum, qfalse ) ) { cs->bs->enemy = enemies[i]; shouldAttack = qtrue; break; } else if ( cs->bs->enemy < 0 ) { cs->lastEnemy = enemies[i]; } } } } // // if we are now safe from the danger, stop running away if ( cs->dangerEntity >= MAX_CLIENTS && Distance( cs->dangerEntityPos, cs->bs->origin ) > cs->dangerDist * 1.5 ) { // don't move, wait for danger to pass } else // are we close enough to the goal? if ( dist > 8 ) { moveresult = AICast_MoveToPos( cs, destorg, -1 ); if ( moveresult ) { //if the movement failed if ( moveresult->failure || moveresult->blocked ) { //reset the avoid reach, otherwise bot is stuck in current area trap_BotResetAvoidReach( bs->ms ); if ( g_entities[cs->dangerEntity].inuse ) { // find a better spot? AICast_GetTakeCoverPos( cs, cs->dangerEntity, cs->dangerEntityPos, cs->takeCoverPos ); } else { VectorCopy( cs->bs->origin, cs->takeCoverPos ); } } } if ( ent->s.onFireEnd < level.time ) { // slow down real close to the goal, so we don't go passed it cs->speedScale = AICast_SpeedScaleForDistance( cs, dist, 0 ); } // // pretend we can still see them while we run to our hide pos, this way they are less likely // to forget about their enemy once they get there if ( ent->s.onFireEnd < level.time && cs->bs->enemy >= 0 && cs->vislist[cs->bs->enemy].real_visible_timestamp && ( cs->vislist[cs->bs->enemy].real_visible_timestamp > level.time - 10000 ) ) { AICast_UpdateVisibility( &g_entities[cs->entityNum], &g_entities[cs->bs->enemy], qfalse, cs->vislist[cs->bs->enemy].real_visible_timestamp == cs->vislist[cs->bs->enemy].lastcheck_timestamp ); } } else { // set our origin as the hidepos, that way if we are still in danger, we should find a better spot VectorCopy( cs->bs->origin, cs->takeCoverPos ); // check for a movement we should be making if ( cs->obstructingTime > level.time ) { AICast_MoveToPos( cs, cs->obstructingPos, -1 ); } // if we are on fire, never stop if ( ent->s.onFireEnd > level.time ) { VectorCopy( cs->bs->origin, cs->dangerEntityPos ); cs->dangerEntityValidTime = level.time + 10000; } } // // if we should be attacking something on our way if ( shouldAttack ) { //attack the enemy if possible AICast_ProcessAttack( cs ); } else { //if (dist < 48) { // if we've recently been in a fight, look towards the enemy if ( cs->lastEnemy >= 0 ) { // if we only just recently saw them, face that direction if ( cs->vislist[cs->lastEnemy].visible_timestamp > ( level.time - 20000 ) ) { vec3_t dir; // VectorSubtract( cs->vislist[cs->lastEnemy].visible_pos, cs->bs->origin, dir ); VectorNormalize( dir ); vectoangles( dir, cs->bs->ideal_viewangles ); } } } // reload? if ( ( cs->bs->cur_ps.ammoclip[BG_FindClipForWeapon( cs->bs->cur_ps.weapon )] < (int)( 0.75 * ammoTable[cs->bs->cur_ps.weapon].maxclip ) ) && cs->bs->cur_ps.ammo[BG_FindAmmoForWeapon( cs->bs->cur_ps.weapon )] ) { trap_EA_Reload( cs->entityNum ); } return NULL; } /* ============ AIFunc_AvoidDangerStart() ============ */ char *AIFunc_AvoidDangerStart( cast_state_t *cs ) { // //if (!AICast_CanMoveWhileFiringWeapon( cs->bs->weaponnum )) { // always run to the cover point cs->bs->attackcrouch_time = 0; //} // make sure we move if we are allowed (scripting will overwrite this if necessary) cs->castScriptStatus.scriptNoMoveTime = 0; // resume following once danger has gone cs->castScriptStatus.scriptGotoId = -1; // cs->aifunc = AIFunc_AvoidDanger; return "AIFunc_AvoidDanger"; } /* ============ AIFunc_BattleTakeCover() ============ */ char *AIFunc_BattleTakeCover( cast_state_t *cs ) { bot_state_t *bs; vec3_t destorg, vec; float dist, moveDist; int enemies[MAX_CLIENTS], numEnemies, i; qboolean shouldAttack; aicast_predictmove_t move; int *ammo; gclient_t *client = &level.clients[cs->entityNum]; // // do we need to avoid a danger? if ( cs->dangerEntityValidTime >= level.time ) { if ( !AICast_GetTakeCoverPos( cs, cs->dangerEntity, cs->dangerEntityPos, cs->takeCoverPos ) ) { // shit?? } // go to a position that cannot be seen from the dangerPos cs->takeCoverTime = cs->dangerEntityValidTime + 1000; cs->bs->attackcrouch_time = 0; return AIFunc_AvoidDangerStart( cs ); } // // are we waiting for a door? if ( cs->doorMarkerTime > level.time - 100 ) { return AIFunc_DoorMarkerStart( cs, cs->doorMarkerDoor, cs->doorMarkerNum ); } // we need to move towards it bs = cs->bs; // // note: removing this will cause problems down below! if ( bs->enemy < 0 ) { return AIFunc_IdleStart( cs ); } // VectorCopy( cs->takeCoverPos, destorg ); VectorSubtract( destorg, cs->bs->origin, vec ); vec[2] *= 0.2; dist = VectorLength( vec ); // // look for things we should attack // if we are out of ammo, we shouldn't bother trying to attack (and we should keep hiding) ammo = cs->bs->cur_ps.ammo; shouldAttack = qfalse; numEnemies = AICast_ScanForEnemies( cs, enemies ); if ( numEnemies == -1 ) { // query mode return NULL; } else if ( numEnemies == -2 ) { // inspection may be required char *retval; // TTimo gcc: suggest parentheses around assignment used as truth value if ( ( retval = AIFunc_InspectFriendlyStart( cs, enemies[0] ) ) ) { return retval; } } else if ( numEnemies == -3 ) { // bullet impact if ( cs->aiState < AISTATE_COMBAT ) { return AIFunc_InspectBulletImpactStart( cs ); } } else if ( numEnemies == -4 ) { // audible event if ( cs->aiState < AISTATE_COMBAT ) { return AIFunc_InspectAudibleEventStart( cs, cs->audibleEventEnt ); } } else if ( AICast_GotEnoughAmmoForWeapon( cs, cs->bs->weaponnum ) ) { if ( numEnemies > 0 ) { // default to the first known enemy, overwrite if we find a clearer shot cs->bs->enemy = enemies[0]; // for ( i = 0; i < numEnemies; i++ ) { if ( AICast_CheckAttack( cs, enemies[i], qfalse ) || AICast_CheckAttack( AICast_GetCastState( enemies[i] ), cs->entityNum, qfalse ) ) { cs->bs->enemy = enemies[i]; shouldAttack = qtrue; break; } else if ( cs->bs->enemy < 0 ) { cs->lastEnemy = enemies[i]; } } } } // if ( !shouldAttack ) { // if the enemy can see our hide position, find a better spot if ( AICast_VisibleFromPos( g_entities[bs->enemy].client->ps.origin, bs->enemy, cs->takeCoverPos, bs->entitynum, qfalse ) ) { if ( !AICast_GetTakeCoverPos( cs, bs->enemy, cs->vislist[bs->enemy].visible_pos, cs->takeCoverPos ) ) { // shit!! umm.. try and fire? return AIFunc_BattleStart( cs ); } else { // recalc distance VectorCopy( cs->takeCoverPos, destorg ); VectorSubtract( destorg, cs->bs->origin, vec ); vec[2] *= 0.2; dist = VectorLength( vec ); } } else if ( dist < 8 ) { // if they can see us, find a better spot if ( AICast_EntityVisible( AICast_GetCastState( cs->bs->enemy ), cs->entityNum, qtrue ) || AICast_CheckAttack( AICast_GetCastState( cs->bs->enemy ), cs->entityNum, qfalse ) ) { if ( !AICast_GetTakeCoverPos( cs, bs->enemy, cs->vislist[bs->enemy].visible_pos, cs->takeCoverPos ) ) { // shit!! umm.. try and fire? return AIFunc_BattleStart( cs ); } else { // recalc distance VectorCopy( cs->takeCoverPos, destorg ); VectorSubtract( destorg, cs->bs->origin, vec ); vec[2] *= 0.2; dist = VectorLength( vec ); } } } //cs->takeCoverTime = level.time + 1000; } // // pretend we can still see them while we run to our hide pos, this way they are less likely // to forget about their enemy once they get there // DISABLED: doesn't work well with new AI system //if (cs->bs->enemy >= 0 && cs->vislist[cs->bs->enemy].real_visible_timestamp && (cs->vislist[cs->bs->enemy].real_visible_timestamp > level.time - 2000)) { // AICast_UpdateVisibility( &g_entities[cs->entityNum], &g_entities[cs->bs->enemy], qfalse, cs->vislist[cs->bs->enemy].real_visible_timestamp == cs->vislist[cs->bs->enemy].lastcheck_timestamp ); //} // memset( &move, 0, sizeof( move ) ); // // are we close enough to the goal? if ( VectorLength( cs->takeCoverPos ) > 1 && dist > 8 ) { const float simTime = 0.8; float enemyDist; // // we haven't reached it yet, make sure we at least wait there for a few seconds after arriving cs->takeCoverTime = level.time + 2000 + rand() % 2000; // moveresult = AICast_MoveToPos( cs, destorg, -1 ); if ( moveresult ) { //if the movement failed if ( moveresult->failure ) { //reset the avoid reach, otherwise bot is stuck in current area trap_BotResetAvoidReach( bs->ms ); // couldn't get there, so stop trying to get there VectorClear( cs->takeCoverPos ); dist = 0; } // if ( moveresult->blocked ) { // abort the TakeCover VectorClear( cs->takeCoverPos ); dist = 0; } } // // if we are going to bump into something soon, abort it AICast_PredictMovement( cs, 1, simTime, &move, &cs->bs->lastucmd, -1 ); enemyDist = Distance( cs->bs->origin, g_entities[cs->bs->enemy].s.pos.trBase ); VectorSubtract( move.endpos, cs->bs->origin, vec ); moveDist = VectorNormalize( vec ); // if ( ( move.numtouch && move.touchents[0] < aicast_maxclients ) // hit something // or moved closer to the enemy || ( ( enemyDist < 128 ) && ( ( enemyDist - 1 ) > ( Distance( move.endpos, g_entities[cs->bs->enemy].s.pos.trBase ) ) ) ) ) { // abort the manouver VectorClear( cs->takeCoverPos ); dist = 0; } // // do we went to play a rolling animation into a cover position? else if ( ( cs->aiFlags & AIFL_DIVE_ANIM && !client->ps.torsoTimer && cs->castScriptStatus.castScriptEventIndex < 0 && cs->lastRollMove < level.time - 800 && move.numtouch == 0 && moveDist > 64 && move.groundEntityNum == ENTITYNUM_WORLD ) && ( shouldAttack && !AICast_VisibleFromPos( g_entities[cs->bs->enemy].s.pos.trBase, cs->bs->enemy, move.endpos, cs->entityNum, qfalse ) ) ) { VectorClear( cs->takeCoverPos ); // stay there when done rolling return AIFunc_BattleDiveStart( cs, vec ); } // // we should slow down on approaching the destination point else if ( dist < 64 ) { cs->speedScale = AICast_SpeedScaleForDistance( cs, dist, 0 ); } // // if they cant see us, then stay here if ( !( cs->aiFlags & AIFL_MISCFLAG1 ) && !AICast_VisibleFromPos( cs->vislist[cs->bs->enemy].real_visible_pos, cs->bs->enemy, move.endpos, cs->entityNum, qfalse ) && !AICast_VisibleFromPos( cs->vislist[cs->bs->enemy].real_visible_pos, cs->bs->enemy, cs->bs->origin, cs->entityNum, qfalse ) ) { VectorCopy( move.endpos, cs->takeCoverPos ); dist = 0; cs->aiFlags |= AIFL_MISCFLAG1; // dont do this again } // if ( cs->aiFlags & AIFL_FLIP_ANIM && cs->lastRollMove < level.time - 800 && !client->ps.torsoTimer && cs->castScriptStatus.castScriptEventIndex < 0 && move.numtouch == 0 && moveDist > simTime * cs->attributes[RUNNING_SPEED] * 0.9 && move.groundEntityNum == ENTITYNUM_WORLD && cs->bs->attackcrouch_time < trap_AAS_Time() ) { int destarea, simarea, starttravel, simtravel; // if we'll be closer after the move, proceed destarea = BotPointAreaNum( destorg ); simarea = BotPointAreaNum( move.endpos ); starttravel = trap_AAS_AreaTravelTimeToGoalArea( cs->bs->areanum, cs->bs->origin, destarea, cs->travelflags ); simtravel = trap_AAS_AreaTravelTimeToGoalArea( simarea, move.endpos, destarea, cs->travelflags ); if ( simtravel < starttravel ) { return AIFunc_FlipMoveStart( cs, vec ); } } // set crouching status //if (dist && (cs->thinkFuncChangeTime < level.time - 2000) && (cs->crouchHideFlag || cs->aiFlags & AIFL_ATTACK_CROUCH)) { if ( cs->crouchHideFlag || cs->aiFlags & AIFL_ATTACK_CROUCH ) { cs->bs->attackcrouch_time = trap_AAS_Time() + 1; } } else { // // have we been Taking Cover for enough time? if ( level.time > cs->takeCoverTime ) { return AIFunc_DefaultStart( cs ); } // // check for a movement we should be making if ( cs->obstructingTime > level.time ) { VectorClear( cs->takeCoverPos ); AICast_MoveToPos( cs, cs->obstructingPos, -1 ); } // if we have some enemies that we can attack immediately (without going anywhere to chase them) if ( shouldAttack ) { return AIFunc_BattleStart( cs ); } // if we have some enemies in sight, but they can't attack us, flee if possible, otherwise if we are not afraid, go attack them else if ( numEnemies ) { // we can't hit them and they cant hit us, so dont bother doing anything //if (!AICast_GetTakeCoverPos( cs, bs->enemy, cs->vislist[bs->enemy].visible_pos, cs->takeCoverPos )) { //if (!AICast_WantsToTakeCover(cs, qfalse)) //return AIFunc_BattleStart( cs ); //} } // do we need to go to our leader? else if ( cs->leaderNum >= 0 && Distance( cs->bs->origin, g_entities[cs->leaderNum].r.currentOrigin ) > MAX_LEADER_DIST ) { // wait until we've been hiding for long enough if ( level.time > cs->takeCoverTime ) { return AIFunc_ChaseGoalStart( cs, cs->leaderNum, AICAST_LEADERDIST_MAX, qtrue ); } } // else, crouch while we hide if ( cs->attributes[ATTACK_CROUCH] > 0.1 || cs->crouchHideFlag ) { cs->bs->attackcrouch_time = trap_AAS_Time() + 1; } } // // if we should be attacking something on our way if ( shouldAttack ) { vec3_t vec, dir; float dist; // // if they are close, and we're heading for them, we should abort this manouver VectorSubtract( g_entities[bs->enemy].client->ps.origin, bs->origin, vec ); if ( ( dist = VectorNormalize( vec ) ) < 256 ) { if ( VectorNormalize2( bs->velocity, dir ) > 20 ) { // we are moving if ( DotProduct( dir, vec ) > 0 ) { // abort return AIFunc_BattleStart( cs ); } } } // // if the enemy can see our hide position, abort the manouver if ( ( cs->thinkFuncChangeTime < level.time - 1000 ) && ( AICast_VisibleFromPos( g_entities[bs->enemy].client->ps.origin, bs->enemy, cs->takeCoverPos, bs->entitynum, qfalse ) ) ) { // abort return AIFunc_BattleStart( cs ); } // // if we are tactical and can crouch, do so if ( !move.numtouch && ( dist > 128 ) && cs->attributes[TACTICAL] > 0.4 && cs->attributes[ATTACK_CROUCH] > 0.1 && ( cs->bs->attackcrouch_time >= trap_AAS_Time() ) ) { cs->bs->attackcrouch_time = trap_AAS_Time() + 1; } // //attack the enemy if possible AICast_ProcessAttack( cs ); // } else /*if (dist < 48)*/ { // if we've recently been in a fight, look towards the enemy if ( cs->bs->enemy >= 0 ) { AICast_AimAtEnemy( cs ); } else if ( cs->lastEnemy >= 0 ) { // if we are not moving, face them if ( VectorLength( cs->bs->cur_ps.velocity ) < 50 ) { vec3_t dir; // VectorSubtract( cs->vislist[cs->lastEnemy].visible_pos, cs->bs->origin, dir ); VectorNormalize( dir ); vectoangles( dir, cs->bs->ideal_viewangles ); } } else if ( !cs->crouchHideFlag ) { // no enemy, and no need to crouch, so stop crouching //if (cs->bs->attackcrouch_time > trap_AAS_Time() + 1) { // cs->bs->attackcrouch_time = trap_AAS_Time() + 1; //} } // reload? if ( ( cs->bs->cur_ps.ammoclip[BG_FindClipForWeapon( cs->bs->cur_ps.weapon )] < (int)( 0.75 * ammoTable[cs->bs->cur_ps.weapon].maxclip ) ) && cs->bs->cur_ps.ammo[BG_FindAmmoForWeapon( cs->bs->cur_ps.weapon )] ) { trap_EA_Reload( cs->entityNum ); } } return NULL; } /* ============ AIFunc_BattleTakeCoverStart() ============ */ char *AIFunc_BattleTakeCoverStart( cast_state_t *cs ) { // debugging #ifdef DEBUG if ( cs->attributes[AGGRESSION] >= 1.0 ) { AICast_Printf( 0, "AI taking cover with full aggression!\n" ); } #endif if ( !AICast_CanMoveWhileFiringWeapon( cs->bs->weaponnum ) ) { // always run to the cover point cs->bs->attackcrouch_time = 0; cs->aiFlags &= ~AIFL_ATTACK_CROUCH; } else { // if we arent crouching, start crouching soon after we start retreating if ( cs->attributes[ATTACK_CROUCH] > 0.1 ) { cs->aiFlags |= AIFL_ATTACK_CROUCH; } else { cs->aiFlags &= ~AIFL_ATTACK_CROUCH; } } // miscflag1 used to set predicted point as our goal, so we dont keep setting this over and over cs->aiFlags &= ~AIFL_MISCFLAG1; cs->aifunc = AIFunc_BattleTakeCover; return "AIFunc_BattleTakeCover"; } /* ============ AIFunc_GrenadeFlush() ============ */ char *AIFunc_GrenadeFlush( cast_state_t *cs ) { vec3_t dir; gentity_t *followent, *grenade, *ent; bot_state_t *bs; vec3_t destorg, endPos; qboolean moved = qfalse; int hitclient; qboolean attacked = qfalse; float dist, oldyaw; int grenadeType; int *ammo; bs = cs->bs; ent = &g_entities[cs->entityNum]; // // if we are throwing the grenade, keep holding down fire // (SA) probably read the fweapon from t if ( cs->grenadeFlushFiring ) { if ( cs->weaponFireTimes[cs->grenadeFlushFiring] < cs->thinkFuncChangeTime ) { // have we switched weapons? if ( cs->bs->weaponnum != cs->grenadeFlushFiring ) { // damn hitclient = -1; } else { // keep checking it's ok grenade = weapon_grenadelauncher_fire( ent, WP_GRENADE_LAUNCHER ); hitclient = AICast_SafeMissileFire( grenade, grenade->nextthink - level.time, cs->bs->enemy, destorg, cs->entityNum, NULL ); G_FreeEntity( grenade ); } if ( hitclient == -1 ) { // doh //G_Printf("aborted grenade\n"); cs->castScriptStatus.scriptNoMoveTime = 0; cs->lockViewAnglesTime = 0; AICast_ChooseWeapon( cs, qfalse ); return AIFunc_DefaultStart( cs ); } if ( !cs->bs->cur_ps.grenadeTimeLeft ) { // hold fire button down trap_EA_Attack( bs->client ); bs->flags |= BFL_ATTACKED; } cs->lockViewAnglesTime = level.time + 500; return NULL; } // the grenade/pineapple has been released! cs->lockViewAnglesTime = -1; cs->startGrenadeFlushTime = level.time + 2000 + rand() % 2000; // dont throw one again for a bit return AIFunc_DefaultStart( cs ); } // // do we need to avoid a danger? if ( cs->dangerEntityValidTime >= level.time ) { if ( !AICast_GetTakeCoverPos( cs, cs->dangerEntity, cs->dangerEntityPos, cs->takeCoverPos ) ) { // shit?? } // go to a position that cannot be seen from the dangerPos cs->takeCoverTime = cs->dangerEntityValidTime + 1000; cs->bs->attackcrouch_time = 0; return AIFunc_AvoidDangerStart( cs ); } // // are we waiting for a door? if ( cs->doorMarkerTime > level.time - 100 ) { return AIFunc_DoorMarkerStart( cs, cs->doorMarkerDoor, cs->doorMarkerNum ); } // if ( cs->bs->weaponnum && ( cs->castScriptStatus.scriptFlags & SFL_NOCHANGEWEAPON ) ) { return AIFunc_IdleStart( cs ); } // if ( bs->enemy < 0 ) { return AIFunc_IdleStart( cs ); } // // if we have started a script, abort the grenade flush if ( cs->castScriptStatus.castScriptEventIndex >= 0 ) { return AIFunc_IdleStart( cs ); } // trying for too long? if ( cs->startGrenadeFlushTime < level.time - 4000 ) { cs->startGrenadeFlushTime = level.time; return AIFunc_IdleStart( cs ); } // followent = &g_entities[bs->enemy]; // // if the entity is not ready yet if ( !followent->inuse ) { // if it's a connecting client, wait if ( !( ( bs->enemy < MAX_CLIENTS ) && ( ( followent->client && followent->client->pers.connected == CON_CONNECTING ) || ( level.time < 3000 ) ) ) ) { // they don't exist anymore, stop attacking bs->enemy = -1; } return AIFunc_IdleStart( cs ); } // // if we can see them, go back to an attack state after some time if ( AICast_CheckAttack( cs, bs->enemy, qfalse ) && cs->obstructingTime < level.time ) { // give us some time to throw the grenade, otherwise go back to attack state if ( ( cs->grenadeFlushEndTime > 0 && cs->grenadeFlushEndTime < level.time ) ) { //G_Printf("aborting, enemy is attackable\n"); return AIFunc_BattleStart( cs ); } else if ( cs->grenadeFlushEndTime < 0 ) { cs->grenadeFlushEndTime = level.time + 1500; } //attack the enemy if possible AICast_ProcessAttack( cs ); attacked = qtrue; } else { // not visible, go to their previously visible position if ( !cs->vislist[bs->enemy].visible_timestamp || Distance( bs->origin, cs->vislist[bs->enemy].real_visible_pos ) < 16 ) { // we're done attacking, go back to default state, which in turn will recall previous state // // face the direction they currently are from this position (bit of a hack, but it looks best) VectorSubtract( g_entities[bs->enemy].client->ps.origin, cs->vislist[bs->enemy].visible_pos, dir ); vectoangles( dir, cs->bs->ideal_viewangles ); // //G_Printf("aborting, reached visible pos\n"); return AIFunc_DefaultStart( cs ); } } // is there someone else we can go for instead? numEnemies = AICast_ScanForEnemies( cs, enemies ); if ( numEnemies == -1 ) { // query mode return NULL; } else if ( numEnemies == -2 ) { // inspection may be required char *retval; // TTimo gcc: suggest parentheses around assignment used as truth value if ( ( retval = AIFunc_InspectFriendlyStart( cs, enemies[0] ) ) ) { return retval; } } else if ( !( cs->bs->flags & BFL_ATTACKED ) && numEnemies > 0 ) { int i; for ( i = 0; i < numEnemies; i++ ) { if ( enemies[i] != cs->bs->enemy && AICast_CheckAttack( cs, enemies[i], qfalse ) ) { cs->bs->enemy = enemies[i]; //G_Printf("aborting, other enemy\n"); return AIFunc_BattleStart( cs ); } } } if ( followent->client ) { // go to the last visible position VectorCopy( cs->vislist[bs->enemy].visible_pos, destorg ); } else // assume we know where other entities are { VectorCopy( followent->s.pos.trBase, destorg ); } // dist = VectorDistance( destorg, cs->bs->origin ); // if ( cs->vislist[bs->enemy].lastcheck_timestamp > cs->vislist[bs->enemy].real_visible_timestamp || dist > 128 ) { // // go to them // if ( followent->client && followent->health <= 0 ) { cs->bs->enemy = -1; //G_Printf("aborting, enemy dead\n"); return AIFunc_DefaultStart( cs ); } // // ........................................................... // Do the movement.. // // move straight to them if we can if ( ( cs->leaderNum < 0 ) && ( cs->bs->cur_ps.groundEntityNum != ENTITYNUM_NONE || g_entities[cs->entityNum].waterlevel > 1 ) ) { aicast_predictmove_t move; vec3_t dir; bot_input_t bi; usercmd_t ucmd; trace_t tr; // trace will eliminate most unsuccessful paths trap_Trace( &tr, cs->bs->origin, g_entities[cs->entityNum].r.mins, g_entities[cs->entityNum].r.maxs, followent->r.currentOrigin, cs->entityNum, g_entities[cs->entityNum].clipmask ); if ( tr.entityNum == followent->s.number ) { // try walking straight to them VectorSubtract( followent->r.currentOrigin, cs->bs->origin, dir ); VectorNormalize( dir ); if ( !ent->waterlevel ) { dir[2] = 0; } trap_EA_Move( cs->entityNum, dir, 400 ); trap_EA_GetInput( cs->entityNum, (float) level.time / 1000, &bi ); AICast_InputToUserCommand( cs, &bi, &ucmd, bs->cur_ps.delta_angles ); AICast_PredictMovement( cs, 5, 2.0, &move, &ucmd, bs->enemy ); if ( move.stopevent == PREDICTSTOP_HITENT ) { // success! vectoangles( dir, cs->bs->ideal_viewangles ); cs->bs->ideal_viewangles[2] *= 0.5; moved = qtrue; } } } // just go to them if ( !moved ) { moveresult = AICast_MoveToPos( cs, destorg, bs->enemy ); if ( moveresult && moveresult->failure ) { // no path, so go back to idle behaviour cs->bs->enemy = -1; //G_Printf("aborting, movement failure\n"); return AIFunc_DefaultStart( cs ); } else { moved = qtrue; } } } // // ........................................................... // if we throw a grenade from here, will it get their last visible position? // ammo = cs->bs->cur_ps.ammo; if ( AICast_GotEnoughAmmoForWeapon( cs, WP_GRENADE_LAUNCHER ) ) { grenadeType = WP_GRENADE_LAUNCHER; } else if ( AICast_GotEnoughAmmoForWeapon( cs, WP_GRENADE_PINEAPPLE ) ) { grenadeType = WP_GRENADE_PINEAPPLE; } else { // not enough ammo, abort return AIFunc_DefaultStart( cs ); } CalcMuzzlePoints( ent, grenadeType ); // fire a dummy grenade grenade = weapon_grenadelauncher_fire( ent, WP_GRENADE_LAUNCHER ); // check to see what will happen hitclient = AICast_SafeMissileFire( grenade, grenade->nextthink - level.time, cs->bs->enemy, destorg, cs->entityNum, endPos ); // kill the grenade G_FreeEntity( grenade ); if ( !attacked ) { cs->bs->weaponnum = grenadeType; // select grenade launcher } // set our angles for the next frame oldyaw = cs->bs->ideal_viewangles[YAW]; AICast_AimAtEnemy( cs ); // if we can't see them, keep facing our movement dir, but use the pitch information if ( !AICast_EntityVisible( cs, cs->bs->enemy, qtrue ) ) { cs->bs->ideal_viewangles[YAW] = oldyaw; } if ( hitclient == 1 ) { // it will hit their last visible position // give us some time to aim and adjust if ( cs->thinkFuncChangeTime < level.time - 200 ) { trap_EA_Attack( bs->client ); bs->flags |= BFL_ATTACKED; cs->bs->weaponnum = grenadeType; // select grenade launcher cs->grenadeFlushFiring = cs->bs->weaponnum; // pause for a bit, so the grenade comes out correctly cs->lockViewAnglesTime = level.time + 1200; if ( cs->castScriptStatus.scriptNoMoveTime < level.time + 1200 ) { cs->castScriptStatus.scriptNoMoveTime = level.time + 1200; } return NULL; } } else if ( hitclient == -1 ) { // hit a friendly cs->startGrenadeFlushTime = level.time + 3000; // don't try again for a while //G_Printf("aborting, too dangerous\n"); return AIFunc_DefaultStart( cs ); } else if ( hitclient == -2 ) { // went too far, so angle down a bit cs->bs->ideal_viewangles[PITCH] += 15 * random(); } else { if ( cs->thinkFuncChangeTime < level.time - 200 ) { // if it went reasonably close to them, but safe from us, then fire away if ( Distance( endPos, cs->bs->origin ) > 100 + Distance( endPos, g_entities[cs->bs->enemy].r.currentOrigin ) ) { trap_EA_Attack( bs->client ); bs->flags |= BFL_ATTACKED; cs->bs->weaponnum = grenadeType; // select grenade launcher cs->grenadeFlushFiring = cs->bs->weaponnum; // pause for a bit, so the grenade comes out correctly cs->lockViewAnglesTime = level.time + 1200; if ( cs->castScriptStatus.scriptNoMoveTime < level.time + 1200 ) { cs->castScriptStatus.scriptNoMoveTime = level.time + 1200; } return NULL; } } cs->bs->ideal_viewangles[PITCH] += -10 * random(); } // return NULL; } /* ============ AIFunc_GrenadeFlushStart() ============ */ char *AIFunc_GrenadeFlushStart( cast_state_t *cs ) { lastGrenadeFlush = level.time; // + rand()%2000; cs->startGrenadeFlushTime = level.time; cs->grenadeFlushEndTime = -1; cs->lockViewAnglesTime = 0; cs->combatGoalTime = 0; cs->grenadeFlushFiring = qfalse; // don't wait too long before taking cover, if we just aborted one if ( cs->takeCoverTime > level.time + 1000 ) { cs->takeCoverTime = level.time + 500 + rand() % 500; } // cs->aifunc = AIFunc_GrenadeFlush; return "AIFunc_GrenadeFlush"; } /* ============ AIFunc_BattleMG42() ============ */ char *AIFunc_BattleMG42( cast_state_t *cs ) { bot_state_t *bs; gentity_t *mg42, *ent; vec3_t angles, vec, bestangles; mg42 = &g_entities[cs->mountedEntity]; ent = &g_entities[cs->entityNum]; bs = cs->bs; // have we dismounted the MG42? if ( !g_entities[cs->entityNum].active ) { return AIFunc_DefaultStart( cs ); } // if enemy is dead, stop attacking them if ( g_entities[bs->enemy].health <= 0 ) { bs->enemy = -1; } //if no enemy, or our current enemy isn't attackable, look for a better enemy if ( bs->enemy >= 0 ) { if ( cs->vislist[bs->enemy].real_visible_timestamp && cs->vislist[bs->enemy].real_visible_timestamp > ( level.time - 5000 ) ) { VectorSubtract( cs->vislist[bs->enemy].real_visible_pos, mg42->r.currentOrigin, vec ); } else if ( cs->vislist[bs->enemy].visible_timestamp && cs->vislist[bs->enemy].visible_timestamp > ( level.time - 5000 ) ) { VectorSubtract( cs->vislist[bs->enemy].visible_pos, mg42->r.currentOrigin, vec ); } else { // just aim straight forward AngleVectors( mg42->s.angles, vec, NULL, NULL ); } VectorNormalize( vec ); vectoangles( vec, angles ); angles[PITCH] = AngleNormalize180( angles[PITCH] ); VectorCopy( angles, bestangles ); } if ( bs->enemy < 0 || !AICast_CheckAttack( cs, bs->enemy, qfalse ) || ( fabs( AngleDifference( angles[YAW], mg42->s.angles[YAW] ) ) > mg42->harc ) || ( angles[PITCH] < 0 && angles[PITCH] + 5 < -mg42->varc ) || ( angles[PITCH] > 0 && angles[PITCH] - 5 > 5.0 ) ) { qboolean shouldAttack; // look for a better enemy numEnemies = AICast_ScanForEnemies( cs, enemies ); if ( numEnemies == -1 ) { // query mode return NULL; } if ( numEnemies == -2 ) { // inspection may be required char *retval; // TTimo gcc: suggest parentheses around assignment used as truth value if ( ( retval = AIFunc_InspectFriendlyStart( cs, enemies[0] ) ) ) { return retval; } } shouldAttack = qfalse; if ( numEnemies > 0 ) { int i; // default to the first known enemy, overwrite if we find a clearer shot cs->bs->enemy = enemies[0]; // for ( i = 0; i < numEnemies; i++ ) { if ( !cs->vislist[enemies[i]].real_visible_timestamp || ( cs->vislist[enemies[i]].real_visible_timestamp < level.time - 2000 ) ) { // we can't see them, ignore them continue; } // if they are in the range if ( cs->vislist[enemies[i]].real_visible_timestamp > ( level.time - 5000 ) ) { VectorSubtract( cs->vislist[enemies[i]].real_visible_pos, mg42->r.currentOrigin, vec ); } else { VectorSubtract( cs->vislist[enemies[i]].visible_pos, mg42->r.currentOrigin, vec ); } VectorNormalize( vec ); vectoangles( vec, angles ); angles[PITCH] = AngleNormalize180( angles[PITCH] ); if ( !( ( fabs( AngleDifference( angles[YAW], mg42->s.angles[YAW] ) ) > mg42->harc ) || ( angles[YAW] < 0 && angles[YAW] + 2 < -mg42->varc ) || ( angles[YAW] > 0 && angles[YAW] - 2 > 5.0 ) ) ) { if ( AICast_CheckAttack( cs, enemies[i], qfalse ) ) { VectorCopy( angles, bestangles ); cs->bs->enemy = enemies[i]; shouldAttack = qtrue; break; } else if ( AICast_CheckAttack( cs, enemies[i], qtrue ) ) { // keep firing at anything behind solids, in case they find a position where they can shoot us, but our checkattack() doesn't find a clear shot VectorCopy( angles, bestangles ); cs->bs->enemy = enemies[i]; shouldAttack = qtrue; } } } } if ( !shouldAttack ) { // keep firing at anything behind solids, in case they find a position where they can shoot us, but our checkattack() doesn't find a clear shot if ( bs->enemy < 0 || !AICast_CheckAttack( cs, bs->enemy, qtrue ) || ( !cs->vislist[bs->enemy].real_visible_timestamp || ( cs->vislist[bs->enemy].real_visible_timestamp < level.time - 2000 ) ) ) { // face straight forward cs->bs->ideal_viewangles[PITCH] = 0; return NULL; } } } // // hold down fire, and track them // // TODO: play a special "holding mg42" torso animation // VectorCopy( angles, bs->ideal_viewangles ); if ( cs->triggerReleaseTime < level.time ) { trap_EA_Attack( bs->client ); bs->flags |= BFL_ATTACKED; if ( cs->triggerReleaseTime < level.time - 3000 ) { cs->triggerReleaseTime = level.time + 700 + rand() % 700; } } // return NULL; } /* ============ AIFunc_BattleMG42Start() ============ */ char *AIFunc_BattleMG42Start( cast_state_t *cs ) { cs->aifunc = AIFunc_BattleMG42; return "AIFunc_BattleMG42"; } /* ============ AIFunc_InspectBody() go up to the enemy, and have a good look at them, randomly taunt them ============ */ char *AIFunc_InspectBody( cast_state_t *cs ) { bot_state_t *bs; vec3_t destorg, enemyOrg; // // stop crouching cs->bs->attackcrouch_time = 0; // // do we need to avoid a danger? if ( cs->dangerEntityValidTime >= level.time ) { if ( !AICast_GetTakeCoverPos( cs, cs->dangerEntity, cs->dangerEntityPos, cs->takeCoverPos ) ) { // shit?? } // go to a position that cannot be seen from the dangerPos cs->takeCoverTime = cs->dangerEntityValidTime + 1000; cs->bs->attackcrouch_time = 0; return AIFunc_AvoidDangerStart( cs ); } // // are we waiting for a door? if ( cs->doorMarkerTime > level.time - 100 ) { return AIFunc_DoorMarkerStart( cs, cs->doorMarkerDoor, cs->doorMarkerNum ); } // // if running a script if ( cs->castScriptStatus.castScriptEventIndex >= 0 ) { cs->bs->enemy = -1; return AIFunc_IdleStart( cs ); } // bs = cs->bs; // if ( bs->enemy < 0 ) { return AIFunc_IdleStart( cs ); } // // look for things we should attack numEnemies = AICast_ScanForEnemies( cs, enemies ); if ( numEnemies == -1 ) { // query mode return NULL; } else if ( numEnemies == -2 ) { // inspection may be required char *retval; // TTimo gcc: suggest parentheses around assignment used as truth value if ( ( retval = AIFunc_InspectFriendlyStart( cs, enemies[0] ) ) ) { return retval; } } else if ( numEnemies == -3 ) { // bullet impact if ( cs->aiState < AISTATE_COMBAT ) { return AIFunc_InspectBulletImpactStart( cs ); } } else if ( numEnemies == -4 ) { // audible event if ( cs->aiState < AISTATE_COMBAT ) { return AIFunc_InspectAudibleEventStart( cs, cs->audibleEventEnt ); } } else if ( numEnemies > 0 ) { cs->bs->enemy = enemies[0]; // just attack the first one return AIFunc_BattleStart( cs ); } // VectorCopy( cs->vislist[bs->enemy].visible_pos, enemyOrg ); if ( ( cs->inspectBodyTime < 0 ) && ( Distance( cs->bs->origin, enemyOrg ) > 64 ) ) { // if they were gibbed, don't go all the way if ( g_entities[cs->bs->enemy].health < GIB_HEALTH && ( Distance( cs->bs->origin, enemyOrg ) < 180 ) ) { cs->inspectBodyTime = level.time + 1000 + rand() % 1000; trap_EA_Gesture( cs->entityNum ); G_AddEvent( &g_entities[cs->entityNum], EV_GENERAL_SOUND, G_SoundIndex( aiDefaults[cs->aiCharacter].ordersSoundScript ) ); } // walk to them if ( cs->movestate != MS_CROUCH ) { cs->movestate = MS_WALK; } cs->movestateType = MSTYPE_TEMPORARY; // moveresult = AICast_MoveToPos( cs, enemyOrg, -1 ); //if the movement failed if ( !moveresult || moveresult->failure || moveresult->blocked ) { //reset the avoid reach, otherwise bot is stuck in current area trap_BotResetAvoidReach( bs->ms ); // couldn't get there, so stop trying to get there cs->bs->enemy = -1; return AIFunc_IdleStart( cs ); } if ( Distance( cs->bs->origin, enemyOrg ) < 180 ) { // look down at them VectorSubtract( enemyOrg, cs->bs->origin, destorg ); destorg[2] -= 20; VectorNormalize( destorg ); vectoangles( destorg, cs->bs->ideal_viewangles ); } } else if ( cs->inspectBodyTime < 0 ) { // just reached them cs->inspectBodyTime = level.time + 1000 + rand() % 1000; trap_EA_Gesture( cs->entityNum ); G_AddEvent( &g_entities[cs->entityNum], EV_GENERAL_SOUND, G_SoundIndex( aiDefaults[cs->aiCharacter].ordersSoundScript ) ); } else if ( cs->inspectBodyTime < level.time ) { vec3_t vec; VectorSubtract( cs->startOrigin, cs->bs->origin, vec ); vec[2] = 0; // ready to go back to start position if ( VectorLength( vec ) > 64 ) { if ( cs->movestate != MS_CROUCH ) { cs->movestate = MS_WALK; } cs->movestateType = MSTYPE_TEMPORARY; moveresult = AICast_MoveToPos( cs, cs->startOrigin, -1 ); //if the movement failed if ( !moveresult || moveresult->failure || moveresult->blocked ) { //reset the avoid reach, otherwise bot is stuck in current area trap_BotResetAvoidReach( bs->ms ); // couldn't get there, so stop trying to get there cs->bs->enemy = -1; return AIFunc_IdleStart( cs ); } // stay looking at them for a bit after starting to walk back if ( cs->inspectBodyTime + 750 > level.time ) { // look down at them VectorSubtract( enemyOrg, cs->bs->origin, destorg ); destorg[2] -= 20; VectorNormalize( destorg ); vectoangles( destorg, cs->bs->ideal_viewangles ); } } else { cs->attackSNDtime = level.time; cs->bs->enemy = -1; return AIFunc_IdleStart( cs ); } } // return NULL; } /* ============ AIFunc_InspectBodyStart() ============ */ char *AIFunc_InspectBodyStart( cast_state_t *cs ) { static int lastInspect; // // if an inspection was already started not long ago, forget it if ( lastInspect <= level.time && lastInspect > level.time - 1000 ) { cs->inspectBodyTime = 1; // go back to start position } else { lastInspect = level.time; cs->inspectBodyTime = -1; } cs->aifunc = AIFunc_InspectBody; return "AIFunc_InspectBody"; } /* ============ AIFunc_GrenadeKick() ============ */ char *AIFunc_GrenadeKick( cast_state_t *cs ) { bot_state_t *bs; vec3_t destorg, vec; float dist, speed; int enemies[MAX_CLIENTS], numEnemies, i; qboolean shouldAttack; int *ammo; gentity_t *danger; gentity_t *ent; vec3_t end; trace_t tr; vec3_t dir; int weapon; // !!! NOTE: the only way control should pass out of here, is by calling AIFunc_DefaultStart() ent = &g_entities[cs->entityNum]; danger = &g_entities[cs->dangerEntity]; weapon = cs->grenadeKickWeapon; // just to be sure, give us the grenade launcher //ent->client->ps.weapons |= (1 << weapon); // // NOTE: ignore all danger, since we are trying to solve the situation anyway // // we need to move towards it bs = cs->bs; // // are we throwing it back? if ( cs->grenadeFlushFiring ) { // wait until the animation is done if ( !ent->client->ps.legsTimer ) { return AIFunc_DefaultStart( cs ); } // wait till its finished return NULL; /* cs->bs->weaponnum = weapon; // select grenade launcher // if (cs->weaponFireTimes[weapon] < cs->thinkFuncChangeTime) { if (!cs->bs->cur_ps.grenadeTimeLeft) { // hold fire button down AICast_AimAtEnemy( cs ); trap_EA_Attack(bs->client); bs->flags |= BFL_ATTACKED; } // return NULL; } // the grenade has been released! // // modify the explode time g_entities[ent->grenadeFired].nextthink = ent->grenadeExplodeTime; if (g_entities[ent->grenadeFired].nextthink < level.time + 200) { // cut them some slack g_entities[ent->grenadeFired].nextthink = level.time + 200 + rand()%500; } // make sure no-one tries to throw this back again (hot potatoe syndrome) g_entities[ent->grenadeFired].flags |= FL_AI_GRENADE_KICK; // cs->grenadeFlushFiring = qfalse; cs->lockViewAnglesTime = -1; cs->startGrenadeFlushTime = level.time + 2000 + rand()%2000; // dont throw one again for a bit level.lastGrenadeKick = level.time; return AIFunc_DefaultStart( cs ); */ } // /* // have we caught the grenade? if (!(ent->flags & FL_AI_GRENADE_KICK)) { // select grenades cs->bs->weaponnum = weapon; // select grenade launcher AICast_AimAtEnemy( cs ); // hold fire trap_EA_Attack(bs->client); bs->flags |= BFL_ATTACKED; cs->grenadeFlushFiring = qtrue; // return NULL; } */ // // is it about to explode in our face? if ( level.time > danger->nextthink - (int)( 2.0 * VectorDistance( cs->bs->origin, danger->r.currentOrigin ) ) ) { // abort!! if ( !AICast_GetTakeCoverPos( cs, cs->dangerEntity, cs->dangerEntityPos, cs->takeCoverPos ) ) { // shit?? } // go to a position that cannot be seen from the dangerPos cs->takeCoverTime = danger->nextthink + 1000; cs->bs->attackcrouch_time = 0; level.lastGrenadeKick = level.time; return AIFunc_AvoidDangerStart( cs ); } // /* // are we close enough to start crouching? if (danger->s.pos.trDelta[2] < 40 && VectorDistance( danger->r.currentOrigin, cs->bs->origin ) < 48 && (danger->r.currentOrigin[2] < cs->bs->origin[2]) && VectorLength(danger->s.pos.trDelta) < 40) { // crouch to pick it up cs->bs->attackcrouch_time = trap_AAS_Time() + 0.3; } */ cs->bs->attackcrouch_time = 0; // animation is played from standing start // // are we close enough to pick it up? if ( /*cs->grenadeGrabFlag <= 0 || */ ( danger->s.pos.trDelta[2] < 20 && VectorDistance( danger->r.currentOrigin, cs->bs->origin ) < 48 && ( danger->r.currentOrigin[2] < cs->bs->origin[2] ) && VectorLength( danger->s.pos.trDelta ) < 50 ) ) { // // we have a choice here, either pick up and return, or just kick it // if ((cs->grenadeGrabFlag == -1) || (cs->grenadeGrabFlag == qtrue && level.time > danger->nextthink - 2000)) { // kick // play the kick anim if ( cs->grenadeGrabFlag == qtrue ) { AICast_AimAtEnemy( cs ); // play the kick anim BG_AnimScriptEvent( &ent->client->ps, ANIM_ET_KICKGRENADE, qfalse, qtrue ); cs->grenadeGrabFlag = -1; // stop the grenade from moving away danger->s.pos.trDelta[0] = 0; danger->s.pos.trDelta[1] = 0; if ( danger->s.pos.trDelta[2] > 0 ) { danger->s.pos.trDelta[2] = 0; } } else if ( ent->client->ps.legsTimer < 800 ) { // send the grenade on its way cs->grenadeFlushFiring = qtrue; AngleVectors( cs->bs->viewangles, dir, NULL, NULL ); dir[2] = 0.4; VectorNormalize( dir ); speed = 400; if ( cs->bs->enemy >= 0 ) { speed = 1.5 * VectorDistance( danger->r.currentOrigin, g_entities[cs->bs->enemy].r.currentOrigin ); if ( speed > 650 ) { speed = 650; } } VectorScale( dir, speed, danger->s.pos.trDelta ); danger->r.ownerNum = ent->s.number; // we are now the owner, let it pass through us danger->s.pos.trTime = level.time - 50; // move a bit on the very first frame VectorCopy( danger->r.currentOrigin, danger->s.pos.trBase ); danger->s.pos.trType = TR_GRAVITY; SnapVector( danger->s.pos.trDelta ); // save net bandwidth } /* } else { // throw if (cs->grenadeGrabFlag == qtrue) { AICast_AimAtEnemy( cs ); // play the pickup anim BG_AnimScriptEvent( &ent->client->ps, ANIM_ET_PICKUPGRENADE, qfalse, qtrue ); cs->grenadeGrabFlag = qfalse; // stop the grenade from moving away danger->s.pos.trDelta[0] = 0; danger->s.pos.trDelta[1] = 0; if (danger->s.pos.trDelta[2] > 0) { danger->s.pos.trDelta[2] = 0; } } else if (ent->client->ps.legsTimer < 400) { // send the grenade on its way cs->grenadeFlushFiring = qtrue; AngleVectors( cs->bs->viewangles, dir, NULL, NULL ); dir[2] = 0.4; VectorNormalize( dir ); speed = 500; if (cs->bs->enemy >= 0) { speed = 2*VectorDistance(danger->r.currentOrigin, g_entities[cs->bs->enemy].r.currentOrigin); if (speed > 650) speed = 650; } VectorScale( dir, speed, danger->s.pos.trDelta ); trap_LinkEntity( danger ); } else if (ent->client->ps.legsTimer < 800) { // stop showing the grenade trap_UnlinkEntity( danger ); } } */ // return NULL; } // cs->grenadeGrabFlag = qtrue; // we must play the anim before we can grab it // // is the danger gone? if ( level.time > cs->dangerEntityValidTime || !danger->inuse ) { return AIFunc_DefaultStart( cs ); } // // update the predicted position of the grenade if ( G_PredictMissile( danger, danger->nextthink - level.time, cs->takeCoverPos, qfalse ) ) { // make sure it's a valid position, and drop it down to the ground cs->takeCoverPos[2] += -ent->r.mins[2] + 8; VectorCopy( cs->takeCoverPos, end ); end[2] -= 80; trap_Trace( &tr, cs->takeCoverPos, ent->r.mins, ent->r.maxs, end, cs->entityNum, MASK_SOLID ); if ( tr.startsolid ) { // not a valid position, abort level.lastGrenadeKick = level.time; return AIFunc_DefaultStart( cs ); } VectorCopy( tr.endpos, cs->takeCoverPos ); } else { // prediction failed, so use current position VectorCopy( danger->r.currentOrigin, cs->takeCoverPos ); cs->takeCoverPos[2] += 16; // lift it off the floor } VectorCopy( cs->takeCoverPos, destorg ); VectorSubtract( destorg, cs->bs->origin, vec ); //vec[2] *= 0.2; dist = VectorLength( vec ); // // look for things we should attack // if we are out of ammo, we shouldn't bother trying to attack ammo = cs->bs->cur_ps.ammo; shouldAttack = qfalse; numEnemies = 0; if ( AICast_GotEnoughAmmoForWeapon( cs, cs->bs->weaponnum ) ) { numEnemies = AICast_ScanForEnemies( cs, enemies ); if ( numEnemies == -1 ) { // query mode return NULL; } if ( numEnemies == -2 ) { // inspection may be required char *retval; // TTimo gcc: suggest parentheses around assignment used as truth value if ( ( retval = AIFunc_InspectFriendlyStart( cs, enemies[0] ) ) ) { return retval; } } if ( numEnemies > 0 ) { // default to the first known enemy, overwrite if we find a clearer shot cs->bs->enemy = enemies[0]; // for ( i = 0; i < numEnemies; i++ ) { if ( AICast_CheckAttack( cs, enemies[i], qfalse ) || AICast_CheckAttack( AICast_GetCastState( enemies[i] ), cs->entityNum, qfalse ) ) { cs->bs->enemy = enemies[i]; shouldAttack = qtrue; break; } else if ( cs->bs->enemy < 0 ) { cs->lastEnemy = enemies[i]; } } } } // // are we close enough to the goal? if ( dist > 12 ) { // not close enough // moveresult = AICast_MoveToPos( cs, destorg, -1 ); if ( moveresult ) { //if the movement failed if ( moveresult->failure ) { //reset the avoid reach, otherwise bot is stuck in current area trap_BotResetAvoidReach( bs->ms ); // couldn't get there, so stop trying to get there level.lastGrenadeKick = level.time; return AIFunc_DefaultStart( cs ); } // if ( moveresult->blocked ) { // abort if we get blocked at any point level.lastGrenadeKick = level.time; return AIFunc_DefaultStart( cs ); } } // we should slow down on approaching it cs->speedScale = AICast_SpeedScaleForDistance( cs, dist, 0 ); } // // if we are close, put our weapon away and get ready to catch it if ( VectorDistance( danger->r.currentOrigin, cs->bs->origin ) < 128 ) { // put weapon away, select grenades // FIXME: does this fail if we don't have any grenades? cs->bs->weaponnum = weapon; // select grenade launcher shouldAttack = qfalse; // don't attack until we've caught it } // if we should be attacking something on our way if ( shouldAttack ) { vec3_t vec, dir; //attack the enemy if possible AICast_ProcessAttack( cs ); // // if they are close, and we're heading for them, we should abort this manouver VectorSubtract( g_entities[bs->enemy].client->ps.origin, bs->origin, vec ); if ( VectorNormalize( vec ) < 64 ) { if ( VectorNormalize2( bs->velocity, dir ) > 20 ) { // we are moving if ( DotProduct( dir, vec ) > 0 ) { // abort level.lastGrenadeKick = level.time; return AIFunc_DefaultStart( cs ); } } } } else { // face the direction that the grenade is coming VectorSubtract( danger->r.currentOrigin, cs->bs->origin, dir ); dir[2] = 0; VectorNormalize( dir ); vectoangles( dir, cs->bs->ideal_viewangles ); } return NULL; } /* ============= AIFunc_GrenadeKickStart() ============= */ char *AIFunc_GrenadeKickStart( cast_state_t *cs ) { gentity_t *danger; gentity_t *ent; //gentity_t *trav; //int numFriends, i; //G_Printf( "Excuse me, you dropped something\n" ); ent = &g_entities[cs->entityNum]; danger = &g_entities[cs->dangerEntity]; // should we dive onto the grenade? /* if (danger->s.pos.trDelta[2] < 30) { // count the number of friends near us numFriends = 0; for (i=0, trav=g_entities; iinuse) continue; if (trav->aiInactive) continue; if (trav->health <= 0) continue; if (!AICast_SameTeam( cs, i )) continue; if (VectorDistance( cs->takeCoverPos, trav->r.currentOrigin ) > 200) continue; numFriends++; } // if there are enough friends around, and we have a clear path to the position, sacrifice ourself! if (numFriends > 2) { trace_t tr; trap_Trace( &tr, cs->bs->origin, ent->r.mins, ent->r.maxs, cs->takeCoverPos, cs->entityNum, MASK_SOLID ); if (tr.fraction == 1.0 && !tr.startsolid) { return AIFunc_GrenadeDiveStart( cs ); } } } */ // // we have decided to kick or throw the grenade away cs->grenadeKickWeapon = danger->s.weapon; cs->grenadeFlushFiring = qfalse; cs->aifunc = AIFunc_GrenadeKick; return "AIFunc_GrenadeKick"; } /* ============ AIFunc_Battle() ============ */ char *AIFunc_Battle( cast_state_t *cs ) { bot_moveresult_t moveresult; int tfl; bot_state_t *bs; gentity_t *ent, *enemy; ent = &g_entities[cs->entityNum]; enemy = &g_entities[cs->bs->enemy]; // if we are not in combat mode, then go there now! if ( cs->aiState < AISTATE_COMBAT ) { AICast_StateChange( cs, AISTATE_COMBAT ); // just go straight to combat mode } // // do we need to avoid a danger? if ( cs->dangerEntityValidTime >= level.time ) { if ( !AICast_GetTakeCoverPos( cs, cs->dangerEntity, cs->dangerEntityPos, cs->takeCoverPos ) ) { // shit?? } // go to a position that cannot be seen from the dangerPos cs->takeCoverTime = cs->dangerEntityValidTime + 1000; cs->bs->attackcrouch_time = 0; return AIFunc_AvoidDangerStart( cs ); } // // are we waiting for a door? if ( cs->doorMarkerTime > level.time - 100 ) { return AIFunc_DoorMarkerStart( cs, cs->doorMarkerDoor, cs->doorMarkerNum ); } // // do we need to go to our leader? if ( cs->leaderNum >= 0 && Distance( cs->bs->origin, g_entities[cs->leaderNum].r.currentOrigin ) > MAX_LEADER_DIST ) { return AIFunc_ChaseGoalStart( cs, cs->leaderNum, AICAST_LEADERDIST_MAX, qtrue ); } bs = cs->bs; //if no enemy if ( bs->enemy < 0 ) { // go back to whatever our default action is return AIFunc_DefaultStart( cs ); } // if ( enemy->health <= 0 ) { // go back to whatever our default action is if ( g_entities[cs->entityNum].aiTeam == AITEAM_NAZI ) { return AIFunc_InspectBodyStart( cs ); } else { return AIFunc_DefaultStart( cs ); } } // // if we are crouching, don't stay down for too long after we finish fighting if ( cs->aiFlags & AIFL_ATTACK_CROUCH ) { bs->attackcrouch_time = trap_AAS_Time() + 1; } else { bs->attackcrouch_time = 0; // only set it if we need it } // // if the enemy is no longer visible if ( ( cs->bs->cur_ps.weaponTime < 100 ) // if reloading, don't chase until ready && ( cs->castScriptStatus.scriptNoMoveTime < level.time ) && ( !AICast_EntityVisible( cs, bs->enemy, qtrue ) || !AICast_CheckAttack( cs, bs->enemy, qfalse ) ) ) { // if we are heading for a combatGoal, give us some time to get there if ( cs->combatGoalTime > level.time ) { if ( cs->combatGoalTime > level.time + 3000 ) { cs->combatGoalTime = level.time + 2000 + rand() % 1000; cs->combatSpotDelayTime = level.time + 4000 + rand() % 3000; } } else if ( cs->leaderNum >= 0 ) { // chase them, nothing else to do return AIFunc_BattleChaseStart( cs ); } else // if we weren't moving, it is likely they have dodged back behind something, ready to duck out and take another // shot. so, we could fool them by hiding from the position we last saw them from, in the hope that when they // return to fire at us, we won't be in their sight. if ( cs->attributes[TACTICAL] > 0.3 && cs->attributes[AGGRESSION] < 1.0 && cs->attributes[AGGRESSION] < ( random() + 0.5 * cs->attributes[TACTICAL] ) && ( cs->takeCoverTime < level.time ) && AICast_GetTakeCoverPos( cs, cs->bs->enemy, cs->vislist[cs->bs->enemy].real_visible_pos, cs->takeCoverPos ) ) { // start taking cover cs->takeCoverTime = level.time + 2000 + rand() % 4000; // only move a little bit //cs->bs->attackcrouch_time = 0; // get out of here real quick return AIFunc_BattleTakeCoverStart( cs ); } else // if we haven't thrown a grenade in a bit, go into "grenade flush mode" if ( ( lastGrenadeFlush > level.time || lastGrenadeFlush < level.time - 7000 ) && ( cs->castScriptStatus.castScriptEventIndex < 0 ) && ( ( ( COM_BitCheck( cs->bs->cur_ps.weapons, WP_GRENADE_LAUNCHER ) ) && ( AICast_GotEnoughAmmoForWeapon( cs, WP_GRENADE_LAUNCHER ) ) && ( cs->weaponFireTimes[WP_GRENADE_LAUNCHER] < level.time - (int)( aicast_skillscale * 3000 ) ) ) || ( ( COM_BitCheck( cs->bs->cur_ps.weapons, WP_GRENADE_PINEAPPLE ) ) && ( AICast_GotEnoughAmmoForWeapon( cs, WP_GRENADE_PINEAPPLE ) ) && ( cs->weaponFireTimes[WP_GRENADE_PINEAPPLE] < level.time - (int)( aicast_skillscale * 3000 ) ) ) ) && !( cs->bs->weaponnum && ( cs->castScriptStatus.scriptFlags & SFL_NOCHANGEWEAPON ) ) && ( Distance( cs->bs->origin, cs->vislist[cs->bs->enemy].real_visible_pos ) > 100 ) && ( Distance( cs->bs->origin, cs->vislist[cs->bs->enemy].real_visible_pos ) < 1200 ) && ( AICast_WantsToChase( cs ) ) ) { // try and flush them out with a grenade //G_Printf("get outta there..\n"); return AIFunc_GrenadeFlushStart( cs ); } else // not visible, should we chase them? if ( AICast_WantsToChase( cs ) ) { // chase them return AIFunc_BattleChaseStart( cs ); } else // Take Cover? if ( AICast_WantsToTakeCover( cs, qfalse ) && ( cs->takeCoverTime < level.time ) && AICast_GetTakeCoverPos( cs, cs->bs->enemy, cs->vislist[cs->bs->enemy].real_visible_pos, cs->takeCoverPos ) ) { // go to a position that cannot be seen from the last place we saw the enemy, and wait there for some time cs->takeCoverTime = level.time + 4000 + rand() % 2000; //cs->bs->attackcrouch_time = 0; return AIFunc_BattleTakeCoverStart( cs ); } else { // chase them, nothing else to do return AIFunc_BattleChaseStart( cs ); } } // if we are obstructing someone else, move out the way if ( cs->obstructingTime > level.time ) { // setup a combatgoal in the obstructionYaw direction //cs->combatGoalTime = level.time + 10; //VectorCopy( cs->obstructingPos, cs->combatGoalOrigin ); AICast_MoveToPos( cs, cs->obstructingPos, -1 ); // if not crouching, walk instead of running cs->speedScale = cs->attributes[WALKING_SPEED] / cs->attributes[RUNNING_SPEED]; } // if the enemy is really close, avoid them else if ( ( cs->obstructingTime < ( level.time - 500 + rand() % 300 ) ) && ( Distance( cs->bs->origin, cs->vislist[cs->bs->enemy].real_visible_pos ) < 100 ) ) { if ( AICast_GetAvoid( cs, NULL, cs->obstructingPos, qtrue, cs->bs->enemy ) ) { cs->obstructingTime = level.time + 500; } else { cs->obstructingTime = level.time - 1; // wait a bit before trying again } } // // setup for the fight // tfl = cs->travelflags; //if in lava or slime the bot should be able to get out if ( BotInLava( bs ) ) { tfl |= TFL_LAVA; } if ( BotInSlime( bs ) ) { tfl |= TFL_SLIME; } // /* moveresult = AICast_CombatMove(cs, tfl); //if the movement failed if (moveresult.failure) { //reset the avoid reach, otherwise bot is stuck in current area trap_BotResetAvoidReach(bs->ms); // reset the combatgoal cs->combatGoalTime = 0; } else if (cs->combatGoalTime > level.time && VectorLength(cs->bs->cur_ps.velocity)) { // crouch if moving? if (cs->attributes[ATTACK_CROUCH] > 0.1) { AICast_RequestCrouchAttack( cs, cs->bs->origin, 0.5 ); } } */ // AICast_Blocked( cs, &moveresult, qfalse, NULL ); // // Retreat? if ( AICast_WantToRetreat( cs ) ) { if ( AICast_GetTakeCoverPos( cs, cs->bs->enemy, cs->vislist[cs->bs->enemy].visible_pos, cs->takeCoverPos ) ) { // go to a position that cannot be seen from the last place we saw the enemy, and wait there for some time cs->takeCoverTime = level.time + 2000 + rand() % 3000; return AIFunc_BattleTakeCoverStart( cs ); } } // // Lob a Grenade? // if we haven't thrown a grenade in a bit, go into "grenade flush mode" if ( ( lastGrenadeFlush > level.time || lastGrenadeFlush < level.time - 7000 ) && ( cs->castScriptStatus.castScriptEventIndex < 0 ) && ( cs->startGrenadeFlushTime < level.time - 3000 ) && ( COM_BitCheck( cs->bs->cur_ps.weapons, WP_GRENADE_LAUNCHER ) ) && ( AICast_GotEnoughAmmoForWeapon( cs, WP_GRENADE_LAUNCHER ) ) && ( cs->weaponFireTimes[WP_GRENADE_LAUNCHER] < level.time - (int)( aicast_skillscale * 3000 ) ) && ( ( cs->bs->weaponnum == WP_GRENADE_LAUNCHER ) || !( cs->castScriptStatus.scriptFlags & SFL_NOCHANGEWEAPON ) ) && ( Distance( cs->bs->origin, cs->vislist[cs->bs->enemy].real_visible_pos ) > 100 ) && ( Distance( cs->bs->origin, cs->vislist[cs->bs->enemy].real_visible_pos ) < 2000 ) ) { // try and flush them out with a grenade //G_Printf("pineapple?\n"); return AIFunc_GrenadeFlushStart( cs ); } // // Dodge enemy aim? if ( ( cs->attributes[AGGRESSION] < 1.0 ) && ( ent->client->ps.weapon ) && ( ent->client->ps.groundEntityNum == ENTITYNUM_WORLD ) && ( !cs->lastRollMove || cs->lastRollMove < level.time - 4000 ) && ( cs->attributes[TACTICAL] > 0.5 ) && ( cs->aiFlags & AIFL_ROLL_ANIM ) && ( VectorLength( cs->bs->cur_ps.velocity ) < 1 ) ) { vec3_t aim, enemyVec, right; // are they aiming at us? AngleVectors( enemy->client->ps.viewangles, aim, right, NULL ); VectorSubtract( cs->bs->origin, enemy->r.currentOrigin, enemyVec ); VectorNormalize( enemyVec ); // if they are looking at us, we should avoid them if ( DotProduct( aim, enemyVec ) > 0.97 ) { aicast_predictmove_t move; vec3_t dir; bot_input_t bi, bi_back; usercmd_t ucmd; float simTime = 0.8; cs->lastRollMove = level.time; trap_EA_GetInput( cs->entityNum, (float) level.time / 1000, &bi_back ); trap_EA_ResetInput( cs->entityNum, NULL ); if ( level.time % 200 < 100 ) { VectorNegate( right, dir ); } else { VectorCopy( right, dir );} trap_EA_Move( cs->entityNum, dir, 400 ); trap_EA_GetInput( cs->entityNum, (float) level.time / 1000, &bi ); AICast_InputToUserCommand( cs, &bi, &ucmd, bs->cur_ps.delta_angles ); AICast_PredictMovement( cs, 4, simTime / 4, &move, &ucmd, bs->enemy ); trap_EA_ResetInput( cs->entityNum, &bi_back ); if ( move.groundEntityNum == ENTITYNUM_WORLD && VectorDistance( move.endpos, cs->bs->origin ) > simTime * cs->attributes[RUNNING_SPEED] * 0.8 ) { // good enough if ( AICast_CheckAttackAtPos( cs->entityNum, cs->bs->enemy, move.endpos, cs->bs->cur_ps.viewheight == cs->bs->cur_ps.crouchViewHeight, qfalse ) ) { return AIFunc_BattleRollStart( cs, dir ); } } } } // // reload? if ( ( cs->bs->cur_ps.ammoclip[BG_FindClipForWeapon( cs->bs->cur_ps.weapon )] < (int)( ammoTable[cs->bs->cur_ps.weapon].uses ) ) ) { if ( AICast_GotEnoughAmmoForWeapon( cs, cs->bs->weaponnum ) ) { trap_EA_Reload( cs->entityNum ); } else { // no ammo, switch? AICast_ChooseWeapon( cs, qfalse ); if ( !AICast_GotEnoughAmmoForWeapon( cs, cs->bs->weaponnum ) ) { // no ammo, get out of here return AIFunc_DefaultStart( cs ); } } } else { //attack the enemy if possible AICast_ProcessAttack( cs ); } // return NULL; } /* ============ AIFunc_BattleStart() ============ */ char *AIFunc_BattleStart( cast_state_t *cs ) { char *rval; int lastweap; // make sure we don't avoid any areas when we start again trap_BotInitAvoidReach( cs->bs->ms ); // wait some time before taking cover again cs->takeCoverTime = level.time + 300 + rand() % ( 2000 + (int)( 2000.0 * cs->attributes[AGGRESSION] ) ); // wait some time before going to a combat spot cs->combatSpotDelayTime = level.time + 1500 + rand() % 2500; // // start a crouch attack? if ( ( random() * 3.0 + 1.0 < cs->attributes[ATTACK_CROUCH] ) && AICast_RequestCrouchAttack( cs, cs->bs->origin, 0.0 ) ) { cs->aiFlags |= AIFL_ATTACK_CROUCH; } else { cs->bs->attackcrouch_time = 0; cs->aiFlags &= ~AIFL_ATTACK_CROUCH; } // cs->lastEnemy = cs->bs->enemy; cs->startAttackCount++; cs->crouchHideFlag = qfalse; // // get out of talking state cs->aiFlags &= ~AIFL_TALKING; // //update the attack inventory values AICast_UpdateBattleInventory( cs, cs->bs->enemy ); // // if we have a special attack, call the correct AI routine recheck: rval = NULL; // ignore special attacks until we are facing our enemy if ( fabs( AngleDifference( cs->bs->ideal_viewangles[YAW], cs->bs->viewangles[YAW] ) ) < 10 ) { // select a weapon AICast_ChooseWeapon( cs, qtrue ); // if ( ( cs->bs->weaponnum == WP_MONSTER_ATTACK1 ) && cs->aifuncAttack1 ) { if ( AICast_CheckAttack( cs, cs->bs->enemy, qfalse ) ) { rval = cs->aifuncAttack1( cs ); } else { rval = AIFunc_BattleChaseStart( cs ); } } else if ( ( cs->bs->weaponnum == WP_MONSTER_ATTACK2 ) && cs->aifuncAttack2 ) { if ( AICast_CheckAttack( cs, cs->bs->enemy, qfalse ) ) { rval = cs->aifuncAttack2( cs ); } else { rval = AIFunc_BattleChaseStart( cs ); } } else if ( ( cs->bs->weaponnum == WP_MONSTER_ATTACK3 ) && cs->aifuncAttack3 ) { if ( AICast_CheckAttack( cs, cs->bs->enemy, qfalse ) ) { rval = cs->aifuncAttack3( cs ); } else { rval = AIFunc_BattleChaseStart( cs ); } } // if ( !rval && cs->bs->weaponnum >= WP_MONSTER_ATTACK1 && cs->bs->weaponnum <= WP_MONSTER_ATTACK3 ) { // don't use this weapon again for a while cs->weaponFireTimes[cs->bs->weaponnum] = level.time; // select a different weapon lastweap = cs->bs->weaponnum; AICast_ChooseWeapon( cs, qfalse ); // qfalse so we don't choose a special weapon if ( cs->bs->weaponnum == lastweap ) { return NULL; } // try again goto recheck; } } else { // normal weapons // select a weapon AICast_ChooseWeapon( cs, qfalse ); rval = NULL; } // if ( !rval ) { // use the generic battle routine for all "normal" weapons if ( cs->bs->weaponnum >= WP_MONSTER_ATTACK1 && cs->bs->weaponnum <= WP_MONSTER_ATTACK3 ) { // monster attacks are not allowed to go into the normal battle mode return NULL; } else { cs->aifunc = AIFunc_Battle; return "AIFunc_Battle"; } } // // we decided to start a special monster attack return rval; } /* ============ AIFunc_DefaultStart() ============ */ char *AIFunc_DefaultStart( cast_state_t *cs ) { qboolean first = qfalse; char *rval = NULL; // if ( cs->aiFlags & AIFL_JUST_SPAWNED ) { first = qtrue; cs->aiFlags &= ~AIFL_JUST_SPAWNED; } // switch ( cs->aiCharacter ) { case AICHAR_FEMZOMBIE: if ( first ) { return AIFunc_FZombie_IdleStart( cs ); } break; case AICHAR_ZOMBIE: // portal zombie, requires spawning effect if ( first && ( g_entities[cs->entityNum].spawnflags & 4 ) ) { return AIFunc_FlameZombie_PortalStart( cs ); } break; case AICHAR_HELGA: if ( first ) { return AIFunc_Helga_IdleStart( cs ); } break; } // // if they have an enemy, then pursue if ( cs->bs->enemy >= 0 ) { rval = AIFunc_BattleStart( cs ); } // if ( !rval ) { return AIFunc_IdleStart( cs ); } // return rval; }