//===========================================================================
//
// Name:			ai_cast_fight.c
// Function:		Wolfenstein AI Character Fighting/Combat
// Programmer:		Ridah
// Tab Size:		4 (real tabs)
//===========================================================================

#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 "../game/be_ai_weap.h"
#include "../botai/botai.h"			//bot ai interface

#include "ai_cast.h"

/*
Support routines for the Decision Making layer.
*/

// FIXME: go through here and convert all weapon/character parameters to #define's
// and move them to a seperate header file for easy modification

/*
=================
AICast_StateChange

  returns qfalse if scripting has denied the action
=================
*/
qboolean AICast_StateChange( cast_state_t *cs, aistateEnum_t newaistate )
{
	gentity_t *ent;
	int result, scriptIndex;
	aistateEnum_t oldstate;

	ent = &g_entities[cs->entityNum];

	oldstate = cs->aiState;
	cs->aiState = newaistate;

	// if moving from query mode, kill the anim and pausetime
	if (oldstate == AISTATE_QUERY) {
		// stop playing the animation
		ent->client->ps.torsoTimer = 0;
		ent->client->ps.legsTimer = 0;
		cs->pauseTime = 0;
	}

	// if moving to combat mode, default back to normal movetype (fast)
	if (newaistate == AISTATE_COMBAT) {
		cs->movestate = MS_DEFAULT;
		cs->movestateType = MSTYPE_NONE;
	}

	scriptIndex = cs->scriptCallIndex;

	// check scripting to see if this event should be ignored (no anim or handling)
	cs->aiFlags &= ~AIFL_DENYACTION;
	AICast_ScriptEvent( cs, "statechange", va("%s %s", animStateStr[oldstate].string, animStateStr[newaistate].string ) );

	if (!(cs->aiFlags & AIFL_DENYACTION)) {
		// if no script was found, try enemysight
		if (newaistate == AISTATE_COMBAT && cs->scriptCallIndex == scriptIndex) {	// no script was found, so default back to enemysight
			AICast_ScriptEvent( cs, "enemysight", g_entities[cs->bs->enemy].aiName );
			if (!(cs->aiFlags & AIFL_DENYACTION)) {
				G_AddEvent( ent, EV_GENERAL_SOUND, G_SoundIndex( aiDefaults[ent->aiCharacter].sightSoundScript ) );
			}

			if (cs->aiFlags & AIFL_DENYACTION) {
				// don't run any dynamic handling or default anims
				return qfalse;
			}
		}

		// look for an animation
		result = BG_AnimScriptStateChange( &ent->client->ps, newaistate, oldstate );

		if (result > 0) {
			// pause while the animation plays
			cs->pauseTime = level.time + result;
		}
	}

	// set query mode fields
	if (newaistate == AISTATE_QUERY) {
		cs->queryStartTime = level.time;
		if (cs->queryCountValidTime < level.time) {
			cs->queryCount = 0;
		} else {
			cs->queryCount++;
		}
		cs->queryCountValidTime = level.time + 60000;	// one minute
		switch (cs->queryCount) {
		case 0:
			cs->queryAlertSightTime = level.time + 1000;
			break;
		case 1:
			cs->queryAlertSightTime = level.time + 500;
			break;
		default:
			cs->queryAlertSightTime = -1;	// IMMEDIATE COMBAT MODE
			break;
		}
	}

	return qtrue;
}

/*
=================
AICast_ScanForEnemies

  returns the number of enemies visible, filling the "enemies" list before exiting

  if we only found queryEnemies (possibly hostile, but not sure) while relaxed,
  then a negative count is returned
=================
*/
int AICast_ScanForEnemies( cast_state_t *cs, int *enemies )
{
	int i, j, enemyCount, queryCount, friendlyAlertCount;
	static float distances[MAX_CLIENTS];
	static int sortedEnemies[MAX_CLIENTS];
	float	lastDist;
	int		best, oldEnemy, oldPauseTime;
	cast_state_t *ocs;

	if (cs->castScriptStatus.scriptAttackEnt >= 0) {
		if (g_entities[cs->castScriptStatus.scriptAttackEnt].health <= 0) {
			cs->castScriptStatus.scriptAttackEnt = -1;
		} else {
			// if we are not in combat mode, then an enemy should trigger a state change straight to combat mode
			if (cs->aiState < AISTATE_COMBAT) {
				AICast_StateChange( cs, AISTATE_COMBAT );	// just go straight to combat mode
			}
			enemies[0] = cs->castScriptStatus.scriptAttackEnt;
			return 1;
		}
	}

	if (cs->castScriptStatus.scriptNoAttackTime >= level.time) {
		return qfalse;
	}

	if (cs->noAttackTime >= level.time) {
		return qfalse;
	}

	if (cs->castScriptStatus.scriptNoSightTime >= level.time) {
		return qfalse;
	}

	enemyCount = 0;
	queryCount = 0;
	friendlyAlertCount = 0;

	// while we're here, may as well check for some baddies
	for (i=0; i<g_maxclients.integer; i++)
	{
		if (g_entities[i].inuse)
		{
			// try not to commit suicide
			if (i != cs->bs->entitynum)
			{
				if (AICast_EntityVisible( cs, i, qfalse ))
				{
					// how should we deal with them?
					if ((g_entities[i].health > 0) && AICast_HostileEnemy( cs, i ))
					{	// visible and a baddy!
						enemies[enemyCount] = i;
						enemyCount++;
						queryCount = 0;
						friendlyAlertCount = 0;
					} else if (!enemyCount && (g_entities[i].health > 0) && AICast_QueryEnemy( cs, i ) && (cs->vislist[i].flags & AIVIS_PROCESS_SIGHTING)) {
						enemies[queryCount] = i;
						queryCount++;
						friendlyAlertCount = 0;
					} else if (!queryCount && !enemyCount && (cs->vislist[i].flags & AIVIS_INSPECT)) {
						enemies[friendlyAlertCount] = i;
						friendlyAlertCount++;
					}
					// the sighting has been processed
					cs->vislist[i].flags &= ~AIVIS_PROCESS_SIGHTING;
				}
			}
		}
	}

	if (!enemyCount) {
		if (queryCount)
			enemyCount = queryCount;
		else if (friendlyAlertCount)
			enemyCount = friendlyAlertCount;
	}

	if (!enemyCount) {	// nothing worth doing anything about
		// look for audible events that we should investigate
		if (cs->audibleEventTime && cs->audibleEventTime < level.time && cs->audibleEventTime > level.time - 2000) {
			return -4;
		}
		// look for bullet impacts that have occured recently
		if (cs->bulletImpactTime && cs->bulletImpactTime < level.time && cs->bulletImpactTime > level.time - 1000) {
			return -3;
		}
		return 0;
	}

	// sort the enemies by distance
	for (i=0; i<enemyCount; i++) {
		distances[i] = Distance( cs->bs->origin, g_entities[enemies[i]].client->ps.origin );
		if (!distances[i]) {
			G_Printf("WARNING: zero distance between enemies:\n%s at %s, %s at %s\n", g_entities[cs->entityNum].aiName, vtos(cs->bs->origin), g_entities[enemies[i]].aiName, vtos(g_entities[enemies[i]].client->ps.origin) );
			distances[i] = 999998;	// try to ignore them (HACK)
		}
	}
	for (j=0; j<enemyCount; j++) {
		lastDist = 999999;
		best = -1;
		for (i=0; i<enemyCount; i++) {
			if (distances[i] && distances[i] < lastDist) {
				lastDist = distances[i];
				best = i;
			}
		}
		if (best < 0) {
			G_Error("error sorting enemies by distance\n");
		}
		sortedEnemies[j] = enemies[best];
		distances[best] = -1;
	}
	memcpy( enemies, sortedEnemies, sizeof(int)*enemyCount );

	// if we are not in combat mode, then an enemy should trigger a state change straight to combat mode
	if (!queryCount && !friendlyAlertCount && enemyCount && cs->aiState < AISTATE_COMBAT) {
		// if only one enemy, face them while making the transition
		oldEnemy = cs->bs->enemy;
		if (enemyCount == 1) {
			cs->bs->enemy = enemies[0];
			AICast_AimAtEnemy( cs );
			// leave it set so StateChange() can use the name for the script calls
		}
		AICast_StateChange( cs, AISTATE_COMBAT );	// just go straight to combat mode
		cs->bs->enemy = oldEnemy;
	}

	// if we are in relaxed state, and we see a query enemy, then go into query mode
	if (queryCount) {
		if (cs->aiState == AISTATE_RELAXED) {
			// go into query mode
			if (AICast_StateChange( cs, AISTATE_QUERY )) {
				cs->bs->enemy = enemies[0];	// lock onto the closest potential enemy
				return -1;
			}
			return 0;	// scripting obviously doesn't want us to progress from relaxed just yet
		}
		// else ignore the query mode, since we are already above relaxed mode
		return 0;
	}
	if (friendlyAlertCount) {
		// call a script event
		oldPauseTime = cs->scriptPauseTime;
		if (g_entities[enemies[0]].health <= 0) {
			AICast_ForceScriptEvent( cs, "inspectbodystart", g_entities[enemies[0]].aiName );
			if (cs->aiFlags & AIFL_DENYACTION) {
				// ignore this friendly
				cs->vislist[i].flags |= AIVIS_INSPECTED;
				return 0;
			}
		}
		//
		if (cs->aiState < AISTATE_COMBAT) {
			// go into alert mode, and return this entity so we can inspect it or something,
			// but let dynamic AI sort out what it wants to do
			if (cs->aiState == AISTATE_ALERT || AICast_StateChange( cs, AISTATE_ALERT )) {
				// only return the entity if they are in combat mode or dead
				ocs = AICast_GetCastState(enemies[0]);
				if ((g_entities[enemies[0]].health <= 0) || (ocs->aiState >= AISTATE_COMBAT)) {
					return -2;
				}
			}
			return 0;	// scripting failed or they're not worth physically inspecting
		}
		// ignore the friendly, we have our hands full
		return 0;
	}

	// must be hostile enemy(s) found, so return them
	return enemyCount;
}

/*
==================
AICast_EntityVisible
==================
*/
qboolean AICast_EntityVisible( cast_state_t *cs, int enemynum, qboolean directview )
{
	cast_visibility_t *vis;
	int		last_visible;
	int		reactionTime;
	float	dist;

	if (enemynum >= MAX_CLIENTS)
		return qtrue;			// FIXME: do a visibility calculation on non-client entities?

	vis = &cs->vislist[enemynum];

	if (!vis->visible_timestamp && !vis->real_visible_timestamp)
		return qfalse;	// they are not visible at all

	if (directview)
		last_visible = vis->real_visible_timestamp;
	else
		last_visible = vis->visible_timestamp;

	reactionTime = (int)(1000*cs->attributes[REACTION_TIME]);
	if (cs->startAttackCount > 1) {
		// we recently saw them, so we are more "aware" of their presence
		reactionTime /= 2;
	}

	// if they are close, we should react faster
	if (cs->bs && enemynum == cs->bs->enemy) {
		dist = cs->enemyDist;
	} else {
		dist = VectorDistance( g_entities[cs->entityNum].client->ps.origin, cs->vislist[enemynum].visible_pos );
	}
	if (dist < 384) {
		reactionTime *= 0.5 + 0.5*(dist / 384);
	}

	if (vis->notvisible_timestamp < (level.time - reactionTime)) {
		// make sure we've seen them since we've last not seen them (since the visibility checking is spread amongst server frames)
		if (vis->notvisible_timestamp < last_visible)
			return qtrue;
	}

	// we can't directly see them, but if they've just left our sight, pretend we can see them for another second or so

	if (!directview && last_visible) {
		if (vis->notvisible_timestamp > last_visible) {
			if (vis->notvisible_timestamp < (last_visible + 5000))
				return qtrue;
		}
	}
	

	return qfalse;
}

/*
==================
AICast_HostileEnemy

  returns qtrue if the entity is hostile
==================
*/
qboolean AICast_HostileEnemy( cast_state_t *cs, int enemynum )
{
	// if we hate them, they are an enemy
	if (cs->vislist[enemynum].flags & AIVIS_ENEMY)
		return qtrue;
	else
		return qfalse;
}

/*
==================
AICast_QueryEnemy

  returns qtrue if the entity can become hostile (they hurt us or we recognize them)
==================
*/
qboolean AICast_QueryEnemy( cast_state_t *cs, int enemynum )
{

	if (g_entities[cs->entityNum].aiTeam != g_entities[enemynum].aiTeam) {
		if (g_entities[cs->entityNum].aiTeam == AITEAM_MONSTER || g_entities[enemynum].aiTeam == AITEAM_MONSTER) {
			// monsters hate all non-monsters and vice-versa
			return qtrue;
		} else {
			// neutral's can only possibly become hostile if they hurt us, otherwise they are assumed to be harmless
			if (g_entities[cs->entityNum].aiTeam == AITEAM_NEUTRAL || g_entities[enemynum].aiTeam == AITEAM_NEUTRAL) {
				return qfalse;	// one of us is neutral, assume harmless
			}
			return qtrue;
		}
	} else {	// same team
		return qfalse;	// be cool bitch
	}

}

/*
==================
AICast_SameTeam

  the player is always team 1, AI's default to team 0

  MONSTER's hate everyone else except other MONSTER's

  NEUTRAL's are cool with everyone that hasn't hurt them
==================
*/
qboolean AICast_SameTeam( cast_state_t *cs, int enemynum )
{

	if (g_entities[cs->entityNum].aiTeam != g_entities[enemynum].aiTeam) {
		if (g_entities[cs->entityNum].aiTeam == AITEAM_NEUTRAL || g_entities[enemynum].aiTeam == AITEAM_NEUTRAL) {
			// if we hate them, they are an enemy
			if (cs->vislist[enemynum].flags & AIVIS_ENEMY)
				return qfalse;
			else
				return qtrue;
		} else {
			return qfalse;	// they are an enemy
		}
	} else {
		return qtrue;	// be cool bitch
	}

}

/*
==================
AICast_WeaponRange
==================
*/
float AICast_WeaponRange( cast_state_t *cs, int weaponnum )
{
	switch( weaponnum ) {
	case WP_GAUNTLET:
		return 32 - 4;		// to be safe
	case WP_TESLA:
		return (TESLA_RANGE*0.9) - 50;	// allow for bounding box
	case WP_FLAMETHROWER:
		return (FLAMETHROWER_RANGE*0.6) - 50;	// allow for bounding box
	case WP_PANZERFAUST:
	case WP_ROCKET_LAUNCHER:
		return 8000;

	case WP_SPEARGUN:	//----(SA)	
	case WP_SPEARGUN_CO2:
		return 300;

	case WP_GRENADE_LAUNCHER:
	case WP_GRENADE_PINEAPPLE:
		return 800;
	case WP_MONSTER_ATTACK1:
		switch (cs->aiCharacter) {
		case AICHAR_WARZOMBIE:
				return 44;
		case AICHAR_LOPER:	// close attack, head-butt, fist
		case AICHAR_SEALOPER:
			return 60;
		case AICHAR_BLACKGUARD:
			return BLACKGUARD_MELEE_RANGE;
		case AICHAR_STIMSOLDIER3:
			return TESLA_RANGE;
		case AICHAR_ZOMBIE:	// zombie flaming attack
			//return (FLAMETHROWER_RANGE*0.6) - 50;	// allow for bounding box
			return ZOMBIE_FLAME_RADIUS-100;		// get well within range before starting
		case AICHAR_REJECTX:
			return REJECT_MELEE_RANGE;
		}
		break;
	case WP_MONSTER_ATTACK2:
		switch (cs->aiCharacter) {
		case AICHAR_ZOMBIE:	// zombie spirit attack
			return 800;
		case AICHAR_LOPER:	// loper leap attack
			return 256;
		}
		break;
	case WP_MONSTER_ATTACK3:
		switch (cs->aiCharacter) {
		case AICHAR_LOPER:	// loper ground attack
			return LOPER_GROUND_RANGE;
		case AICHAR_WARZOMBIE:	// warzombie defense
			return 2000;
		}
		break;

	// Rafael added these changes as per Mikes request 
	case WP_MAUSER:
	case WP_GARAND:
	case WP_SNIPERRIFLE:
	case WP_SNOOPERSCOPE:
		return 8000;
		break;
	

	}
	// default range
	return 3000;
}

/*
==================
AICast_CheckAttack_real
==================
*/
qboolean AICast_CheckAttack_real(cast_state_t *cs, int enemy, qboolean allowHitWorld) {
	//float points;
	vec3_t forward, right, start, end, dir, up, angles;
	weaponinfo_t wi;
	trace_t trace;
	float	traceDist;
	static vec3_t smins = {-6, -6, -6}, smaxs = {6, 6, 6};
	static vec3_t fmins = {-30, -30, -24}, fmaxs = {30, 30, 24};
	float *mins, *maxs;
	float halfHeight;
	int traceMask;
	int fuzzyCount, i;
	gentity_t *ent, *enemyEnt;
	float	dist;
	int		passEnt;
	int		weapnum;
	//
	if (enemy < 0) return qfalse;
	ent = &g_entities[cs->entityNum];
	enemyEnt = &g_entities[enemy];
	//
	if (cs->bs)
		weapnum = cs->bs->weaponnum;
	else
		weapnum = ent->client->ps.weapon;
	//
	if (!weapnum)
		return qfalse;
	//
	// don't attack while in air (like on a ladder)
	if (!ent->waterlevel && ent->client->ps.groundEntityNum == ENTITYNUM_NONE && !ent->active) {
		// stim is allowed to fire while in air for flying attack
		if (!ent->client->ps.powerups[PW_FLIGHT]) {
			return qfalse;
		}
	}
	//
	if (ent->health <= 0) {
		return qfalse;
	}
	// can't attack without any ammo
	if (cs->bs) {
		if (!AICast_GotEnoughAmmoForWeapon( cs, cs->bs->weaponnum )) {
			return qfalse;
		}
	}
	// special case: femzombie can always use portal lightning attack when available
	if (cs->aiCharacter == AICHAR_FEMZOMBIE && weapnum == WP_MONSTER_ATTACK1) {
		return qtrue;
	}
	// special case: warzombie should play laughing anim at first sight
	if (cs->aiCharacter == AICHAR_WARZOMBIE && weapnum == WP_MONSTER_ATTACK2) {
		return qtrue;
	}
	//
	//if the enemy isn't directly visible
	if (!allowHitWorld && cs->vislist[enemy].real_visible_timestamp != cs->vislist[enemy].real_update_timestamp) {
		return qfalse;
	}
	//
	//get the weapon info (FIXME: hard-code the weapon info?)
	memset( &wi, 0, sizeof(weaponinfo_t) );
	//
	traceMask = MASK_SHOT;	// FIXME: assign mask's to different weapons
	//end point aiming at
	if (!ent->active) {
		//get the start point shooting from
		VectorCopy( enemyEnt->r.currentOrigin, start );
		start[2] += enemyEnt->client->ps.viewheight;
		VectorCopy( ent->r.currentOrigin, end );
		end[2] += ent->client->ps.viewheight;
		VectorSubtract(start, end, dir);
		vectoangles( dir, angles );
		AngleVectors(angles, forward, right, up);
		CalcMuzzlePoint( &g_entities[cs->entityNum], weapnum, forward, right, up, start );

		traceDist = AICast_WeaponRange( cs, weapnum );
		switch( weapnum ) {
		case WP_GAUNTLET:
			mins = NULL;
			maxs = NULL;
			break;
		case WP_DYNAMITE:
		case WP_DYNAMITE2:
		case WP_PANZERFAUST:
		case WP_ROCKET_LAUNCHER:
		case WP_GRENADE_LAUNCHER:
		case WP_GRENADE_PINEAPPLE:
		case WP_PROX:
			traceMask = MASK_MISSILESHOT;
			mins = smins;
			maxs = smaxs;
			break;
		case WP_FLAMETHROWER:
			mins = fmins;
			maxs = fmaxs;
			break;
		default:
			mins = smins;
			maxs = smaxs;
			break;
		}
		passEnt = cs->entityNum;

		// don't try too far
		dist = Distance( ent->r.currentOrigin, enemyEnt->r.currentOrigin );
		if (traceDist > dist) {
			traceDist = dist;
			fuzzyCount = 6;
		} else if (dist > traceDist + 32) {
			return qfalse;
		} else {
			fuzzyCount = 0;
		}
	} else {
		gentity_t *mg42;
		// we are mounted on a weapon
		mg42 = &g_entities[cs->mountedEntity];
		VectorCopy( enemyEnt->r.currentOrigin, start );
		start[2] += enemyEnt->client->ps.viewheight;
		VectorCopy( mg42->r.currentOrigin, end );
		VectorSubtract(start, end, dir);
		vectoangles( dir, angles );
		AngleVectors(angles, forward, right, up);

		VectorCopy (mg42->r.currentOrigin, start);
		VectorMA (start, 16, forward, start);
		VectorMA (start, 16, up, start);
		// snap to integer coordinates for more efficient network bandwidth usage
		SnapVector( start );

		traceDist = 8192;
		mins = NULL;
		maxs = NULL;
		if (mg42->mg42BaseEnt >= 0)
			passEnt = mg42->mg42BaseEnt;
		else
			passEnt = cs->entityNum;

		// don't try too far
		dist = Distance( start, enemyEnt->r.currentOrigin );
		if ((traceDist - dist) > 0) {
			traceDist = dist;
			fuzzyCount = 6;
		} else if ((dist - traceDist) > 32 ) {
			return qfalse;
		} else {
			fuzzyCount = 0;
		}
	}

	for (i=0; i<=fuzzyCount; i++) {
		VectorMA(start, traceDist, forward, end);

		// fuzzy end point
		if (i>0) {
			VectorMA( end, enemyEnt->r.maxs[0] * 0.9 * (float)((i%2)*2 - 1), right, end );
			halfHeight = (enemyEnt->r.maxs[2] - enemyEnt->r.mins[2]) / 2.0;
			end[2] = (enemyEnt->r.currentOrigin[2] + enemyEnt->r.mins[2]) + halfHeight;
			VectorMA( end, halfHeight * 0.9 * (((float)((i-1)-((i-1)%2))/2 - 1.0) ), up, end );
		}

		if (allowHitWorld && !trap_InPVS( start, end )) {
			// not possibly attackable
			continue;
		}

		trap_Trace(&trace, start, mins, maxs, end, passEnt, traceMask);
		if (trace.fraction == 1.0) {
			//return qfalse;
			continue;
		}
		//if won't hit the enemy
		if (trace.entityNum != enemy) {
			if (!allowHitWorld)
				continue;

			if (trace.startsolid)
				continue;

			//if the entity is a client
			if (trace.entityNum >= 0 && trace.entityNum < MAX_CLIENTS) {
				//if a teammate is hit
				if (AICast_SameTeam(cs, trace.entityNum)) {
					return qfalse;
				}
			}
			//if the projectile does a radial damage
			if (cs->bs->weaponnum == WP_ROCKET_LAUNCHER || cs->bs->weaponnum == WP_PANZERFAUST) {
				if (Distance(trace.endpos, g_entities[enemy].s.pos.trBase) > 120) {
					continue;
				}
				//FIXME: check if a teammate gets radial damage
			}
		}
		// will successfully hit enemy
		return qtrue;
	}
	//
	return qfalse;
}

/*
==================
AICast_CheckAttackAtPos
==================
*/
qboolean AICast_CheckAttackAtPos(int entnum, int enemy, vec3_t pos, qboolean ducking, qboolean allowHitWorld)
{
	gentity_t	*ent;
	vec3_t	savepos;
	int	saveview;
	qboolean	rval;
	cast_state_t *cs;

	cs = AICast_GetCastState(entnum);
	ent = &g_entities[cs->bs->entitynum];

	VectorCopy( ent->r.currentOrigin, savepos );
	VectorCopy( pos, ent->r.currentOrigin );

	saveview = ent->client->ps.viewheight;
	if (ducking) {
		if (ent->client->ps.viewheight != ent->client->ps.crouchViewHeight)
			ent->client->ps.viewheight = ent->client->ps.crouchViewHeight;
	} else {
		if (ent->client->ps.viewheight != ent->client->ps.standViewHeight)
			ent->client->ps.viewheight = ent->client->ps.standViewHeight;
	}

	rval = AICast_CheckAttack_real( cs, enemy, allowHitWorld );

	VectorCopy( savepos, ent->r.currentOrigin );
	ent->client->ps.viewheight = saveview;

	return rval;
}

/*
==================
AICast_CheckAttack

  optimization, uses the cache to avoid possible duplicate calls with same world paramaters
==================
*/
qboolean AICast_CheckAttack(cast_state_t *cs, int enemy, qboolean allowHitWorld)
{
	if (cs->bs) {
		if (	(cs->checkAttackCache.time == level.time)
			&&	(cs->checkAttackCache.enemy == enemy)
			&&	(cs->checkAttackCache.weapon == cs->bs->weaponnum)
			&&	(cs->checkAttackCache.allowHitWorld == allowHitWorld)) {
			//G_Printf( "checkattack cache hit\n" );
			return (cs->checkAttackCache.result);
		} else {
			cs->checkAttackCache.allowHitWorld = allowHitWorld;
			cs->checkAttackCache.enemy = enemy;
			cs->checkAttackCache.time = level.time;
			cs->checkAttackCache.weapon = cs->bs->weaponnum;
			return (cs->checkAttackCache.result = AICast_CheckAttack_real( cs, enemy, allowHitWorld ));
		}
	} else {
		return AICast_CheckAttack_real( cs, enemy, allowHitWorld );
	}
}

/*
==================
AICast_UpdateBattleInventory
==================
*/
void AICast_UpdateBattleInventory(cast_state_t *cs, int enemy)
{
	vec3_t dir;
	int		i;

	if (enemy >= 0) {
		VectorSubtract(cs->vislist[cs->bs->enemy].visible_pos, cs->bs->origin, dir);
		cs->enemyHeight = (int) dir[2];
		cs->enemyDist = (int) VectorLength(dir);
	}

	// stock up ammo that should never run out
	for (i=0; i<MAX_WEAPONS; i++) {
		if (g_entities[cs->bs->entitynum].client->ps.ammo[ BG_FindAmmoForWeapon(i)] > 800) {
			//g_entities[cs->bs->entitynum].client->ps.ammo[ BG_FindAmmoForWeapon(i)] = 999;
			Add_Ammo( &g_entities[cs->entityNum], cs->bs->weaponnum, 999, qfalse );
		}
	}

	BotAI_GetClientState( cs->entityNum, &(cs->bs->cur_ps) );

}

/*
==============
AICast_WeaponWantScale
==============
*/
float AICast_WeaponWantScale( cast_state_t *cs, int weapon )
{
	switch( weapon ) {
	case WP_GAUNTLET:
		return 0.1;
	case WP_FLAMETHROWER:
		return 2.0;		// if we have this up close, definately use it
	default:
		return 1.0;
	}
}

/*
==============
AICast_GotEnoughAmmoForWeapon
==============
*/
qboolean AICast_GotEnoughAmmoForWeapon( cast_state_t *cs, int weapon )
{
	gentity_t *ent;
	int ammo, clip;

	ent = &g_entities[cs->entityNum];
	ammo = ent->client->ps.ammo[BG_FindAmmoForWeapon(weapon)];
	clip = ent->client->ps.ammoclip[BG_FindClipForWeapon(weapon)];

	// TODO!! check some kind of weapon list that holds the minimum requirements for each weapon
	switch( weapon ) {
	case WP_GAUNTLET:
		return qtrue;
	default:
		return (qboolean)((clip >= ammoTable[weapon].uses) || (ammo >= ammoTable[weapon].uses));	//----(SA)	
	}
}

/*
==============
AICast_WeaponUsable

  This is used to prevent weapons from being selected, even if they have ammo.

  This can be used to add a delay between firing for special attacks, or make certain
  that certain weapons are only selected within a certain range or under certain conditions.

  NOTE: that monster_attack2 will always override monster_attack1 if both are usable
==============
*/
qboolean AICast_WeaponUsable( cast_state_t *cs, int weaponNum )
{
	int delay, oldweap, hitclient;
	float	dist=-1;
	gentity_t *ent, *grenade;

	if (cs->bs->enemy >= 0) {
		dist = Distance( cs->bs->origin, g_entities[cs->bs->enemy].s.pos.trBase );
	}

	oldweap = cs->bs->weaponnum;
	ent = &g_entities[cs->entityNum];
	delay = -1;

	// just return qfalse if this weapon isn't ready for use
	switch (weaponNum) {
	// don't attempt to lob a grenade more than this often, since we will abort a grenade
	// throw if it's not safe, we shouldn't keep switching back too quickly
	case WP_DYNAMITE:
	case WP_DYNAMITE2:
	case WP_GRENADE_LAUNCHER:
	case WP_GRENADE_PINEAPPLE:
		if (cs->bs->enemy < 0) {
			return qfalse;
		}
		delay = 5000;
		if (dist > 0 && dist < 200) {
			return qfalse;
		}
		if (cs->weaponFireTimes[weaponNum] < level.time - delay) {
			// make sure it's safe
			CalcMuzzlePoints(ent, weaponNum);
			grenade = weapon_grenadelauncher_fire( ent, weaponNum );
			hitclient = AICast_SafeMissileFire( grenade, grenade->nextthink - level.time, cs->bs->enemy, g_entities[cs->bs->enemy].s.pos.trBase, cs->entityNum, NULL );
			G_FreeEntity( grenade );
			if (hitclient > -1) {
				return qtrue;
			} else {
				return qfalse;	// it's not safe
			}
		}
		break;
	case WP_ROCKET_LAUNCHER:
		switch (cs->aiCharacter) {
		case AICHAR_STIMSOLDIER2:
			return qfalse;	// stime only uses Rocket Launcher in jumping attack
			//delay = 1000 + (cs->weaponFireTimes[weaponNum]%2000);	// somewhat randomize it so it's less predictable
			//break;
		}
		break;
	case WP_TESLA:
		switch (cs->aiCharacter) {
		case AICHAR_STIMSOLDIER3:
			if (dist<0 || dist>=TESLA_RANGE) {
				return qfalse;
			}
		}
		break;
	case WP_MONSTER_ATTACK1:
		switch (g_entities[cs->entityNum].aiCharacter) {
		case AICHAR_ZOMBIE:	// zombie flaming attack
			delay = 4000;
			if (dist < 0) {// || dist < 128) {
				return qfalse;
			}
			if (dist > 1200) {
				return qfalse;
			}
			if (cs->bs->enemy < 0) {
				return qfalse;
			}
			//if (cs->vislist[cs->bs->enemy].notvisible_timestamp > level.time - 500) {
			//	return qfalse;
			//}
			break;

		// melee attacks are always available
		case AICHAR_LOPER:
		case AICHAR_WARZOMBIE:
		case AICHAR_REJECTX:
		case AICHAR_SEALOPER:
			return qtrue;	// always usable

		case AICHAR_STIMSOLDIER2:
			delay = 7000;
			if (dist < 0 || dist < 300) {
				return qfalse;
			}
			break;
		case AICHAR_STIMSOLDIER3:	// stim flying tesla attack
			delay = 7000;
			if (dist < 0 || dist < 300) {
				return qfalse;
			}
			break;
		case AICHAR_BLACKGUARD:
			delay = 2000;
			if (dist < 0 || dist > BLACKGUARD_MELEE_RANGE) {
				return qfalse;
			}
			break;
		default:
			delay = -1;
			break;
		}
		break;
	case WP_MONSTER_ATTACK2:
		switch (g_entities[cs->entityNum].aiCharacter) {
		case AICHAR_WARZOMBIE:
			delay = 9999999;
			break;
		case AICHAR_ZOMBIE:
			delay = 6000;
			// zombie "flying spirit" attack
			if (dist < 0 || dist < 64) {
				return qfalse;
			}
			if (dist > 1200) {
				return qfalse;
			}
			if (cs->bs->enemy < 0) {
				return qfalse;
			}
			if (cs->vislist[cs->bs->enemy].notvisible_timestamp > level.time - 500) {
				return qfalse;
			}
			break;
		case AICHAR_LOPER:	// loper leap attack
			delay = 3500;
			if (dist < 200)
				return qfalse;
			break;
		case AICHAR_FEMZOMBIE:	// hand lightning
			delay = 2500;
			break;
		default:
			delay = -1;
			break;
		}
		break;
	case WP_MONSTER_ATTACK3:
		switch (g_entities[cs->entityNum].aiCharacter) {
		case AICHAR_LOPER:	// loper ground zap
			delay = 3500;
			if (dist < 0 || dist > LOPER_GROUND_RANGE)
				return qfalse;
			break;
		case AICHAR_WARZOMBIE:	// warzombie defense
			delay = 2000;
			if (dist < 0 || dist > 2000)
				return qfalse;
			break;
		default:
			delay = -1;
			break;
		}
		break;
	default:
		delay = -1;
	}
	//
	return (!cs->weaponFireTimes[weaponNum] || (cs->weaponFireTimes[weaponNum] < level.time - delay));
}

/*
==============
AICast_ChooseWeapon
==============
*/
void AICast_ChooseWeapon( cast_state_t *cs, qboolean battleFunc )
{
	int	i;
	int	*ammo;
	float	wantScale, bestWantScale;

	BotAI_GetClientState( cs->entityNum, &(cs->bs->cur_ps) );
	ammo = cs->bs->cur_ps.ammo;
	bestWantScale = 0.0;
	// read back in the weapon that we are really using
	//cs->bs->weaponnum = cs->bs->cur_ps.weapon;

	if (cs->bs->cur_ps.weaponstate == WEAPON_RAISING ||
		cs->bs->cur_ps.weaponstate == WEAPON_DROPPING) {
		return;
	}

// disabled this, makes grenade guy keep trying to throw a grenade he doesn't have
//	if (cs->bs->cur_ps.weaponDelay || cs->bs->cur_ps.weaponTime)
//		return;

	if (cs->bs->weaponnum && (cs->castScriptStatus.scriptFlags & SFL_NOCHANGEWEAPON)) {
		if (AICast_GotEnoughAmmoForWeapon( cs, cs->bs->weaponnum ) && AICast_WeaponUsable(cs, cs->bs->weaponnum)) {
			return;
		} else {
			cs->castScriptStatus.scriptFlags &= ~SFL_NOCHANGEWEAPON;
		}
	}

	// choose the best weapon to fight with
	for (i=0; i<MAX_WEAPONS; i++) {
		if (i == WP_GRENADE_LAUNCHER || i == WP_GRENADE_PINEAPPLE) {
			continue;	// never choose grenades at will, only when going into grenade flush mode
		}

		if (!battleFunc && (i == WP_MONSTER_ATTACK1) && cs->aifuncAttack1) {
			continue;	// only choose this weapon from within AIFunc_BattleStart()
		}
		if (!battleFunc && (i == WP_MONSTER_ATTACK2) && cs->aifuncAttack2) {
			continue;	// only choose this weapon from within AIFunc_BattleStart()
		}
		if (!battleFunc && (i == WP_MONSTER_ATTACK3) && cs->aifuncAttack3) {
			continue;	// only choose this weapon from within AIFunc_BattleStart()
		}

		if (COM_BitCheck(cs->bs->cur_ps.weapons, i)) {
			// check that our ammo is enough
			if (!AICast_GotEnoughAmmoForWeapon( cs, i ) ||
				!AICast_WeaponUsable(cs, i)) {
				continue;
			}
			// get the wantScale for this weapon given the current circumstances (0.0 - 1.0)
			wantScale = AICast_WeaponWantScale( cs, i );
			//
			if (wantScale >= bestWantScale) {
				cs->bs->weaponnum = i;
				bestWantScale = wantScale;
			}
		}
	}
}

/*
==================
AICast_Aggression

  Check all possible reasons why we shouldn't attack, returning a value from 1.0 (fully willing)
  to 0.0 (please don't hurt me).
==================
*/
float AICast_Aggression(cast_state_t *cs)
{
	bot_state_t *bs;
	float	scale, dist;
	int		painTime;
	int		*ammo;

	bs = cs->bs;

	// if we are out of ammo, we should never chase
	ammo = cs->bs->cur_ps.ammo;
	if (g_entities[cs->entityNum].aiTeam != AITEAM_MONSTER) {
		if (!AICast_GotEnoughAmmoForWeapon( cs, cs->bs->weaponnum )) {
			return 0;
		}
	}

	// start fully willing to attack
	scale = 1.0;

	//if the enemy is located way higher
	//if (cs->enemyHeight > 200)
	//	scale -= (cs->enemyHeight)/800.0;

	//if very low on health
	if (bs->cur_ps.stats[STAT_HEALTH] < 50)
		scale -= (1.0 - cs->attributes[AGGRESSION])	* (1.0 - ((float)bs->cur_ps.stats[STAT_HEALTH] / 50.0));

	// if they've recently hit us, factor that in, so we get scared off by being
	// damaged, but later return once we've regained our confidence
	painTime = 15000 - (int)(10000.0 * cs->attributes[AGGRESSION] * cs->attributes[AGGRESSION]);
	if (cs->lastPain + painTime > level.time)
		scale -= 3*(1.0 - cs->attributes[AGGRESSION])	* ((float)(cs->lastPain + painTime - level.time) / (float)painTime);

	// if we just rolled, stay out of view if we jumped behind cover
	painTime = 10000 - (int)(10000.0 * cs->attributes[AGGRESSION] * cs->attributes[AGGRESSION]);
	if (cs->battleRollTime + painTime > level.time)
		scale -= 2*(1.0 - cs->attributes[AGGRESSION])	* ((float)(cs->battleRollTime + painTime - level.time) / (float)painTime);

	// gain in confidence the further we are away
	if (cs->bs->enemy >= 0) {
		dist = Distance( cs->bs->origin, g_entities[bs->enemy].s.pos.trBase );
		//if (dist > 512) {
			scale += (dist - 800.0) / (8000.0);
		//}
	}

	// if our weapon is reloading, we should hide
	if (cs->bs->cur_ps.weaponTime > 0) {
		scale -= ((float)cs->bs->cur_ps.weaponTime / 1000.0);
	}

	scale *= cs->attributes[AGGRESSION];

	// this should increase the chances of an ambush attack
	if (((level.time + 2000*g_entities[cs->entityNum].aiTeam)%(4000 + 500*g_entities[cs->entityNum].aiTeam)) > 4000) {
		scale += 0.3;
	}

	if (scale < 0)
		scale = 0;

	return scale;
}

/*
==================
AICast_WantsToChase
==================
*/
int AICast_WantsToChase(cast_state_t *cs)
{
	int		*ammo;
	ammo = cs->bs->cur_ps.ammo;
	if (g_entities[cs->entityNum].aiTeam != AITEAM_MONSTER) {
		if (!AICast_GotEnoughAmmoForWeapon( cs, cs->bs->weaponnum )) {
			return qfalse;
		}
	}
	if (cs->attributes[AGGRESSION] == 1.0)
		return qtrue;
	if (AICast_Aggression(cs) > 0.6) {
		return qtrue;
	}
	return qfalse;
}

/*
==================
AICast_WantsToTakeCover
==================
*/
int AICast_WantsToTakeCover(cast_state_t *cs, qboolean attacking)
{
	float	aggrScale;
	int		*ammo;

	ammo = cs->bs->cur_ps.ammo;
	if (g_entities[cs->entityNum].aiTeam != AITEAM_MONSTER) {
		if (!cs->bs->weaponnum) {
			return qtrue;
		}
		if (!AICast_GotEnoughAmmoForWeapon( cs, cs->bs->weaponnum )) {
			return qtrue;
		}
	}
	if (cs->attributes[AGGRESSION] == 1.0)
		return qfalse;
	// if currently attacking, we should stick around if not getting hurt
	if (attacking) aggrScale = 1.2;
	else aggrScale = 0.8 /*+ 0.4 * random()*/;
	//
	// if currently following someone, we should be more aggressive
	if (cs->leaderNum >= 0) aggrScale *= 3;
	//
	// Dodge enemy aim?
	if (cs->attributes[AGGRESSION] < 1.0 && attacking && (cs->bs->enemy >= 0) && (g_entities[cs->bs->enemy].client->ps.weapon) && (cs->attributes[TACTICAL] > 0.5) && (cs->aiFlags & AIFL_ROLL_ANIM) && (VectorLength(cs->bs->cur_ps.velocity) < 1)) {
		vec3_t aim, enemyVec;
		// are they aiming at us?
		AngleVectors( g_entities[cs->bs->enemy].client->ps.viewangles, aim, NULL, NULL );
		VectorSubtract( cs->bs->origin, g_entities[cs->bs->enemy].r.currentOrigin, enemyVec );
		VectorNormalize( enemyVec );
		// if they are looking at us, we should avoid them
		if (DotProduct(aim, enemyVec) > 0.97) {
			//G_Printf("%s: I'm in danger, I should probably avoid\n", g_entities[cs->entityNum].aiName);
			aggrScale *= 0.6;
		}
	}
	//
	// FIXME: instead of a constant, call a "attack danger"
	// function, so we only attack if our aggression is greater than
	// the danger
	if (AICast_Aggression(cs) * aggrScale < 0.4) {
		//G_Printf("%s: run for your life!\n", g_entities[cs->entityNum].aiName);
		return qtrue;
	}
	//
	return qfalse;
}

/*
==================
AICast_CombatMove
==================
*/
bot_moveresult_t AICast_CombatMove(cast_state_t *cs, int tfl)
{
	bot_state_t *bs;
	float attack_skill, croucher, dist;
	vec3_t forward, backward;//, up = {0, 0, 1};
	bot_moveresult_t moveresult;
	bot_goal_t goal;

	bs = cs->bs;

	//get the enemy entity info
	memset(&moveresult, 0, sizeof(bot_moveresult_t));
	//
	attack_skill = cs->attributes[ATTACK_SKILL];
	croucher = (cs->attributes[ATTACK_CROUCH] > 0.1);

	//initialize the movement state
	BotSetupForMovement(bs);
	//direction towards the enemy
	VectorSubtract(cs->vislist[cs->bs->enemy].visible_pos, bs->origin, forward);
	//the distance towards the enemy
	dist = VectorNormalize(forward);
	VectorNegate(forward, backward);
	//
	// do we have somewhere we are trying to get to?
	if (cs->combatGoalTime > level.time)
	{
		if (VectorLength( cs->combatGoalOrigin ) > 1) {
			//create the chase goal
			goal.areanum = BotPointAreaNum(cs->combatGoalOrigin);
			VectorCopy(cs->combatGoalOrigin, goal.origin);
		
			// if we are really close, stop going for it
			// FIXME: a better way of doing this, so we don't stop short of the goal?
			if ((dist = Distance( goal.origin, cs->bs->origin )) < 32) {
				if (cs->combatGoalTime > level.time + 3000) {
					cs->combatGoalTime = level.time + 2000 + rand()%1000;
					cs->combatSpotDelayTime = level.time + 4000 + rand()%3000;
				}
				VectorClear( cs->combatGoalOrigin );
			} else {
				aicast_predictmove_t	move;
				//
				AICast_MoveToPos( cs, cs->combatGoalOrigin, -1 );
				cs->speedScale = AICast_SpeedScaleForDistance(cs, dist, 32);
				//
				// if we are going to move out of view very soon, stop moving
				AICast_PredictMovement( cs, 1, 0.8, &move, &cs->bs->lastucmd, -1 );
				//
				if (move.numtouch || !AICast_CheckAttackAtPos(cs->entityNum, cs->bs->enemy, move.endpos, qfalse, qfalse )) {
					// abort the manouver, reached a good spot
					cs->combatGoalTime = 0;
					cs->combatSpotAttackCount = cs->startAttackCount;
				}
			}

		// if we are there, and the enemy can see us, but we cant hit them, abort immediately
		} else if (!AICast_CheckAttack(cs, cs->bs->enemy, qfalse) &&
					AICast_VisibleFromPos( cs->vislist[cs->bs->enemy].visible_pos, cs->bs->enemy, cs->bs->origin, cs->entityNum, qfalse)) {
			cs->combatGoalTime = 0;
			cs->combatSpotAttackCount = cs->startAttackCount;
		}
	} else {	// look for a good position to move to?
		if (	(	(cs->attributes[CAMPER] < random())
				&&	(cs->takeCoverTime < level.time)
				&&	(cs->combatSpotAttackCount < cs->startAttackCount)
				&&	(cs->combatSpotDelayTime < level.time))) {

			if (	(cs->attributes[TACTICAL] > 0.3 + random()*0.5)
				&&	trap_AAS_RT_GetHidePos( cs->bs->origin, cs->bs->entitynum, cs->bs->areanum, cs->vislist[cs->bs->enemy].visible_pos, bs->enemy, BotPointAreaNum(cs->vislist[cs->bs->enemy].visible_pos), cs->combatGoalOrigin )) {
				cs->combatGoalTime = level.time + 10000;				// give us plenty of time to get there
				//cs->combatSpotAttackCount = cs->startAttackCount + 3;	// don't keep moving around to different positions on our own
				cs->combatSpotDelayTime = level.time + 3000 + rand()%3000;
			} else {
				// don't look again until we've moved
				//cs->combatSpotAttackCount = cs->startAttackCount;
				cs->combatSpotDelayTime = level.time + 3000 + rand()%3000;
			}
		}
	}
	//
	return moveresult;
}

/*
==================
AICast_WeaponSway

  Some weapons should be "sprayed" around a bit while firing
==================
*/
void AICast_WeaponSway( cast_state_t *cs, vec3_t ofs )
{
	VectorClear( ofs );
	switch (cs->bs->weaponnum) {
	case WP_MONSTER_ATTACK1:
		if (cs->aiCharacter != AICHAR_ZOMBIE)
			break;		// only allow flaming zombie beyond here
	case WP_FLAMETHROWER:
		ofs[PITCH] = (3.0 + 4.0*sin( ((float)level.time / 320.0) )) * sin( ((float)level.time / 500.0) );
		ofs[YAW] = (6.0 + 8.0*sin( ((float)level.time / 250.0) )) * sin( ((float)level.time / 400.0) );
		ofs[ROLL] = 0;
		break;
	case WP_VENOM:
		ofs[PITCH] = 2 * (float)cos( (level.time / 200) );
		ofs[YAW] = 10 * (float)sin( (level.time / 150) ) * (float)sin( (level.time / 100) );
		ofs[ROLL] = 0;
		break;
	}
}

/*
==================
AICast_AimAtEnemy
==================
*/
qboolean AICast_AimAtEnemy(cast_state_t *cs)
{
	bot_state_t *bs;
	float aim_skill, aim_accuracy;
	vec3_t dir, bestorigin, start, enemyOrg;
	vec3_t mins = {-4,-4,-4}, maxs = {4, 4, 4};
	bsp_trace_t trace;
	float dist;
	cast_visibility_t *vis;

	//
	if (cs->castScriptStatus.scriptNoAttackTime >= (level.time + 500)) {
		return qfalse;
	}
	//
	if (cs->noAttackTime >= level.time) {
		return qfalse;
	}
	//
	bs = cs->bs;
	//
	//if the bot has no enemy
	if (bs->enemy < 0) return qfalse;
	//
	aim_skill = cs->attributes[AIM_SKILL];
	aim_accuracy = AICast_GetAccuracy(cs->entityNum);
	if (aim_accuracy <= 0) aim_accuracy = 0.0001;

	// StimSoldier is very good at firing Rocket Launcher
	if (cs->aiCharacter == AICHAR_STIMSOLDIER2 && cs->bs->weaponnum == WP_ROCKET_LAUNCHER) {
		aim_skill = 1;
		aim_accuracy = 1;
	}

	//get the weapon information

	//get the enemy entity information
	vis = &cs->vislist[bs->enemy];
	if (vis->visible_timestamp < vis->lastcheck_timestamp) {
		// use our last visible position of them
		if (vis->real_visible_timestamp == vis->lastcheck_timestamp)
			VectorCopy( vis->real_visible_pos, bestorigin );
		else
			VectorCopy( vis->visible_pos, bestorigin );
	} else {
		// we can see them, if this weapon isn't a direct-hit weapon (bullets),
		// then predict where they are going to be
		if (cs->bs->weaponnum == WP_GRENADE_LAUNCHER || cs->bs->weaponnum == WP_GRENADE_PINEAPPLE) {
			aicast_predictmove_t	move;
			AICast_PredictMovement( AICast_GetCastState(cs->bs->enemy), 1, 1.0, &move, &g_entities[bs->enemy].client->pers.cmd, -1 );
			VectorCopy( move.endpos, bestorigin );
		} else {	// they are visible, use actual position
			VectorCopy( g_entities[bs->enemy].client->ps.origin, bestorigin );
		}
	}

	bestorigin[2] += g_entities[bs->enemy].client->ps.viewheight;
	//get the start point shooting from
	//NOTE: the x and y projectile start offsets are ignored
	VectorCopy(bs->origin, start);
	start[2] += bs->cur_ps.viewheight;
	//
	VectorCopy( cs->vislist[bs->enemy].visible_pos, enemyOrg );
	//
	// grenade hack: aim grenades at their feet if they are close
	if (cs->bs->weaponnum == WP_GRENADE_LAUNCHER || cs->bs->weaponnum == WP_GRENADE_PINEAPPLE) {
		if (Distance( start, bestorigin ) < 180)
			bestorigin[2] = enemyOrg[2] + g_entities[bs->enemy].r.mins[2] + crandom()*20;
		else if (Distance( start, bestorigin ) > 400)	// aim up a bit for distance
			bestorigin[2] += 12 + Distance( start, bestorigin )/50 + crandom()*20;
	}
	dist = Distance( bs->eye, bestorigin );
	// rocket launcher should aim ahead
	if (bs->weaponnum == WP_ROCKET_LAUNCHER || bs->weaponnum == WP_PANZERFAUST) {
		VectorMA( bestorigin, aim_skill*aim_skill * (dist/900), g_entities[bs->enemy].client->ps.velocity, bestorigin );
	}
	//
	BotAI_Trace(&trace, start, mins, maxs, bestorigin, bs->entitynum, MASK_SHOT);
	//if the enemy is NOT hit
	if (trace.fraction <= 1 && trace.ent != bs->enemy) {
		if (bs->weaponnum == WP_ROCKET_LAUNCHER || bs->weaponnum == WP_PANZERFAUST) {
			if (Distance( trace.endpos, bestorigin ) > 100) {
				// remove the prediction
				VectorMA( bestorigin, aim_skill*aim_skill * (dist/900), g_entities[bs->enemy].client->ps.velocity, bestorigin );
				// aim downwards
				bestorigin[2] -= 16;
			}
		} else {
			bestorigin[2] += 16;
		}
	}
	//
	// adjust accuracy with distance if upclose, so we don't start firing the wrong direction
/*
	scale = 1;
	switch (cs->bs->weaponnum) {
	// these weapons don't do random offsetting
	case WP_FLAMETHROWER:
		break;
	default:
		{
			float scale;

			if (dist < 256)
				scale *= (dist / 256);
			else
				scale *= 1.0;

			bestorigin[0] += scale * 96 * sin((float)level.time/(200.0 + (40.0*((cs->entityNum+3)%4)))) * (1 - aim_accuracy);
			bestorigin[1] += scale * 96 * cos((float)level.time/(220.0 + (36.0*((cs->entityNum+1)%5)))) * (1 - aim_accuracy);
			bestorigin[2] += scale * 48 * sin((float)level.time/(210.0 + (32.0*((cs->entityNum+2)%6)))) * (1 - aim_accuracy);
		}
		break;
	}
*/
	// if the enemy is moving, they are harder to hit
	if (dist > 256) {
		VectorMA( bestorigin, (0.3 + 0.7*(1 - aim_accuracy)) * 0.4 * sin((float)level.time/(500.0 + (100.0*((cs->entityNum+3)%4)))), g_entities[bs->enemy].client->ps.velocity, bestorigin );
	}
	//get aim direction
	VectorSubtract(bestorigin, bs->eye, dir);
	//set the ideal view angles
	vectoangles(dir, bs->ideal_viewangles);

	return qtrue;	// do real aim checking after we've moved the angles
}

/*
==================
AICast_CanMoveWhileFiringWeapon
==================
*/
qboolean AICast_CanMoveWhileFiringWeapon( int weaponnum )
{
	switch (weaponnum) {
	case WP_MAUSER:
	case WP_GARAND:
	case WP_SNIPERRIFLE:	//----(SA)	added
	case WP_SNOOPERSCOPE:	//----(SA)	added
	case WP_FG42SCOPE:		//----(SA)	added
	case WP_PANZERFAUST:
	case WP_ROCKET_LAUNCHER:
		return qfalse;
	default:
		return qtrue;
	}
}

/*
================
AICast_RandomTriggerRelease
================
*/
qboolean AICast_RandomTriggerRelease( cast_state_t *cs )
{
	// some characters override all weapon settings for trigger release
	switch (cs->aiCharacter) {
	case AICHAR_BLACKGUARD:		// this is here since his "ready" frame is different to his firing frame, so it looks wierd to keep swapping between them
	case AICHAR_STIMSOLDIER1:
	case AICHAR_STIMSOLDIER2:
	case AICHAR_STIMSOLDIER3:
		return qfalse;
		break;
	}

	switch (cs->bs->weaponnum) {
	case WP_MP40:
	case WP_VENOM:
	//case WP_FLAMETHROWER:
		return qtrue;
	default:
		return qfalse;
	}
}

/*
==================
AICast_ProcessAttack

  NOTE: this should always be called after the movement has been processed
==================
*/
void AICast_ProcessAttack(cast_state_t *cs)
{
	bot_state_t *bs;

	// if our enemy is dead, stop attacking them
	if (cs->bs->enemy >= 0 && g_entities[cs->bs->enemy].health <= 0) {
		return;
	}
	//
	if (cs->castScriptStatus.scriptNoAttackTime >= level.time) {
		return;
	}
	//
	if (cs->noAttackTime >= level.time) {
		return;
	}
	// select a weapon
	AICast_ChooseWeapon( cs, qfalse );
	//
	bs = cs->bs;
	// check for not firing while moving flag, if present, abort attack if any movement has been issued
	if (!AICast_CanMoveWhileFiringWeapon( cs->bs->weaponnum )) {
		// if we are moving, don't fire
		bot_input_t bi;
		if (cs->bs->cur_ps.weaponTime > 200) {
			// if we recently fired, don't let us move for a bit
			cs->speedScale = 0;
			AICast_AimAtEnemy( cs );	// keep looking at them regardless
		}
		// if we're trying to move somewhere, don't let us shoot, until we've arrived
		trap_EA_GetInput(bs->client, (float) level.time / 1000, &bi);
		if (	(cs->castScriptStatus.scriptNoMoveTime < level.time) &&
				(	(bi.actionflags & ACTION_MOVEFORWARD) ||
					(bi.actionflags & ACTION_MOVEBACK) ||
					(bi.actionflags & ACTION_MOVELEFT) ||
					(bi.actionflags & ACTION_MOVERIGHT) ||
					(bi.speed))) {
			return;
		}
	}
	//
	// if we are stuck in this position, we should duck if we can't hit them
	if (!AICast_CheckAttack( cs, bs->enemy, qfalse )) {
		// we should duck if the enemy is shooting at us, and we can't hit them
		if (cs->attributes[ATTACK_CROUCH] && (cs->castScriptStatus.scriptNoMoveTime >= level.time)) {
			if (!AICast_CheckAttackAtPos( cs->entityNum, bs->enemy, cs->bs->origin, qfalse, qfalse )) {
				cs->bs->attackcrouch_time = trap_AAS_Time() + 2;
			} else {
				cs->bs->attackcrouch_time = 0;	// we can attack them if we stand, so go for it
			}
		}
		return;
	}
	//
	//aim at the enemy
	if (!AICast_AimAtEnemy( cs )) {
		// not fully facing them yet
		return;
	}
	//
	// release the trigger every now and then
	if (AICast_RandomTriggerRelease(cs) && cs->triggerReleaseTime < (level.time - 500)) {
		if (rand()%5 == 0) {
			cs->triggerReleaseTime = level.time + 100 + rand()%100;
			return;
		}
	}
	//
	if (cs->triggerReleaseTime > level.time) {
		return;
	}
	//
	// FIXME: handle fire-on-release weapons?
	trap_EA_Attack(bs->client);
	//
	bs->flags |= BFL_ATTACKED;

}

/*
==============
AICast_GetTakeCoverPos
==============
*/
qboolean AICast_GetTakeCoverPos( cast_state_t *cs, int enemyNum, vec3_t enemyPos, vec3_t returnPos )
{
	cs->crouchHideFlag = qfalse;
	//
	if (cs->castScriptStatus.scriptNoMoveTime > level.time) 
		return qfalse;
	//
	cs->lastGetHidePos = level.time;
	//
	// can we just crouch?
	if (	(cs->bs->attackcrouch_time < trap_AAS_Time())
		&&	(enemyNum < aicast_maxclients)
		&&	AICast_CheckAttackAtPos( cs->entityNum, enemyNum, cs->bs->origin, qfalse, qfalse )
		&&	!AICast_CheckAttackAtPos( cs->entityNum, enemyNum, cs->bs->origin, qtrue, qfalse )) {

		// do a more thorough check to see if the enemy can see us if we crouch
		vec3_t omaxs;
		gentity_t *ent;
		qboolean visible;

		ent = &g_entities[cs->entityNum];
		VectorCopy( ent->r.maxs, omaxs );
		ent->r.maxs[2] = ent->client->ps.crouchMaxZ + 4;	// + 4 to be safe

		visible = AICast_VisibleFromPos( g_entities[enemyNum].r.currentOrigin, enemyNum, cs->bs->origin, cs->entityNum, qfalse );

		ent->r.maxs[2] = omaxs[2];
		
		if (!visible) {
			VectorCopy( enemyPos, cs->takeCoverEnemyPos );
			VectorCopy( cs->bs->origin, returnPos );
			cs->crouchHideFlag = qtrue;
			return qtrue;
		}
	}
	// look for a hiding spot
	if (trap_AAS_RT_GetHidePos( cs->bs->origin, cs->bs->entitynum, cs->bs->areanum, enemyPos, enemyNum, BotPointAreaNum(enemyPos), returnPos )) {
		return qtrue;
	}
	// if we are hiding from a dangerous entity, try and avoid it
	if (cs->dangerEntity == enemyNum && cs->dangerEntityValidTime > level.time) {
		if (cs->dangerLastGetAvoid > level.time - 750) {
			return qtrue;
		} else if (AICast_GetAvoid( cs, NULL, cs->takeCoverPos, qtrue, cs->dangerEntity )) {
			cs->dangerLastGetAvoid = level.time;
			return qtrue;
		}
	}
	//
	return qfalse;
}

/*
==============
AICast_AIDamageOK
==============
*/
qboolean AICast_AIDamageOK( cast_state_t *cs, cast_state_t *ocs )
{
	if (cs->castScriptStatus.scriptFlags & SFL_NOAIDAMAGE) {
		return qfalse;
	} else {
		
		if (cs->aiCharacter == AICHAR_LOPER && ocs->aiCharacter == AICHAR_LOPER)
			return qfalse;

		return qtrue;
	}
}

/*
===============
AICast_RecordWeaponFire

  used for scripting, so we know when the weapon has been fired
===============
*/
void AICast_RecordWeaponFire( gentity_t *ent )
{
	cast_state_t *cs;
	float	range;

	cs = AICast_GetCastState( ent->s.number );
	cs->lastWeaponFired = level.time;
	cs->lastWeaponFiredWeaponNum = ent->client->ps.weapon;
	VectorCopy( ent->r.currentOrigin, cs->lastWeaponFiredPos );

	cs->weaponFireTimes[cs->lastWeaponFiredWeaponNum] = level.time;

	// do sighting
	range = AICast_GetWeaponSoundRange( cs->lastWeaponFiredWeaponNum );

	AICast_AudibleEvent( cs->entityNum, cs->lastWeaponFiredPos, range );
	//AICast_SightSoundEvent( cs, range );
	/*
	if (!cs->sightSoundTime || cs->sightSoundRange < range) {
		cs->sightSoundRange = range;
		cs->sightSoundTime = level.time;
	}
	*/

	if (cs->bs) {	// real player's don't need to play AI sounds	
		AIChar_AttackSound (cs);
	}
}

/*
===============
AICast_GetWeaponSoundRange
===============
*/
float AICast_GetWeaponSoundRange( int weapon )
{
	// NOTE: made this a case, that way changing the ordering of weapons won't cause problems, as it would
	// with an array lookup

	switch (weapon) {
	case	WP_NONE:
		return 0;
	case	WP_KNIFE:
	case	WP_KNIFE2:
	case	WP_GAUNTLET:
	case	WP_SPEARGUN:
	case	WP_SPEARGUN_CO2:
	case	WP_STEN:
	case	WP_SILENCER:
		return 64;
	case	WP_GRENADE_LAUNCHER:
	case	WP_GRENADE_PINEAPPLE:
	case	WP_PROX:	//----(SA)	added
		return 1500;
	case	WP_SNIPERRIFLE:
		return 1500;
	case	WP_SNOOPERSCOPE:
		return 128;
	case	WP_LUGER:
	case	WP_COLT:
	case	WP_AKIMBO:
		return 700;

	case	WP_MONSTER_ATTACK1:
	case	WP_MONSTER_ATTACK2:
	case	WP_MONSTER_ATTACK3:
		// TODO: case for each monster
		return 1000;

	case	WP_GARAND:
	case	WP_MP40:
	case	WP_THOMPSON:
		return 1000;

	case	WP_BAR:
	case	WP_BAR2:
		return 2000;	//----(SA)	added

	case	WP_FG42:
	case	WP_FG42SCOPE:
		return 1500;

	case	WP_MAUSER:	//----(SA)	change for DM
		return 1500;

	case	WP_DYNAMITE:
	case	WP_DYNAMITE2:
		return 3000;

	case	WP_PANZERFAUST:
	case	WP_ROCKET_LAUNCHER:
	case	WP_VENOM:
	case	WP_VENOM_FULL:
	case	WP_FLAMETHROWER:
	case	WP_CROSS:
	case	WP_TESLA:
		return 1000;
	}

	G_Error( "AICast_GetWeaponSoundRange: unknown weapon index: %i\n", weapon );
	return 0;	// shutup the compiler
}

/*
===============
AICast_StopAndAttack

  returns qtrue if they should go back to a battle state to attack,
  qfalse if they should keep chasing while they attack (like the Zombie)
===============
*/
qboolean AICast_StopAndAttack( cast_state_t *cs )
{
	float	dist=-1;

	if (cs->bs->enemy >= 0) {
		dist = Distance( cs->bs->origin, g_entities[cs->bs->enemy].r.currentOrigin );
	}
/*
	switch (cs->bs->weaponnum) {

		// removed

	}
*/
	return qtrue;
}

/*
===============
AICast_GetAccuracy
===============
*/
float AICast_GetAccuracy( int entnum )
{
	#define AICAST_VARIABLE_ACC_ENABLED		1
	#define	AICAST_ACC_VISTIME	4000
	#define	AICAST_ACC_SCALE	0.4
	cast_state_t *cs;
	float	acc;

	cs = AICast_GetCastState( entnum );
	// the more they stay in our sights, the more accurate we get
	acc = cs->attributes[AIM_ACCURACY];

	if (AICAST_VARIABLE_ACC_ENABLED) {
		if (cs->bs->enemy >= 0) {
			if (cs->vislist[cs->bs->enemy].real_notvisible_timestamp < level.time - AICAST_ACC_VISTIME)
				acc += AICAST_ACC_SCALE;
			else
				acc += AICAST_ACC_SCALE*((float)(level.time - cs->vislist[cs->bs->enemy].real_notvisible_timestamp) / (float)(AICAST_ACC_VISTIME));

			if (acc > 1.0)
				acc = 1.0;
			else if (acc < 0.0)
				acc = 0.0;
		}
	}
	return (acc);
}

/*
==============
AICast_WantToRetreat
==============
*/
qboolean AICast_WantToRetreat( cast_state_t *cs )
{
	int		*ammo;

	ammo = cs->bs->cur_ps.ammo;
	if (g_entities[cs->entityNum].aiTeam != AITEAM_MONSTER) {
		if (!cs->bs->weaponnum) {
			return qtrue;
		}
		if (!AICast_GotEnoughAmmoForWeapon( cs, cs->bs->weaponnum )) {
			return qtrue;
		}
	}

	if (cs->attributes[AGGRESSION] >= 1.0)
		return qfalse;

	if	(cs->leaderNum < 0)
	if	(	(cs->attributes[TACTICAL] > 0.11+random()*0.5) &&
			(	(cs->bs->cur_ps.weaponTime > 500) ||
				(	(cs->takeCoverTime < level.time - 100) &&
					(AICast_WantsToTakeCover( cs, qtrue )))))
	{
		return qtrue;
	}
	//
	return qfalse;
}

/*
==============
AICast_SafeMissileFire

  checks to see if firing the missile will be successful, neutral, or dangerous to us or a friendly
==============
*/
int AICast_SafeMissileFire( gentity_t *ent, int duration, int enemyNum, vec3_t enemyPos, int selfNum, vec3_t endPos ) {
	int			rval;
	vec3_t		org;
	gentity_t	*trav;

	if (!G_PredictMissile( ent, duration, org, qtrue )) {
		// not point firing, since it won't explode
		return 0;
	}

	if (endPos) {
		VectorCopy( org, endPos );
	}

	// at end of life, so do radius damage
	rval = (Distance( org, enemyPos ) < ent->splashRadius) && AICast_VisibleFromPos(org, ent->s.number, enemyPos, enemyNum, qfalse );
	if (rval) {
		// don't hurt ourselves
		// disabled, don't worry about us, we can get out the way in time (we hope!)
		//if (Distance( org, g_entities[selfNum].r.currentOrigin ) < ent->splashRadius*1.5)
		//	return -1;
		// make sure we don't injure a friendly
		for (trav = g_entities; trav < g_entities+g_maxclients.integer; trav++) {
			if (!trav->inuse)
				continue;
			if (!trav->client)
				continue;
			if (trav->health <= 0)
				continue;
			if (trav->s.number == selfNum)
				continue;
			if (AICast_SameTeam( AICast_GetCastState( selfNum ), trav->s.number )) {
				if (Distance( org, trav->r.currentOrigin ) < ent->splashRadius) {
					return -1;
				}
			}
		}
	}
	// if it overshot the mark
	if (!rval && Distance( g_entities[ent->r.ownerNum].r.currentOrigin, org ) > Distance( g_entities[ent->r.ownerNum].r.currentOrigin, enemyPos )) {
		return -2;	// so the AI can try aiming down a bit next time
	}
	//
	return rval;
}

/*
=============
AICast_CheckDangerousEntity

  check to see if the given entity can harm an AI character, if so, informs them
  appropriately
=============
*/
void AICast_CheckDangerousEntity( gentity_t *ent, int dangerFlags, float dangerDist, float tacticalLevel, float aggressionLevel, qboolean hurtFriendly ) {
	vec3_t org, fwd, vec;
	cast_state_t *cs, *dcs;
	gentity_t *trav;
	int i, endTime;
	float dist;
	//
	//
	if (dangerFlags & DANGER_MISSILE) {
		// predict where the entity will explode
		if (!(endTime = G_PredictMissile( ent, ent->nextthink - level.time, org, qtrue )))
			return;	// missile won't explode, so no danger
	} else {
		// just avoid it for a bit, then forget it
		endTime = level.time + 1000;
		VectorCopy( ent->r.currentOrigin, org );
	}
	if (dangerFlags & DANGER_CLIENTAIM) {
		AngleVectors( ent->client->ps.viewangles, fwd, NULL, NULL );
	}
	//
	if (ent->client)
		dcs = AICast_GetCastState( ent->s.number );
	else
		dcs = NULL;
	//
	// see if this will hurt anyone
	for (trav=g_entities, cs=AICast_GetCastState(0), i=0; i<level.numPlayingClients; cs++, trav++) {
		if (!trav->inuse || !trav->client)
			continue;
		i++;	// found a connected client
		if (trav == ent)	// don't be afraid of ourself
			continue;
		if (trav->health <= 0)
			continue;
		if (!cs->bs)	// not an AI, they should look out for themselves
			continue;
		if (cs->castScriptStatus.scriptNoSightTime >= level.time)
			continue;		// absolutely no sight (or hear) information allowed
		if (!hurtFriendly && ent->s.number < MAX_CLIENTS && AICast_SameTeam( cs, ent->s.number ))
			continue;	// trust that friends will not hurt us
		if ((dangerFlags & DANGER_FLAMES) && (cs->aiFlags & AIFL_NO_FLAME_DAMAGE))
			continue;	// venom not effected by flames
		if (cs->attributes[TACTICAL] < tacticalLevel)	// not smart enough
			continue;
		if (cs->aiState >= AISTATE_COMBAT && cs->attributes[AGGRESSION] > aggressionLevel)	// we are too aggressive to worry about being hurt by this
			continue;
		// if they are below alert mode, and the danger is not in FOV, then ignore it
		if (cs->aiState < AISTATE_ALERT) {
			vec3_t ang, dir;
			VectorSubtract( ent->r.currentOrigin, cs->bs->origin, dir );
			VectorNormalize( dir );
			vectoangles( dir, ang );
			if (!AICast_InFieldOfVision( cs->bs->viewangles, cs->attributes[FOV], ang )) {
				// can't see it
				continue;
			}
		}
		if (	ent->client &&
				(!cs->vislist[ent->s.number].visible_timestamp || (cs->vislist[ent->s.number].visible_timestamp < level.time - 3000)))
				//	(!dcs->vislist[trav->s.number].visible_timestamp || (dcs->vislist[trav->s.number].visible_timestamp < level.time - 3000))))
			continue;	// not aware of them, and they're not aware of us
		// are they in danger?
		if (cs->dangerEntityValidTime < level.time+50) {
			VectorSubtract( cs->bs->cur_ps.origin, org, vec );
			dist = VectorLength( vec );
			if (dist < dangerDist) {
				if (dangerFlags & DANGER_CLIENTAIM) {
					// also check aiming
					if (DotProduct( vec, fwd ) < (dist*0.95 - 100)) {
						continue;
					}
				}
				//
				cs->aiFlags &= ~AIFL_DENYACTION;
				AICast_ScriptEvent( cs, "avoiddanger", ent->classname );
				if (cs->aiFlags & AIFL_DENYACTION) {
					continue;
				}
				cs->dangerEntity = ent->s.number;
				VectorCopy( org, cs->dangerEntityPos );
				cs->dangerEntityValidTime = endTime + 50;
				cs->dangerDist = dangerDist*1.5;	// when we hide from it, get a good distance away
				cs->dangerEntityTimestamp = level.time;
			}
		}
	}
}


qboolean AICast_HasFiredWeapon (int entNum, int weapon)
{
	if (AICast_GetCastState (entNum)->weaponFireTimes[weapon])
		return qtrue;

	return qfalse;
}

qboolean AICast_AllowFlameDamage (int entNum)
{
	// DHM - Nerve :: caststates are not initialized in multiplayer
	if ( g_gametype.integer != GT_SINGLE_PLAYER )
		return qtrue;
	// dhm

	if (caststates[entNum].aiFlags & AIFL_NO_FLAME_DAMAGE) {
		return qfalse;
	}
	return qtrue;
}

/*
=============
AICast_ProcessBullet
=============
*/
void AICast_ProcessBullet( gentity_t *attacker, vec3_t start, vec3_t end )
{
	gentity_t	*tent;
	int i;
	float	dist;
	vec3_t	vProj, vDir, dir;
	cast_state_t *cs;

	VectorSubtract( end, start, dir );

	// RF, AI should hear this pass by them really closely, or hitting a wall close by
	for (cs=caststates, tent=g_entities, i=0; i<level.maxclients; i++, tent++, cs++) {
		if (!tent->inuse)
			continue;
		if (tent == attacker)
			continue;
		if (tent->aiInactive)
			continue;
		if (tent->health <= 0)
			continue;
		if (cs->castScriptStatus.scriptNoSightTime > level.time)
			continue;
		if (!(tent->r.svFlags & SVF_CASTAI))
			continue;
		if (cs->aiState >= AISTATE_COMBAT)	// RF add	// already fighting, not interested in bullet impacts
			continue;
		if (cs->bulletImpactIgnoreTime > level.time)
			continue;
		dist = Distance( tent->client->ps.origin, end );
		if (dist <= cs->attributes[INNER_DETECTION_RADIUS]) {
			// close enough to hear/see the impact?
			// first check pvs
			if (!trap_InPVS( tent->client->ps.origin, end ))
				continue;
			// heard it
			goto heard;
		}
		// are they within radius of the bullet path to hear it travel through the air?
		ProjectPointOntoVector( tent->client->ps.origin, start, end, vProj );
		VectorSubtract( vProj, start, vDir );
		if (DotProduct( vDir, dir ) < 0)	// they are behind the path of the bullet
			continue;
		if (Distance( vProj, tent->client->ps.origin ) > 0.5*cs->attributes[INNER_DETECTION_RADIUS]) {
			continue;
		}
heard:
		// call the script event
		AICast_ScriptEvent( cs, "bulletimpact", "" );
		if (cs->aiFlags & AIFL_DENYACTION) {
			continue;	// ignore the bullet
		}
		//
		cs->bulletImpactTime = level.time + 200 + rand()%300;	// random reaction delay;
		VectorCopy( start, cs->bulletImpactStart );
		VectorCopy( end, cs->bulletImpactEnd );
	}
}

/*
================
AICast_AudibleEvent
================
*/
void AICast_AudibleEvent( int srcnum, vec3_t pos, float range )
{
	int i;
	cast_state_t *cs, *scs;
	gentity_t *ent, *sent;

	// DHM - Nerve :: caststates are not initialized in multiplayer
	if ( g_gametype.integer != GT_SINGLE_PLAYER )
		return;
	// dhm

	sent = &g_entities[srcnum];
	scs = AICast_GetCastState( srcnum );

	for (ent=g_entities, cs=caststates, i=0; i<level.maxclients; i++, cs++, ent++) {
		if (!cs->bs)
			continue;
		//if (cs->aiState >= AISTATE_COMBAT)
		//	continue;
		if (ent == sent)
			continue;
		if (cs->castScriptStatus.scriptNoSightTime > level.time)
			continue;
		if (ent->health <= 0)
			continue;
		// if within range, and this sound was made by an enemy
		if (scs->aiState < AISTATE_COMBAT && !AICast_QueryEnemy( cs, srcnum ))
			continue;
		if (Distance( pos, ent->s.pos.trBase ) > range)
			continue;
		// we heard it
		cs->audibleEventTime = level.time + 200 + rand()%300;	// random reaction delay
		VectorCopy( pos, cs->audibleEventOrg );
		cs->audibleEventEnt = ent->s.number;
	}
}
