/* =======================================================================
 *
 * Stalker.
 *
 * =======================================================================
 */

#include "../../header/local.h"
#include "stalker.h"
#include <math.h>

static int sound_pain;
static int sound_die;
static int sound_sight;
static int sound_punch_hit1;
static int sound_punch_hit2;
static int sound_idle;

int stalker_do_pounce(edict_t *self, vec3_t dest);
void stalker_stand(edict_t *self);
void stalker_run(edict_t *self);
void stalker_walk(edict_t *self);
void stalker_jump(edict_t *self);
void stalker_dodge_jump(edict_t *self);
void stalker_swing_check_l(edict_t *self);
void stalker_swing_check_r(edict_t *self);
void stalker_swing_attack(edict_t *self);
void stalker_jump_straightup(edict_t *self);
void stalker_jump_wait_land(edict_t *self);
void stalker_false_death(edict_t *self);
void stalker_false_death_start(edict_t *self);
qboolean stalker_ok_to_transition(edict_t *self);

#define STALKER_ON_CEILING(ent) (ent->gravityVector[2] > 0 ? 1 : 0)

#define PI 3.14159
#define RAD2DEG(x) (x * (float)180.0 / (float)PI)
#define DEG2RAD(x) (x * (float)PI / (float)180.0)
#define FAUX_GRAVITY 800.0

extern qboolean SV_PointCloseEnough(edict_t *ent, vec3_t goal, float dist);
extern void drawbbox(edict_t *self);

qboolean
stalker_ok_to_transition(edict_t *self)
{
	trace_t trace;
	vec3_t pt, start;
	float max_dist;
	float margin;
	float end_height;

	if (!self)
	{
		return false;
	}

	if (STALKER_ON_CEILING(self))
	{
		max_dist = -384;
		margin = self->mins[2] - 8;
	}
	else
	{
		/* her stalkers are just better */
		if (self->monsterinfo.aiflags & AI_SPAWNED_WIDOW)
		{
			max_dist = 256;
		}
		else
		{
			max_dist = 180;
		}

		margin = self->maxs[2] + 8;
	}

	VectorCopy(self->s.origin, pt);
	pt[2] += max_dist;
	trace = gi.trace(self->s.origin, self->mins, self->maxs,
			pt, self, MASK_MONSTERSOLID);

	if ((trace.fraction == 1.0) ||
		!(trace.contents & CONTENTS_SOLID) ||
		(trace.ent != world))
	{
		if (STALKER_ON_CEILING(self))
		{
			if (trace.plane.normal[2] < 0.9)
			{
				return false;
			}
		}
		else
		{
			if (trace.plane.normal[2] > -0.9)
			{
				return false;
			}
		}
	}

	end_height = trace.endpos[2];

	/* check the four corners, tracing only to the
	   endpoint of the center trace (vertically). */
	pt[0] = self->absmin[0];
	pt[1] = self->absmin[1];
	pt[2] = trace.endpos[2] + margin; /* give a little margin of error to allow slight inclines */
	VectorCopy(pt, start);
	start[2] = self->s.origin[2];
	trace = gi.trace(start, vec3_origin, vec3_origin, pt, self, MASK_MONSTERSOLID);

	if ((trace.fraction == 1.0) || !(trace.contents & CONTENTS_SOLID) ||
		(trace.ent != world))
	{
		return false;
	}

	if (fabsf(end_height + margin - trace.endpos[2]) > 8)
	{
		return false;
	}

	pt[0] = self->absmax[0];
	pt[1] = self->absmin[1];
	VectorCopy(pt, start);
	start[2] = self->s.origin[2];
	trace = gi.trace(start, vec3_origin, vec3_origin, pt, self, MASK_MONSTERSOLID);

	if ((trace.fraction == 1.0) || !(trace.contents & CONTENTS_SOLID) ||
		(trace.ent != world))
	{
		return false;
	}

	if (fabsf(end_height + margin - trace.endpos[2]) > 8)
	{
		return false;
	}

	pt[0] = self->absmax[0];
	pt[1] = self->absmax[1];
	VectorCopy(pt, start);
	start[2] = self->s.origin[2];
	trace = gi.trace(start, vec3_origin, vec3_origin, pt, self, MASK_MONSTERSOLID);

	if ((trace.fraction == 1.0) || !(trace.contents & CONTENTS_SOLID) || (trace.ent != world))
	{
		return false;
	}

	if (fabsf(end_height + margin - trace.endpos[2]) > 8)
	{
		return false;
	}

	pt[0] = self->absmin[0];
	pt[1] = self->absmax[1];
	VectorCopy(pt, start);
	start[2] = self->s.origin[2];
	trace = gi.trace(start, vec3_origin, vec3_origin, pt, self, MASK_MONSTERSOLID);

	if ((trace.fraction == 1.0) || !(trace.contents & CONTENTS_SOLID) || (trace.ent != world))
	{
		return false;
	}

	if (fabsf(end_height + margin - trace.endpos[2]) > 8)
	{
		return false;
	}

	return true;
}

void
stalker_sight(edict_t *self, edict_t *other /* unused */)
{
	if (!self)
	{
		return;
	}

	gi.sound(self, CHAN_WEAPON, sound_sight, 1, ATTN_NORM, 0);
}

void
stalker_idle_noise(edict_t *self)
{
	if (!self)
	{
		return;
	}

	gi.sound(self, CHAN_WEAPON, sound_idle, 0.5, ATTN_IDLE, 0);
}

static mframe_t stalker_frames_idle[] = {
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},

	{ai_stand, 0, NULL},
	{ai_stand, 0, stalker_idle_noise},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},

	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},

	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},

	{ai_stand, 0, NULL}
};

mmove_t stalker_move_idle = {
	FRAME_idle01,
   	FRAME_idle21,
   	stalker_frames_idle,
   	stalker_stand
};

static mframe_t stalker_frames_idle2[] = {
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},

	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},

	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL}
};

mmove_t stalker_move_idle2 = {
	FRAME_idle201,
   	FRAME_idle213,
   	stalker_frames_idle2,
   	stalker_stand
};

void
stalker_idle(edict_t *self)
{
	if (!self)
	{
		return;
	}

	if (random() < 0.35)
	{
		self->monsterinfo.currentmove = &stalker_move_idle;
	}
	else
	{
		self->monsterinfo.currentmove = &stalker_move_idle2;
	}
}

static mframe_t stalker_frames_stand[] = {
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},

	{ai_stand, 0, NULL},
	{ai_stand, 0, stalker_idle_noise},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},

	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},

	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},
	{ai_stand, 0, NULL},

	{ai_stand, 0, NULL}
};

mmove_t stalker_move_stand = {
	FRAME_idle01,
   	FRAME_idle21,
   	stalker_frames_stand,
   	stalker_stand
};

void
stalker_stand(edict_t *self)
{
	if (!self)
	{
		return;
	}

	if (random() < 0.25)
	{
		self->monsterinfo.currentmove = &stalker_move_stand;
	}
	else
	{
		self->monsterinfo.currentmove = &stalker_move_idle2;
	}
}

static mframe_t stalker_frames_run[] = {
	{ai_run, 13, NULL},
	{ai_run, 17, NULL},
	{ai_run, 21, NULL},
	{ai_run, 18, NULL}
};

mmove_t stalker_move_run = {
	FRAME_run01,
   	FRAME_run04,
   	stalker_frames_run,
   	NULL
};

void
stalker_run(edict_t *self)
{
	if (!self)
	{
		return;
	}

	if (self->monsterinfo.aiflags & AI_STAND_GROUND)
	{
		self->monsterinfo.currentmove = &stalker_move_stand;
	}
	else
	{
		self->monsterinfo.currentmove = &stalker_move_run;
	}
}

static mframe_t stalker_frames_walk[] = {
	{ai_walk, 4, NULL},
	{ai_walk, 6, NULL},
	{ai_walk, 8, NULL},
	{ai_walk, 5, NULL},

	{ai_walk, 4, NULL},
	{ai_walk, 6, NULL},
	{ai_walk, 8, NULL},
	{ai_walk, 4, NULL}
};

mmove_t stalker_move_walk = {
	FRAME_walk01,
   	FRAME_walk08,
   	stalker_frames_walk,
   	stalker_walk
};

void
stalker_walk(edict_t *self)
{
	if (!self)
	{
		return;
	}

	self->monsterinfo.currentmove = &stalker_move_walk;
}

static mframe_t stalker_frames_reactivate[] = {
	{ai_move, 0, NULL},
	{ai_move, 0, NULL},
	{ai_move, 0, NULL},
	{ai_move, 0, NULL}
};

mmove_t stalker_move_false_death_end = {
	FRAME_reactive01,
   	FRAME_reactive04,
   	stalker_frames_reactivate,
   	stalker_run
};

void
stalker_reactivate(edict_t *self)
{
	if (!self)
	{
		return;
	}

	self->monsterinfo.aiflags &= ~AI_STAND_GROUND;
	self->monsterinfo.currentmove = &stalker_move_false_death_end;
}

void
stalker_heal(edict_t *self)
{
	if (!self)
	{
		return;
	}

	if (skill->value == SKILL_HARD)
	{
		self->health += 2;
	}
	else if (skill->value == SKILL_HARDPLUS)
	{
		self->health += 3;
	}
	else
	{
		self->health++;
	}

	if (self->health > (self->max_health / 2))
	{
		self->s.skinnum = 0;
	}

	if (self->health >= self->max_health)
	{
		self->health = self->max_health;
		stalker_reactivate(self);
	}
}

static mframe_t stalker_frames_false_death[] = {
	{ai_move, 0, stalker_heal},
	{ai_move, 0, stalker_heal},
	{ai_move, 0, stalker_heal},
	{ai_move, 0, stalker_heal},
	{ai_move, 0, stalker_heal},

	{ai_move, 0, stalker_heal},
	{ai_move, 0, stalker_heal},
	{ai_move, 0, stalker_heal},
	{ai_move, 0, stalker_heal},
	{ai_move, 0, stalker_heal}
};

mmove_t stalker_move_false_death = {
	FRAME_twitch01,
   	FRAME_twitch10,
   	stalker_frames_false_death,
	stalker_false_death
};

void
stalker_false_death(edict_t *self)
{
	if (!self)
	{
		return;
	}

	self->monsterinfo.currentmove = &stalker_move_false_death;
}

static mframe_t stalker_frames_false_death_start[] = {
	{ai_move, 0, NULL},
	{ai_move, 0, NULL},
	{ai_move, 0, NULL},
	{ai_move, 0, NULL},
	{ai_move, 0, NULL},

	{ai_move, 0, NULL},
	{ai_move, 0, NULL},
	{ai_move, 0, NULL},
	{ai_move, 0, NULL},
};

mmove_t stalker_move_false_death_start = {
	FRAME_death01,
   	FRAME_death09,
   	stalker_frames_false_death_start,
	stalker_false_death
};

void
stalker_false_death_start(edict_t *self)
{
	if (!self)
	{
		return;
	}

	self->s.angles[2] = 0;
	VectorSet(self->gravityVector, 0, 0, -1);

	self->monsterinfo.aiflags |= AI_STAND_GROUND;
	self->monsterinfo.currentmove = &stalker_move_false_death_start;
}

static mframe_t stalker_frames_pain[] = {
	{ai_move, 0, NULL},
	{ai_move, 0, NULL},
	{ai_move, 0, NULL},
	{ai_move, 0, NULL}
};

mmove_t stalker_move_pain = {
	FRAME_pain01,
   	FRAME_pain04,
   	stalker_frames_pain,
   	stalker_run
};

void
stalker_pain(edict_t *self, edict_t *other /* unused */, float kick, int damage)
{
	if (!self)
	{
		return;
	}

	if (self->deadflag == DEAD_DEAD)
	{
		return;
	}

	if (self->health < (self->max_health / 2))
	{
		self->s.skinnum = 1;
	}

	if (skill->value == SKILL_HARDPLUS)
	{
		return; /* no pain anims in nightmare */
	}

	if (self->groundentity == NULL)
	{
		return;
	}

	/* if we're reactivating or false dying, ignore the pain. */
	if ((self->monsterinfo.currentmove == &stalker_move_false_death_end) ||
		(self->monsterinfo.currentmove == &stalker_move_false_death_start))
	{
		return;
	}

	if (self->monsterinfo.currentmove == &stalker_move_false_death)
	{
		stalker_reactivate(self);
		return;
	}

	if ((self->health > 0) && (self->health < (self->max_health / 4)))
	{
		if (random() < (0.2 * skill->value))
		{
			if (!STALKER_ON_CEILING(self) || stalker_ok_to_transition(self))
			{
				stalker_false_death_start(self);
				return;
			}
		}
	}

	if (level.time < self->pain_debounce_time)
	{
		return;
	}

	self->pain_debounce_time = level.time + 3;

	if (damage > 10) /* don't react unless the damage was significant */
	{
		/* stalker should dodge jump periodically to help avoid damage. */
		if (self->groundentity && (random() < 0.5))
		{
			stalker_dodge_jump(self);
		}
		else
		{
			self->monsterinfo.currentmove = &stalker_move_pain;
		}

		gi.sound(self, CHAN_WEAPON, sound_pain, 1, ATTN_NORM, 0);
	}
}

void
stalker_shoot_attack(edict_t *self)
{
	vec3_t offset, start, f, r, dir;
	vec3_t end;
	float time, dist;
	trace_t trace;

	if (!self)
	{
		return;
	}

	if (!has_valid_enemy(self))
	{
		return;
	}

	if (self->groundentity && (random() < 0.33))
	{
		VectorSubtract(self->enemy->s.origin, self->s.origin, dir);
		dist = VectorLength(dir);

		if ((dist > 256) || (random() < 0.5))
		{
			stalker_do_pounce(self, self->enemy->s.origin);
		}
		else
		{
			stalker_jump_straightup(self);
		}
	}

	AngleVectors(self->s.angles, f, r, NULL);
	VectorSet(offset, 24, 0, 6);
	G_ProjectSource(self->s.origin, offset, f, r, start);

	VectorSubtract(self->enemy->s.origin, start, dir);

	if (random() < (0.20 + 0.1 * skill->value))
	{
		dist = VectorLength(dir);
		time = dist / 1000;
		VectorMA(self->enemy->s.origin, time, self->enemy->velocity, end);
		VectorSubtract(end, start, dir);
	}
	else
	{
		VectorCopy(self->enemy->s.origin, end);
	}

	trace = gi.trace(start, vec3_origin, vec3_origin, end, self, MASK_SHOT);

	if ((trace.ent == self->enemy) || (trace.ent == world))
	{
		monster_fire_blaster2(self, start, dir, 15, 800, MZ2_STALKER_BLASTER, EF_BLASTER);
	}
}

void
stalker_shoot_attack2(edict_t *self)
{
	if (!self)
	{
		return;
	}

	if (random() < (0.4 + (0.1 * (float)skill->value)))
	{
		stalker_shoot_attack(self);
	}
}

static mframe_t stalker_frames_shoot[] = {
	{ai_charge, 13, NULL},
	{ai_charge, 17, stalker_shoot_attack},
	{ai_charge, 21, NULL},
	{ai_charge, 18, stalker_shoot_attack2}
};

mmove_t stalker_move_shoot = {
	FRAME_run01,
   	FRAME_run04,
   	stalker_frames_shoot,
   	stalker_run
};

void
stalker_attack_ranged(edict_t *self)
{
	if (!self)
	{
		return;
	}

	if (!has_valid_enemy(self))
	{
		return;
	}

	/* circle strafe stuff */
	if (random() > (1.0 - (0.5 / (float)(skill->value))))
	{
		self->monsterinfo.attack_state = AS_STRAIGHT;
	}
	else
	{
		if (random() <= 0.5) /* switch directions */
		{
			self->monsterinfo.lefty = 1 - self->monsterinfo.lefty;
		}

		self->monsterinfo.attack_state = AS_SLIDING;
	}

	self->monsterinfo.currentmove = &stalker_move_shoot;
}

void
stalker_swing_attack(edict_t *self)
{
	vec3_t aim;

	if (!self)
	{
		return;
	}

	VectorSet(aim, MELEE_DISTANCE, 0, 0);

	if (fire_hit(self, aim, (5 + (rand() % 5)), 50))
	{
		if (self->s.frame < FRAME_attack08)
		{
			gi.sound(self, CHAN_WEAPON, sound_punch_hit2, 1, ATTN_NORM, 0);
		}
		else
		{
			gi.sound(self, CHAN_WEAPON, sound_punch_hit1, 1, ATTN_NORM, 0);
		}
	}
}

static mframe_t stalker_frames_swing_l[] = {
	{ai_charge, 2, NULL},
	{ai_charge, 4, NULL},
	{ai_charge, 6, NULL},
	{ai_charge, 10, NULL},
	{ai_charge, 5, stalker_swing_attack},
	{ai_charge, 5, NULL},
	{ai_charge, 5, NULL},
	{ai_charge, 5, NULL} /* stalker_swing_check_l */
};

mmove_t stalker_move_swing_l = {
	FRAME_attack01,
   	FRAME_attack08,
   	stalker_frames_swing_l,
   	stalker_run
};

static mframe_t stalker_frames_swing_r[] = {
	{ai_charge, 4, NULL},
	{ai_charge, 6, NULL},
	{ai_charge, 6, stalker_swing_attack},
	{ai_charge, 10, NULL},
	{ai_charge, 5, NULL} /* stalker_swing_check_r */
};

mmove_t stalker_move_swing_r = {
	FRAME_attack11,
   	FRAME_attack15,
   	stalker_frames_swing_r,
   	stalker_run
};

void
stalker_attack_melee(edict_t *self)
{
	if (!self)
	{
		return;
	}

	if (!has_valid_enemy(self))
	{
		return;
	}

	if (random() < 0.5)
	{
		self->monsterinfo.currentmove = &stalker_move_swing_l;
	}
	else
	{
		self->monsterinfo.currentmove = &stalker_move_swing_r;
	}
}

void
calcJumpAngle(vec3_t start, vec3_t end, float velocity, vec3_t angles)
{
	float distV, distH;
	float one, cosU;
	float l, U;
	vec3_t dist;

	VectorSubtract(end, start, dist);
	distH = (float)sqrt(dist[0] * dist[0] + dist[1] * dist[1]);
	distV = dist[2];

	if (distV < 0)
	{
		distV = 0 - distV;
	}

	if (distV)
	{
		l = (float)sqrt(distH * distH + distV * distV);
		U = (float)atan(distV / distH);

		if (dist[2] > 0)
		{
			U = (float)0.0 - U;
		}

		angles[2] = 0.0;

		cosU = (float)cos(U);
		one = l * FAUX_GRAVITY * (cosU * cosU);
		one = one / (velocity * velocity);
		one = one - (float)sin(U);
		angles[0] = (float)asin(one);

		if (isnan(angles[0]))
		{
			angles[2] = 1.0;
		}

		angles[1] = (float)PI - angles[0];

		if (isnan(angles[1]))
		{
			angles[2] = 1.0;
		}

		angles[0] = RAD2DEG((angles[0] - U) / 2.0);
		angles[1] = RAD2DEG((angles[1] - U) / 2.0);
	}
	else
	{
		l = (float)sqrt(distH * distH + distV * distV);

		angles[2] = 0.0;

		one = l * FAUX_GRAVITY;
		one = one / (velocity * velocity);
		angles[0] = (float)asin(one);

		if (isnan(angles[0]))
		{
			angles[2] = 1.0;
		}

		angles[1] = (float)PI - angles[0];

		if (isnan(angles[1]))
		{
			angles[2] = 1.0;
		}

		angles[0] = RAD2DEG((angles[0]) / 2.0);
		angles[1] = RAD2DEG((angles[1]) / 2.0);
	}
}

int
stalker_check_lz(edict_t *self, edict_t *target, vec3_t dest)
{
	vec3_t jumpLZ;

	if (!self || !target)
	{
		return 0;
	}

	if ((gi.pointcontents(dest) & MASK_WATER) || (target->waterlevel))
	{
		return false;
	}

	if (!target->groundentity)
	{
		return false;
	}

	/* check under the player's four corners
	   if they're not solid, bail. */
	jumpLZ[0] = self->enemy->mins[0];
	jumpLZ[1] = self->enemy->mins[1];
	jumpLZ[2] = self->enemy->mins[2] - 0.25;

	if (!(gi.pointcontents(jumpLZ) & MASK_SOLID))
	{
		return false;
	}

	jumpLZ[0] = self->enemy->maxs[0];
	jumpLZ[1] = self->enemy->mins[1];

	if (!(gi.pointcontents(jumpLZ) & MASK_SOLID))
	{
		return false;
	}

	jumpLZ[0] = self->enemy->maxs[0];
	jumpLZ[1] = self->enemy->maxs[1];

	if (!(gi.pointcontents(jumpLZ) & MASK_SOLID))
	{
		return false;
	}

	jumpLZ[0] = self->enemy->mins[0];
	jumpLZ[1] = self->enemy->maxs[1];

	if (!(gi.pointcontents(jumpLZ) & MASK_SOLID))
	{
		return false;
	}

	return true;
}

int
stalker_do_pounce(edict_t *self, vec3_t dest)
{
	vec3_t forward, right;
	vec3_t dist;
	vec_t length;
	vec3_t jumpAngles;
	vec3_t jumpLZ;
	float velocity = 400.1;
	trace_t trace;
	int preferHighJump;

	if (!self)
	{
		return 0;
	}

	/* don't pounce when we're on the ceiling */
	if (STALKER_ON_CEILING(self))
	{
		return false;
	}

	if (!stalker_check_lz(self, self->enemy, dest))
	{
		return false;
	}

	VectorSubtract(dest, self->s.origin, dist);

	/* make sure we're pointing in that direction 15deg margin of error. */
	vectoangles2(dist, jumpAngles);

	if (fabsf(jumpAngles[YAW] - self->s.angles[YAW]) > 45)
	{
		return false; /* not facing the player... */
	}

	self->ideal_yaw = jumpAngles[YAW];
	M_ChangeYaw(self);

	length = VectorLength(dist);

	if (length > 450)
	{
		return false; /* can't jump that far... */
	}

	VectorCopy(dest, jumpLZ);

	preferHighJump = 0;

	/* if we're having to jump up a distance, jump a little too high to compensate. */
	if (dist[2] >= 32.0)
	{
		preferHighJump = 1;
		jumpLZ[2] += 32;
	}

	trace = gi.trace(self->s.origin, vec3_origin, vec3_origin, dest,
			self, MASK_MONSTERSOLID);

	if ((trace.fraction < 1) && (trace.ent != self->enemy))
	{
		preferHighJump = 1;
	}

	/* find a valid angle/velocity combination */
	while (velocity <= 800)
	{
		calcJumpAngle(self->s.origin, jumpLZ, velocity, jumpAngles);

		if ((!isnan(jumpAngles[0])) || (!isnan(jumpAngles[1])))
		{
			break;
		}

		velocity += 200;
	}

	if (!preferHighJump && (!isnan(jumpAngles[0])))
	{
		AngleVectors(self->s.angles, forward, right, NULL);
		VectorNormalize(forward);

		VectorScale(forward, velocity * cos(DEG2RAD(jumpAngles[0])), self->velocity);
		self->velocity[2] = velocity * sin(DEG2RAD(jumpAngles[0])) + (0.5 * sv_gravity->value * FRAMETIME);
		return 1;
	}

	if (!isnan(jumpAngles[1]))
	{
		AngleVectors(self->s.angles, forward, right, NULL);
		VectorNormalize(forward);

		VectorScale(forward, velocity * cos(DEG2RAD(jumpAngles[1])), self->velocity);
		self->velocity[2] = velocity * sin(DEG2RAD(jumpAngles[1])) + (0.5 * sv_gravity->value * FRAMETIME);
		return 1;
	}

	return 0;
}

void
stalker_jump_straightup(edict_t *self)
{
	if (!self)
	{
		return;
	}

	if (self->deadflag == DEAD_DEAD)
	{
		return;
	}

	if (STALKER_ON_CEILING(self))
	{
		if (stalker_ok_to_transition(self))
		{
			self->gravityVector[2] = -1;
			self->s.angles[2] += 180.0;

			if (self->s.angles[2] > 360.0)
			{
				self->s.angles[2] -= 360.0;
			}

			self->groundentity = NULL;
		}
	}
	else if (self->groundentity) /* make sure we're standing on SOMETHING... */
	{
		self->velocity[0] += ((random() * 10) - 5);
		self->velocity[1] += ((random() * 10) - 5);
		self->velocity[2] += -400 * self->gravityVector[2];

		if (stalker_ok_to_transition(self))
		{
			self->gravityVector[2] = 1;
			self->s.angles[2] = 180.0;
			self->groundentity = NULL;
		}
	}
}

static mframe_t stalker_frames_jump_straightup[] = {
	{ai_move, 1, stalker_jump_straightup},
	{ai_move, 1, stalker_jump_wait_land},
	{ai_move, -1, NULL},
	{ai_move, -1, NULL}
};

mmove_t stalker_move_jump_straightup = {
	FRAME_jump04,
   	FRAME_jump07,
   	stalker_frames_jump_straightup,
   	stalker_run
};

void
stalker_dodge_jump(edict_t *self)
{
	if (!self)
	{
		return;
	}

	self->monsterinfo.currentmove = &stalker_move_jump_straightup;
}

static mframe_t stalker_frames_dodge_run[] = {
	{ai_run, 13, NULL},
	{ai_run, 17, NULL},
	{ai_run, 21, NULL},
	{ai_run, 18, monster_done_dodge}
};

mmove_t stalker_move_dodge_run = {
	FRAME_run01,
   	FRAME_run04,
   	stalker_frames_dodge_run,
   	NULL
};

void
stalker_dodge(edict_t *self, edict_t *attacker, float eta, trace_t *tr /* unused */)
{
	if (!self || !attacker)
	{
		return;
	}

	if (!self->groundentity || (self->health <= 0))
	{
		return;
	}

	if (!self->enemy)
	{
		self->enemy = attacker;
		FoundTarget(self);
		return;
	}

	if ((eta < 0.1) || (eta > 5))
	{
		return;
	}

	/* this will override the foundtarget call of stalker_run */
	stalker_dodge_jump(self);
}

void
stalker_jump_down(edict_t *self)
{
	vec3_t forward, up;

	if (!self)
	{
		return;
	}

	monster_jump_start(self);

	AngleVectors(self->s.angles, forward, NULL, up);
	VectorMA(self->velocity, 100, forward, self->velocity);
	VectorMA(self->velocity, 300, up, self->velocity);
}

void
stalker_jump_up(edict_t *self)
{
	vec3_t forward, up;

	if (!self)
	{
		return;
	}

	monster_jump_start(self);

	AngleVectors(self->s.angles, forward, NULL, up);
	VectorMA(self->velocity, 200, forward, self->velocity);
	VectorMA(self->velocity, 450, up, self->velocity);
}

void
stalker_jump_wait_land(edict_t *self)
{
	if (!self)
	{
		return;
	}

	if ((random() < (0.3 + (0.1 * (float)(skill->value)))) &&
		(level.time >= self->monsterinfo.attack_finished))
	{
		self->monsterinfo.attack_finished = level.time + 0.3;
		stalker_shoot_attack(self);
	}

	if (self->groundentity == NULL)
	{
		self->gravity = 1.3;
		self->monsterinfo.nextframe = self->s.frame;

		if (monster_jump_finished(self))
		{
			self->gravity = 1;
			self->monsterinfo.nextframe = self->s.frame + 1;
		}
	}
	else
	{
		self->gravity = 1;
		self->monsterinfo.nextframe = self->s.frame + 1;
	}
}

static mframe_t stalker_frames_jump_up[] = {
	{ai_move, -8, NULL},
	{ai_move, -8, NULL},
	{ai_move, -8, NULL},
	{ai_move, -8, NULL},

	{ai_move, 0, stalker_jump_up},
	{ai_move, 0, stalker_jump_wait_land},
	{ai_move, 0, NULL}
};

mmove_t stalker_move_jump_up = {
	FRAME_jump01,
   	FRAME_jump07,
   	stalker_frames_jump_up,
   	stalker_run
};

static mframe_t stalker_frames_jump_down[] = {
	{ai_move, 0, NULL},
	{ai_move, 0, NULL},
	{ai_move, 0, NULL},
	{ai_move, 0, NULL},

	{ai_move, 0, stalker_jump_down},
	{ai_move, 0, stalker_jump_wait_land},
	{ai_move, 0, NULL}
};

mmove_t stalker_move_jump_down = {
	FRAME_jump01,
   	FRAME_jump07,
   	stalker_frames_jump_down,
   	stalker_run
};

void
stalker_jump(edict_t *self)
{
	if (!self)
	{
		return;
	}

	if (!self->enemy)
	{
		return;
	}

	if (self->enemy->absmin[2] >= self->absmin[2])
	{
		self->monsterinfo.currentmove = &stalker_move_jump_up;
	}
	else
	{
		self->monsterinfo.currentmove = &stalker_move_jump_down;
	}
}

qboolean
stalker_blocked(edict_t *self, float dist)
{
	qboolean onCeiling;

	if (!self)
	{
		return false;
	}

	if (!has_valid_enemy(self))
	{
		return false;
	}

	onCeiling = false;

	if (self->gravityVector[2] > 0)
	{
		onCeiling = true;
	}

	if (!onCeiling)
	{
		if (visible(self, self->enemy))
		{
			stalker_do_pounce(self, self->enemy->s.origin);
			return true;
		}

		if (blocked_checkjump(self, dist, 256, 68))
		{
			stalker_jump(self);
			return true;
		}

		if (blocked_checkplat(self, dist))
		{
			return true;
		}
	}
	else
	{
		if (stalker_ok_to_transition(self))
		{
			self->gravityVector[2] = -1;
			self->s.angles[2] += 180.0;

			if (self->s.angles[2] > 360.0)
			{
				self->s.angles[2] -= 360.0;
			}

			self->groundentity = NULL;

			return true;
		}
	}

	return false;
}

void
stalker_dead(edict_t *self)
{
	if (!self)
	{
		return;
	}

	VectorSet(self->mins, -28, -28, -18);
	VectorSet(self->maxs, 28, 28, -4);
	self->movetype = MOVETYPE_TOSS;
	self->svflags |= SVF_DEADMONSTER;
	self->nextthink = 0;
	gi.linkentity(self);
}

static mframe_t stalker_frames_death[] = {
	{ai_move, 0, NULL},
	{ai_move, -5, NULL},
	{ai_move, -10, NULL},
	{ai_move, -20, NULL},

	{ai_move, -10, NULL},
	{ai_move, -10, NULL},
	{ai_move, -5, NULL},
	{ai_move, -5, NULL},

	{ai_move, 0, NULL}
};

mmove_t stalker_move_death = {
	FRAME_death01,
   	FRAME_death09,
   	stalker_frames_death,
   	stalker_dead
};

void
stalker_die(edict_t *self, edict_t *inflictor /* unused */, edict_t *attacker /* unused */,
		int damage, vec3_t point /* unused */)
{
	int n;

	if (!self)
	{
		return;
	}

	/* dude bit it, make him fall! */
	self->movetype = MOVETYPE_TOSS;
	self->s.angles[2] = 0;
	VectorSet(self->gravityVector, 0, 0, -1);

	/* check for gib */
	if (self->health <= self->gib_health)
	{
		gi.sound(self, CHAN_VOICE, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0);

		for (n = 0; n < 2; n++)
		{
			ThrowGib(self, "models/objects/gibs/bone/tris.md2", damage, GIB_ORGANIC);
		}

		for (n = 0; n < 4; n++)
		{
			ThrowGib(self, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC);
		}

		ThrowHead(self, "models/objects/gibs/head2/tris.md2", damage, GIB_ORGANIC);
		self->deadflag = DEAD_DEAD;
		return;
	}

	if (self->deadflag == DEAD_DEAD)
	{
		return;
	}

	/* regular death */
	gi.sound(self, CHAN_VOICE, sound_die, 1, ATTN_NORM, 0);
	self->deadflag = DEAD_DEAD;
	self->takedamage = DAMAGE_YES;
	self->monsterinfo.currentmove = &stalker_move_death;
}

/*
 * QUAKED monster_stalker (1 .5 0) (-28 -28 -18) (28 28 18) Ambush Trigger_Spawn Sight OnRoof
 * Spider Monster
 *
 * ONROOF - Monster starts sticking to the roof.
 */
void
SP_monster_stalker(edict_t *self)
{
	if (!self)
	{
		return;
	}

	if (deathmatch->value)
	{
		G_FreeEdict(self);
		return;
	}

	sound_pain = gi.soundindex("stalker/pain.wav");
	sound_die = gi.soundindex("stalker/death.wav");
	sound_sight = gi.soundindex("stalker/sight.wav");
	sound_punch_hit1 = gi.soundindex("stalker/melee1.wav");
	sound_punch_hit2 = gi.soundindex("stalker/melee2.wav");
	sound_idle = gi.soundindex("stalker/idle.wav");

	gi.modelindex("models/proj/laser2/tris.md2");

	self->s.modelindex = gi.modelindex("models/monsters/stalker/tris.md2");
	VectorSet(self->mins, -28, -28, -18);
	VectorSet(self->maxs, 28, 28, 18);
	self->movetype = MOVETYPE_STEP;
	self->solid = SOLID_BBOX;

	self->health = 250;
	self->gib_health = -50;
	self->mass = 250;
	self->viewheight = 15;

	self->pain = stalker_pain;
	self->die = stalker_die;

	self->monsterinfo.stand = stalker_stand;
	self->monsterinfo.walk = stalker_walk;
	self->monsterinfo.run = stalker_run;
	self->monsterinfo.attack = stalker_attack_ranged;
	self->monsterinfo.sight = stalker_sight;
	self->monsterinfo.idle = stalker_idle;
	self->monsterinfo.dodge = stalker_dodge;
	self->monsterinfo.blocked = stalker_blocked;
	self->monsterinfo.melee = stalker_attack_melee;

	gi.linkentity(self);

	self->monsterinfo.currentmove = &stalker_move_stand;
	self->monsterinfo.scale = MODEL_SCALE;

	self->monsterinfo.aiflags |= AI_WALK_WALLS;

	if (self->spawnflags & 8)
	{
		self->s.angles[2] = 180;
		self->gravityVector[2] = 1;
	}

	walkmonster_start(self);
}
