/*
Copyright (C) 1997-2001 Id Software, Inc.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/

#include "g_local.h"

void G_AssignMoverSounds( edict_t *ent, char *start, char *move, char *stop )
{
	if( st.noise && Q_stricmp(st.noise, "default") ) {
		if( Q_stricmp(st.noise, "silent") )
			ent->moveinfo.sound_middle = trap_SoundIndex( st.noise );
	} else if( move )
		ent->moveinfo.sound_middle = trap_SoundIndex( move );

	if( st.noise_start && Q_stricmp(st.noise_start, "default") ) {
		if( Q_stricmp(st.noise_start, "silent") )
			ent->moveinfo.sound_start = trap_SoundIndex( st.noise_start );
	} else if( start )
		ent->moveinfo.sound_start = trap_SoundIndex( start );

	if( st.noise_stop && Q_stricmp(st.noise_stop, "default") ) {
		if( Q_stricmp(st.noise_stop, "silent") )
			ent->moveinfo.sound_end = trap_SoundIndex( st.noise_stop );
	} else if( stop )
		ent->moveinfo.sound_end = trap_SoundIndex( stop );
}

//=========================================================
//
//  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 STATE_STOPPED		0
#define STATE_ACCEL			1
#define STATE_FULLSPEED		2
#define STATE_DECEL			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
//wsw
#define DOOR_DIE_ONCE		1024 // auto set when health is > 0


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

void Move_Done( edict_t *ent )
{
	VectorClear( ent->velocity );
	ent->moveinfo.endfunc(ent);
}

void Move_Final( edict_t *ent )
{
	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.timemsec + game.framemsec;
}

void Move_Begin( edict_t *ent )
{
	float	frames;
	float	traveltime;

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

	// set up velocity vector
	VectorScale( ent->moveinfo.dir, ent->moveinfo.speed, ent->velocity );
	//frames = floor((ent->moveinfo.remaining_distance / ent->moveinfo.speed) / FRAMETIME);

	// divide dist by speed to get time to reach dest
	traveltime = ent->moveinfo.remaining_distance / ent->moveinfo.speed;
	if( traveltime < FRAMETIME )
	{
		Move_Final(ent);
		return;
	}
	
	// find number of frames to be run within the give time (quite useless now)
	frames = floor(traveltime / FRAMETIME);
	ent->moveinfo.remaining_distance -= frames * ent->moveinfo.speed * FRAMETIME;
	
	// set nextthink to trigger a think when dest is reached
	ent->nextthink = level.timemsec + (frames * game.framemsec);
	ent->think = Move_Final;
}


void Think_AccelMove( edict_t *ent );

void Move_Calc( edict_t *ent, vec3_t dest, void(*func)(edict_t*) )
{
	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.timemsec + game.framemsec;
			ent->think = Move_Begin;
		}
	}
	else
	{
		// accelerative
		ent->moveinfo.current_speed = 0;
		ent->think = Think_AccelMove;
		ent->nextthink = level.timemsec + game.framemsec;
	}
}


//
// Support routines for angular movement (changes in angle using avelocity)
//

void AngleMove_Done( edict_t *ent )
{
	VectorClear( ent->avelocity );
	ent->moveinfo.endfunc(ent);
}

void AngleMove_Final( edict_t *ent )
{
	vec3_t	move;

	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.timemsec + game.framemsec;
}

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

	// 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.timemsec + frames * game.framemsec;
	ent->think = AngleMove_Final;
}

void AngleMove_Calc( edict_t *ent, void(*func)(edict_t*) )
{
	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.timemsec + game.framemsec;
		ent->think = AngleMove_Begin;
	}
}


//==============
//Think_AccelMove
//
//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;

	moveinfo->move_speed = moveinfo->speed;
	
	if( moveinfo->remaining_distance < moveinfo->accel * FRAMETIME )
	{
		moveinfo->current_speed = moveinfo->remaining_distance;
		return;
	}

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

	// jal : wsw : I don't understand very well what's this case doing :/
	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 )
{
	// 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;
}


void Think_AccelMove( edict_t *ent )
{
	ent->moveinfo.remaining_distance -= ent->moveinfo.current_speed * FRAMETIME;
	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 * FRAMETIME )
	{
		Move_Final(ent);
		return;
	}
	VectorScale( ent->moveinfo.dir, ent->moveinfo.current_speed, ent->velocity );
	ent->nextthink = level.timemsec + game.framemsec;
	ent->think = Think_AccelMove;
}


void plat_go_down( edict_t *ent );

void plat_hit_top( edict_t *ent )
{
	if( !(ent->flags & FL_TEAMSLAVE) )
	{
		if( ent->moveinfo.sound_end )
			G_Sound( ent, CHAN_VOICE, ent->moveinfo.sound_end, 1, ATTN_STATIC );
		ent->s.sound = 0;
	}
	ent->moveinfo.state = STATE_TOP;

	ent->think = plat_go_down;
	ent->nextthink = level.timemsec + 3000;
}

void plat_hit_bottom( edict_t *ent )
{
	if( !(ent->flags & FL_TEAMSLAVE) )
	{
		if( ent->moveinfo.sound_end )
			G_Sound (ent, CHAN_VOICE, ent->moveinfo.sound_end, 1, ATTN_STATIC);
		ent->s.sound = 0;
	}
	ent->moveinfo.state = STATE_BOTTOM;
}

void plat_go_down( edict_t *ent )
{
	if( !(ent->flags & FL_TEAMSLAVE) )
	{
		if( ent->moveinfo.sound_start )
			G_Sound( ent, CHAN_VOICE, ent->moveinfo.sound_start, 1, ATTN_STATIC );
		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->flags & FL_TEAMSLAVE) )
	{
		if( ent->moveinfo.sound_start )
			G_Sound( ent, CHAN_VOICE, ent->moveinfo.sound_start, 1, ATTN_STATIC );
		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( !(other->r.svflags & SVF_MONSTER) && (!other->r.client) )
	{
		// give it a chance to go away on its 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 )
			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, edict_t *activator )
{ 
	if( ent->think )
		return;		// already down
	plat_go_down (ent);
}


void Touch_Plat_Center( edict_t *ent, edict_t *other, cplane_t *plane, int surfFlags )
{
	if( !other->r.client )
		return;
	if( G_IsDead(other) )
		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.timemsec + 1000;	// the player is still on the plat, so delay going down
}

void plat_spawn_inside_trigger( edict_t *ent )
{
	edict_t	*trigger;
	vec3_t	tmin, tmax;

//
// middle trigger
//	
	trigger = G_Spawn();
	trigger->touch = Touch_Plat_Center;
	trigger->movetype = MOVETYPE_NONE;
	trigger->r.solid = SOLID_TRIGGER;
	trigger->enemy = ent;
	
	tmin[0] = ent->r.mins[0] + 25;
	tmin[1] = ent->r.mins[1] + 25;
	tmin[2] = ent->r.mins[2];

	tmax[0] = ent->r.maxs[0] - 25;
	tmax[1] = ent->r.maxs[1] - 25;
	tmax[2] = ent->r.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->r.mins[0] + ent->r.maxs[0]) * 0.5;
		tmax[0] = tmin[0] + 1;
	}
	if (tmax[1] - tmin[1] <= 0)
	{
		tmin[1] = (ent->r.mins[1] + ent->r.maxs[1]) * 0.5;
		tmax[1] = tmin[1] + 1;
	}
	
	VectorCopy( tmin, trigger->r.mins );
	VectorCopy( tmax, trigger->r.maxs );

	trap_LinkEntity( trigger );
}


/*QUAKED func_plat (0 .5 .8) ? LOW_TRIGGER
Rising platform the player can ride to reach higher places. Plats must always be drawn in the raised position, so they will operate and be lighted correctly but they spawn in the lowered position. The plat will stay in the raised position until the player steps off.
-------- KEYS --------
speed : determines how fast the plat moves (default 300).
accel : default 20
deccel : default 20
lip : lip remaining at end of move (default 8).
height : if set, this will determine the total amount of vertical travel of the plat, instead of being implicitly assigned by the model's height.
dmg : damage to inflict on player when he blocks operation of plat (default 2). Plat will reverse direction when blocked.
targetname : if set, the trigger that points to this will raise the plat each time it fires.
noise_start : overrides default start moving sound
noise : overrides default movement sound
noise_stop : overrides default stop moving sound
light : constantLight radius of .md3 model included with entity. Has no effect on the entity's brushes (default 0).
color : constantLight color of .md3 model included with entity. Has no effect on the entity's brushes (default 1 1 1).
model2 : path/name of model to include (eg: models/mapobjects/pipe/pipe02.md3).
origin : alternate method of setting XYZ origin of .md3 model included with entity (See Notes).
notsingle : when set to 1, entity will not spawn in Single Player mode
notfree : when set to 1, entity will not spawn in "Free for all" and "Tournament" modes.
notduel : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
notteam : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. 
notctf : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
-------- SPAWNFLAGS --------
LOW_TRIGGER : &1 the plat is triggered from its lower side
-------- NOTES --------
By default, the total amount of vertical travel of a platform is implicitly determined by
the overall vertical size of the brushes of which it's made minus the lip value.
But if the "height" key is used, then the total amount of vertical travel of the plat will
be exactly that value regardless of the shape and size of the plat and regardless of the
value of the "lip" key. Using the "height" key is the best method for any kind of platforms
and the only possible one for thin plats which need to travel vertical distances many times
their own thickness. Setting the origin key is simply an alternate method to using an
origin brush. When using the model2 key, the origin point of the model will correspond
to the origin point defined by either the origin brush or the origin coordinate value.
*/

void SP_func_plat( edict_t *ent )
{
	G_InitMover( ent );

	VectorClear( ent->s.angles );

	ent->blocked = plat_blocked;

	if( !ent->speed )
		ent->speed = 300;

	if( !ent->accel )
		ent->accel = 20;

	if( !ent->decel )
		ent->decel = 20;

	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->r.maxs[2] - ent->r.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 );
		trap_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 );

	G_AssignMoverSounds( ent, S_PLAT_START, S_PLAT_MOVE, S_PLAT_STOP );
}




//====================================================================

/*QUAKED func_rotating (0 .5 .8) ? START_OFF REVERSE X_AXIS Y_AXIS TOUCH_PAIN STOP - -
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.
-------- KEYS --------
targetname : name to be targeted
target : names to target.
speed : determines how fast entity rotates (default 100).
noise_start : overrides default start moving sound
noise : overrides default movement sound
noise_stop : overrides default stop moving sound
model2 : path/name of model to include (eg: models/mapobjects/bitch/fembotbig.md3).
origin : alternate method of setting XYZ origin of entity's rotation axis and .md3 model included with entity (default "0 0 0" - See Notes).
light : constantLight radius of .md3 model included with entity. Has no effect on the entity's brushes (default 0).
color : constantLight color of .md3 model included with entity. Has no effect on the entity's brushes (default 1 1 1).
notsingle : when set to 1, entity will not spawn in Single Player mode
notfree : when set to 1, entity will not spawn in "Free for all" and "Tournament" modes.
notduel : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
notteam : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. 
notctf : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
-------- SPAWNFLAGS --------
START_OFF : &1 must be triggered to start moving.
REVERSE : &2 will cause the it to rotate in the opposite direction.
X_AXIS : &4 entity will rotate along the X axis.
Y_AXIS : &8 entity will rotate along the Y axis.
TOUCH_PAIN : &16 dunno what it does (jal)
STOP : &32 it will stop moving instead when blocked, instead of pushing or crushing them.
-------- NOTES --------
You need to have an origin brush as part of this entity. The center of that brush will be the point through which the rotation axis passes. Setting the origin key is simply an alternate method to using an origin brush. It will rotate along the Z axis by default. You can check either the X_AXIS or Y_AXIS box to change that. When using the model2 key, the origin point of the model will correspond to the origin point defined by either the origin brush or the origin coordinate value.*/


void rotating_blocked( edict_t *self, edict_t *other )
{
	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, int surfFlags )
{
	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 Think_RotateAccel( edict_t *self )
{
   if( self->moveinfo.current_speed >= self->speed ) 
   {  // has reached full speed

	  // if calculation causes it to go a little over, readjust
      if( self->moveinfo.current_speed != self->speed )
         VectorScale( self->movedir, self->speed, self->avelocity );

      self->think = NULL;
      self->moveinfo.state = STATE_FULLSPEED;
      return;
   } // has reached full speed

   // if here, some more acceleration needs to be done
   // add acceleration value to current speed to cause accel
   self->moveinfo.current_speed += self->accel;
   VectorScale( self->movedir, self->moveinfo.current_speed, self->avelocity );
   self->nextthink = level.timemsec + game.framemsec;
}

void Think_RotateDecel( edict_t *self )
{
   if( self->moveinfo.current_speed <= 0 ) 
   {  // has reached full speed

      // if calculation cause it to go a little under, readjust
      if( self->moveinfo.current_speed != 0 )
      {
         VectorClear( self->avelocity );
         self->moveinfo.current_speed = 0;
      }

      self->think = NULL;
      self->moveinfo.state = STATE_STOPPED;
      return;
   } // has reached full stop

   // if here, some more deceleration needs to be done
   // subtract deceleration value from current speed to cause decel
   self->moveinfo.current_speed -= self->decel;
   VectorScale( self->movedir, self->moveinfo.current_speed, self->avelocity );
   self->nextthink = level.timemsec + game.framemsec;
}

void rotating_use( edict_t *self, edict_t *other, edict_t *activator )
{
	// first, figure out what state we are in
	if( self->moveinfo.state == STATE_ACCEL || self->moveinfo.state == STATE_FULLSPEED )
	{
		// if decel is 0 then just stop
		if( self->decel == 0 ) 
		{
			VectorClear( self->avelocity );
			self->moveinfo.current_speed = 0;
			self->touch = NULL;
			self->think = NULL;
			self->moveinfo.state = STATE_STOPPED;
		} 
		else 
		{ 
			// otherwise decelerate
			self->think = Think_RotateDecel;
			self->nextthink = level.timemsec + game.framemsec;
			self->moveinfo.state = STATE_DECEL;
		} // decelerate
	}
	else
	{
		self->s.sound = self->moveinfo.sound_middle;
		// check if accel is 0.  If so, just start the rotation
		if( self->accel == 0 ) 
		{
			VectorScale( self->movedir, self->speed, self->avelocity );
			self->moveinfo.state = STATE_FULLSPEED;
		}
		else 
		{ 
			// accelerate baybee
			self->think = Think_RotateAccel;
			self->nextthink = level.timemsec + game.framemsec;
			self->moveinfo.state = STATE_ACCEL;
		}
	}

	// setup touch function if needed
	if( self->spawnflags & 16)
		self->touch = rotating_touch;
}

void SP_func_rotating( edict_t *ent )
{
	G_InitMover( ent );

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

	ent->moveinfo.state = STATE_STOPPED; // rotating thingy starts out idle

	// 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 // Z_AXIS
		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;

	if( ent->accel < 0 ) // sanity check
		ent->accel = 0;
	else
		ent->accel *= 0.1f;
	
	if( ent->decel < 0 ) // sanity check
		ent->decel = 0;
	else
		ent->decel *= 0.1f;
	
	ent->moveinfo.current_speed = 0;

	ent->use = rotating_use;
	if( ent->dmg )
		ent->blocked = rotating_blocked;

	G_AssignMoverSounds( ent, S_FUNC_ROTATING_START, S_FUNC_ROTATING_MOVE, S_FUNC_ROTATING_STOP );

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

	trap_LinkEntity( ent );
}


//======================================================================
//
//BUTTONS
//
//======================================================================


/*QUAKED func_button (0 .5 .8) ?
When a button is touched by a player, it moves in the direction set by the "angle" key, triggers all its targets, stays pressed by an amount of time set by the "wait" key, then returns to it's original position where it can be operated again.
-------- KEYS --------
angle : determines the direction in which the button will move (up = -1, down = -2).
target : all entities with a matching targetname will be triggered.
speed : speed of button's displacement (default 40).
wait : number of seconds button stays pressed (default 1, -1 = return immediately).
lip : lip remaining at end of move (default 4 units).
health : (default 0) if set to any non-zero value, the button must take damage (any amount) to activate.
noise : custom noise to be played when activated
light : constantLight radius of .md3 model included with entity. Has no effect on the entity's brushes (default 0).
color : constantLight color of .md3 model included with entity. Has no effect on the entity's brushes (default 1 1 1).
model2 : path/name of model to include (eg: models/mapobjects/pipe/pipe02.md3).
origin : alternate method of setting XYZ origin of .md3 model included with entity (See Notes).
notsingle : when set to 1, entity will not spawn in Single Player mode
notfree : when set to 1, entity will not spawn in "Free for all" and "Tournament" modes.
notduel : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
notteam : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. 
notctf : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
nobot : bots can't trigger it  (jal: todo)
nomonster : monsters can't trigger it  (jal: todo)
-------- NOTES --------
Setting the origin key is simply an alternate method to using an origin brush. When using the model2 key, the origin point of the model will correspond to the origin point defined by either the origin brush or the origin coordinate value.*/

void button_done( edict_t *self )
{
	self->moveinfo.state = STATE_BOTTOM;
}

void button_return( edict_t *self )
{
	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 )
{
	self->moveinfo.state = STATE_TOP;

	G_UseTargets( self, self->activator );
	self->s.frame = 1;
	if( self->moveinfo.wait >= 0 )
	{
		self->nextthink = level.timemsec + (self->moveinfo.wait * 1000);
		self->think = button_return;
	}
}

void button_fire( edict_t *self )
{
	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) )
		G_Sound( self, CHAN_VOICE, self->moveinfo.sound_start, 1, ATTN_STATIC );
	Move_Calc( self, self->moveinfo.end_origin, button_wait );
}

void button_use( edict_t *self, edict_t *other, edict_t *activator )
{
	self->activator = activator;
	button_fire( self );
}

void button_touch( edict_t *self, edict_t *other, cplane_t *plane, int surfFlags )
{
	if( !other->r.client )
		return;
	if( G_IsDead(other) )
		return;

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

void button_killed( edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point )
{
	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;

	G_InitMover( ent );
	G_SetMovedir( ent->s.angles, ent->movedir );

	if( st.noise && Q_stricmp(st.noise, "default") ) {
		if( Q_stricmp( st.noise, "silent" ) != 0 )
			ent->moveinfo.sound_start = trap_SoundIndex( st.noise );
	} else
		ent->moveinfo.sound_start = trap_SoundIndex( S_BUTTON_START );
	
	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->r.size[0] + abs_movedir[1] * ent->r.size[1] + abs_movedir[2] * ent->r.size[2] - st.lip;
	VectorMA( ent->pos1, dist, ent->movedir, ent->pos2 );

	ent->use = button_use;

	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 );

	trap_LinkEntity(ent);
}


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


/*QUAKED func_door (0 .5 .8) ? START_OPEN - CRUSHER NOMONSTER - TOGGLE -
Normal sliding door entity. By default, the door will activate when player walks close to it or when damage is inflicted to it.
-------- KEYS --------
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 of door (up = -1, down = -2).
speed : determines how fast the door moves (default 600).
wait : number of seconds before door returns (2 default, -1 = never return)
lip : lip remaining at end of move (default 8)
targetname : if set, a func_button or trigger is required to activate the door.
target : fire entities with this targetname when activated
health : (default 0) if set to any non-zero value, the button must take damage (any amount) to activate.
dmg : damage to inflict on player when he blocks operation of door (default 2). Door will reverse direction when blocked unless CRUSHER spawnflag is set.
team: assign the same team name to multiple doors that should operate together (see Notes).
noise_start : overrides default start moving sound
noise : overrides default movement sound
noise_stop : overrides default stop moving sound
light : constantLight radius of .md3 model included with entity. Has no effect on the entity's brushes (default 0).
color : constantLight color of .md3 model included with entity. Has no effect on the entity's brushes (default 1 1 1).
model2 : path/name of model to include (eg: models/mapobjects/pipe/pipe02.md3).
origin : alternate method of setting XYZ origin of .md3 model included with entity (See Notes).
notsingle : when set to 1, entity will not spawn in Single Player mode
notfree : when set to 1, entity will not spawn in "Free for all" and "Tournament" modes.
notduel : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
notteam : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. 
notctf : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
-------- SPAWNFLAGS --------
START_OPEN : &1 the door will spawn in the open state and operate in reverse.
CRUSHER : &4 door will not reverse direction when blocked and will keep damaging player until he dies or gets out of the way.
NOMONSTER : &8 monsters will not trigger this door
TOGGLE : &32 wait in both the start and end states for a trigger event
-------- NOTES --------
*/


void door_use_areaportals( edict_t *self, qboolean open )
{
	if( self->flags & FL_TEAMSLAVE )
		return;		// only the team master does this

	trap_SetAreaPortalState( self, open );
}

void door_go_down( edict_t *self );

void door_hit_top( edict_t *self )
{
	if( !(self->flags & FL_TEAMSLAVE) )
	{
		if( self->moveinfo.sound_end )
			G_Sound( self, CHAN_VOICE, self->moveinfo.sound_end, 1, ATTN_STATIC );
		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.timemsec + (self->moveinfo.wait * 1000);
	}
}

void door_hit_bottom( edict_t *self )
{
	if( !(self->flags & FL_TEAMSLAVE) )
	{
		if( self->moveinfo.sound_end )
			G_Sound( self, CHAN_VOICE, self->moveinfo.sound_end, 1, ATTN_STATIC );
		self->s.sound = 0;
	}
	self->moveinfo.state = STATE_BOTTOM;
	door_use_areaportals( self, qfalse );
}

void door_go_down( edict_t *self )
{
	if( !(self->flags & FL_TEAMSLAVE) )
	{
		if( self->moveinfo.sound_start )
			G_Sound( self, CHAN_VOICE, self->moveinfo.sound_start, 1, ATTN_STATIC );
		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( !Q_stricmp(self->classname, "func_door") )
		Move_Calc( self, self->moveinfo.start_origin, door_hit_bottom );
	else if( !Q_stricmp(self->classname, "func_door_rotating") )
		AngleMove_Calc( self, door_hit_bottom );
}

void door_go_up( edict_t *self, edict_t *activator )
{
	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.timemsec + (self->moveinfo.wait * 1000);
		return;
	}
	
	if( !(self->flags & FL_TEAMSLAVE) )
	{
		if( self->moveinfo.sound_start )
			G_Sound( self, CHAN_VOICE, self->moveinfo.sound_start, 1, ATTN_STATIC );
		self->s.sound = self->moveinfo.sound_middle;
	}
	self->moveinfo.state = STATE_UP;
	if( !Q_stricmp(self->classname, "func_door") )
		Move_Calc( self, self->moveinfo.end_origin, door_hit_top );
	else if( !Q_stricmp(self->classname, "func_door_rotating") )
		AngleMove_Calc( self, door_hit_top );

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

void door_use( edict_t *self, edict_t *other, edict_t *activator )
{
	edict_t	*ent;

	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, int surfFlags )
{
	if( G_IsDead(other) )
		return;

	if( self->s.team && other->s.team != self->s.team )
		return;
	//MBotgame[start] (allow monsters to open standard doors)
	if( !(other->r.svflags & SVF_MONSTER) && (!other->r.client) && (other->ai.type != AI_ISMONSTER) )
		return;
	//MBotgame[end]
	if( (self->r.owner->spawnflags & DOOR_NOMONSTER) && (other->r.svflags & SVF_MONSTER) )
		return;
	if( level.time < self->touch_debounce_time )
		return;

	self->touch_debounce_time = level.time + 1.0;
	door_use( self->r.owner, other, other );
}

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

	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;
	float		expand_size = 80;	// was 60

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

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

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

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

	other = G_Spawn();
	VectorCopy( mins, other->r.mins );
	VectorCopy( maxs, other->r.maxs );
	other->r.owner = ent;
	other->r.solid = SOLID_TRIGGER;
	other->movetype = MOVETYPE_NONE;
	other->touch = Touch_DoorTrigger;
	other->s.team = ent->s.team;
	trap_LinkEntity(other);

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

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

	if( !(other->r.svflags & SVF_MONSTER) && (!other->r.client) )
	{
		// give it a chance to go away on its 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 )
			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, edict_t *attacker, int damage, vec3_t point )
{
	edict_t	*ent;

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

	if( !self->s.team || self->s.team == attacker->s.team )
		door_use( self->teammaster, attacker, attacker );
}

void door_touch( edict_t *self, edict_t *other, cplane_t *plane, int surfFlags )
{
	if( !other->r.client )
		return;
	if( level.time < self->touch_debounce_time )
		return;

	self->touch_debounce_time = level.time + 5.0;

	G_CenterPrintMsg( other, "%s", self->message );
	G_Sound( other, CHAN_AUTO, trap_SoundIndex (S_WORLD_MESSAGE), 1, ATTN_NORM );
}

void SP_func_door( edict_t *ent )
{
	vec3_t	abs_movedir;

	G_InitMover( ent );
	G_SetMovedir( ent->s.angles, ent->movedir );

	G_AssignMoverSounds( ent, S_DOOR_START, S_DOOR_MOVE, S_DOOR_STOP );

	ent->blocked = door_blocked;
	ent->use = door_use;
	
	if( !ent->speed )
		ent->speed = 600;

	if( !ent->accel )
		ent->accel = ent->speed;
	if( !ent->decel )
		ent->decel = ent->speed;

	if( !ent->wait )
		ent->wait = 2;
	if( !st.lip )
		st.lip = 8;
	if( !ent->dmg )
		ent->dmg = 2;

	if( ent->health < 0 ) {
		ent->health = 0;
	} else if( !ent->health ) {
		ent->health = 1;
	} else
		ent->spawnflags |= DOOR_DIE_ONCE; // not used by the editor

	if( st.gameteam >= TEAM_SPECTATOR && st.gameteam < GS_MAX_TEAMS ) {
		ent->s.team = st.gameteam;
	} else {
		ent->s.team = TEAM_SPECTATOR;
	}

	// 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->r.size[0] + abs_movedir[1] * ent->r.size[1] + abs_movedir[2] * ent->r.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->max_health = ent->health;
		ent->takedamage = DAMAGE_YES;
		ent->die = door_killed;
	}
	else if( ent->targetname && ent->message )
	{
		trap_SoundIndex( S_WORLD_MESSAGE ); // precache
		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 );

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

	trap_LinkEntity(ent);

	ent->nextthink = level.timemsec + game.framemsec;
	if( ent->targetname )
		ent->think = Think_CalcMoveSpeed;
	else
		ent->think = Think_SpawnDoorTrigger;
}


/*QUAKED func_door_rotating (0 .5 .8) ? START_OPEN REVERSE CRUSHER NOMONSTER - TOGGLE X_AXIS Y_AXIS
Door which opens by rotating in one axis. You need to have an origin brush as part of this entity.
-------- KEYS --------
message : is printed when the door is touched if it is a trigger door and it hasn't been fired yet
distance : is how many degrees the door will be rotated.
angle : determines the opening direction of door (up = -1, down = -2).
speed : determines how fast the door moves (default 100).
wait : wait before returning (3 default, -1 = never return)
targetname : if set, a func_button or trigger is required to activate the door.
target : fire entities with this targetname when activated
health : if set, door must be shot open (default 0).
dmg : damage to inflict on player when he blocks operation of door (default 2). Door will reverse direction when blocked unless CRUSHER spawnflag is set.
team: assign the same team name to multiple doors that should operate together.
noise_start : overrides default start moving sound
noise : overrides default movement sound
noise_stop : overrides default stop moving sound
light : constantLight radius of .md3 model included with entity. Has no effect on the entity's brushes (default 0).
color : constantLight color of .md3 model included with entity. Has no effect on the entity's brushes (default 1 1 1).
model2 : path/name of model to include (eg: models/mapobjects/pipe/pipe02.md3).
origin : alternate method of setting XYZ origin of .md3 model included with entity (See Notes).
notsingle : when set to 1, entity will not spawn in Single Player mode
notfree : when set to 1, entity will not spawn in "Free for all" and "Tournament" modes.
notduel : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
notteam : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. 
notctf : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
-------- SPAWNFLAGS --------
START_OPEN : &1 the door will spawn in the open state and operate in reverse.
REVERSE : &2 will cause the door to rotate in the opposite direction.
CRUSHER : &4 door will not reverse direction when blocked and will keep damaging player until he dies or gets out of the way.
NOMONSTER : &8 monsters can't trigger this door.
TOGGLE : &32 wait in both the start and end states for a trigger event
X_AXIS : &64 rotate in the X axis
Y_AXIS : &128 rotate in the Y axis
-------- NOTES --------
The center of the origin brush determines 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.
*/
void SP_func_door_rotating( edict_t *ent )
{
	G_InitMover( ent );

	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 )
	{
		if( developer->integer )
			G_Printf( "%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->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;

	G_AssignMoverSounds( ent, S_DOOR_ROTATING_START, S_DOOR_ROTATING_MOVE, S_DOOR_ROTATING_STOP );

	// 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 )
	{
		trap_SoundIndex( S_WORLD_MESSAGE ); // precache
		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 );

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

	trap_LinkEntity(ent);

	ent->nextthink = level.timemsec + game.framemsec;
	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.
-------- KEYS --------
angle : determines the opening direction. Up or down only (up = -1, down = -2).
speed : determines how fast it moves (default 25).
wait : wait before returning (-1 default, -1 = TOGGLE)
lip : lip remaining at end of move (default 0)
targetname : set targetname so it can be targeted
target : fire entities with this targetname when activated
team: assign the same team name to multiple funcs that should operate together.
noise_start : overrides default (silent) start moving sound
noise : overrides default (silent) movement sound
noise_stop : overrides default (silent) stop moving sound
notsingle : when set to 1, entity will not spawn in Single Player mode
notfree : when set to 1, entity will not spawn in "Free for all" and "Tournament" modes.
notduel : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
notteam : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. 
notctf : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
-------- SPAWNFLAGS --------
START_OPEN : &1  Causes the water to move to its destination when spawned and operate in reverse
-------- NOTES --------
Use a non-water texture at your own risk.
*/
void SP_func_water( edict_t *self )
{
	vec3_t	abs_movedir;

	G_InitMover( self );

	// 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->r.size[0] + abs_movedir[1] * self->r.size[1] + abs_movedir[2] * self->r.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";

	//sounds
	G_AssignMoverSounds( self, NULL, NULL, NULL );

	trap_LinkEntity(self);
}


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

/*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.
-------- KEYS --------
speed : speed of displacement of train (default 100).
dmg : default 2
target : point to first entity in the path
targetname : train's targetname
noise_start : start moving sound (default silent)
noise : movement sound (default silent)
noise_stop : stop moving sound (default silent)
model2 : path/name of model to include (eg: models/mapobjects/pipe/pipe02.md3).
origin : alternate method of setting XYZ origin of the train's brush(es) and .md3 model included with entity (See Notes).
light : constantLight radius of .md3 model included with entity. Has no effect on the entity's brushes (default 0).
color : constantLight color of .md3 model included with entity. Has no effect on the entity's brushes (default 1 1 1).
notsingle : when set to 1, entity will not spawn in Single Player mode
notfree : when set to 1, entity will not spawn in "Free for all" and "Tournament" modes.
notduel : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
notteam : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. 
notctf : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
-------- SPAWNFLAGS --------
START_ON : &1
TOGGLE : &2
BLOCK_STOPS : &4
-------- NOTES --------
Setting the origin key is simply an alternate method to using an origin brush. When using the model2 key, the origin point of the model will correspond to the origin point defined by either the origin brush or the origin coordinate value.*/


void train_next( edict_t *self );

void train_blocked( edict_t *self, edict_t *other )
{
	if( !(other->r.svflags & SVF_MONSTER) && (!other->r.client) )
	{
		// give it a chance to go away on its 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 )
			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->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->r.inuse )
			return;
	}

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

		if( !(self->flags & FL_TEAMSLAVE) )
		{
			if( self->moveinfo.sound_end )
				G_Sound( self, CHAN_VOICE, self->moveinfo.sound_end, 1, ATTN_STATIC );
			self->s.sound = 0;
		}
	}
	else
	{
		train_next(self);
	}
	
}

void train_next( edict_t *self )
{
	edict_t		*ent;
	vec3_t		dest;
	qboolean	first;

	first = qtrue;
again:
	if( !self->target )
	{
//		G_Printf ("train_next: no next target\n");
		return;
	}

	ent = G_PickTarget( self->target );
	if( !ent )
	{
		if( developer->integer )
			G_Printf( "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 )
		{
			if( developer->integer )
				G_Printf( "connected teleport path_corners, see %s at %s\n", ent->classname, vtos(ent->s.origin) );

			return;
		}

		first = qfalse;
		VectorSubtract( ent->s.origin, self->r.mins, self->s.origin );
		VectorCopy( self->s.origin, self->s.old_origin );
		trap_LinkEntity(self);
		G_AddEvent( self, EV_TELEPORT, 0, qtrue );
		goto again;
	}

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

	if( !(self->flags & FL_TEAMSLAVE) )
	{
		if( self->moveinfo.sound_start )
			G_Sound( self, CHAN_VOICE, self->moveinfo.sound_start, 1, ATTN_STATIC );
		self->s.sound = self->moveinfo.sound_middle;
	}

	VectorSubtract( ent->s.origin, self->r.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;

	ent = self->target_ent;

	VectorSubtract( ent->s.origin, self->r.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->target )
	{
		if( developer->integer )
			G_Printf( "train_find: no target\n" );
		return;
	}

	ent = G_PickTarget( self->target );
	if( !ent )
	{
		if( developer->integer )
			G_Printf( "train_find: target %s not found\n", self->target );
		return;
	}

	self->target = ent->target;

	VectorSubtract( ent->s.origin, self->r.mins, self->s.origin );
	trap_LinkEntity(self);

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

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

void train_use( edict_t *self, edict_t *other, edict_t *activator )
{
	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 )
{
	G_InitMover( self );

	VectorClear( self->s.angles );
	self->blocked = train_blocked;
	if( self->spawnflags & TRAIN_BLOCK_STOPS )
		self->dmg = 0;
	else
	{
		if( !self->dmg )
			self->dmg = 100;
	}

	G_AssignMoverSounds( self, NULL, NULL, NULL );

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

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

	self->use = train_use;

	trap_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.timemsec + game.framemsec;
		self->think = func_train_find;
	}
	else
	{
		if( developer->integer )
			G_Printf( "func_train without a target at %s\n", vtos(self->r.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 )
{
	edict_t *target;

	if( self->movetarget->nextthink )
	{
//		G_Printf( "elevator busy\n" );
		return;
	}

	if( !other->pathtarget )
	{
		if( developer->integer )
			G_Printf( "elevator used with no pathtarget\n" );
		return;
	}

	target = G_PickTarget( other->pathtarget );
	if( !target )
	{
		if( developer->integer )
			G_Printf( "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->target )
	{
		if( developer->integer )
			G_Printf( "trigger_elevator has no target\n" );
		return;
	}
	self->movetarget = G_PickTarget( self->target );
	if( !self->movetarget )
	{
		if( developer->integer )
			G_Printf( "trigger_elevator unable to find target %s\n", self->target );
		return;
	}
	if( Q_stricmp(self->movetarget->classname, "func_train") )
	{
		if( developer->integer )
			G_Printf( "trigger_elevator target %s is not a train\n", self->target );
		return;
	}

	self->use = trigger_elevator_use;
	self->r.svflags = SVF_NOCLIENT;

}

void SP_trigger_elevator( edict_t *self )
{
	self->think = trigger_elevator_init;
	self->nextthink = level.timemsec + game.framemsec;
}


/*QUAKED func_timer (0 .5 .8) (-8 -8 -8) (8 8 8) START_ON
Time delay trigger that will continuously fire its targets after a preset time delay. When triggered, the timer will toggle on/off.
-------- KEYS --------
wait : delay in seconds between each triggering of its targets (default 1).
random : random time variance in seconds added or subtracted from "wait" delay (default 0 - see Notes).
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
target : this points to the entities to trigger.
targetname : a func_button or trigger that points to this will toggle the timer on/off when activated.
notsingle : when set to 1, entity will not spawn in Single Player mode
notfree : when set to 1, entity will not spawn in "Free for all" and "Tournament" modes.
notduel : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
notteam : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. 
notctf : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
-------- SPAWNFLAGS --------
START_ON : &1 timer will start on in the game and continuously fire its targets.
-------- NOTES --------
When the random key is set, its value is used to calculate a minimum and a maximum delay. The final time delay will be a random value anywhere between the minimum and maximum values: (min delay = wait - random) (max delay = wait + random).*/

void func_timer_think( edict_t *self )
{
	G_UseTargets( self, self->activator );
	self->nextthink = level.timemsec + (self->wait * 1000) + ( crandom() * self->random * 1000 );
}

void func_timer_use( edict_t *self, edict_t *other, edict_t *activator )
{
	self->activator = activator;

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

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

void SP_func_timer( edict_t *self )
{
	if( !self->random )
		self->random = 1.0;
	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;
		if( developer->integer )
			G_Printf( "func_timer at %s has random >= wait\n", vtos(self->s.origin) );
	}

	if( self->spawnflags & 1 )
	{
		self->nextthink = level.timemsec + 1000 +
			( st.pausetime * 1000 ) + ( self->delay * 1000 ) +
			( self->wait * 1000 ) + ( crandom() * self->random * 1000 );

		self->activator = self;
	}

	self->r.svflags = SVF_NOCLIENT;
}


/*QUAKED func_conveyor (0 .5 .8) ? START_ON TOGGLE
Conveyors are stationary brushes that move what's on them.
-------- KEYS --------
speed : speed of displacement (default 100).
target : targetname to activate when used
targetname : to be targeted with
noise_start : start moving sound (default silent)
noise : movement sound (default silent)
noise_stop : stop moving sound (default silent)
notsingle : when set to 1, entity will not spawn in Single Player mode
notfree : when set to 1, entity will not spawn in "Free for all" and "Tournament" modes.
notduel : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
notteam : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. 
notctf : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
-------- SPAWNFLAGS --------
START_ON : &1 start activated
TOGGLE : &2 must be triggered again to toogle it's state
-------- NOTES --------
The brush should be have a surface with at least one current content enabled.*/

void func_conveyor_use( edict_t *self, edict_t *other, edict_t *activator )
{
	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 )
{
	G_InitMover(self);

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

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

	self->use = func_conveyor_use;

	G_AssignMoverSounds( self, NULL, NULL, NULL );
}


/*QUAKED func_door_secret (0 .5 .8) ? ALWAYS_SHOOT 1ST_LEFT 1ST_DOWN 
A secret door.  Slide back and then to the side.
-------- KEYS --------
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 of door (up = -1, down = -2).
speed : determines how fast the door moves (default 50).
wait : number of seconds before door returns (5 default, -1 = never return)
targetname : if set, a func_button or trigger is required to activate the door.
target : fire entities with this targetname when activated
health : (default 0) if set to any non-zero value, the button must take damage (any amount) to activate.
dmg : damage to inflict on player when he blocks operation of door (default 2). Door will reverse direction when blocked unless CRUSHER spawnflag is set.
team: assign the same team name to multiple doors that should operate together (see Notes).
noise_start : overrides default start moving sound
noise : overrides default movement sound
noise_stop : overrides default stop moving sound
light : constantLight radius of .md3 model included with entity. Has no effect on the entity's brushes (default 0).
color : constantLight color of .md3 model included with entity. Has no effect on the entity's brushes (default 1 1 1).
model2 : path/name of model to include (eg: models/mapobjects/pipe/pipe02.md3).
origin : alternate method of setting XYZ origin of .md3 model included with entity (See Notes).
notsingle : when set to 1, entity will not spawn in Single Player mode
notfree : when set to 1, entity will not spawn in "Free for all" and "Tournament" modes.
notduel : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
notteam : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. 
notctf : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
-------- SPAWNFLAGS --------
ALWAYS_SHOOT : &1 door is shooteable even if targeted
1ST_LEFT : &2 1st move is left of arrow
1ST_DOWN : &4 1st move is down from arrow
-------- NOTES --------
Slide back and then to the side.
*/

#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, edict_t *activator )
{
	// 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, qtrue );
}

void door_secret_move1( edict_t *self )
{
	self->nextthink = level.timemsec + 1000;
	self->think = door_secret_move2;
}

void door_secret_move2( edict_t *self )
{
	Move_Calc( self, self->pos2, door_secret_move3 );
}

void door_secret_move3( edict_t *self )
{
	if (self->wait == -1)
		return;

	self->nextthink = level.timemsec + ( self->wait * 1000 );
	self->think = door_secret_move4;
}

void door_secret_move4( edict_t *self )
{
	Move_Calc( self, self->pos1, door_secret_move5 );
}

void door_secret_move5( edict_t *self )
{
	self->nextthink = level.timemsec + 1000;
	self->think = door_secret_move6;
}

void door_secret_move6( edict_t *self )
{
	Move_Calc( self, vec3_origin, door_secret_done );
}

void door_secret_done( edict_t *self )
{
	if( !(self->targetname) || (self->spawnflags & SECRET_ALWAYS_SHOOT) )
	{
		self->health = 0;
		self->takedamage = DAMAGE_YES;
	}
	door_use_areaportals( self, qfalse );
}

void door_secret_blocked( edict_t *self, edict_t *other )
{
	if( !(other->r.svflags & SVF_MONSTER) && (!other->r.client) )
	{
		// give it a chance to go away on its 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 )
			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, edict_t *attacker, int damage, vec3_t point )
{
	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;

	G_InitMover(ent);
	G_SetMovedir( ent->s.angles, ent->movedir );

	G_AssignMoverSounds( ent, NULL, NULL, NULL );

	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->r.size) );
	else
		width = fabs( DotProduct(right, ent->r.size) );
	length = fabs( DotProduct(forward, ent->r.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 )
	{
		trap_SoundIndex( S_WORLD_MESSAGE ); // precache
		ent->touch = door_touch;
	}
	
	ent->classname = "func_door";

	trap_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, edict_t *activator )
{
	KillBox(self);
}

void SP_func_killbox( edict_t *ent )
{
	ent->use = use_killbox;
	ent->r.svflags = SVF_NOCLIENT;
}


/*QUAKED func_bobbing (0 .5 .8) ? X_AXIS Y_AXIS
Solid entity that oscillates back and forth in a linear motion. Entity bobs on the Z axis (up-down) by default. It can also emit sound if the "noise" key is set. Will crush the player when blocked.
-------- KEYS --------
speed : amount of time in seconds for one complete oscillation cycle (default 4).
height : sets the amount of travel of the oscillation movement (default 32). 
phase : sets the start offset of the oscillation cycle. Values must be 0 < phase < 1. Any integer phase value is the same as no offset (default 0).
noise_start : start moving sound (default is silent)
noise : path/name of .wav file to play (default is silent). Use looping sounds only (eg. sounds/world/drone6.wav - See Notes).
noise_stop : stop moving sound (default is silent)
model2 : path/name of model to include.
origin : alternate method of setting XYZ origin of sound and .md3 model included with entity (See Notes).
light : constantLight radius of .md3 model included with entity. Has no effect on the entity's brushes (default 0).
color : constantLight color of .md3 model included with entity. Has no effect on the entity's brushes (default 1 1 1).
notsingle : when set to 1, entity will not spawn in Single Player mode
notfree : when set to 1, entity will not spawn in "Free for all" and "Tournament" modes.
notduel : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
notteam : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. 
notctf : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
-------- SPAWNFLAGS --------
X_AXIS : &1 entity will bob along the X axis.
Y_AXIS : &2 entity will bob along the Y axis.
-------- NOTES --------
In order for the sound to be emitted from the entity, it is recommended to include a brush with an origin shader at its center, otherwise the sound will not follow the entity as it moves. Setting the origin key is simply an alternate method to using an origin brush. When using the model2 key, the origin point of the model will correspond to the origin point defined by either the origin brush or the origin coordinate value.
Start and stop sounds will only be played if the entity is set to be triggered*/

//================
//func_bobbing_blocked
//================
void func_bobbing_blocked( edict_t *self, edict_t *other )
{
	T_Damage( other, self, self, vec3_origin, other->s.origin, vec3_origin, 100000, 1, 0, MOD_CRUSH );
}

//================
//func_bobbing_use
//================
void func_bobbing_use( edict_t *self, edict_t *other, edict_t *activator )
{
	if( self->flags & FL_TEAMSLAVE ) {
		G_UseTargets( self->teammaster, activator );
		return;
	}

	self->activator = activator;
	G_UseTargets( self, self->activator );
}

//================
//func_bobbing_think
//================
void func_bobbing_think( edict_t *ent )
{
	float delta;
	float phase;

	delta = ( level.time - ent->speed * ent->moveinfo.phase ) / ent->speed;
	delta = delta - (int)delta;
	phase = sin( delta * M_TWOPI );

	VectorMA( ent->moveinfo.start_origin, phase, ent->moveinfo.dir, ent->velocity );
	VectorSubtract( ent->velocity, ent->s.origin, ent->velocity );

	ent->nextthink = level.timemsec + game.framemsec;
}

//================
//SP_func_bobbing
//================
void SP_func_bobbing( edict_t *ent )
{
	G_InitMover(ent);

	if ( !ent->speed )
		ent->speed = 4;
	if ( !ent->dmg )
		ent->dmg = 2;
	if ( !st.height )
		st.height = 32;

	ent->moveinfo.phase = st.phase;
	VectorClear( ent->moveinfo.dir );

	// set the axis of bobbing
	if( ent->spawnflags & 1 )
		ent->moveinfo.dir[0] = st.height;
	else if ( ent->spawnflags & 2 )
		ent->moveinfo.dir[1] = st.height;
	else
		ent->moveinfo.dir[2] = st.height;

	VectorClear( ent->s.angles );
	VectorClear( ent->velocity );
	VectorCopy( ent->s.origin, ent->moveinfo.start_origin );

	ent->think = func_bobbing_think;
	ent->nextthink = level.timemsec + game.framemsec;
	ent->blocked = func_bobbing_blocked;
	ent->use = func_bobbing_use;

	G_AssignMoverSounds( ent, NULL, NULL, NULL );

	trap_LinkEntity(ent);
}

//===============================================================================
//
//PENDULUM
//
//===============================================================================


//================
//func_pendulum_blocked
//================
void func_pendulum_blocked( edict_t *self, edict_t *other )
{
	T_Damage( other, self, self, vec3_origin, other->s.origin, vec3_origin, 100000, 1, 0, MOD_CRUSH );
}

//================
//func_pendulum_use
//================
void func_pendulum_use( edict_t *self, edict_t *other, edict_t *activator )
{
	if( self->flags & FL_TEAMSLAVE ) {
		G_UseTargets( self->teammaster, activator );
		return;
	}

	self->activator = activator;
	G_UseTargets( self, self->activator );
}

//================
//func_pendulum_think
//================
void func_pendulum_think( edict_t *ent )
{
	float delta;
	float phase;

	delta = ( level.time + ent->moveinfo.wait ) * ent->moveinfo.phase;
	delta = delta - (int)delta;
	phase = sin( delta * M_TWOPI );
	VectorMA( ent->moveinfo.start_angles, phase, ent->moveinfo.dir, ent->avelocity );
	VectorSubtract( ent->avelocity, ent->s.angles, ent->avelocity );
	ent->nextthink = level.timemsec + game.framemsec;
}

/*QUAKED func_pendulum (0 .5 .8) ?
You need to have an origin brush as part of this entity. Solid entity that describes a pendulum back and forth rotation movement. Rotates on the X axis by default. Pendulum frequency is a physical constant based on the length of the beam and gravity. Blocking the pendulum instantly kills a player.
-------- KEYS --------
angle : angle offset of axis of rotation from default X axis (default 0).
speed : angle of swing arc in either direction from initial position (default 30).
phase : sets the start offset of the swinging cycle. Values must be 0 < phase < 1. Any integer phase value is the same as no offset (default 0).
noise_start :	start moving sound to be played only if triggered (default is silent)
noise : path/name of .wav file to play. Use looping sounds only (default is silent).
noise_stop : stop moving sound to be played only if triggered (default is silent)
model2 : path/name of model to include (eg: models/mapobjects/jets/jets01.md3).
origin : alternate method of setting XYZ origin of entity's rotation axis and .md3 model included with entity (default "0 0 0" - See Notes).
light : constantLight radius of .md3 model included with entity. Has no effect on the entity's brushes (default 0).
color : constantLight color of .md3 model included with entity. Has no effect on the entity's brushes (default 1 1 1).
notsingle : when set to 1, entity will not spawn in Single Player mode
notfree : when set to 1, entity will not spawn in "Free for all" and "Tournament" modes.
notduel : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
notteam : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. 
notctf : when set to 1, entity will not spawn in "Teamplay" and "CTF" modes. (jal: todo)
-------- NOTES --------
You need to have an origin brush as part of this entity. The center of that brush will be the point through which the rotation axis passes. Setting the origin key is simply an alternate method to using an origin brush. Pendulum will rotate along the X axis by default. Very crude operation: pendulum cannot rotate along Z axis, the speed of swing (frequency) is not adjustable. When using the model2 key, the origin point of the model will correspond to the origin point defined by either the origin brush or the origin coordinate value. Pendulums always swing north / south on unrotated models. Add an angles field to the model to allow rotation in other directions. Pendulum frequency is a physical constant based on the length of the beam and gravity.*/
void SP_func_pendulum( edict_t *ent )
{
	float		freq;
	float		length;

	G_InitMover(ent);

	if( !ent->speed )
		ent->speed = 30;
	if( !ent->dmg )
		ent->dmg = 2;

	// find pendulum length
	length = fabs ( ent->r.mins[2] );
	if( length < 8 ) {
		length = 8;
	}

	freq = 1 / ( M_PI * 2 ) * sqrt( g_gravity->value / ( 3 * length ) );

	VectorCopy( ent->s.angles, ent->moveinfo.start_angles );
	VectorClear( ent->moveinfo.dir );
	ent->moveinfo.phase = freq;
	ent->moveinfo.wait = st.phase / ent->moveinfo.phase;
	ent->moveinfo.dir[2] = ent->speed;

	ent->think = func_pendulum_think;
	ent->nextthink = level.timemsec + game.framemsec;
	ent->blocked = func_pendulum_blocked;
	ent->use = func_pendulum_use;

	G_AssignMoverSounds( ent, NULL, NULL, NULL );

	trap_LinkEntity(ent);
}



