/*
 * =======================================================================
 *
 * Level functions. Platforms, buttons, dooors and so on.
 *
 * =======================================================================
 */

#include "header/local.h"

/*
 * PLATS
 *
 * movement options:
 *
 * linear
 * smooth start, hard stop
 * smooth start, smooth stop
 *
 * start
 * end
 * acceleration
 * speed
 * deceleration
 * begin sound
 * end sound
 * target fired when reaching end
 * wait at end
 *
 * object characteristics that use move segments
 * ---------------------------------------------
 * movetype_push, or movetype_stop
 * action when touched
 * action when blocked
 * action when used
 *  disabled?
 * auto trigger spawning
 */

#define PLAT_LOW_TRIGGER 1
#define STATE_TOP 0
#define STATE_BOTTOM 1
#define STATE_UP 2
#define STATE_DOWN 3
#define DOOR_START_OPEN 1
#define DOOR_REVERSE 2
#define DOOR_CRUSHER 4
#define DOOR_NOMONSTER 8
#define DOOR_TOGGLE 32
#define DOOR_X_AXIS 64
#define DOOR_Y_AXIS 128

/* Support routines for movement (changes in origin using velocity) */

void
Move_Done(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

   VectorClear(ent->velocity);
   ent->moveinfo.endfunc(ent);
}

void
Move_Final(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	if (ent->moveinfo.remaining_distance == 0)
	{
		Move_Done(ent);
		return;
	}

	VectorScale(ent->moveinfo.dir,
			ent->moveinfo.remaining_distance / FRAMETIME,
			ent->velocity);

	ent->think = Move_Done;
	ent->nextthink = level.time + FRAMETIME;
}

void
Move_Begin(edict_t *ent)
{
	float frames;

	if (!ent)
	{
		return;
	}

	if ((ent->moveinfo.speed * FRAMETIME) >= ent->moveinfo.remaining_distance)
	{
		Move_Final(ent);
		return;
	}

	VectorScale(ent->moveinfo.dir, ent->moveinfo.speed, ent->velocity);
	frames = floor((ent->moveinfo.remaining_distance /
			 ent->moveinfo.speed) / FRAMETIME);
	ent->moveinfo.remaining_distance -= frames * ent->moveinfo.speed * FRAMETIME;
	ent->nextthink = level.time + (frames * FRAMETIME);
	ent->think = Move_Final;
}

void Think_AccelMove(edict_t *ent);

void
Move_Calc(edict_t *ent, vec3_t dest, void (*func)(edict_t *))
{
 	if (!ent || !func)
	{
		return;
	}

	VectorClear(ent->velocity);
	VectorSubtract(dest, ent->s.origin, ent->moveinfo.dir);
	ent->moveinfo.remaining_distance = VectorNormalize(ent->moveinfo.dir);
	ent->moveinfo.endfunc = func;

	if ((ent->moveinfo.speed == ent->moveinfo.accel) &&
		(ent->moveinfo.speed == ent->moveinfo.decel))
	{
		if (level.current_entity ==
			((ent->flags & FL_TEAMSLAVE) ? ent->teammaster : ent))
		{
			Move_Begin(ent);
		}
		else
		{
			ent->nextthink = level.time + FRAMETIME;
			ent->think = Move_Begin;
		}
	}
	else
	{
		/* accelerative */
		ent->moveinfo.current_speed = 0;
		ent->think = Think_AccelMove;
		ent->nextthink = level.time + FRAMETIME;
	}
}

/*
 * Support routines for angular movement
 * (changes in angle using avelocity)
 */
void
AngleMove_Done(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	VectorClear(ent->avelocity);
	ent->moveinfo.endfunc(ent);
}

void
AngleMove_Final(edict_t *ent)
{
	vec3_t move;

	if (!ent)
	{
		return;
	}

	if (ent->moveinfo.state == STATE_UP)
	{
		VectorSubtract(ent->moveinfo.end_angles, ent->s.angles, move);
	}
	else
	{
		VectorSubtract(ent->moveinfo.start_angles, ent->s.angles, move);
	}

	if (VectorCompare(move, vec3_origin))
	{
		AngleMove_Done(ent);
		return;
	}

	VectorScale(move, 1.0 / FRAMETIME, ent->avelocity);

	ent->think = AngleMove_Done;
	ent->nextthink = level.time + FRAMETIME;
}

void
AngleMove_Begin(edict_t *ent)
{
	vec3_t destdelta;
	float len;
	float traveltime;
	float frames;

	if (!ent)
	{
		return;
	}

	/* set destdelta to the vector needed to move */
	if (ent->moveinfo.state == STATE_UP)
	{
		VectorSubtract(ent->moveinfo.end_angles, ent->s.angles, destdelta);
	}
	else
	{
		VectorSubtract(ent->moveinfo.start_angles, ent->s.angles, destdelta);
	}

	/* calculate length of vector */
	len = VectorLength(destdelta);

	/* divide by speed to get time to reach dest */
	traveltime = len / ent->moveinfo.speed;

	if (traveltime < FRAMETIME)
	{
		AngleMove_Final(ent);
		return;
	}

	frames = floor(traveltime / FRAMETIME);

	/* scale the destdelta vector by the time spent traveling to get velocity */
	VectorScale(destdelta, 1.0 / traveltime, ent->avelocity);

	/* set nextthink to trigger a think when dest is reached */
	ent->nextthink = level.time + frames * FRAMETIME;
	ent->think = AngleMove_Final;
}

void
AngleMove_Calc(edict_t *ent, void (*func)(edict_t *))
{
 	if (!ent || !func)
	{
		return;
	}

	VectorClear(ent->avelocity);
	ent->moveinfo.endfunc = func;

	if (level.current_entity ==
		((ent->flags & FL_TEAMSLAVE) ? ent->teammaster : ent))
	{
		AngleMove_Begin(ent);
	}
	else
	{
		ent->nextthink = level.time + FRAMETIME;
		ent->think = AngleMove_Begin;
	}
}

/*
 * The team has completed a frame of movement, so
 * change the speed for the next frame
 */

#define AccelerationDistance(target, rate) (target * ((target / rate) + 1) / 2)

void
plat_CalcAcceleratedMove(moveinfo_t *moveinfo)
{
	float accel_dist;
	float decel_dist;

	if (!moveinfo)
	{
		return;
	}

	moveinfo->move_speed = moveinfo->speed;

	if (moveinfo->remaining_distance < moveinfo->accel)
	{
		moveinfo->current_speed = moveinfo->remaining_distance;
		return;
	}

	accel_dist = AccelerationDistance(moveinfo->speed, moveinfo->accel);
	decel_dist = AccelerationDistance(moveinfo->speed, moveinfo->decel);

	if ((moveinfo->remaining_distance - accel_dist - decel_dist) < 0)
	{
		float f;

		f = (moveinfo->accel + moveinfo->decel) / (moveinfo->accel * moveinfo->decel);
		moveinfo->move_speed = (-2 + sqrt(4 - 4 * f * (-2 * moveinfo->remaining_distance))) / (2 * f);
		decel_dist = AccelerationDistance(moveinfo->move_speed, moveinfo->decel);
	}

	moveinfo->decel_distance = decel_dist;
}

void
plat_Accelerate(moveinfo_t *moveinfo)
{
	if (!moveinfo)
	{
		return;
	}

	/* are we decelerating? */
	if (moveinfo->remaining_distance <= moveinfo->decel_distance)
	{
		if (moveinfo->remaining_distance < moveinfo->decel_distance)
		{
			if (moveinfo->next_speed)
			{
				moveinfo->current_speed = moveinfo->next_speed;
				moveinfo->next_speed = 0;
				return;
			}

			if (moveinfo->current_speed > moveinfo->decel)
			{
				moveinfo->current_speed -= moveinfo->decel;
			}
		}

		return;
	}

	/* are we at full speed and need to start decelerating during this move? */
	if (moveinfo->current_speed == moveinfo->move_speed)
	{
		if ((moveinfo->remaining_distance - moveinfo->current_speed) <
			moveinfo->decel_distance)
		{
			float p1_distance;
			float p2_distance;
			float distance;

			p1_distance = moveinfo->remaining_distance - moveinfo->decel_distance;
			p2_distance = moveinfo->move_speed * (1.0 - (p1_distance / moveinfo->move_speed));
			distance = p1_distance + p2_distance;
			moveinfo->current_speed = moveinfo->move_speed;
			moveinfo->next_speed = moveinfo->move_speed - moveinfo->decel * (p2_distance / distance);
			return;
		}
	}

	/* are we accelerating? */
	if (moveinfo->current_speed < moveinfo->speed)
	{
		float old_speed;
		float p1_distance;
		float p1_speed;
		float p2_distance;
		float distance;

		old_speed = moveinfo->current_speed;

		/* figure simple acceleration up to move_speed */
		moveinfo->current_speed += moveinfo->accel;

		if (moveinfo->current_speed > moveinfo->speed)
		{
			moveinfo->current_speed = moveinfo->speed;
		}

		/* are we accelerating throughout this entire move? */
		if ((moveinfo->remaining_distance - moveinfo->current_speed) >=
			moveinfo->decel_distance)
		{
			return;
		}

		/* during this move we will accelrate from current_speed to move_speed
		   and cross over the decel_distance; figure the average speed for the
		   entire move */
		p1_distance = moveinfo->remaining_distance - moveinfo->decel_distance;
		p1_speed = (old_speed + moveinfo->move_speed) / 2.0;
		p2_distance = moveinfo->move_speed * (1.0 - (p1_distance / p1_speed));
		distance = p1_distance + p2_distance;
		moveinfo->current_speed = (p1_speed * (p1_distance /
					distance)) + (moveinfo->move_speed * (p2_distance / distance));
		moveinfo->next_speed = moveinfo->move_speed - moveinfo->decel *
							   (p2_distance / distance);
		return;
	}

	/* we are at constant velocity (move_speed) */
	return;
}

/*
 * The team has completed a frame of movement,
 * so change the speed for the next frame
 */
void
Think_AccelMove(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	ent->moveinfo.remaining_distance -= ent->moveinfo.current_speed;

	if (ent->moveinfo.current_speed == 0) /* starting or blocked */
	{
		plat_CalcAcceleratedMove(&ent->moveinfo);
	}

	plat_Accelerate(&ent->moveinfo);

	/* will the entire move complete on next frame? */
	if (ent->moveinfo.remaining_distance <= ent->moveinfo.current_speed)
	{
		Move_Final(ent);
		return;
	}

	VectorScale(ent->moveinfo.dir, ent->moveinfo.current_speed * 10,
			ent->velocity);
	ent->nextthink = level.time + FRAMETIME;
	ent->think = Think_AccelMove;
}

void plat_go_down(edict_t *ent);

void
plat_hit_top(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	if (!(ent->flags & FL_TEAMSLAVE))
	{
		if (ent->moveinfo.sound_end)
		{
			gi.sound(ent, CHAN_NO_PHS_ADD + CHAN_VOICE,
					ent->moveinfo.sound_end, 1, ATTN_STATIC, 0);
		}

		ent->s.sound = 0;
	}

	ent->moveinfo.state = STATE_TOP;

	ent->think = plat_go_down;
	ent->nextthink = level.time + 3;
}

void
plat_hit_bottom(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	if (!(ent->flags & FL_TEAMSLAVE))
	{
		if (ent->moveinfo.sound_end)
		{
			gi.sound(ent, CHAN_NO_PHS_ADD + CHAN_VOICE, ent->moveinfo.sound_end,
					1, ATTN_STATIC, 0);
		}

		ent->s.sound = 0;
	}

	ent->moveinfo.state = STATE_BOTTOM;
}

void
plat_go_down(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	if (!(ent->flags & FL_TEAMSLAVE))
	{
		if (ent->moveinfo.sound_start)
		{
			gi.sound(ent, CHAN_NO_PHS_ADD + CHAN_VOICE, ent->moveinfo.sound_start,
					1, ATTN_STATIC, 0);
		}

		ent->s.sound = ent->moveinfo.sound_middle;
	}

	ent->moveinfo.state = STATE_DOWN;
	Move_Calc(ent, ent->moveinfo.end_origin, plat_hit_bottom);
}

void
plat_go_up(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	if (!(ent->flags & FL_TEAMSLAVE))
	{
		if (ent->moveinfo.sound_start)
		{
			gi.sound(ent, CHAN_NO_PHS_ADD + CHAN_VOICE,
					ent->moveinfo.sound_start, 1, ATTN_STATIC, 0);
		}

		ent->s.sound = ent->moveinfo.sound_middle;
	}

	ent->moveinfo.state = STATE_UP;
	Move_Calc(ent, ent->moveinfo.start_origin, plat_hit_top);
}

void
plat_blocked(edict_t *self, edict_t *other)
{
	if (!self || !other)
	{
		return;
	}

	if (!(other->svflags & SVF_MONSTER) && (!other->client))
	{
		/* give it a chance to go away on it's own terms (like gibs) */
		T_Damage(other, self, self, vec3_origin, other->s.origin,
				vec3_origin, 100000, 1, 0, MOD_CRUSH);

		/* if it's still there, nuke it */
		if (other)
		{
			/* Hack for entity without it's origin near the model */
			VectorMA (other->absmin, 0.5, other->size, other->s.origin);
			BecomeExplosion1(other);
		}

		return;
	}

	T_Damage(other, self, self, vec3_origin, other->s.origin,
			vec3_origin, self->dmg, 1, 0, MOD_CRUSH);

	if (self->moveinfo.state == STATE_UP)
	{
		plat_go_down(self);
	}
	else if (self->moveinfo.state == STATE_DOWN)
	{
		plat_go_up(self);
	}
}

void
Use_Plat(edict_t *ent, edict_t *other /* unused */, edict_t *activator /* unused */)
{
	if (!ent)
	{
		return;
	}

	if (ent->think)
	{
		return; /* already down */
	}

	plat_go_down(ent);
}

void
Touch_Plat_Center(edict_t *ent, edict_t *other, cplane_t *plane /* unused */,
		csurface_t *surf /* unused */)
{
	if (!ent || !other)
	{
		return;
	}

	if (!other->client)
	{
		return;
	}

	if (other->health <= 0)
	{
		return;
	}

	ent = ent->enemy; /* now point at the plat, not the trigger */

	if (ent->moveinfo.state == STATE_BOTTOM)
	{
		plat_go_up(ent);
	}
	else if (ent->moveinfo.state == STATE_TOP)
	{
		ent->nextthink = level.time + 1; /* the player is still on the plat, so delay going down */
	}
}

void
plat_spawn_inside_trigger(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	edict_t *trigger;
	vec3_t tmin, tmax;

	/* middle trigger */
	trigger = G_Spawn();
	trigger->touch = Touch_Plat_Center;
	trigger->movetype = MOVETYPE_NONE;
	trigger->solid = SOLID_TRIGGER;
	trigger->enemy = ent;

	tmin[0] = ent->mins[0] + 25;
	tmin[1] = ent->mins[1] + 25;
	tmin[2] = ent->mins[2];

	tmax[0] = ent->maxs[0] - 25;
	tmax[1] = ent->maxs[1] - 25;
	tmax[2] = ent->maxs[2] + 8;

	tmin[2] = tmax[2] - (ent->pos1[2] - ent->pos2[2] + st.lip);

	if (ent->spawnflags & PLAT_LOW_TRIGGER)
	{
		tmax[2] = tmin[2] + 8;
	}

	if (tmax[0] - tmin[0] <= 0)
	{
		tmin[0] = (ent->mins[0] + ent->maxs[0]) * 0.5;
		tmax[0] = tmin[0] + 1;
	}

	if (tmax[1] - tmin[1] <= 0)
	{
		tmin[1] = (ent->mins[1] + ent->maxs[1]) * 0.5;
		tmax[1] = tmin[1] + 1;
	}

	VectorCopy(tmin, trigger->mins);
	VectorCopy(tmax, trigger->maxs);

	gi.linkentity(trigger);
}

/*
 * QUAKED func_plat (0 .5 .8) ? PLAT_LOW_TRIGGER
 *
 * speed -> default 150
 *
 * Plats are always drawn in the extended position,
 * so they will light correctly.
 *
 * If the plat is the target of another trigger or button,
 * it will start out disabled in the extended position until
 * it is trigger, when it will lower and become a normal plat.
 *
 * "speed"	overrides default 200.
 * "accel"  overrides default 500
 * "lip"	overrides default 8 pixel lip
 *
 * If the "height" key is set, that will determine the amount
 * the plat moves, instead of being implicitly determoveinfoned
 * by the model's height.
 *
 * Set "sounds" to one of the following:
 * 1) base fast
 * 2) chain slow
 */
void
SP_func_plat(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	VectorClear(ent->s.angles);
	ent->solid = SOLID_BSP;
	ent->movetype = MOVETYPE_PUSH;

	gi.setmodel(ent, ent->model);

	ent->blocked = plat_blocked;

	if (!ent->speed)
	{
		ent->speed = 20;
	}
	else
	{
		ent->speed *= 0.1;
	}

	if (!ent->accel)
	{
		ent->accel = 5;
	}
	else
	{
		ent->accel *= 0.1;
	}

	if (!ent->decel)
	{
		ent->decel = 5;
	}
	else
	{
		ent->decel *= 0.1;
	}

	if (!ent->dmg)
	{
		ent->dmg = 2;
	}

	if (!st.lip)
	{
		st.lip = 8;
	}

	/* pos1 is the top position, pos2 is the bottom */
	VectorCopy(ent->s.origin, ent->pos1);
	VectorCopy(ent->s.origin, ent->pos2);

	if (st.height)
	{
		ent->pos2[2] -= st.height;
	}
	else
	{
		ent->pos2[2] -= (ent->maxs[2] - ent->mins[2]) - st.lip;
	}

	ent->use = Use_Plat;

	plat_spawn_inside_trigger(ent); /* the "start moving" trigger */

	if (ent->targetname)
	{
		ent->moveinfo.state = STATE_UP;
	}
	else
	{
		VectorCopy(ent->pos2, ent->s.origin);
		gi.linkentity(ent);
		ent->moveinfo.state = STATE_BOTTOM;
	}

	ent->moveinfo.speed = ent->speed;
	ent->moveinfo.accel = ent->accel;
	ent->moveinfo.decel = ent->decel;
	ent->moveinfo.wait = ent->wait;
	VectorCopy(ent->pos1, ent->moveinfo.start_origin);
	VectorCopy(ent->s.angles, ent->moveinfo.start_angles);
	VectorCopy(ent->pos2, ent->moveinfo.end_origin);
	VectorCopy(ent->s.angles, ent->moveinfo.end_angles);

	ent->moveinfo.sound_start = gi.soundindex("plats/pt1_strt.wav");
	ent->moveinfo.sound_middle = gi.soundindex("plats/pt1_mid.wav");
	ent->moveinfo.sound_end = gi.soundindex("plats/pt1_end.wav");
}

/* ==================================================================== */

/*
 * QUAKED func_rotating (0 .5 .8) ? START_ON REVERSE X_AXIS Y_AXIS
 * TOUCH_PAIN STOP ANIMATED ANIMATED_FAST
 *
 * You need to have an origin brush as part of this entity.
 * The center of that brush will be the point around which it
 * is rotated. It will rotate around the Z axis by default.
 * You can check either the X_AXIS or Y_AXIS box to change that.
 *
 * "speed"  determines how fast it moves; default value is 100.
 * "dmg"	damage to inflict when blocked (2 default)
 *
 * REVERSE will cause the it to rotate in the opposite direction.
 * STOP mean it will stop moving instead of pushing entities
 */
void
rotating_blocked(edict_t *self, edict_t *other)
{
	if (!self || !other)
	{
		return;
	}

	T_Damage(other, self, self, vec3_origin, other->s.origin,
			vec3_origin, self->dmg, 1, 0, MOD_CRUSH);
}

void
rotating_touch(edict_t *self, edict_t *other, cplane_t *plane /* unused */, csurface_t *surf /* unused */)
{
	if (!self || !other)
	{
		return;
	}

	if (self->avelocity[0] || self->avelocity[1] || self->avelocity[2])
	{
		T_Damage(other, self, self, vec3_origin, other->s.origin,
				vec3_origin, self->dmg, 1, 0, MOD_CRUSH);
	}
}

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

	if (!VectorCompare(self->avelocity, vec3_origin))
	{
		self->s.sound = 0;
		VectorClear(self->avelocity);
		self->touch = NULL;
	}
	else
	{
		self->s.sound = self->moveinfo.sound_middle;
		VectorScale(self->movedir, self->speed, self->avelocity);

		if (self->spawnflags & 16)
		{
			self->touch = rotating_touch;
		}
	}
}

void
SP_func_rotating(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	ent->solid = SOLID_BSP;

	if (ent->spawnflags & 32)
	{
		ent->movetype = MOVETYPE_STOP;
	}
	else
	{
		ent->movetype = MOVETYPE_PUSH;
	}

	/* set the axis of rotation */
	VectorClear(ent->movedir);

	if (ent->spawnflags & 4)
	{
		ent->movedir[2] = 1.0;
	}
	else if (ent->spawnflags & 8)
	{
		ent->movedir[0] = 1.0;
	}
	else
	{
		ent->movedir[1] = 1.0;
	}

	/* check for reverse rotation */
	if (ent->spawnflags & 2)
	{
		VectorNegate(ent->movedir, ent->movedir);
	}

	if (!ent->speed)
	{
		ent->speed = 100;
	}

	if (!ent->dmg)
	{
		ent->dmg = 2;
	}

	ent->use = rotating_use;

	if (ent->dmg)
	{
		ent->blocked = rotating_blocked;
	}

	if (ent->spawnflags & 1)
	{
		ent->use(ent, NULL, NULL);
	}

	if (ent->spawnflags & 64)
	{
		ent->s.effects |= EF_ANIM_ALL;
	}

	if (ent->spawnflags & 128)
	{
		ent->s.effects |= EF_ANIM_ALLFAST;
	}

	gi.setmodel(ent, ent->model);
	gi.linkentity(ent);
}

/* ==================================================================== */

/* BUTTONS */

/*
 * QUAKED func_button (0 .5 .8) ?
 *
 * When a button is touched, it moves some distance
 * in the direction of it's angle, triggers all of it's
 * targets, waits some time, then returns to it's original
 * position where it can be triggered again.
 *
 * "angle"		determines the opening direction
 * "target"	    all entities with a matching targetname will be used
 * "speed"		override the default 40 speed
 * "wait"		override the default 1 second wait (-1 = never return)
 * "lip"		override the default 4 pixel lip remaining at end of move
 * "health"	    if set, the button must be killed instead of touched
 * "sounds"
 *    1) silent
 *    2) steam metal
 *    3) wooden clunk
 *    4) metallic click
 *    5) in-out
 */
void
button_done(edict_t *self)
{
	if (!self)
	{
		return;
	}

	self->moveinfo.state = STATE_BOTTOM;
	self->s.effects &= ~EF_ANIM23;
	self->s.effects |= EF_ANIM01;
}

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

	self->moveinfo.state = STATE_DOWN;

	Move_Calc(self, self->moveinfo.start_origin, button_done);

	self->s.frame = 0;

	if (self->health)
	{
		self->takedamage = DAMAGE_YES;
	}
}

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

	self->moveinfo.state = STATE_TOP;
	self->s.effects &= ~EF_ANIM01;
	self->s.effects |= EF_ANIM23;

	G_UseTargets(self, self->activator);
	self->s.frame = 1;

	if (self->moveinfo.wait >= 0)
	{
		self->nextthink = level.time + self->moveinfo.wait;
		self->think = button_return;
	}
}

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

	if ((self->moveinfo.state == STATE_UP) ||
		(self->moveinfo.state == STATE_TOP))
	{
		return;
	}

	self->moveinfo.state = STATE_UP;

	if (self->moveinfo.sound_start && !(self->flags & FL_TEAMSLAVE))
	{
		gi.sound(self, CHAN_NO_PHS_ADD + CHAN_VOICE, self->moveinfo.sound_start,
				1, ATTN_STATIC, 0);
	}

	Move_Calc(self, self->moveinfo.end_origin, button_wait);
}

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

	self->activator = activator;
	button_fire(self);
}

void
button_touch(edict_t *self, edict_t *other, cplane_t *plane /* unused */, csurface_t *surf /* unused */)
{
	if (!self || !other)
	{
		return;
	}

	if (!other->client)
	{
		return;
	}

	if (other->health <= 0)
	{
		return;
	}

	self->activator = other;
	button_fire(self);
}

void
button_killed(edict_t *self, edict_t *inflictor /* unused */, edict_t *attacker /* unsued */,
		int damage /* unused */, vec3_t point /* unused */)
{
	if (!self)
	{
		return;
	}

	self->activator = attacker;
	self->health = self->max_health;
	self->takedamage = DAMAGE_NO;
	button_fire(self);
}

void
SP_func_button(edict_t *ent)
{
	vec3_t abs_movedir;
	float dist;

	if (!ent)
	{
		return;
	}

	G_SetMovedir(ent->s.angles, ent->movedir);
	ent->movetype = MOVETYPE_STOP;
	ent->solid = SOLID_BSP;
	gi.setmodel(ent, ent->model);

	if (ent->sounds != 1)
	{
		ent->moveinfo.sound_start = gi.soundindex("switches/butn2.wav");
	}

	if (!ent->speed)
	{
		ent->speed = 40;
	}

	if (!ent->accel)
	{
		ent->accel = ent->speed;
	}

	if (!ent->decel)
	{
		ent->decel = ent->speed;
	}

	if (!ent->wait)
	{
		ent->wait = 3;
	}

	if (!st.lip)
	{
		st.lip = 4;
	}

	VectorCopy(ent->s.origin, ent->pos1);
	abs_movedir[0] = fabs(ent->movedir[0]);
	abs_movedir[1] = fabs(ent->movedir[1]);
	abs_movedir[2] = fabs(ent->movedir[2]);
	dist = abs_movedir[0] * ent->size[0] + abs_movedir[1] * ent->size[1] +
		   abs_movedir[2] * ent->size[2] - st.lip;
	VectorMA(ent->pos1, dist, ent->movedir, ent->pos2);

	ent->use = button_use;
	ent->s.effects |= EF_ANIM01;

	if (ent->health)
	{
		ent->max_health = ent->health;
		ent->die = button_killed;
		ent->takedamage = DAMAGE_YES;
	}
	else if (!ent->targetname)
	{
		ent->touch = button_touch;
	}

	ent->moveinfo.state = STATE_BOTTOM;

	ent->moveinfo.speed = ent->speed;
	ent->moveinfo.accel = ent->accel;
	ent->moveinfo.decel = ent->decel;
	ent->moveinfo.wait = ent->wait;
	VectorCopy(ent->pos1, ent->moveinfo.start_origin);
	VectorCopy(ent->s.angles, ent->moveinfo.start_angles);
	VectorCopy(ent->pos2, ent->moveinfo.end_origin);
	VectorCopy(ent->s.angles, ent->moveinfo.end_angles);

	gi.linkentity(ent);
}

/* ==================================================================== */

/*
 * DOORS
 *
 * spawn a trigger surrounding the entire team
 * unless it is already targeted by another
 */

void door_go_down(edict_t *self);

/*
 * QUAKED func_door (0 .5 .8) ? START_OPEN x CRUSHER NOMONSTER ANIMATED TOGGLE ANIMATED_FAST
 *
 * TOGGLE		wait in both the start and end states for a trigger event.
 * START_OPEN	the door to moves to its destination when spawned, and operate in reverse.
 *              It is used to temporarily or permanently close off an area when triggered
 *              (not useful for touch or takedamage doors).
 * NOMONSTER	monsters will not trigger this door
 *
 * "message"	is printed when the door is touched if it is a trigger door and it hasn't been fired yet
 * "angle"		determines the opening direction
 * "targetname" if set, no touch field will be spawned and a remote button or trigger field activates the door.
 * "health"	    if set, door must be shot open
 * "speed"		movement speed (100 default)
 * "wait"		wait before returning (3 default, -1 = never return)
 * "lip"		lip remaining at end of move (8 default)
 * "dmg"		damage to inflict when blocked (2 default)
 * "sounds"
 *    1)	silent
 *    2)	light
 *    3)	medium
 *    4)	heavy
 */

void
door_use_areaportals(edict_t *self, qboolean open)
{
	edict_t *t = NULL;

	if (!self)
	{
		return;
	}

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

	while ((t = G_Find(t, FOFS(targetname), self->target)))
	{
		if (Q_stricmp(t->classname, "func_areaportal") == 0)
		{
			gi.SetAreaPortalState(t->style, open);
		}
	}
}

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

	if (!(self->flags & FL_TEAMSLAVE))
	{
		if (self->moveinfo.sound_end)
		{
			gi.sound(self, CHAN_NO_PHS_ADD + CHAN_VOICE,
					self->moveinfo.sound_end, 1,
					ATTN_STATIC, 0);
		}

		self->s.sound = 0;
	}

	self->moveinfo.state = STATE_TOP;

	if (self->spawnflags & DOOR_TOGGLE)
	{
		return;
	}

	if (self->moveinfo.wait >= 0)
	{
		self->think = door_go_down;
		self->nextthink = level.time + self->moveinfo.wait;
	}
}

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

	if (!(self->flags & FL_TEAMSLAVE))
	{
		if (self->moveinfo.sound_end)
		{
			gi.sound(self, CHAN_NO_PHS_ADD + CHAN_VOICE,
					self->moveinfo.sound_end, 1,
					ATTN_STATIC, 0);
		}

		self->s.sound = 0;
	}

	self->moveinfo.state = STATE_BOTTOM;
	door_use_areaportals(self, false);
}

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

	if (!(self->flags & FL_TEAMSLAVE))
	{
		if (self->moveinfo.sound_start)
		{
			gi.sound(self, CHAN_NO_PHS_ADD + CHAN_VOICE,
					self->moveinfo.sound_start, 1,
					ATTN_STATIC, 0);
		}

		self->s.sound = self->moveinfo.sound_middle;
	}

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

	self->moveinfo.state = STATE_DOWN;

	if (strcmp(self->classname, "func_door") == 0)
	{
		Move_Calc(self, self->moveinfo.start_origin, door_hit_bottom);
	}
	else if (strcmp(self->classname, "func_door_rotating") == 0)
	{
		AngleMove_Calc(self, door_hit_bottom);
	}
}

void
door_go_up(edict_t *self, edict_t *activator)
{
	if (!self || !activator)
	{
		return;
	}

	if (self->moveinfo.state == STATE_UP)
	{
		return; /* already going up */
	}

	if (self->moveinfo.state == STATE_TOP)
	{
		/* reset top wait time */
		if (self->moveinfo.wait >= 0)
		{
			self->nextthink = level.time + self->moveinfo.wait;
		}

		return;
	}

	if (!(self->flags & FL_TEAMSLAVE))
	{
		if (self->moveinfo.sound_start)
		{
			gi.sound(self, CHAN_NO_PHS_ADD + CHAN_VOICE,
					self->moveinfo.sound_start, 1,
					ATTN_STATIC, 0);
		}

		self->s.sound = self->moveinfo.sound_middle;
	}

	self->moveinfo.state = STATE_UP;

	if (strcmp(self->classname, "func_door") == 0)
	{
		Move_Calc(self, self->moveinfo.end_origin, door_hit_top);
	}
	else if (strcmp(self->classname, "func_door_rotating") == 0)
	{
		AngleMove_Calc(self, door_hit_top);
	}

	G_UseTargets(self, activator);
	door_use_areaportals(self, true);
}

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

	edict_t *ent;

	if (!self)
		return;

	if (self->flags & FL_TEAMSLAVE)
	{
		return;
	}

	if (self->spawnflags & DOOR_TOGGLE)
	{
		if ((self->moveinfo.state == STATE_UP) ||
			(self->moveinfo.state == STATE_TOP))
		{
			/* trigger all paired doors */
			for (ent = self; ent; ent = ent->teamchain)
			{
				ent->message = NULL;
				ent->touch = NULL;
				door_go_down(ent);
			}

			return;
		}
	}

	/* trigger all paired doors */
	for (ent = self; ent; ent = ent->teamchain)
	{
		ent->message = NULL;
		ent->touch = NULL;
		door_go_up(ent, activator);
	}
}

void
Touch_DoorTrigger(edict_t *self, edict_t *other, cplane_t *plane /* unused */,
		csurface_t *surf /* unused */)
{
	if (!self || !other)
	{
		return;
	}

	if (other->health <= 0)
	{
		return;
	}

	if (!(other->svflags & SVF_MONSTER) && (!other->client))
	{
		return;
	}

	if ((self->owner->spawnflags & DOOR_NOMONSTER) &&
		(other->svflags & SVF_MONSTER))
	{
		return;
	}

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

	self->touch_debounce_time = level.time + 1.0;

	door_use(self->owner, other, other);
}

void
Think_CalcMoveSpeed(edict_t *self)
{
	edict_t *ent;
	float min;
	float time;
	float newspeed;
	float ratio;
	float dist;

	if (!self)
	{
		return;
	}

	if (self->flags & FL_TEAMSLAVE)
	{
		return; /* only the team master does this */
	}

	/* find the smallest distance any member of the team will be moving */
	min = fabs(self->moveinfo.distance);

	for (ent = self->teamchain; ent; ent = ent->teamchain)
	{
		dist = fabs(ent->moveinfo.distance);

		if (dist < min)
		{
			min = dist;
		}
	}

	time = min / self->moveinfo.speed;

	/* adjust speeds so they will all complete at the same time */
	for (ent = self; ent; ent = ent->teamchain)
	{
		newspeed = fabs(ent->moveinfo.distance) / time;
		ratio = newspeed / ent->moveinfo.speed;

		if (ent->moveinfo.accel == ent->moveinfo.speed)
		{
			ent->moveinfo.accel = newspeed;
		}
		else
		{
			ent->moveinfo.accel *= ratio;
		}

		if (ent->moveinfo.decel == ent->moveinfo.speed)
		{
			ent->moveinfo.decel = newspeed;
		}
		else
		{
			ent->moveinfo.decel *= ratio;
		}

		ent->moveinfo.speed = newspeed;
	}
}

void
Think_SpawnDoorTrigger(edict_t *ent)
{
	edict_t *other;
	vec3_t mins, maxs;

	if (!ent)
	{
		return;
	}

	if (ent->flags & FL_TEAMSLAVE)
	{
		return; /* only the team leader spawns a trigger */
	}

	VectorCopy(ent->absmin, mins);
	VectorCopy(ent->absmax, maxs);

	for (other = ent->teamchain; other; other = other->teamchain)
	{
		AddPointToBounds(other->absmin, mins, maxs);
		AddPointToBounds(other->absmax, mins, maxs);
	}

	/* expand */
	mins[0] -= 60;
	mins[1] -= 60;
	maxs[0] += 60;
	maxs[1] += 60;

	other = G_Spawn();
	VectorCopy(mins, other->mins);
	VectorCopy(maxs, other->maxs);
	other->owner = ent;
	other->solid = SOLID_TRIGGER;
	other->movetype = MOVETYPE_NONE;
	other->touch = Touch_DoorTrigger;
	gi.linkentity(other);

	if (ent->spawnflags & DOOR_START_OPEN)
	{
		door_use_areaportals(ent, true);
	}

	Think_CalcMoveSpeed(ent);
}

void
door_blocked(edict_t *self, edict_t *other)
{
	edict_t *ent;

	if (!self || !other)
	{
		return;
	}

	if (!(other->svflags & SVF_MONSTER) && (!other->client))
	{
		/* give it a chance to go away on it's own terms (like gibs) */
		T_Damage(other, self, self, vec3_origin, other->s.origin,
				vec3_origin, 100000, 1, 0, MOD_CRUSH);

		/* if it's still there, nuke it */
		if (other)
		{
			/* Hack for entitiy without their origin near the model */
			VectorMA (other->absmin, 0.5, other->size, other->s.origin);
			BecomeExplosion1(other);
		}

		return;
	}

	T_Damage(other, self, self, vec3_origin, other->s.origin, vec3_origin,
			self->dmg, 1, 0, MOD_CRUSH);

	if (self->spawnflags & DOOR_CRUSHER)
	{
		return;
	}

	/* if a door has a negative wait, it would never
	   come back if blocked, so let it just squash the
	   object to death real fast */
	if (self->moveinfo.wait >= 0)
	{
		if (self->moveinfo.state == STATE_DOWN)
		{
			for (ent = self->teammaster; ent; ent = ent->teamchain)
			{
				door_go_up(ent, ent->activator);
			}
		}
		else
		{
			for (ent = self->teammaster; ent; ent = ent->teamchain)
			{
				door_go_down(ent);
			}
		}
	}
}

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

	if (!self || !attacker)
	{
		return;
	}

	for (ent = self->teammaster; ent; ent = ent->teamchain)
	{
		ent->health = ent->max_health;
		ent->takedamage = DAMAGE_NO;
	}

	door_use(self->teammaster, attacker, attacker);
}

void
door_touch(edict_t *self, edict_t *other, cplane_t *plane /* unused */, csurface_t *surf /* unused */)
{
 	if (!self || !other)
	{
		return;
	}

	if (!other->client)
	{
		return;
	}

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

	self->touch_debounce_time = level.time + 5.0;

	gi.centerprintf(other, "%s", self->message);
	gi.sound(other, CHAN_AUTO, gi.soundindex("misc/talk1.wav"), 1, ATTN_NORM, 0);
}

void
SP_func_door(edict_t *ent)
{
	vec3_t abs_movedir;

	if (!ent)
	{
		return;
	}

	if (ent->sounds != 1)
	{
		ent->moveinfo.sound_start = gi.soundindex("doors/dr1_strt.wav");
		ent->moveinfo.sound_middle = gi.soundindex("doors/dr1_mid.wav");
		ent->moveinfo.sound_end = gi.soundindex("doors/dr1_end.wav");
	}

	G_SetMovedir(ent->s.angles, ent->movedir);
	ent->movetype = MOVETYPE_PUSH;
	ent->solid = SOLID_BSP;
	gi.setmodel(ent, ent->model);

	ent->blocked = door_blocked;
	ent->use = door_use;

	if (!ent->speed)
	{
		ent->speed = 100;
	}

	if (deathmatch->value)
	{
		ent->speed *= 2;
	}

	if (!ent->accel)
	{
		ent->accel = ent->speed;
	}

	if (!ent->decel)
	{
		ent->decel = ent->speed;
	}

	if (!ent->wait)
	{
		ent->wait = 3;
	}

	if (!st.lip)
	{
		st.lip = 8;
	}

	if (!ent->dmg)
	{
		ent->dmg = 2;
	}

	/* calculate second position */
	VectorCopy(ent->s.origin, ent->pos1);
	abs_movedir[0] = fabs(ent->movedir[0]);
	abs_movedir[1] = fabs(ent->movedir[1]);
	abs_movedir[2] = fabs(ent->movedir[2]);
	ent->moveinfo.distance = abs_movedir[0] * ent->size[0] + abs_movedir[1] *
							 ent->size[1] + abs_movedir[2] * ent->size[2] -
							 st.lip;
	VectorMA(ent->pos1, ent->moveinfo.distance, ent->movedir, ent->pos2);

	/* if it starts open, switch the positions */
	if (ent->spawnflags & DOOR_START_OPEN)
	{
		VectorCopy(ent->pos2, ent->s.origin);
		VectorCopy(ent->pos1, ent->pos2);
		VectorCopy(ent->s.origin, ent->pos1);
	}

	ent->moveinfo.state = STATE_BOTTOM;

	if (ent->health)
	{
		ent->takedamage = DAMAGE_YES;
		ent->die = door_killed;
		ent->max_health = ent->health;
	}
	else if (ent->targetname && ent->message)
	{
		gi.soundindex("misc/talk.wav");
		ent->touch = door_touch;
	}

	ent->moveinfo.speed = ent->speed;
	ent->moveinfo.accel = ent->accel;
	ent->moveinfo.decel = ent->decel;
	ent->moveinfo.wait = ent->wait;
	VectorCopy(ent->pos1, ent->moveinfo.start_origin);
	VectorCopy(ent->s.angles, ent->moveinfo.start_angles);
	VectorCopy(ent->pos2, ent->moveinfo.end_origin);
	VectorCopy(ent->s.angles, ent->moveinfo.end_angles);

	if (ent->spawnflags & 16)
	{
		ent->s.effects |= EF_ANIM_ALL;
	}

	if (ent->spawnflags & 64)
	{
		ent->s.effects |= EF_ANIM_ALLFAST;
	}

	/* to simplify logic elsewhere, make non-teamed doors into a team of one */
	if (!ent->team)
	{
		ent->teammaster = ent;
	}

	gi.linkentity(ent);

	ent->nextthink = level.time + FRAMETIME;

	if (ent->health || ent->targetname)
	{
		ent->think = Think_CalcMoveSpeed;
	}
	else
	{
		ent->think = Think_SpawnDoorTrigger;
	}
}

/*
 * QUAKED func_door_rotating (0 .5 .8) ? START_OPEN REVERSE CRUSHER NOMONSTER ANIMATED TOGGLE X_AXIS Y_AXIS
 *
 * TOGGLE       causes the door to wait in both the start and end states for a trigger event.
 * START_OPEN	the door to moves to its destination when spawned, and operate in reverse.
 *              It is used to temporarily or permanently close off an area when triggered
 *              (not useful for touch or takedamage doors).
 * NOMONSTER	monsters will not trigger this door
 *
 * You need to have an origin brush as part of this entity.  The center of that brush will be
 * the point around which it is rotated. It will rotate around the Z axis by default.  You can
 * check either the X_AXIS or Y_AXIS box to change that.
 *
 * "distance" is how many degrees the door will be rotated.
 * "speed" determines how fast the door moves; default value is 100.
 *
 * REVERSE will cause the door to rotate in the opposite direction.
 *
 * "message"	is printed when the door is touched if it is a trigger door and it hasn't been fired yet
 * "angle"		determines the opening direction
 * "targetname" if set, no touch field will be spawned and a remote button or trigger field activates the door.
 * "health"	    if set, door must be shot open
 * "speed"		movement speed (100 default)
 * "wait"		wait before returning (3 default, -1 = never return)
 * "dmg"		damage to inflict when blocked (2 default)
 * "sounds"
 *    1)	silent
 *    2)	light
 *    3)	medium
 *    4)	heavy
 */

void
SP_func_door_rotating(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	VectorClear(ent->s.angles);

	/* set the axis of rotation */
	VectorClear(ent->movedir);

	if (ent->spawnflags & DOOR_X_AXIS)
	{
		ent->movedir[2] = 1.0;
	}
	else if (ent->spawnflags & DOOR_Y_AXIS)
	{
		ent->movedir[0] = 1.0;
	}
	else /* Z_AXIS */
	{
		ent->movedir[1] = 1.0;
	}

	/* check for reverse rotation */
	if (ent->spawnflags & DOOR_REVERSE)
	{
		VectorNegate(ent->movedir, ent->movedir);
	}

	if (!st.distance)
	{
		gi.dprintf("%s at %s with no distance set\n", ent->classname,
				vtos(ent->s.origin));
		st.distance = 90;
	}

	VectorCopy(ent->s.angles, ent->pos1);
	VectorMA(ent->s.angles, st.distance, ent->movedir, ent->pos2);
	ent->moveinfo.distance = st.distance;

	ent->movetype = MOVETYPE_PUSH;
	ent->solid = SOLID_BSP;
	gi.setmodel(ent, ent->model);

	ent->blocked = door_blocked;
	ent->use = door_use;

	if (!ent->speed)
	{
		ent->speed = 100;
	}

	if (!ent->accel)
	{
		ent->accel = ent->speed;
	}

	if (!ent->decel)
	{
		ent->decel = ent->speed;
	}

	if (!ent->wait)
	{
		ent->wait = 3;
	}

	if (!ent->dmg)
	{
		ent->dmg = 2;
	}

	if (ent->sounds != 1)
	{
		ent->moveinfo.sound_start = gi.soundindex("doors/dr1_strt.wav");
		ent->moveinfo.sound_middle = gi.soundindex("doors/dr1_mid.wav");
		ent->moveinfo.sound_end = gi.soundindex("doors/dr1_end.wav");
	}

	/* if it starts open, switch the positions */
	if (ent->spawnflags & DOOR_START_OPEN)
	{
		VectorCopy(ent->pos2, ent->s.angles);
		VectorCopy(ent->pos1, ent->pos2);
		VectorCopy(ent->s.angles, ent->pos1);
		VectorNegate(ent->movedir, ent->movedir);
	}

	if (ent->health)
	{
		ent->takedamage = DAMAGE_YES;
		ent->die = door_killed;
		ent->max_health = ent->health;
	}

	if (ent->targetname && ent->message)
	{
		gi.soundindex("misc/talk.wav");
		ent->touch = door_touch;
	}

	ent->moveinfo.state = STATE_BOTTOM;
	ent->moveinfo.speed = ent->speed;
	ent->moveinfo.accel = ent->accel;
	ent->moveinfo.decel = ent->decel;
	ent->moveinfo.wait = ent->wait;
	VectorCopy(ent->s.origin, ent->moveinfo.start_origin);
	VectorCopy(ent->pos1, ent->moveinfo.start_angles);
	VectorCopy(ent->s.origin, ent->moveinfo.end_origin);
	VectorCopy(ent->pos2, ent->moveinfo.end_angles);

	if (ent->spawnflags & 16)
	{
		ent->s.effects |= EF_ANIM_ALL;
	}

	/* to simplify logic elsewhere, make non-teamed doors into a team of one */
	if (!ent->team)
	{
		ent->teammaster = ent;
	}

	gi.linkentity(ent);

	ent->nextthink = level.time + FRAMETIME;

	if (ent->health || ent->targetname)
	{
		ent->think = Think_CalcMoveSpeed;
	}
	else
	{
		ent->think = Think_SpawnDoorTrigger;
	}
}

/* ==================================================================== */

/*
 * QUAKED func_water (0 .5 .8) ? START_OPEN
 *
 * func_water is a moveable water brush.  It must be targeted to operate.
 * Use a non-water texture at your own risk.
 *
 * START_OPEN causes the water to move to its destination when spawned
 *             and operate in reverse.
 *
 * "angle"		determines the opening direction (up or down only)
 * "speed"		movement speed (25 default)
 * "wait"		wait before returning (-1 default, -1 = TOGGLE)
 * "lip"		lip remaining at end of move (0 default)
 * "sounds"	    (yes, these need to be changed)
 *    0)	no sound
 *    1)	water
 *    2)	lava
 */
void
SP_func_water(edict_t *self)
{
	vec3_t abs_movedir;

	if (!self)
	{
		return;
	}

	G_SetMovedir(self->s.angles, self->movedir);
	self->movetype = MOVETYPE_PUSH;
	self->solid = SOLID_BSP;
	gi.setmodel(self, self->model);

	switch (self->sounds)
	{
		default:
			break;

		case 1: /* water */
			self->moveinfo.sound_start = gi.soundindex("world/mov_watr.wav");
			self->moveinfo.sound_end = gi.soundindex("world/stp_watr.wav");
			break;

		case 2: /* lava */
			self->moveinfo.sound_start = gi.soundindex("world/mov_watr.wav");
			self->moveinfo.sound_end = gi.soundindex("world/stp_watr.wav");
			break;
	}

	/* calculate second position */
	VectorCopy(self->s.origin, self->pos1);
	abs_movedir[0] = fabs(self->movedir[0]);
	abs_movedir[1] = fabs(self->movedir[1]);
	abs_movedir[2] = fabs(self->movedir[2]);
	self->moveinfo.distance = abs_movedir[0] * self->size[0] + abs_movedir[1] *
							  self->size[1] + abs_movedir[2] * self->size[2] -
							  st.lip;
	VectorMA(self->pos1, self->moveinfo.distance, self->movedir, self->pos2);

	/* if it starts open, switch the positions */
	if (self->spawnflags & DOOR_START_OPEN)
	{
		VectorCopy(self->pos2, self->s.origin);
		VectorCopy(self->pos1, self->pos2);
		VectorCopy(self->s.origin, self->pos1);
	}

	VectorCopy(self->pos1, self->moveinfo.start_origin);
	VectorCopy(self->s.angles, self->moveinfo.start_angles);
	VectorCopy(self->pos2, self->moveinfo.end_origin);
	VectorCopy(self->s.angles, self->moveinfo.end_angles);

	self->moveinfo.state = STATE_BOTTOM;

	if (!self->speed)
	{
		self->speed = 25;
	}

	self->moveinfo.accel = self->moveinfo.decel = self->moveinfo.speed = self->speed;

	if (!self->wait)
	{
		self->wait = -1;
	}

	self->moveinfo.wait = self->wait;

	self->use = door_use;

	if (self->wait == -1)
	{
		self->spawnflags |= DOOR_TOGGLE;
	}

	self->classname = "func_door";

	gi.linkentity(self);
}

/* ==================================================================== */

#define TRAIN_START_ON 1
#define TRAIN_TOGGLE 2
#define TRAIN_BLOCK_STOPS 4

void train_next(edict_t *self);

/*
 * QUAKED func_train (0 .5 .8) ? START_ON TOGGLE BLOCK_STOPS
 *
 * Trains are moving platforms that players can ride.
 * The targets origin specifies the min point of the train
 * at each corner. The train spawns at the first target it
 * is pointing at. If the train is the target of a button
 * or trigger, it will not begin moving until activated.
 *
 * speed	default 100
 * dmg		default	2
 * noise	looping sound to play when the train is in motion
 *
 */
void train_next(edict_t *self);

void
train_blocked(edict_t *self, edict_t *other)
{
	if (!self || !other)
	{
		return;
	}

	if (!(other->svflags & SVF_MONSTER) && (!other->client))
	{
		/* give it a chance to go away on it's own terms (like gibs) */
		T_Damage(other, self, self, vec3_origin, other->s.origin,
				vec3_origin, 100000, 1, 0, MOD_CRUSH);

		/* if it's still there, nuke it */
		if (other)
		{
			/* Hack for entity without an origin near the model */			
			VectorMA (other->absmin, 0.5, other->size, other->s.origin);
			BecomeExplosion1(other);
		}

		return;
	}

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

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

	self->touch_debounce_time = level.time + 0.5;
	T_Damage(other, self, self, vec3_origin, other->s.origin,
			vec3_origin, self->dmg, 1, 0, MOD_CRUSH);
}

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

	if (self->target_ent->pathtarget)
	{
		char *savetarget;
		edict_t *ent;

		ent = self->target_ent;
		savetarget = ent->target;
		ent->target = ent->pathtarget;
		G_UseTargets(ent, self->activator);
		ent->target = savetarget;

		/* make sure we didn't get killed by a killtarget */
		if (!self->inuse)
		{
			return;
		}
	}

	if (self->moveinfo.wait)
	{
		if (self->moveinfo.wait > 0)
		{
			self->nextthink = level.time + self->moveinfo.wait;
			self->think = train_next;
		}
		else if (self->spawnflags & TRAIN_TOGGLE)
		{
			train_next(self);
			self->spawnflags &= ~TRAIN_START_ON;
			VectorClear(self->velocity);
			self->nextthink = 0;
		}

		if (!(self->flags & FL_TEAMSLAVE))
		{
			if (self->moveinfo.sound_end)
			{
				gi.sound(self, CHAN_NO_PHS_ADD + CHAN_VOICE,
						self->moveinfo.sound_end, 1,
						ATTN_STATIC, 0);
			}

			self->s.sound = 0;
		}
	}
	else
	{
		train_next(self);
	}
}

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

	edict_t *ent;
	vec3_t dest;
	qboolean first;

	first = true;

again:

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

	ent = G_PickTarget(self->target);

	if (!ent)
	{
		gi.dprintf("train_next: bad target %s\n", self->target);
		return;
	}

	self->target = ent->target;

	/* check for a teleport path_corner */
	if (ent->spawnflags & 1)
	{
		if (!first)
		{
			gi.dprintf("connected teleport path_corners, see %s at %s\n",
					ent->classname, vtos(ent->s.origin));
			return;
		}

		first = false;
		VectorSubtract(ent->s.origin, self->mins, self->s.origin);
		VectorCopy(self->s.origin, self->s.old_origin);
		self->s.event = EV_OTHER_TELEPORT;
		gi.linkentity(self);
		goto again;
	}

	self->moveinfo.wait = ent->wait;
	self->target_ent = ent;

	if (!(self->flags & FL_TEAMSLAVE))
	{
		if (self->moveinfo.sound_start)
		{
			gi.sound(self, CHAN_NO_PHS_ADD + CHAN_VOICE,
					self->moveinfo.sound_start, 1,
					ATTN_STATIC, 0);
		}

		self->s.sound = self->moveinfo.sound_middle;
	}

	VectorSubtract(ent->s.origin, self->mins, dest);
	self->moveinfo.state = STATE_TOP;
	VectorCopy(self->s.origin, self->moveinfo.start_origin);
	VectorCopy(dest, self->moveinfo.end_origin);
	Move_Calc(self, dest, train_wait);
	self->spawnflags |= TRAIN_START_ON;
}

void
train_resume(edict_t *self)
{
	edict_t *ent;
	vec3_t dest;

	if (!self)
	{
		return;
	}

	ent = self->target_ent;

	VectorSubtract(ent->s.origin, self->mins, dest);
	self->moveinfo.state = STATE_TOP;
	VectorCopy(self->s.origin, self->moveinfo.start_origin);
	VectorCopy(dest, self->moveinfo.end_origin);
	Move_Calc(self, dest, train_wait);
	self->spawnflags |= TRAIN_START_ON;
}

void
func_train_find(edict_t *self)
{
	edict_t *ent;

	if (!self)
	{
		return;
	}

	if (!self->target)
	{
		gi.dprintf("train_find: no target\n");
		return;
	}

	ent = G_PickTarget(self->target);

	if (!ent)
	{
		gi.dprintf("train_find: target %s not found\n", self->target);
		return;
	}

	self->target = ent->target;

	VectorSubtract(ent->s.origin, self->mins, self->s.origin);
	gi.linkentity(self);

	/* if not triggered, start immediately */
	if (!self->targetname)
	{
		self->spawnflags |= TRAIN_START_ON;
	}

	if (self->spawnflags & TRAIN_START_ON)
	{
		self->nextthink = level.time + FRAMETIME;
		self->think = train_next;
		self->activator = self;
	}
}

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

	self->activator = activator;

	if (self->spawnflags & TRAIN_START_ON)
	{
		if (!(self->spawnflags & TRAIN_TOGGLE))
		{
			return;
		}

		self->spawnflags &= ~TRAIN_START_ON;
		VectorClear(self->velocity);
		self->nextthink = 0;
	}
	else
	{
		if (self->target_ent)
		{
			train_resume(self);
		}
		else
		{
			train_next(self);
		}
	}
}

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

	self->movetype = MOVETYPE_PUSH;

	VectorClear(self->s.angles);
	self->blocked = train_blocked;

	if (self->spawnflags & TRAIN_BLOCK_STOPS)
	{
		self->dmg = 0;
	}
	else
	{
		if (!self->dmg)
		{
			self->dmg = 100;
		}
	}

	self->solid = SOLID_BSP;
	gi.setmodel(self, self->model);

	if (st.noise)
	{
		self->moveinfo.sound_middle = gi.soundindex(st.noise);
	}

	if (!self->speed)
	{
		self->speed = 100;
	}

	self->moveinfo.speed = self->speed;
	self->moveinfo.accel = self->moveinfo.decel = self->moveinfo.speed;

	self->use = train_use;

	gi.linkentity(self);

	if (self->target)
	{
		/* start trains on the second frame, to make
		 * sure their targets have had a chance to spawn */
		self->nextthink = level.time + FRAMETIME;
		self->think = func_train_find;
	}
	else
	{
		gi.dprintf("func_train without a target at %s\n", vtos(self->absmin));
	}
}

/* ==================================================================== */

/*
 * QUAKED trigger_elevator (0.3 0.1 0.6) (-8 -8 -8) (8 8 8)
 */
void
trigger_elevator_use(edict_t *self, edict_t *other, edict_t *activator /* unused */)
{
	edict_t *target;

	if (!self || !other)
	{
		return;
	}

	if (self->movetarget->nextthink)
	{
		return;
	}

	if (!other->pathtarget)
	{
		gi.dprintf("elevator used with no pathtarget\n");
		return;
	}

	target = G_PickTarget(other->pathtarget);

	if (!target)
	{
		gi.dprintf("elevator used with bad pathtarget: %s\n", other->pathtarget);
		return;
	}

	self->movetarget->target_ent = target;
	train_resume(self->movetarget);
}

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

	if (!self->target)
	{
		gi.dprintf("trigger_elevator has no target\n");
		return;
	}

	self->movetarget = G_PickTarget(self->target);

	if (!self->movetarget)
	{
		gi.dprintf("trigger_elevator unable to find target %s\n", self->target);
		return;
	}

	if (strcmp(self->movetarget->classname, "func_train") != 0)
	{
		gi.dprintf("trigger_elevator target %s is not a train\n", self->target);
		return;
	}

	self->use = trigger_elevator_use;
	self->svflags = SVF_NOCLIENT;
}

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

	self->think = trigger_elevator_init;
	self->nextthink = level.time + FRAMETIME;
}

/* ==================================================================== */

/*
 * QUAKED func_timer (0.3 0.1 0.6) (-8 -8 -8) (8 8 8) START_ON
 *
 * "wait"	base time between triggering all targets, default is 1
 * "random"	wait variance, default is 0
 *
 * so, the basic time between firing is a random time
 * between (wait - random) and (wait + random)
 *
 * "delay"			delay before first firing when turned on, default is 0
 * "pausetime"		additional delay used only the very first time
 *                  and only if spawned with START_ON
 *
 * These can used but not touched.
 */
void
func_timer_think(edict_t *self)
{
	if (!self)
	{
		return;
	}

	G_UseTargets(self, self->activator);
	self->nextthink = level.time + self->wait + crandom() * self->random;
}

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

	self->activator = activator;

	/* if on, turn it off */
	if (self->nextthink)
	{
		self->nextthink = 0;
		return;
	}

	/* turn it on */
	if (self->delay)
	{
		self->nextthink = level.time + self->delay;
	}
	else
	{
		func_timer_think(self);
	}
}

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

	if (!self->wait)
	{
		self->wait = 1.0;
	}

	self->use = func_timer_use;
	self->think = func_timer_think;

	if (self->random >= self->wait)
	{
		self->random = self->wait - FRAMETIME;
		gi.dprintf("func_timer at %s has random >= wait\n", vtos(self->s.origin));
	}

	if (self->spawnflags & 1)
	{
		self->nextthink = level.time + 1.0 + st.pausetime + self->delay +
						  self->wait + crandom() * self->random;
		self->activator = self;
	}

	self->svflags = SVF_NOCLIENT;
}

/* ==================================================================== */

/*
 * QUAKED func_conveyor (0 .5 .8) ? START_ON TOGGLE
 *
 * Conveyors are stationary brushes that move what's on them.
 * The brush should be have a surface with at least one current
 * content enabled.
 *
 * speed	default 100
 */
void
func_conveyor_use(edict_t *self, edict_t *other /* unused */, edict_t *activator /* unused */)
{
	if (!self)
	{
		return;
	}

	if (self->spawnflags & 1)
	{
		self->speed = 0;
		self->spawnflags &= ~1;
	}
	else
	{
		self->speed = self->count;
		self->spawnflags |= 1;
	}

	if (!(self->spawnflags & 2))
	{
		self->count = 0;
	}
}

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

	if (!self->speed)
	{
		self->speed = 100;
	}

	if (!(self->spawnflags & 1))
	{
		self->count = self->speed;
		self->speed = 0;
	}

	self->use = func_conveyor_use;

	gi.setmodel(self, self->model);
	self->solid = SOLID_BSP;
	gi.linkentity(self);
}

/* ==================================================================== */

/*
 * QUAKED func_door_secret (0 .5 .8) ? always_shoot 1st_left 1st_down
 * A secret door. Slide back and then to the side.
 *
 * open_once	doors never closes
 * 1st_left		1st move is left of arrow
 * 1st_down		1st move is down from arrow
 * always_shoot	door is shootebale even if targeted
 *
 * "angle"		determines the direction
 * "dmg"		damage to inflic when blocked (default 2)
 * "wait"		how long to hold in the open position (default 5, -1 means hold)
 */

#define SECRET_ALWAYS_SHOOT 1
#define SECRET_1ST_LEFT 2
#define SECRET_1ST_DOWN 4

void door_secret_move1(edict_t *self);
void door_secret_move2(edict_t *self);
void door_secret_move3(edict_t *self);
void door_secret_move4(edict_t *self);
void door_secret_move5(edict_t *self);
void door_secret_move6(edict_t *self);
void door_secret_done(edict_t *self);

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

	/* make sure we're not already moving */
	if (!VectorCompare(self->s.origin, vec3_origin))
	{
		return;
	}

	Move_Calc(self, self->pos1, door_secret_move1);
	door_use_areaportals(self, true);
}

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

	self->nextthink = level.time + 1.0;
	self->think = door_secret_move2;
}

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

	Move_Calc(self, self->pos2, door_secret_move3);
}

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

	if (self->wait == -1)
	{
		return;
	}

	self->nextthink = level.time + self->wait;
	self->think = door_secret_move4;
}

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

	Move_Calc(self, self->pos1, door_secret_move5);
}

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

	self->nextthink = level.time + 1.0;
	self->think = door_secret_move6;
}

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

	Move_Calc(self, vec3_origin, door_secret_done);
}

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

	if (!(self->targetname) || (self->spawnflags & SECRET_ALWAYS_SHOOT))
	{
		self->health = 0;
		self->takedamage = DAMAGE_YES;
	}

	door_use_areaportals(self, false);
}

void
door_secret_blocked(edict_t *self, edict_t *other)
{
	if (!self || !other)
	{
		return;
	}

	if (!(other->svflags & SVF_MONSTER) && (!other->client))
	{
		/* give it a chance to go away on it's own terms (like gibs) */
		T_Damage(other, self, self, vec3_origin, other->s.origin,
				vec3_origin, 100000, 1, 0, MOD_CRUSH);

		/* if it's still there, nuke it */
		if (other)
		{
			/* Hack for entities without their origin near the model */
			VectorMA (other->absmin, 0.5, other->size, other->s.origin);
			BecomeExplosion1(other);
		}

		return;
	}

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

	self->touch_debounce_time = level.time + 0.5;

	T_Damage(other, self, self, vec3_origin, other->s.origin,
			vec3_origin, self->dmg, 1, 0, MOD_CRUSH);
}

void
door_secret_die(edict_t *self, edict_t *inflictor /* unused */, edict_t *attacker,
		int damage /* unused */, vec3_t point /* unused */)
{
	if (!self || !attacker)
	{
		return;
	}

	self->takedamage = DAMAGE_NO;
	door_secret_use(self, attacker, attacker);
}

void
SP_func_door_secret(edict_t *ent)
{
	vec3_t forward, right, up;
	float side;
	float width;
	float length;

	if (!ent)
	{
		return;
	}

	ent->moveinfo.sound_start = gi.soundindex("doors/dr1_strt.wav");
	ent->moveinfo.sound_middle = gi.soundindex("doors/dr1_mid.wav");
	ent->moveinfo.sound_end = gi.soundindex("doors/dr1_end.wav");

	ent->movetype = MOVETYPE_PUSH;
	ent->solid = SOLID_BSP;
	gi.setmodel(ent, ent->model);

	ent->blocked = door_secret_blocked;
	ent->use = door_secret_use;

	if (!(ent->targetname) || (ent->spawnflags & SECRET_ALWAYS_SHOOT))
	{
		ent->health = 0;
		ent->takedamage = DAMAGE_YES;
		ent->die = door_secret_die;
	}

	if (!ent->dmg)
	{
		ent->dmg = 2;
	}

	if (!ent->wait)
	{
		ent->wait = 5;
	}

	ent->moveinfo.accel = ent->moveinfo.decel = ent->moveinfo.speed = 50;

	/* calculate positions */
	AngleVectors(ent->s.angles, forward, right, up);
	VectorClear(ent->s.angles);
	side = 1.0 - (ent->spawnflags & SECRET_1ST_LEFT);

	if (ent->spawnflags & SECRET_1ST_DOWN)
	{
		width = fabs(DotProduct(up, ent->size));
	}
	else
	{
		width = fabs(DotProduct(right, ent->size));
	}

	length = fabs(DotProduct(forward, ent->size));

	if (ent->spawnflags & SECRET_1ST_DOWN)
	{
		VectorMA(ent->s.origin, -1 * width, up, ent->pos1);
	}
	else
	{
		VectorMA(ent->s.origin, side * width, right, ent->pos1);
	}

	VectorMA(ent->pos1, length, forward, ent->pos2);

	if (ent->health)
	{
		ent->takedamage = DAMAGE_YES;
		ent->die = door_killed;
		ent->max_health = ent->health;
	}
	else if (ent->targetname && ent->message)
	{
		gi.soundindex("misc/talk.wav");
		ent->touch = door_touch;
	}

	ent->classname = "func_door";

	gi.linkentity(ent);
}

/* ==================================================================== */

/*
 * QUAKED func_killbox (1 0 0) ?
 *
 * Kills everything inside when fired,
 * irrespective of protection.
 */
void
use_killbox(edict_t *self, edict_t *other /* unused */, edict_t *activator /* unused */)
{
	if (!self)
	{
		return;
	}

	KillBox(self);

	/* Hack to make sure that really everything is killed */
	self->count--;

	if (!self->count)
	{
		self->think = G_FreeEdict;
		self->nextthink = level.time + 1;
	}
}

void
SP_func_killbox(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	gi.setmodel(ent, ent->model);
	ent->use = use_killbox;
	ent->svflags = SVF_NOCLIENT;
}

/*
 * QUAKED rotating_light (0 .5 .8) (-8 -8 -8) (8 8 8) START_OFF ALARM
 * "health"	if set, the light may be killed.
 */

#define START_OFF 1

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

	if (self->spawnflags & START_OFF)
	{
		self->think = NULL;
		self->nextthink = 0;
	}
	else
	{
		gi.sound(self, CHAN_NO_PHS_ADD + CHAN_VOICE,
				self->moveinfo.sound_start, 1,
				ATTN_STATIC, 0);
		self->nextthink = level.time + 1;
	}
}

void
rotating_light_killed(edict_t *self, edict_t *inflictor /* unused */,
		edict_t *attacker /* unused */, int damage /* unused */,
		vec3_t point /* unused */)
{
	if (!self)
	{
		return;
	}

	gi.WriteByte(svc_temp_entity);
	gi.WriteByte(TE_WELDING_SPARKS);
	gi.WriteByte(30);
	gi.WritePosition(self->s.origin);
	gi.WriteDir(vec3_origin);
	gi.WriteByte(0xe0 + (rand() & 7));
	gi.multicast(self->s.origin, MULTICAST_PVS);

	self->s.effects &= ~EF_SPINNINGLIGHTS;
	self->use = NULL;

	self->think = G_FreeEdict;
	self->nextthink = level.time + 0.1;
}

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

	if (self->spawnflags & START_OFF)
	{
		self->spawnflags &= ~START_OFF;
		self->s.effects |= EF_SPINNINGLIGHTS;

		if (self->spawnflags & 2)
		{
			self->think = rotating_light_alarm;
			self->nextthink = level.time + 0.1;
		}
	}
	else
	{
		self->spawnflags |= START_OFF;
		self->s.effects &= ~EF_SPINNINGLIGHTS;
	}
}

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

	self->movetype = MOVETYPE_STOP;
	self->solid = SOLID_BBOX;

	self->s.modelindex = gi.modelindex("models/objects/light/tris.md2");

	self->s.frame = 0;

	self->use = rotating_light_use;

	if (self->spawnflags & START_OFF)
	{
		self->s.effects &= ~EF_SPINNINGLIGHTS;
	}
	else
	{
		self->s.effects |= EF_SPINNINGLIGHTS;
	}

	if (!self->speed)
	{
		self->speed = 32;
	}

	if (!self->health)
	{
		self->health = 10;
		self->max_health = self->health;
		self->die = rotating_light_killed;
		self->takedamage = DAMAGE_YES;
	}
	else
	{
		self->max_health = self->health;
		self->die = rotating_light_killed;
		self->takedamage = DAMAGE_YES;
	}

	if (self->spawnflags & 2)
	{
		self->moveinfo.sound_start = gi.soundindex("misc/alarm.wav");
	}

	gi.linkentity(self);
}

/*
 * QUAKED func_object_repair (1 .5 0) (-8 -8 -8) (8 8 8)
 * object to be repaired.
 * The default delay is 1 second
 * "delay" the delay in seconds for spark to occur
 */
void
object_repair_fx(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	ent->nextthink = level.time + ent->delay;

	if (ent->health <= 100)
	{
		ent->health++;
	}
	else
	{
		gi.WriteByte(svc_temp_entity);
		gi.WriteByte(TE_WELDING_SPARKS);
		gi.WriteByte(10);
		gi.WritePosition(ent->s.origin);
		gi.WriteDir(vec3_origin);
		gi.WriteByte(0xe0 + (rand() & 7));
		gi.multicast(ent->s.origin, MULTICAST_PVS);
	}
}

void
object_repair_dead(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	G_UseTargets(ent, ent);
	ent->nextthink = level.time + 0.1;
	ent->think = object_repair_fx;
}

void
object_repair_sparks(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	if (ent->health < 0)
	{
		ent->nextthink = level.time + 0.1;
		ent->think = object_repair_dead;
		return;
	}

	ent->nextthink = level.time + ent->delay;

	gi.WriteByte(svc_temp_entity);
	gi.WriteByte(TE_WELDING_SPARKS);
	gi.WriteByte(10);
	gi.WritePosition(ent->s.origin);
	gi.WriteDir(vec3_origin);
	gi.WriteByte(0xe0 + (rand() & 7));
	gi.multicast(ent->s.origin, MULTICAST_PVS);
}

void
SP_object_repair(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	ent->movetype = MOVETYPE_NONE;
	ent->solid = SOLID_BBOX;
	ent->classname = "object_repair";
	VectorSet(ent->mins, -8, -8, 8);
	VectorSet(ent->maxs, 8, 8, 8);
	ent->think = object_repair_sparks;
	ent->nextthink = level.time + 1.0;
	ent->health = 100;

	if (!ent->delay)
	{
		ent->delay = 1.0;
	}
}

