Beispiel #1
0
/*
============
AICast_Blocked
============
*/
void AICast_Blocked( cast_state_t *cs, bot_moveresult_t *moveresult, int activate, bot_goal_t *goal ) {
	vec3_t pos, dir;
	aicast_predictmove_t move;
	usercmd_t ucmd;
	bot_input_t bi;
	cast_state_t *ocs;
	int i, blockEnt = -1;
	bot_goal_t ogoal;

	if ( cs->blockedAvoidTime < level.time ) {
		if ( cs->blockedAvoidTime < level.time - 300 ) {
			if ( VectorCompare( cs->bs->cur_ps.velocity, vec3_origin ) && !cs->bs->lastucmd.forwardmove && !cs->bs->lastucmd.rightmove ) {
				// not moving, don't bother checking
				cs->blockedAvoidTime = level.time - 1;
				return;
			}
			// are we going to hit someone soon?
			trap_EA_GetInput( cs->entityNum, (float) level.time / 1000, &bi );
			AICast_InputToUserCommand( cs, &bi, &ucmd, cs->bs->cur_ps.delta_angles );
			AICast_PredictMovement( cs, 1, 0.6, &move, &ucmd, ( goal && goal->entitynum > -1 ) ? goal->entitynum : cs->entityNum );

			// blocked if we hit a client (or non-stationary mover) other than our enemy or goal
			if ( move.stopevent != PREDICTSTOP_HITCLIENT ) {
				// not blocked
				cs->blockedAvoidTime = level.time - 1;
				return;
			}

			// if we stopped passed our goal, ignore it
			if ( goal ) {
				if ( VectorDistance( cs->bs->origin, goal->origin ) < VectorDistance( cs->bs->origin, move.endpos ) ) {
					vec3_t v1, v2;
					VectorSubtract( goal->origin, cs->bs->origin, v1 );
					VectorSubtract( goal->origin, move.endpos, v2 );
					VectorNormalize( v1 );
					VectorNormalize( v2 );
					if ( DotProduct( v1, v2 ) < 0 ) {
						// we went passed the goal, so assume we can reach it
						cs->blockedAvoidTime = level.time - 1;
						return;
					}
				}
			}

			// try and get them to move, in case we can't get around them
			blockEnt = -1;
			for ( i = 0; i < move.numtouch; i++ ) {
				if ( move.touchents[i] >= MAX_CLIENTS ) {
					if ( !Q_stricmp( g_entities[move.touchents[i]].classname, "script_mover" ) ) {
						// avoid script_mover's
						blockEnt = move.touchents[i];
					}
					// if we are close to the impact point, then avoid this entity
					else if ( VectorDistance( cs->bs->origin, move.endpos ) < 10 ) {
						//G_Printf("AI (%s) avoiding %s\n", g_entities[cs->entityNum].aiName, g_entities[move.touchents[i]].classname );
						blockEnt = move.touchents[i];
					}
					continue;
				}
				//
				ocs = AICast_GetCastState( move.touchents[i] );
				if ( !ocs->bs ) {
					blockEnt = move.touchents[i];
				}
				// reject this blocker if we are following or going to them
				else if ( cs->followEntity != ocs->entityNum ) {
					// if they are moving away from us already, let them go
					if ( VectorLength( ocs->bs->cur_ps.velocity ) > 10 ) {
						vec3_t v1, v2;

						VectorSubtract( ocs->bs->origin, cs->bs->origin, v2 );
						VectorNormalize( v2 );
						VectorNormalize2( ocs->bs->cur_ps.velocity, v1 );

						if ( DotProduct( v1, v2 ) > 0.0 ) {
							continue;
						}
					}
					//
					// if they recently were asked to avoid us, then they're probably not listening
					if ( ocs->obstructingTime > level.time - 500 ) {
						blockEnt = move.touchents[i];
					}
					//
					// if they are not avoiding, ignore
					if ( !( ocs->aiFlags & AIFL_NOAVOID ) ) {
						continue;
					}
					//
					// they should avoid us
					if ( ocs->leaderNum >= 0 ) {
						ogoal.entitynum = ocs->leaderNum;
						VectorCopy( g_entities[ocs->leaderNum].r.currentOrigin, ogoal.origin );
						if ( AICast_GetAvoid( ocs, &ogoal, ocs->obstructingPos, qfalse, cs->entityNum ) ) {
							// give them time to move somewhere else
							ocs->obstructingTime = level.time + 1000;
						} else {
							// make sure they don't call GetAvoid() for another few frames to let others avoid also
							ocs->obstructingTime = level.time - 1;
							blockEnt = move.touchents[i];
						}
					} else {
						if ( AICast_GetAvoid( ocs, NULL, ocs->obstructingPos, qfalse, cs->entityNum ) ) {
							// give them time to move somewhere else
							ocs->obstructingTime = level.time + 1000;
						} else {
							// make sure they don't call GetAvoid() for another few frames to let others avoid also
							ocs->obstructingTime = level.time - 1;
							blockEnt = move.touchents[i];
						}
					}
				}
			}

		} else {
			return;
		}

		if ( blockEnt < 0 ) {
			// nothing found to be worth avoding
			cs->blockedAvoidTime = level.time - 1;
			return;
		}

		// something is blocking our path
		if ( g_entities[blockEnt].aiName && g_entities[blockEnt].client ) {
			int oldId = cs->castScriptStatus.scriptId;
			AICast_ScriptEvent( cs, "blocked", g_entities[blockEnt].aiName );
			if ( oldId != cs->castScriptStatus.scriptId ) {
				// the script has changed, so assume the scripting is handling the avoidance
				return;
			}
		}

		// avoid geometry and props, but assume clients will get out the way
		if ( /*blockEnt > MAX_CLIENTS &&*/ AICast_GetAvoid( cs, goal, pos, qfalse, blockEnt ) ) {
			VectorSubtract( pos, cs->bs->cur_ps.origin, dir );
			VectorNormalize( dir );
			cs->blockedAvoidYaw = vectoyaw( dir );
			if ( blockEnt >= MAX_CLIENTS ) {
				cs->blockedAvoidTime = level.time + 100 + rand() % 200;
			} else {
				cs->blockedAvoidTime = level.time + 300 + rand() % 400;
			}
		} else {
			cs->blockedAvoidTime = level.time - 1;    // don't look again for another few frames
			return;
		}
	}

	VectorClear( pos );
	pos[YAW] = cs->blockedAvoidYaw;
	AngleVectors( pos, dir, NULL, NULL );

	if ( moveresult->flags & MOVERESULT_ONTOPOFOBSTACLE ) {
		trap_EA_Jump( cs->bs->entitynum );
	}

	trap_EA_Move( cs->bs->entitynum, dir, 200 ); //400);

	vectoangles( dir, cs->bs->ideal_viewangles );
	cs->bs->ideal_viewangles[2] *= 0.5;
}
const char *AIFunc_Heinrich_Earthquake( cast_state_t *cs ) {
	gentity_t   *ent = &g_entities[cs->entityNum];
	gentity_t   *enemy;
	cast_state_t *ecs;
	vec3_t enemyVec;
	float enemyDist, scale;
	trace_t *tr;

	cs->aiFlags |= AIFL_SPECIAL_FUNC;

	if ( cs->enemyNum < 0 ) {
		if ( !ent->client->ps.torsoTimer ) {
			return AIFunc_DefaultStart( cs );
		}
		return NULL;
	}

	enemy = &g_entities[cs->enemyNum];
	ecs = AICast_GetCastState( cs->enemyNum );

	VectorMA( enemy->r.currentOrigin, HEINRICH_STOMP_DELAY, enemy->client->ps.velocity, enemyVec );
	enemyDist = VectorDistance( ent->r.currentOrigin, enemyVec );

	if ( ent->client->ps.torsoTimer < 500 ) {
		int rnd;
		aicast_predictmove_t move;
		vec3_t vec;

		AICast_PredictMovement( ecs, 2, 0.5, &move, &g_entities[cs->enemyNum].client->pers.cmd, -1 );
		VectorSubtract( move.endpos, cs->bs->origin, vec );
		vec[2] = 0;
		enemyDist = VectorLength( vec );
		enemyDist -= g_entities[cs->enemyNum].r.maxs[0];
		enemyDist -= ent->r.maxs[0];
		//
		if ( enemyDist < 140 ) {
			// combo attack
			rnd = rand() % 3;
			switch ( rnd ) {
			case 0:
				return AIFunc_Heinrich_SwordSideSlashStart( cs );
			case 1:
				return AIFunc_Heinrich_SwordKnockbackStart( cs );
			case 2:
				return AIFunc_Heinrich_SwordLungeStart( cs );
			}
		} else {    // back to roaming
			ent->client->ps.legsTimer = 0;
			ent->client->ps.torsoTimer = 0;
			cs->castScriptStatus.scriptNoMoveTime = 0;
			AICast_Heinrich_Taunt( cs );
			return AIFunc_DefaultStart( cs );
		}
	}

	// time for the thump?
	if ( !( cs->aiFlags & AIFL_MISCFLAG1 ) ) {
		// face them
		AICast_AimAtEnemy( cs );
		// ready for damage?
		if ( cs->thinkFuncChangeTime < level.time - HEINRICH_STOMP_DELAY ) {
			cs->aiFlags |= AIFL_MISCFLAG1;
			// play the stomp sound
			G_AddEvent( ent, EV_GENERAL_SOUND, G_SoundIndex( aiDefaults[ent->aiCharacter].soundScripts[ORDERSDENYSOUNDSCRIPT] ) );
			// check for striking the player
			tr = CheckMeleeAttack( ent, 70, qfalse );
			// do melee damage
			if ( tr && ( tr->entityNum == cs->enemyNum ) ) {
				G_Damage( &g_entities[tr->entityNum], ent, ent, vec3_origin, tr->endpos, HEINRICH_STOMP_DAMAGE, 0, MOD_GAUNTLET );
			}
			// call the debris trigger
			AICast_ScriptEvent( cs, "trigger", "quake" );
		}
	}

	enemyDist = Distance( enemy->s.pos.trBase, ent->s.pos.trBase );

	// do the earthquake effects
	if ( cs->thinkFuncChangeTime < level.time - HEINRICH_STOMP_DELAY ) {
		// throw the player into the air, if they are on the ground
		if ( ( enemy->s.groundEntityNum != ENTITYNUM_NONE ) && enemyDist < HEINRICH_STOMP_RANGE ) {
			scale = 0.5 + 0.5 * ( (float)ent->client->ps.torsoTimer / 1000.0 );
			if ( scale > 1.0 ) {
				scale = 1.0;
			}
			VectorSubtract( ent->s.pos.trBase, enemy->s.pos.trBase, enemyVec );
			VectorScale( enemyVec, 2.0 * ( 0.6 + 0.5 * random() ) * scale * ( 0.6 + 0.6 * ( 1.0 - ( enemyDist / HEINRICH_STOMP_RANGE ) ) ), enemyVec );
			enemyVec[2] = scale * HEINRICH_STOMP_VELOCITY_Z * ( 1.0 - 0.5 * ( enemyDist / HEINRICH_STOMP_RANGE ) );
			// bounce the player using this velocity
			VectorAdd( enemy->client->ps.velocity, enemyVec, enemy->client->ps.velocity );
		}
	}

	return NULL;
}
Beispiel #3
0
/*
============
AICast_GetAvoid
============
*/
qboolean AICast_GetAvoid( cast_state_t *cs, bot_goal_t *goal, vec3_t outpos, qboolean reverse, int blockEnt ) {
	float yaw, oldyaw, distmoved, bestmoved, bestyaw;
	vec3_t bestpos;
	aicast_predictmove_t castmove;
	usercmd_t ucmd;
	qboolean enemyVisible;
	float angleDiff;
	// TTimo might be used uninitialized
	int starttraveltime = 0;
	int besttraveltime, traveltime;
	int invert;
	float inc;
	qboolean averting = qfalse;
	float maxYaw, simTime;
	static int lastTime;

	VectorCopy( vec3_origin, bestpos );

	//
	// if we are in the air, no chance of avoiding
	if ( cs->bs->cur_ps.groundEntityNum == ENTITYNUM_NONE && g_entities[cs->entityNum].waterlevel <= 1 ) {
		return qfalse;
	}
	//
	if ( cs->lastAvoid > level.time - rand() % 500 ) {
		return qfalse;
	}
	cs->lastAvoid = level.time + 50 + rand() % 500;
	//
	if ( lastTime == level.time ) {
		return qfalse;
	}
	lastTime = level.time;

	// if they have an enemy, and can currently see them, don't move out of their view
	enemyVisible =  ( cs->bs->enemy >= 0 ) &&
				   ( AICast_CheckAttack( cs, cs->bs->enemy, qfalse ) );
	//
	// look for a good direction to move out of the way
	bestmoved = 0;
	bestyaw = 360;
	besttraveltime = 9999999;
	if ( goal ) {
		starttraveltime = trap_AAS_AreaTravelTimeToGoalArea( cs->bs->areanum, cs->bs->origin, goal->areanum, cs->travelflags );
	}
	memcpy( &ucmd, &cs->bs->lastucmd, sizeof( usercmd_t ) );
	ucmd.forwardmove = 127;
	ucmd.rightmove = 0;
	ucmd.upmove = 0;
	if ( cs->dangerEntity >= 0 && cs->dangerEntityValidTime >= level.time ) {
		averting = qtrue;
	} else if ( !goal ) {
		averting = qtrue;   // not heading for a goal, so we must be getting out of someone's way
	}
	//
	maxYaw = 0;
	simTime = 1.2;
	//
	if ( averting ) {
		// avoiding danger, go anywhere!
		angleDiff = 300;
		inc = 60;
		invert = 1;
	} else {
		if ( level.time % 1000 < 500 ) {
			invert = 1;
		} else {
			invert = -1;
		}
		angleDiff = 140;
		inc = 35;
	}
	if ( blockEnt > aicast_maxclients ) {
		maxYaw = angleDiff;
		simTime = 0.5;
	}
	//
	for ( yaw = -angleDiff * invert; yaw*invert <= maxYaw; yaw += inc * invert ) {
		if ( !averting && !yaw ) {
			continue;
		}
		oldyaw = cs->bs->cur_ps.viewangles[YAW];
		cs->bs->cur_ps.viewangles[YAW] += yaw + reverse * 180;
		//
		ucmd.angles[YAW] = ANGLE2SHORT( AngleMod( cs->bs->cur_ps.viewangles[YAW] ) );
		//
		AICast_PredictMovement( cs, 5, 0.4, &castmove, &ucmd, -1 );
		// if we have a danger entity, try and get away from it at all costs
		if ( cs->dangerEntity >= 0 && cs->dangerEntityValidTime >= level.time ) {
			distmoved = Distance( castmove.endpos, cs->dangerEntityPos );
		} else if ( goal ) {
			//distmoved = 99999 - trap_AAS_AreaTravelTimeToGoalArea( BotPointAreaNum(castmove.endpos), castmove.endpos, goal->areanum, cs->travelflags );
			distmoved = 99999 - Distance( castmove.endpos, goal->origin );
		} else {
			distmoved = Distance( castmove.endpos, cs->bs->cur_ps.origin );
		}
		if (    ( distmoved > bestmoved )
				//&&	((cs->bs->origin[2] - castmove.endpos[2]) < 64)	// allow up, but not down (falling)
				&&  ( castmove.groundEntityNum != ENTITYNUM_NONE ) ) {
			// they all passed, check any other stuff
			if ( !enemyVisible || AICast_CheckAttackAtPos( cs->entityNum, cs->bs->enemy, castmove.endpos, qfalse, qfalse ) ) {
				if ( !goal || ( traveltime = trap_AAS_AreaTravelTimeToGoalArea( BotPointAreaNum( castmove.endpos ), castmove.endpos, goal->areanum, cs->travelflags ) ) < ( starttraveltime + 200 ) ) {
					bestyaw = yaw;
					bestmoved = distmoved;
					besttraveltime = traveltime;
					VectorCopy( castmove.endpos, bestpos );
				}
			}
		}
		//
		cs->bs->cur_ps.viewangles[YAW] = oldyaw;
	}
	//
	if ( bestmoved > 0 ) {
		VectorCopy( bestpos, outpos );
		return qtrue;
	} else {
		return qfalse;
	}

//G_Printf("GetAvoid: %i ms\n", -pretime + Sys_MilliSeconds() );
}
const char *AIFunc_Heinrich_SwordSideSlash( cast_state_t *cs ) {
	gentity_t *ent = &g_entities[cs->entityNum];
	trace_t *tr;
	vec3_t right, left;
	float enemyDist;
	aicast_predictmove_t move;
	vec3_t vec;
	cast_state_t *ecs;

	cs->aiFlags |= AIFL_SPECIAL_FUNC;

	if ( cs->enemyNum < 0 ) {
		if ( ent->client->ps.torsoTimer ) {
			return NULL;
		}
		return AIFunc_DefaultStart( cs );
	}

	ecs = AICast_GetCastState( cs->enemyNum );

	if ( ent->client->ps.torsoTimer < 500 ) {
		if ( !ent->client->ps.legsTimer ) {
			trap_EA_MoveForward( cs->entityNum );
		}
		ent->client->ps.legsTimer = 0;
		ent->client->ps.torsoTimer = 0;
		cs->castScriptStatus.scriptNoMoveTime = 0;
		AICast_Heinrich_Taunt( cs );
		return AIFunc_BattleChaseStart( cs );
	}

	// time for the melee?
	if ( cs->enemyNum >= 0 && !( cs->aiFlags & AIFL_MISCFLAG1 ) ) {
		// face them
		AICast_AimAtEnemy( cs );
		// keep checking for impact status
		tr = CheckMeleeAttack( ent, HEINRICH_SLASH_RANGE, qfalse );
		// ready for damage?
		if ( cs->thinkFuncChangeTime < level.time - HEINRICH_SLASH_DELAY ) {
			cs->aiFlags |= AIFL_MISCFLAG1;
			// do melee damage
			if ( tr && ( tr->entityNum == cs->enemyNum ) ) {
				AngleVectors( cs->viewangles, NULL, right, NULL );
				VectorNegate( right, left );
				G_Damage( &g_entities[tr->entityNum], ent, ent, left, tr->endpos, HEINRICH_SLASH_DAMAGE, 0, MOD_GAUNTLET );
				// sound
				G_AddEvent( ent, EV_GENERAL_SOUND, heinrichSoundIndex[HEINRICH_SWORDIMPACT] );
				// throw them in direction of impact
				left[2] = 0.5;
				VectorMA( g_entities[cs->enemyNum].client->ps.velocity, 400, left, g_entities[cs->enemyNum].client->ps.velocity );
			}
		}
	}

	// if they are outside range, move forward
	AICast_PredictMovement( ecs, 2, 0.3, &move, &g_entities[cs->enemyNum].client->pers.cmd, -1 );
	VectorSubtract( move.endpos, cs->bs->origin, vec );
	vec[2] = 0;
	enemyDist = VectorLength( vec );
	enemyDist -= g_entities[cs->enemyNum].r.maxs[0];
	enemyDist -= ent->r.maxs[0];
	if ( enemyDist > 30 ) {   // we can get closer
		if ( ent->client->ps.legsTimer ) {
			cs->castScriptStatus.scriptNoMoveTime = level.time + 100;
			ent->client->ps.legsTimer = 0;      // allow legs to move us
		}
		if ( cs->castScriptStatus.scriptNoMoveTime < level.time ) {
			trap_EA_MoveForward( cs->entityNum );
		}
	}

	return NULL;
}
/*
================
AIFunc_Helga_Melee
================
*/
const char *AIFunc_Helga_Melee( cast_state_t *cs ) {
	gentity_t *ent = &g_entities[cs->entityNum];
	gentity_t *enemy;
	cast_state_t *ecs;
	int hitDelay = -1, anim;
	trace_t tr;
	float enemyDist;
	aicast_predictmove_t move;
	vec3_t vec;

	cs->aiFlags |= AIFL_SPECIAL_FUNC;

	if ( !ent->client->ps.torsoTimer || !ent->client->ps.legsTimer ) {
		cs->aiFlags &= ~AIFL_SPECIAL_FUNC;
		return AIFunc_DefaultStart( cs );
	}

	if ( cs->enemyNum < 0 ) {
		ent->client->ps.legsTimer = 0;      // allow legs us to move
		ent->client->ps.torsoTimer = 0;     // allow legs us to move
		cs->aiFlags &= ~AIFL_SPECIAL_FUNC;
		return AIFunc_DefaultStart( cs );
	}

	ecs = AICast_GetCastState( cs->enemyNum );
	enemy = &g_entities[cs->enemyNum];

	anim = ( ent->client->ps.torsoAnim & ~ANIM_TOGGLEBIT ) - BG_AnimationIndexForString( "attack3", cs->entityNum );
	if ( anim < 0 || anim >= NUM_HELGA_ANIMS ) {
		// animation interupted
		cs->aiFlags &= ~AIFL_SPECIAL_FUNC;
		return AIFunc_DefaultStart( cs );
		//G_Error( "AIFunc_HelgaZombieMelee: helgaBoss using invalid or unknown attack anim" );
	}
	if ( cs->animHitCount < MAX_HELGA_IMPACTS && helgaHitTimes[anim][cs->animHitCount] >= 0 ) {

		// face them
		VectorCopy( cs->bs->origin, vec );
		vec[2] += ent->client->ps.viewheight;
		VectorSubtract( enemy->client->ps.origin, vec, vec );
		VectorNormalize( vec );
		vectoangles( vec, cs->ideal_viewangles );
		cs->ideal_viewangles[PITCH] = AngleNormalize180( cs->ideal_viewangles[PITCH] );

		// get hitDelay
		if ( !cs->animHitCount ) {
			hitDelay = helgaHitTimes[anim][cs->animHitCount];
		} else {
			hitDelay = helgaHitTimes[anim][cs->animHitCount] - helgaHitTimes[anim][cs->animHitCount - 1];
		}

		// check for inflicting damage
		if ( level.time - cs->weaponFireTimes[cs->weaponNum] > hitDelay ) {
			// do melee damage
			enemyDist = VectorDistance( enemy->r.currentOrigin, ent->r.currentOrigin );
			enemyDist -= g_entities[cs->enemyNum].r.maxs[0];
			enemyDist -= ent->r.maxs[0];
			if ( enemyDist < 10 + AICast_WeaponRange( cs, cs->weaponNum ) ) {
				trap_Trace( &tr, ent->r.currentOrigin, NULL, NULL, enemy->r.currentOrigin, ent->s.number, MASK_SHOT );
				if ( tr.entityNum == cs->enemyNum ) {
					G_Damage( &g_entities[tr.entityNum], ent, ent, vec3_origin, tr.endpos,
							  helgaHitDamage[anim], 0, MOD_GAUNTLET );
					G_AddEvent( enemy, EV_GENERAL_SOUND, G_SoundIndex( aiDefaults[ent->aiCharacter].soundScripts[STAYSOUNDSCRIPT] ) );
				}
			}
			cs->weaponFireTimes[cs->weaponNum] = level.time;
			cs->animHitCount++;
		}
	}

	// if they are outside range, move forward
	AICast_PredictMovement( ecs, 2, 0.3, &move, &g_entities[cs->enemyNum].client->pers.cmd, -1 );
	VectorSubtract( move.endpos, cs->bs->origin, vec );
	vec[2] = 0;
	enemyDist = VectorLength( vec );
	enemyDist -= g_entities[cs->enemyNum].r.maxs[0];
	enemyDist -= ent->r.maxs[0];
	if ( enemyDist > 8 ) {    // we can get closer
		//if (!ent->client->ps.legsTimer) {
		//	cs->castScriptStatus.scriptNoMoveTime = 0;
		trap_EA_MoveForward( cs->entityNum );
		//}
		//ent->client->ps.legsTimer = 0;		// allow legs us to move
	}

	return NULL;
}
/*
===============
AIFunc_LoperAttack2()

  Loper's leaping long range attack
===============
*/
char *AIFunc_LoperAttack2( cast_state_t *cs ) {
	gentity_t *ent;
	vec3_t vec;
	qboolean onGround = qfalse;
	//
	ent = &g_entities[cs->entityNum];
	//
	// are we waiting to inflict damage?
	if ( ( cs->weaponFireTimes[WP_MONSTER_ATTACK2] < level.time - 100 ) &&
		 ( cs->bs->cur_ps.groundEntityNum == ENTITYNUM_NONE ) ) {
		// ready to inflict damage?
		if ( cs->thinkFuncChangeTime < level.time - LOPER_LEAP_DELAY ) {
			// check for damage
			if ( //(tr = CheckMeleeAttack(&g_entities[cs->entityNum], LOPER_LEAP_RANGE, qtrue)) &&
				( G_RadiusDamage( cs->bs->origin, ent, LOPER_LEAP_DAMAGE, LOPER_LEAP_RANGE, ent, MOD_LOPER_LEAP ) ) ) {
				// draw the client-side lightning effect
				ent->client->ps.eFlags |= EF_MONSTER_EFFECT;
				// do the damage
				//G_Damage( &g_entities[tr->entityNum], ent, ent, vec3_origin, tr->endpos,
				//	LOPER_LEAP_DAMAGE, 0, MOD_LOPER_LEAP );
				G_Sound( &g_entities[cs->entityNum], level.loperZapSound );
				//cs->weaponFireTimes[WP_MONSTER_ATTACK2] = level.time;
				// TODO: client-side visual effect
				// TODO: throw them backwards (away from us)
			}
		}
	}
	//
	// landed?
	if ( cs->bs->cur_ps.groundEntityNum != ENTITYNUM_NONE ) {
		onGround = qtrue;
	} else {    // predict a landing
		aicast_predictmove_t move;
		float changeTime;
		AICast_PredictMovement( cs, 1, 0.2, &move, &cs->bs->lastucmd, cs->bs->enemy );
		if ( move.groundEntityNum != ENTITYNUM_NONE ) {
			onGround = qtrue;
		}
		//
		// adjust velocity
		VectorCopy( g_entities[cs->entityNum].s.pos.trDelta, vec );
		vec[2] = 0;
		VectorNormalize( vec );
		changeTime = 2.0 * ( 0.001 * ( level.time - cs->thinkFuncChangeTime ) );
		if ( changeTime > 1.0 ) {
			changeTime = 1.0;
		}
		VectorScale( vec, LOPER_LEAP_VELOCITY_START + changeTime * ( LOPER_LEAP_VELOCITY_END - LOPER_LEAP_VELOCITY_START ), vec );
		g_entities[cs->entityNum].s.pos.trDelta[0] = vec[0];
		g_entities[cs->entityNum].s.pos.trDelta[1] = vec[1];
	}
	//
	if ( onGround || ( cs->aiFlags & AIFL_LAND_ANIM_PLAYED ) ) {
		// if we just started the attack recently, we probably haven't had a chance to get airborne yet
		if ( cs->thinkFuncChangeTime < level.time - LOPER_LEAP_DELAY ) {
			// loper is back on ground, wait for animation to play out
			if ( !( cs->aiFlags & AIFL_LAND_ANIM_PLAYED ) ) {
				ent->client->ps.legsAnim =
					( ( ent->client->ps.legsAnim & ANIM_TOGGLEBIT ) ^ ANIM_TOGGLEBIT ) | LOPER_LAND_ANIM;
				ent->client->ps.legsTimer = LOPER_LAND_DURATION;
				//
				cs->aiFlags |= AIFL_LAND_ANIM_PLAYED;
				// TODO:play the landing thud
			}
			//
			if ( !ent->client->ps.legsTimer ) {   // we're done
				return AIFunc_DefaultStart( cs );
			}
			// keep moving slightly in our facing direction to simulate landing momentum
			AngleVectors( cs->bs->viewangles, vec, NULL, NULL );
			trap_EA_Move( cs->entityNum, vec, ( (float)ent->client->ps.legsTimer / (float)LOPER_LAND_DURATION ) * (float)LOPER_LEAP_LAND_MOMENTUM );
			return NULL;
		}
	}
	return NULL;
}
/*
================
AIFunc_WarriorZombieMelee
================
*/
char *AIFunc_WarriorZombieMelee( cast_state_t *cs ) {
	gentity_t *ent = &g_entities[cs->entityNum];
	int hitDelay = -1, anim;
	trace_t *tr;
	cast_state_t *ecs = AICast_GetCastState( cs->enemyNum );
	aicast_predictmove_t move;
	float enemyDist;

	if ( !ent->client->ps.torsoTimer ) {
		return AIFunc_DefaultStart( cs );
	}
	//
	if ( cs->enemyNum < 0 ) {
		return NULL;
	}
	if ( ecs ) {

		anim = ( ent->client->ps.torsoAnim & ~ANIM_TOGGLEBIT ) - BG_AnimationIndexForString( "attack1", cs->entityNum );
		if ( anim < 0 || anim >= NUM_WARRIOR_ANIMS ) {
			// animation interupted
			return AIFunc_DefaultStart( cs );
		}
		if ( warriorHitTimes[anim][cs->animHitCount] >= 0 && cs->animHitCount < 3 ) {

			if ( !cs->animHitCount ) {
				hitDelay = warriorHitTimes[anim][cs->animHitCount];
			} else {
				hitDelay = warriorHitTimes[anim][cs->animHitCount] - warriorHitTimes[anim][cs->animHitCount - 1];
			}

			// check for inflicting damage
			if ( level.time - cs->weaponFireTimes[cs->weaponNum] > hitDelay ) {
				// do melee damage
				if ( ( tr = CheckMeleeAttack( ent, 44, qfalse ) ) && ( tr->entityNum == cs->enemyNum ) ) {
					G_Damage( &g_entities[tr->entityNum], ent, ent, vec3_origin, tr->endpos,
							  warriorHitDamage[anim], 0, MOD_GAUNTLET );
					G_AddEvent( ent, EV_GENERAL_SOUND, G_SoundIndex( aiDefaults[ent->aiCharacter].soundScripts[STAYSOUNDSCRIPT] ) );
				} else {
					G_AddEvent( ent, EV_GENERAL_SOUND, G_SoundIndex( aiDefaults[ent->aiCharacter].soundScripts[FOLLOWSOUNDSCRIPT] ) );
				}
				cs->weaponFireTimes[cs->weaponNum] = level.time;
				cs->animHitCount++;
			}
		}
		// face them
		AICast_AimAtEnemy( cs );
		if ( anim < 3 ) { // back handed-swinging, dont allow legs to move
			// if they are outside range, move forward
			AICast_PredictMovement( ecs, 2, 0.5, &move, &g_entities[cs->enemyNum].client->pers.cmd, -1 );
			enemyDist = Distance( move.endpos, cs->bs->origin );
			enemyDist -= g_entities[cs->enemyNum].r.maxs[0];
			enemyDist -= ent->r.maxs[0];
			if ( enemyDist > 16 ) {   // we can get closer
				if ( ent->client->ps.legsTimer ) {
					ent->client->ps.legsTimer = 0;      // allow legs us to move
					if ( cs->castScriptStatus.scriptNoMoveTime < level.time + 200 ) { // dont move until the legs are done lerping out of attack anim
						cs->castScriptStatus.scriptNoMoveTime = level.time + 200;
					}
				}
				if ( cs->castScriptStatus.scriptNoMoveTime < level.time ) {
					trap_EA_MoveForward( cs->entityNum );
				}
			}
		}
	}

	return NULL;
}
/*
===============
AIFunc_LoperAttack2()

  Loper's leaping long range attack
===============
*/
char *AIFunc_LoperAttack2( cast_state_t *cs ) {
	gentity_t *ent;
	vec3_t vec;
	qboolean onGround = qfalse;
	//
	ent = &g_entities[cs->entityNum];
	//
	// are we waiting to inflict damage?
	if ( ( cs->enemyNum >= 0 ) && ( cs->weaponFireTimes[WP_MONSTER_ATTACK2] < level.time - 50 ) &&
		 ( cs->bs->cur_ps.groundEntityNum == ENTITYNUM_NONE ) ) {
		// ready to inflict damage?
		if ( cs->thinkFuncChangeTime < level.time - LOPER_LEAP_DELAY ) {
			// check for damage
			if ( VectorDistance( cs->bs->origin, g_entities[cs->enemyNum].client->ps.origin ) < LOPER_LEAP_RANGE ) {
				// draw the client-side lightning effect
				ent->client->ps.eFlags |= EF_MONSTER_EFFECT;
				// do the damage
				G_Damage( &g_entities[cs->enemyNum], ent, ent, vec3_origin, cs->bs->origin, LOPER_LEAP_DAMAGE, 0, MOD_LOPER_LEAP );
				G_Sound( &g_entities[cs->entityNum], level.loperZapSound );
				cs->weaponFireTimes[WP_MONSTER_ATTACK2] = level.time;
			}
		}
	}
	//
	// landed?
	if ( cs->bs->cur_ps.groundEntityNum != ENTITYNUM_NONE ) {
		onGround = qtrue;
	} else {    // predict a landing
		aicast_predictmove_t move;
		float changeTime;
		AICast_PredictMovement( cs, 1, 0.2, &move, &cs->lastucmd, cs->enemyNum );
		if ( move.groundEntityNum != ENTITYNUM_NONE ) {
			onGround = qtrue;
		}
		//
		// adjust velocity
		VectorCopy( cs->loperLeapVel, vec );
		vec[2] = 0;
		VectorNormalize( vec );
		changeTime = 2.0 * ( 0.001 * ( level.time - cs->thinkFuncChangeTime ) );
		if ( changeTime > 1.0 ) {
			changeTime = 1.0;
		}
		VectorScale( vec, LOPER_LEAP_VELOCITY_START + changeTime * ( LOPER_LEAP_VELOCITY_END - LOPER_LEAP_VELOCITY_START ), vec );
		g_entities[cs->entityNum].s.pos.trDelta[0] = vec[0];
		g_entities[cs->entityNum].s.pos.trDelta[1] = vec[1];
	}
	//
	if ( onGround || ( cs->aiFlags & AIFL_LAND_ANIM_PLAYED ) ) {
		// if we just started the attack recently, we probably haven't had a chance to get airborne yet
		if ( cs->thinkFuncChangeTime < level.time - LOPER_LEAP_DELAY ) {
			// loper is back on ground, wait for animation to play out
			if ( !( cs->aiFlags & AIFL_LAND_ANIM_PLAYED ) ) {
				BG_PlayAnimName( &ent->client->ps, "legs_extra4", ANIM_BP_LEGS, qtrue, qfalse, qtrue );
				//
				cs->aiFlags |= AIFL_LAND_ANIM_PLAYED;
				// TODO:play the landing thud
			}
			//
			if ( ent->client->ps.legsTimer < 800 ) {  // we're done
				ent->client->ps.legsTimer = 0;
				return AIFunc_DefaultStart( cs );
			}
			// keep moving slightly in our facing direction to simulate landing momentum
			AngleVectors( cs->viewangles, vec, NULL, NULL );
			trap_EA_Move( cs->entityNum, vec, ( (float)ent->client->ps.legsTimer / (float)LOPER_LAND_DURATION ) * (float)LOPER_LEAP_LAND_MOMENTUM );
			return NULL;
		}
	}
	ent->client->ps.legsTimer = 500;    // stay on this until landing
	return NULL;
}