//#include maps\mp\_utility;
#include common_scripts\utility;

init()
{
	if ( getdvar( "scr_elevator_disabled" ) == "1" )
		return;

	// skip if no elevators in this level
	elevator_groups = getentarray( "elevator_group", "targetname" );
	if ( !isdefined( elevator_groups ) )
		return;
	if ( !elevator_groups.size )
		return;

	// Press and hold &&1 to call elevator.
	precacheString( &"ELEVATOR_CALL_HINT" );
	// Press and hold &&1 to use elevator.
	precacheString( &"ELEVATOR_USE_HINT" );
	// Press and hold &&1 to select floor.
	precacheString( &"ELEVATOR_FLOOR_SELECT_HINT" );

	precacheMenu( "elevator_floor_selector" );

	thread elevator_update_global_dvars();

	// find and build all elevators in the level
	level.elevators = [];

	// elevator construction dvars:
	// vertical units for "call button" to link near by elevator per floor
	level.elevator_callbutton_link_v = elevator_get_dvar_int( "scr_elevator_callbutton_link_v", "96" );
	// horizontal units for "call button" to link near by elevator per floor
	level.elevator_callbutton_link_h = elevator_get_dvar_int( "scr_elevator_callbutton_link_h", "256" );

	build_elevators();
	position_elevators();
	elevator_call();

	if ( !level.elevators.size )
		return;

	foreach ( elevator in level.elevators )
	{
		elevator thread elevator_think();
		elevator thread elevator_sound_think();
	}

	thread elevator_debug();
}

elevator_update_global_dvars()
{
	while ( 1 )
	{
		level.elevator_accel			 = elevator_get_dvar( "scr_elevator_accel", "0.2" ); 				// acceleration time in seconds
		level.elevator_decel			 = elevator_get_dvar( "scr_elevator_decel", "0.2" ); 				// deceleration time in seconds
		level.elevator_music			 = elevator_get_dvar_int( "scr_elevator_music", "1" ); 				// elevator music
		level.elevator_speed			 = elevator_get_dvar_int( "scr_elevator_speed", "96" ); 			// units per second
		level.elevator_innerdoorspeed	 = elevator_get_dvar_int( "scr_elevator_innerdoorspeed", "14" ); 	// inner door speed
		level.elevator_outterdoorspeed 	 = elevator_get_dvar_int( "scr_elevator_outterdoorspeed", "16" ); 	// outter door speed
		level.elevator_return			 = elevator_get_dvar_int( "scr_elevator_return", "0" ); 			// 1: elevator returns to original floor
		level.elevator_waittime			 = elevator_get_dvar_int( "scr_elevator_waittime", "6" ); 			// wait in seconds before closing door
		level.elevator_aggressive_call	 = elevator_get_dvar_int( "scr_elevator_aggressive_call", "0" ); 	// calls all available elevators to floor
		level.elevator_debug			 = elevator_get_dvar_int( "debug_elevator", "0" ); 					// 2: full 1: simple, 0: debug off

		// mp & sp default differences:
		if ( isSP() )
		{
			level.elevator_motion_detection	 = elevator_get_dvar_int( "scr_elevator_motion_detection", "0" );// calls elevators via motion detection
		}
		else
		{
			level.elevator_motion_detection = elevator_get_dvar_int( "scr_elevator_motion_detection", "1" );// calls elevators via motion detection
		}

		wait 1;
	}
}

//==========================================================================//
//							===	ELEVATOR LOGIC ===							//
//==========================================================================//

elevator_think()
{
	// self is elevator, self.e[]
	self elevator_fsm( "[A]" );
}

elevator_call()
{
	foreach ( callbutton in level.elevator_callbuttons )
		callbutton thread monitor_callbutton();
}

floor_override( inside_trig )
{
	self endon( "elevator_moving" );

	self.floor_override = 0;
	self.overrider = undefined;

	while ( 1 )
	{
		inside_trig waittill( "trigger", player );
		self.floor_override = 1;
		self.overrider = player;
		break;
	}
	self notify( "floor_override" );
}

elevator_fsm( state )
{
	/*				finite state machine
	
	state A: 	rest
	state B: 	closing doors - interrupt threaded
	state C: 	opening doors
	state D: 	moving elevator
	?1: 		if not resting at initial floor
			
				.-------delay-------.
				|					|
			  	?1					V
	start --> [A] --inside_trig--> [B] --delay--> [D]
				^					| 			   |
				|				door_trig		   |
				|		            |			   |
              	|					V			   |
				'-----delay------- [C] <---delay---'
	*/

	/* self is elevator, self.e[] */

	self.eState = state;

	door_trig = self get_housing_door_trigger();		// triggers door interrupt
	inside_trig = self get_housing_inside_trigger();	// presence

	while ( 1 )
	{
		//state A: rest
		if ( self.eState == "[A]" )
		{
			// ?1: if not resting at initial floor
			if ( level.elevator_return && ( self get_curFloor() != self get_initFloor() ) )
			{
				self.moveto_floor = self get_initFloor();
				self thread floor_override( inside_trig );
				self waittill_or_timeout( "floor_override", level.elevator_waittime );

				if ( self.floor_override && isdefined( self.overrider ) && isPlayer( self.overrider ) )
					self get_floor( self.overrider );

				self.eState = "[B]";
				continue;
			}

			// wait for player use trigger
			while ( 1 )
			{
				// if elevator already has destination but interrupted, it should continue previous order
				if ( self.moveto_floor == self get_curFloor() )
					param = inside_trig discrete_waittill( "trigger" );
				else
					param = "elevator_called";	// as if called onto the destination floor

				// elevator call hack
				if ( isString( param ) && ( param == "elevator_called" ) && ( self.moveto_floor != self get_curFloor() ) )
				{
					self.eState = "[B]";
					break;
				}

				if ( isdefined( param ) && isPlayer( param ) && isAlive( param ) )
				{
					isTouching_trigger = ( param istouching( inside_trig ) );
					isTouching_motion_trigger = ( isdefined( inside_trig.motion_trigger ) && param istouching( inside_trig.motion_trigger ) );
					player_isTouching_trigger = ( isTouching_trigger || isTouching_motion_trigger );

					if ( player_isTouching_trigger )
					{
						player = param;
						self get_floor( player );

						if ( self.moveto_floor == self get_curFloor() )
							continue;

						self.eState = "[B]";
						break;
					}
				}
			}
		}

		//state B: closing doors - interrupt threaded
		if ( self.eState == "[B]" )
		{
			self thread elevator_interrupt( door_trig );
			floor_num = self get_curFloor();

			// call to close doors
			self thread close_inner_doors();
			self thread close_outer_doors( floor_num );

			self waittill_any( "closed_inner_doors", "interrupted" );  	// Wait for inner doors to close for the safty of our valued customers.

			if ( self.elevator_interrupted )
			{
				self.eState = "[C]";
				continue;
			}

			self.eState = "[D]";
			continue;
		}

		//state C: opening doors
		if ( self.eState == "[C]" )
		{
			floor_num = self get_curFloor();

			self thread open_inner_doors();
			self thread open_outer_doors( floor_num );

			self waittill( "opened_floor_" + floor_num + "_outer_doors" );

			if ( self.elevator_interrupted )
			{
				self.eState = "[B]";
				continue;
			}

			self.eState = "[A]";
			continue;
		}

		//state D: moving elevator
		if ( self.eState == "[D]" )
		{
			assertex( isdefined( self.moveto_floor ), "Missing destination floor number" );

			if ( self.moveto_floor != self get_curFloor() )
			{
				self thread elevator_move( self.moveto_floor );
				self waittill( "elevator_moved" );
			}

			self.eState = "[C]";
			continue;
		}
	}
}

monitor_callbutton()
{
	while ( 1 )
	{
		// call button used by player
		player = self discrete_waittill( "trigger" );

		call_floor = undefined;
		call_elevators = [];
		// fake loop, always loop only once
		foreach ( idx, linked_elevators in self.e )
		{
			call_floor = idx;
			call_elevators = linked_elevators;
		}
		assert( isdefined( call_floor ) && isdefined( call_elevators ) && call_elevators.size );

		elevator_called = 0;

		// prefilter elevators to see if any already at this floor
		foreach ( elevator in call_elevators )
		{
			moving = elevator elevator_floor_update();

			// if in non-aggressive mode; this elevator is already stationary at the requested floor, we skip!
			if ( !level.elevator_aggressive_call && !moving )
			{
				if ( elevator get_curFloor() == call_floor )
				{
					elevator_called = 1;
					call_elevators = [];
					break;
				}
			}
		}

		// check all elevators to see if any can be called to this floor		
		foreach ( elevator in call_elevators )
		{
			if ( elevator.eState == "[A]" )
			{
				// call only elevators in a state of rest [A]
				elevator call_elevator( call_floor );

				elevator_called = 1;

				// aggressive call
				if ( !level.elevator_aggressive_call )
					break;
			}
		}

		if ( elevator_called )
			self playsound( "elev_bell_ding" );
	}
}

call_elevator( call_floor )
{
	self.moveto_floor = call_floor;

	// call elevator via trigger hack
	inside_trigger = self get_housing_inside_trigger();
	inside_trigger notify( "trigger", "elevator_called" );
	if ( level.elevator_motion_detection )
		inside_trigger.motion_trigger notify( "trigger", "elevator_called" );
}

// opens menu to select floors
get_floor( player )
{
	// skips popup menu if only two floors present
	bifloor = self get_outer_doorsets();
	if ( bifloor.size == 2 )
	{
		curFloor = self get_curFloor();
		self.moveto_floor = !curFloor;
		return;
	}

	player openpopupmenu( "elevator_floor_selector" );
	player setClientDvar( "player_current_floor", self get_curFloor() );

	while ( 1 )
	{
		player waittill( "menuresponse", menu, response );

		if ( menu == "elevator_floor_selector" )
		{
			if ( response != "none" )
				self.moveto_floor = int( response );

			break;
		}
	}
}

elevator_interrupt( door_trig )
{
	self notify( "interrupt_watch" );
	level notify( "elevator_interior_button_pressed" );
	self endon( "interrupt_watch" );	// no duplicate interrupts
	self endon( "elevator_moving" );

	self.elevator_interrupted = 0;
	wait 0.5;// buffer time to have the door jitter...
	door_trig waittill( "trigger", player );

	/*while ( 1 )
	{
		wait 0.5; // buffer time to have the door jitter... realisticaly, im serious!
		
		door_trig waittill( "trigger", player );			
		if ( isdefined( player ) && isPlayer( player ) )
			break;
	}*/

	self notify( "interrupted" );
	self.elevator_interrupted = 1;

	//self elevator_fsm( "[C]" );
}

// returns if elevator is moving, also updates the floor number if not moving
elevator_floor_update()
{
	mainframe = self get_housing_mainframe();
	cur_pos = mainframe.origin;

	moving = 1;
	foreach ( idx, eFloor in self get_outer_doorsets() )
	{
		floor_pos = self.e[ "floor" + idx + "_pos" ];
		if ( cur_pos == floor_pos )
		{
			self.e[ "current_floor" ] = idx;
			moving = 0;
		}
	}

	return moving;
}

elevator_sound_think()
{
	// self is elevator, self.e[]
	// play elevator sounds on notify of behavior

	// always play musak
	musak_model = self get_housing_musak_model();
	
	if ( level.elevator_music && isdefined( musak_model ) )
		musak_model playloopsound( "elev_musak_loop" );

	self thread listen_for ( "closing_inner_doors" );
	self thread listen_for ( "opening_inner_doors" );
	self thread listen_for ( "closed_inner_doors" );
	self thread listen_for ( "opened_inner_doors" );

	foreach ( idx, eFloor in self get_outer_doorsets() )
	{
		self thread listen_for ( "closing_floor_" + idx + "_outer_doors" );
		self thread listen_for ( "opening_floor_" + idx + "_outer_doors" );
		self thread listen_for ( "closed_floor_" + idx + "_outer_doors" );
		self thread listen_for ( "opened_floor_" + idx + "_outer_doors" );
	}

	self thread listen_for ( "interrupted" );
	//self thread listen_for( "interrupt_watch" );
	self thread listen_for ( "elevator_moving" );
	self thread listen_for ( "elevator_moved" );
}

/*
-elev_musak_loop,,music/mx_musak_stolen_elev_proc1.wav,1,1,foley,1,1,120,500,effects1,,,rlooping,,all_mp
-elev_door_open,,events/elev_door_open1.wav,1,1,foley,0.95,1.05,120,2500,voice,,,,,all_mp
-elev_door_close,,events/elev_door_close1.wav,1,1,foley,0.95,1.05,120,2500,voice,,,,,all_mp
-elev_door_interupt,,events/elev_door_interupt1.wav,1,1,foley,0.95,1.05,120,2500,voice,,,,,all_mp
-elev_bell_ding,,events/elev_bell_ding1.wav,1,1,foley,1,1,120,2500,voice,,,,,all_mp
-elev_run_start,,events/elev_run_start01.wav,1,1,foley,1,1,120,500,voice,,,,,all_mp
-elev_run_loop,,events/elev_run_loop01.wav,1,1,foley,1,1,120,500,voice,,,rlooping,,all_mp
-elev_run_end,,events/elev_run_stop01.wav,1,1,foley,1,1,120,500,voice,,,,,all_mp
*/

listen_for ( msg )
{
	while ( 1 )
	{
		self waittill( msg );
		mainframe = self get_housing_mainframe();

		if ( issubstr( msg, "closing_" ) )
			mainframe playsound( "elev_door_close" );

		if ( issubstr( msg, "opening_" ) )
			mainframe playsound( "elev_door_open" );

		if ( msg == "elevator_moving" )
		{
			mainframe playsound( "elev_run_start" );
			mainframe playloopsound( "elev_run_loop" );
		}

		if ( msg == "interrupted" )
			mainframe playsound( "elev_door_interupt" );

		if ( msg == "elevator_moved" )
		{
			mainframe stoploopsound( "elev_run_loop" );
			mainframe playsound( "elev_run_end" );
			mainframe playsound( "elev_bell_ding" );
		}

		//if ( isdefined( level.players[0] ) && level.elevator_debug )
		//	level.players[0] iprintln( "(e" + self.e[ "id" ] + ") " + msg );
	}
}

//==========================================================================//
//							===	MOVEMENTS ===								//
//==========================================================================//

// position elevators
position_elevators()
{
	// closing all outer doors on floors elevator isnt at
	foreach ( e, elevator in level.elevators )
	{
		elevator.moveto_floor = elevator get_curFloor();

		foreach ( floor_num, outer_doorset in elevator get_outer_doorsets() )
		{
			if ( elevator get_curFloor() != floor_num )
				elevator thread close_outer_doors( floor_num );
		}
	}
}

elevator_move( floor_num )
{
	// self is elevator, self.e[]
	self notify( "elevator_moving" );
	self endon( "elevator_moving" );

	mainframe = self get_housing_mainframe();
	delta_vec = self.e[ "floor" + floor_num + "_pos" ] - mainframe.origin;

	speed = level.elevator_speed;
	dist = abs( distance( self.e[ "floor" + floor_num + "_pos" ], mainframe.origin ) );
	moveTime = dist / speed;

	// move housing:
	mainframe moveTo( mainframe.origin + delta_vec, moveTime, moveTime * level.elevator_accel, moveTime * level.elevator_decel );

	// move doors and triggers and other script models
	foreach ( part in self get_housing_children() )
	{
		moveto_pos = part.origin + delta_vec;

		if ( !issubstr( part.classname, "trigger_" ) )
			part moveTo( moveto_pos, moveTime, moveTime * level.elevator_accel, moveTime * level.elevator_decel );
		else
			part.origin = moveto_pos;
	}

	// making sure elevator housing gets to destination floor
	self waittill_finish_moving( mainframe, self.e[ "floor" + floor_num + "_pos" ] );

	self notify( "elevator_moved" );
}

close_inner_doors()
{
	// self is elevator set
	self notify( "closing_inner_doors" );
	self endon( "closing_inner_doors" ); 						// when interrupted
	self endon( "opening_inner_doors" ); 						// when interrupted

	left_door = self get_housing_leftdoor();					// ent
	right_door = self get_housing_rightdoor();					// ent

	mainframe = self get_housing_mainframe();					// ent
	old_closed_pos = self get_housing_closedpos();				// vec
	closed_pos = ( old_closed_pos[ 0 ], old_closed_pos[ 1 ], mainframe.origin[ 2 ] );	// adjusted closed position after floor change

	speed = level.elevator_innerdoorspeed;						// scaler
	dist = abs( distance( left_door.origin, closed_pos ) );		// scaler
	moveTime = dist / speed;										// scaler

	left_door moveTo( closed_pos, moveTime, moveTime * 0.1, moveTime * 0.25 );
	right_door moveTo( closed_pos, moveTime, moveTime * 0.1, moveTime * 0.25 );

	self waittill_finish_moving( left_door, closed_pos, right_door, closed_pos );
	self notify( "closed_inner_doors" );
}

open_inner_doors()
{
	// self is elevator set
	self notify( "opening_inner_doors" );
	self endon( "opening_inner_doors" ); 								// when interrupted

	left_door = self get_housing_leftdoor();							// ent
	right_door = self get_housing_rightdoor();							// ent

	mainframe = self get_housing_mainframe();							// ent
	old_left_opened_pos = self get_housing_leftdoor_opened_pos();		// vec
	old_right_opened_pos = self get_housing_rightdoor_opened_pos();		// vec

	left_opened_pos = ( old_left_opened_pos[ 0 ], old_left_opened_pos[ 1 ], mainframe.origin[ 2 ] );
	right_opened_pos = ( old_right_opened_pos[ 0 ], old_right_opened_pos[ 1 ], mainframe.origin[ 2 ] );

	speed = level.elevator_innerdoorspeed;								// scaler
	dist = abs( distance( left_opened_pos, right_opened_pos ) * 0.5 );	// scaler
	moveTime = ( dist / speed ) * 0.5;										// scaler

	left_door moveTo( left_opened_pos, moveTime, moveTime * 0.1, moveTime * 0.25 );
	right_door moveTo( right_opened_pos, moveTime, moveTime * 0.1, moveTime * 0.25 );

	self waittill_finish_moving( left_door, left_opened_pos, right_door, right_opened_pos );
	self notify( "opened_inner_doors" );
}

// close outer elevator doors per floor
close_outer_doors( floor_num )
{
	// self is elevator set
	self notify( "closing_floor_" + floor_num + "_outer_doors" );
	self endon( "closing_floor_" + floor_num + "_outer_doors" ); 		// when interrupted
	self endon( "opening_floor_" + floor_num + "_outer_doors" ); 		// when interrupted

	left_door = self get_outer_leftdoor( floor_num );					// ent
	right_door = self get_outer_rightdoor( floor_num );					// ent

	left_opened_pos = self get_outer_leftdoor_openedpos( floor_num );	// vec
	closed_pos = self get_outer_closedpos( floor_num );					// vec

	speed = level.elevator_outterdoorspeed;								// scaler
	dist = abs( distance( left_opened_pos, closed_pos ) );				// scaler
	moveTime = dist / speed;												// scaler

	left_door moveTo( closed_pos, moveTime, moveTime * 0.1, moveTime * 0.25 );
	right_door moveTo( closed_pos, moveTime, moveTime * 0.1, moveTime * 0.25 );

	self waittill_finish_moving( left_door, closed_pos, right_door, closed_pos );
	self notify( "closed_floor_" + floor_num + "_outer_doors" );
}

// open outer elevator doors per floor
open_outer_doors( floor_num )
{
	// self is elevator set
	level notify( "elevator_doors_opening" );
	self notify( "opening_floor_" + floor_num + "_outer_doors" );
	self endon( "opening_floor_" + floor_num + "_outer_doors" ); 		// when interrupted

	left_door = self get_outer_leftdoor( floor_num );					// ent
	right_door = self get_outer_rightdoor( floor_num );					// ent

	left_opened_pos = self get_outer_leftdoor_openedpos( floor_num );	// vec
	right_opened_pos = self get_outer_rightdoor_openedpos( floor_num );	// vec
	closed_pos = self get_outer_closedpos( floor_num );					// vec

	speed = level.elevator_outterdoorspeed;								// scaler
	dist = abs( distance( left_opened_pos, closed_pos ) );				// scaler
	moveTime = ( dist / speed ) * 0.5;										// scaler

	left_door moveTo( left_opened_pos, moveTime, moveTime * 0.1, moveTime * 0.25 );
	right_door moveTo( right_opened_pos, moveTime, moveTime * 0.1, moveTime * 0.25 );

	self waittill_finish_moving( left_door, left_opened_pos, right_door, right_opened_pos );
	self notify( "opened_floor_" + floor_num + "_outer_doors" );
}

//==========================================================================//
//							===	BUILD ELEVATORS ===							//
//==========================================================================//

// builds elevators into 'level.elevators' struct array containing parts list
build_elevators()
{
	// triggers that enclose elevator, to distinguish between elevators
	elevator_groups = getentarray( "elevator_group", "targetname" );
	assertex( isdefined( elevator_groups ) && ( elevator_groups.size ), "Radiant: Missing elevator bounding origins" );

	elevator_housings = getentarray( "elevator_housing", "targetname" );
	assertex( isdefined( elevator_housings ) && ( elevator_housings.size >= elevator_groups.size ), "Fail! Missing the whole elevator, script_brushmodel with [targetname = elevator_housing] must be correctly placed" );

	// sets of elevator outter door prefabs placed
	elevator_doorsets = getentarray( "elevator_doorset", "targetname" );
	assertex( isdefined( elevator_doorsets ) && ( elevator_doorsets.size >= elevator_groups.size ), "Radiant: Missing elevator door(s)" );

	// build found elevators
	foreach ( elevator_bound in elevator_groups )
	{
		elevator_bound_end = getent( elevator_bound.target, "targetname" );

		min_max_xy = [];
		min_max_xy[ 0 ] = min( elevator_bound.origin[ 0 ], elevator_bound_end.origin[ 0 ] );// min_x
		min_max_xy[ 1 ] = max( elevator_bound.origin[ 0 ], elevator_bound_end.origin[ 0 ] );// max_x
		min_max_xy[ 2 ] = min( elevator_bound.origin[ 1 ], elevator_bound_end.origin[ 1 ] );// min_y
		min_max_xy[ 3 ] = max( elevator_bound.origin[ 1 ], elevator_bound_end.origin[ 1 ] );// max_y

		parts = spawnstruct();
		parts.e[ "id" ] = level.elevators.size;

		// === build elevator housing ===
		/*
			Elevator's mainframe brushmodel
			
			script_brushmodel targeting:
			"elevator_housing" -> left door -> right door -> door trigger -> inside trigger
				
			Housing is the body that moves between floors
		*/
		parts.e[ "housing" ] = [];
		parts.e[ "housing" ][ "mainframe" ] = [];

		foreach ( elevator_housing in elevator_housings )
		{
			if ( elevator_housing isInbound( min_max_xy ) )
			{
				parts.e[ "housing" ][ "mainframe" ][ parts.e[ "housing" ][ "mainframe" ].size ] = elevator_housing;

				if ( elevator_housing.classname == "script_model" )
					continue;

			if ( elevator_housing.code_classname == "light" )
					continue;

				inner_leftdoor = getent( elevator_housing.target, "targetname" );
				parts.e[ "housing" ][ "left_door" ] = inner_leftdoor;
				parts.e[ "housing" ][ "left_door_opened_pos" ] = inner_leftdoor.origin;

				inner_rightdoor = getent( inner_leftdoor.target, "targetname" );
				parts.e[ "housing" ][ "right_door" ] = inner_rightdoor;
				parts.e[ "housing" ][ "right_door_opened_pos" ] = inner_rightdoor.origin;

				inner_door_closed_pos = ( inner_leftdoor.origin - inner_rightdoor.origin ) * ( 0.5, 0.5, 0.5 ) + inner_rightdoor.origin;
				parts.e[ "housing" ][ "door_closed_pos" ] = inner_door_closed_pos;

				door_trigger = getent( inner_rightdoor.target, "targetname" );
				parts.e[ "housing" ][ "door_trigger" ] = door_trigger;

				inside_trigger = getent( door_trigger.target, "targetname" );
				parts.e[ "housing" ][ "inside_trigger" ] = inside_trigger;
				inside_trigger make_discrete_trigger();

				// for motion triggers
				inside_trigger.motion_trigger = spawn( "trigger_radius", elevator_housing.origin, 0, 64, 128 );
				assert( isdefined( inside_trigger.motion_trigger ) );
			}
		}
		assert( isdefined( parts.e[ "housing" ] ) );

		// === build elevator outer doorsets ===
		/*
			Elevator's outer door sets ( per floor ) are chained together from a script_origin 
			targeting to the left door, then left door targets to right door
			
			Outer door sets are bodies that only moves to open/close
		*/
		parts.e[ "outer_doorset" ] = [];
		foreach ( elevator_doorset in elevator_doorsets )
		{
			if ( elevator_doorset isInbound( min_max_xy ) )
			{
				
				// closed for lighting is for compiling lighting in the closed position rather than the open position. 
				door_starts_closed = isdefined( elevator_doorset.script_noteworthy ) && elevator_doorset.script_noteworthy == "closed_for_lighting";
		
				door_set_id = parts.e[ "outer_doorset" ].size;

				parts.e[ "outer_doorset" ][ door_set_id ] = [];
				parts.e[ "outer_doorset" ][ door_set_id ][ "door_closed_pos" ] = elevator_doorset.origin;

				leftdoor = getent( elevator_doorset.target, "targetname" );
				parts.e[ "outer_doorset" ][ door_set_id ][ "left_door" ] = leftdoor;
				parts.e[ "outer_doorset" ][ door_set_id ][ "left_door_opened_pos" ] = leftdoor.origin;

				rightdoor = getent( leftdoor.target, "targetname" );
				parts.e[ "outer_doorset" ][ door_set_id ][ "right_door" ] = rightdoor;
				parts.e[ "outer_doorset" ][ door_set_id ][ "right_door_opened_pos" ] = rightdoor.origin;
				
				//put them back into the would be positions 
				if( door_starts_closed )
				{
					left_door_vec = elevator_doorset.origin - leftdoor.origin;
					elevator_doorset.origin = leftdoor.origin;
					leftdoor.origin += left_door_vec;
					rightdoor.origin -= left_door_vec; 
					parts.e[ "outer_doorset" ][ door_set_id ][ "door_closed_pos" ] = elevator_doorset.origin;
					parts.e[ "outer_doorset" ][ door_set_id ][ "left_door_opened_pos" ] = leftdoor.origin;
					parts.e[ "outer_doorset" ][ door_set_id ][ "right_door_opened_pos" ] = rightdoor.origin;
				}
			}
		}
		assert( isdefined( parts.e[ "outer_doorset" ] ) );

		// bubble sort floors
		for ( i = 0; i < parts.e[ "outer_doorset" ].size - 1; i++ )
		{
			for ( j = 0; j < parts.e[ "outer_doorset" ].size - 1 - i; j++ )
			{
				if ( parts.e[ "outer_doorset" ][ j + 1 ][ "door_closed_pos" ][ 2 ] < parts.e[ "outer_doorset" ][ j ][ "door_closed_pos" ][ 2 ] )
				{
					//swap j-1 with j
					temp_left_door = parts.e[ "outer_doorset" ][ j ][ "left_door" ];
					temp_left_door_opened_pos = parts.e[ "outer_doorset" ][ j ][ "left_door_opened_pos" ];
					temp_right_door = parts.e[ "outer_doorset" ][ j ][ "right_door" ];
					temp_right_door_opened_pos = parts.e[ "outer_doorset" ][ j ][ "right_door_opened_pos" ];
					temp_closed_pos = parts.e[ "outer_doorset" ][ j ][ "door_closed_pos" ];

					parts.e[ "outer_doorset" ][ j ][ "left_door" ] = parts.e[ "outer_doorset" ][ j + 1 ][ "left_door" ];
					parts.e[ "outer_doorset" ][ j ][ "left_door_opened_pos" ] = parts.e[ "outer_doorset" ][ j + 1 ][ "left_door_opened_pos" ];
					parts.e[ "outer_doorset" ][ j ][ "right_door" ] = parts.e[ "outer_doorset" ][ j + 1 ][ "right_door" ];
					parts.e[ "outer_doorset" ][ j ][ "right_door_opened_pos" ] = parts.e[ "outer_doorset" ][ j + 1 ][ "right_door_opened_pos" ];
					parts.e[ "outer_doorset" ][ j ][ "door_closed_pos" ] = parts.e[ "outer_doorset" ][ j + 1 ][ "door_closed_pos" ];

					parts.e[ "outer_doorset" ][ j + 1 ][ "left_door" ] = temp_left_door;
					parts.e[ "outer_doorset" ][ j + 1 ][ "left_door_opened_pos" ] = temp_left_door_opened_pos;
					parts.e[ "outer_doorset" ][ j + 1 ][ "right_door" ] = temp_right_door;
					parts.e[ "outer_doorset" ][ j + 1 ][ "right_door_opened_pos" ] = temp_right_door_opened_pos;
					parts.e[ "outer_doorset" ][ j + 1 ][ "door_closed_pos" ] = temp_closed_pos;
				}
			}
		}

		// === build elevator floor pos ===
		floor_pos = [];
		foreach ( i, doorset in parts.e[ "outer_doorset" ] )
		{
			mainframe = parts get_housing_mainframe();

			floor_pos = ( mainframe.origin[ 0 ], mainframe.origin[ 1 ], doorset[ "door_closed_pos" ][ 2 ] );
			parts.e[ "floor" + i + "_pos" ] = floor_pos;

			if ( mainframe.origin == floor_pos )
			{
				parts.e[ "initial_floor" ] = i;
				parts.e[ "current_floor" ] = i;
			}
		}

		// === save build info ===
		level.elevators[ level.elevators.size ] = parts;

		// trash all script origins no longer needed
		elevator_bound delete();
		elevator_bound_end delete();
	}
	foreach ( elevator_doorset in elevator_doorsets )
		elevator_doorset delete();

	build_call_buttons();

	if ( !level.elevator_motion_detection )
		setup_hints();

	// turn on all primary lights for elevators if they have em
	foreach ( elevator in level.elevators )
	{
		pLights = elevator get_housing_primarylight();
		if ( isdefined( pLights ) && pLights.size )
		{
			foreach ( pLight in pLights )
				pLight setlightintensity( 0.75 );
		}
	}
}

build_call_buttons()
{
	level.elevator_callbuttons = getentarray( "elevator_call", "targetname" );
	assertex( isdefined( level.elevator_callbuttons ) && ( level.elevator_callbuttons.size > 1 ), "Missing or not enough elevator call buttons" );

	// per call button
	foreach ( callbutton in level.elevator_callbuttons )
	{
		callbutton.e = [];
		callbutton_v_vec = ( 0, 0, callbutton.origin[ 2 ] );
		callbutton_h_vec = ( callbutton.origin[ 0 ], callbutton.origin[ 1 ], 0 );

		temp_elevator_list = [];
		// per elevator
		foreach ( e_idx, elevator in level.elevators )
		{
			// per floor
			foreach ( f_idx, eFloor in elevator get_outer_doorsets() )
			{
				v_vec = ( 0, 0, elevator.e[ "floor" + f_idx + "_pos" ][ 2 ] );
				h_vec = ( elevator.e[ "floor" + f_idx + "_pos" ][ 0 ], elevator.e[ "floor" + f_idx + "_pos" ][ 1 ], 0 );

				if ( abs( distance( callbutton_v_vec, v_vec ) ) <= level.elevator_callbutton_link_v )
				{
					if ( abs( distance( callbutton_h_vec, h_vec ) ) <= level.elevator_callbutton_link_h )
					{
						temp_elevator_list[ temp_elevator_list.size ] = elevator;	// build list of elevators linked on floor f_idx
						callbutton.e[ f_idx ] = temp_elevator_list;
					}
				}
			}
		}
		callbutton make_discrete_trigger();
		assertex( isdefined( callbutton.e ) && callbutton.e.size, "Elevator call button at " + callbutton.origin + " failed to grab near by elevators, placed too far?" );

		// build motion detection triggers for motion triggered calls
		callbutton.motion_trigger = spawn( "trigger_radius", callbutton.origin + ( 0, 0, -32 ), 0, 32, 64 );
	}
}

setup_hints()
{
	// elevator inside use trigger hint
	foreach ( elevator in level.elevators )
	{
		use_trig = elevator get_housing_inside_trigger();
		floors = elevator get_outer_doorsets();
		num_of_floors = floors.size;

		use_trig SetCursorHint( "HINT_NOICON" );
		if ( num_of_floors > 2 )
			// Press and hold &&1 to select floor.
			use_trig setHintString( &"ELEVATOR_FLOOR_SELECT_HINT" );
		else
			// Press and hold &&1 to use elevator.
			use_trig setHintString( &"ELEVATOR_USE_HINT" );
	}

	// elevator call button hint
	foreach ( callbutton in level.elevator_callbuttons )
	{
		callbutton SetCursorHint( "HINT_NOICON" );
		// Press and hold &&1 to call elevator.
		callbutton setHintString( &"ELEVATOR_CALL_HINT" );
	}
}

//==========================================================================//
//								===	HELPERS	===								//
//==========================================================================//

// setup discrete trigger for discrete waittill
make_discrete_trigger()
{
	self.enabled = 1;			// enable now for disabling trigger
	self disable_trigger(); 	// disable trigger, enable again only when waittill on
}

// trigger only exist when waittill upon
discrete_waittill( msg )
{
	assert( isdefined( self.motion_trigger ) );

	self enable_trigger();	// this is for disabling trigger immediately after triggering

	if ( level.elevator_motion_detection )
		self.motion_trigger waittill( msg, param );
	else
		self waittill( msg, param );

	self disable_trigger();
	return param;
}


enable_trigger()
{
	if ( !self.enabled )
	{
		self.enabled = 1;
		self.origin += ( 0, 0, 10000 );

		if ( isdefined( self.motion_trigger ) )
			self.motion_trigger.origin += ( 0, 0, 10000 );
	}
}

disable_trigger()
{
	self notify( "disable_trigger" );
	if ( self.enabled )
		self thread disable_trigger_helper();
}

disable_trigger_helper()
{
	self endon( "disable_trigger" );
	self.enabled = 0;
	wait 1.5;
	self.origin += ( 0, 0, -10000 );

	if ( isdefined( self.motion_trigger ) )
		self.motion_trigger.origin += ( 0, 0, -10000 );
}

// ========== OUTER DOOR SETS ===========
// returns array of outer doorsets for this floor - ent[]
get_outer_doorset( floor_num )
{
	return self.e[ "outer_doorset" ][ floor_num ];
}

// returns array of floors of doorsets - int[]
get_outer_doorsets()
{
	return self.e[ "outer_doorset" ];
}

// returns outer door closed position for this floor - vec
get_outer_closedpos( floor_num )
{
	return self.e[ "outer_doorset" ][ floor_num ][ "door_closed_pos" ];
}

// returns left door entity for this floor - ent
get_outer_leftdoor( floor_num )
{
	return self.e[ "outer_doorset" ][ floor_num ][ "left_door" ];
}

// returns right door entity for this floor - ent
get_outer_rightdoor( floor_num )
{
	return self.e[ "outer_doorset" ][ floor_num ][ "right_door" ];
}

// returns left door opened position for this floor - vec
get_outer_leftdoor_openedpos( floor_num )
{
	return self.e[ "outer_doorset" ][ floor_num ][ "left_door_opened_pos" ];
}

// returns right door opened position for this floor - vec
get_outer_rightdoor_openedpos( floor_num )
{
	return self.e[ "outer_doorset" ][ floor_num ][ "right_door_opened_pos" ];
}

// ========= HOUSING =========
// returns all the entities that will move when the elevator moves between floors - ent[]
get_housing_children()
{
	children = [];

	door_trig 	 = self get_housing_door_trigger();
	use_trig 	 = self get_housing_inside_trigger();
	motion_trig = use_trig.motion_trigger;
	left_door	 = self get_housing_leftdoor();
	right_door	 = self get_housing_rightdoor();

	children[ children.size ] = door_trig;
	children[ children.size ] = use_trig;
	children[ children.size ] = left_door;
	children[ children.size ] = right_door;

	if ( isdefined( motion_trig ) )
		children[ children.size ] = motion_trig;

	// append script models as children of elevator moving body
	script_models = self get_housing_models();
	foreach ( eModel in script_models )
		children[ children.size ] = eModel;

	// append primary light(s) as children of elevator moving body
	primarylights = get_housing_primarylight();
	foreach ( pLight in primarylights )
		children[ children.size ] = pLight;

	return children;
}

// returns only the mainframe script_brushmodel, there can only be one per elevator! - ent
get_housing_mainframe()
{
	parts = self.e[ "housing" ][ "mainframe" ];

	housing_model = undefined;
	foreach ( part in parts )
	{
		if ( part.classname != "script_model" && part.code_classname != "light"  )
		{
			assertex( !isdefined( housing_model ), "Fail! Found more than one elevator housing script_brushmodels per elevator" );
			housing_model = part;
		}
	}
	assertex( isdefined( housing_model ), "Epic fail! No elevator housing script_brushmodel found" );
	return housing_model;
}

// returns an array of script_models used as part of the elevator prefab - ent[]
get_housing_models()
{
	parts = self.e[ "housing" ][ "mainframe" ];
	temp_model_array = [];

	foreach ( part in parts )
	{
		if ( part.classname == "script_model" )
			temp_model_array[ temp_model_array.size ] = part;
	}
	return temp_model_array;
}

// returns an array of lights - ent[]
get_housing_primarylight()
{
	parts = self.e[ "housing" ][ "mainframe" ];
	temp_primarylights = [];

	foreach ( part in parts )
	{
		if ( part.code_classname == "light" )
			temp_primarylights[ temp_primarylights.size ] = part;
	}
	return temp_primarylights;
}

// returns a single model to play elevator music on, this must exist and only one! - ent
get_housing_musak_model()
{
	models = self get_housing_models();
	musak_model = undefined;

	foreach ( eModel in models )
	{
		if ( isdefined( eModel.script_noteworthy ) && eModel.script_noteworthy == "play_musak" )
			musak_model = eModel;
	}
	//assertex( isdefined( musak_model ), "Fail! Missing script_model to play elevator music on, [script_noteworthy = play_musak]" );
	
	return musak_model;
}

// returns interrupt trigger for elevator doors - ent trig
get_housing_door_trigger()
{
	return self.e[ "housing" ][ "door_trigger" ];
}

// returns trigger for floor selection inside the elevator - ent trig
get_housing_inside_trigger()
{
	return self.e[ "housing" ][ "inside_trigger" ];
}

// returns inner door's closing position - vec
get_housing_closedpos()
{
	return self.e[ "housing" ][ "door_closed_pos" ];
}

// returns inner left door entity - ent
get_housing_leftdoor()
{
	return self.e[ "housing" ][ "left_door" ];
}

// returns inner right door entity - ent
get_housing_rightdoor()
{
	return self.e[ "housing" ][ "right_door" ];
}

// returns inner left door opened position - vec
get_housing_leftdoor_opened_pos()
{
	return self.e[ "housing" ][ "left_door_opened_pos" ];
}

// returns inner right door opened position - vec
get_housing_rightdoor_opened_pos()
{
	return self.e[ "housing" ][ "right_door_opened_pos" ];
}

// floor currently the elevator is on - int
get_curFloor()
{
	moving = self elevator_floor_update();
	return self.e[ "current_floor" ];
}

// floor elevator initially at on level load - int
get_initFloor()
{
	return self.e[ "initial_floor" ];
}

waittill_finish_moving( ent1, ent1_moveto_pos, ent2, ent2_moveto_pos )
{
	if ( !isdefined( ent2 ) && !isdefined( ent2_moveto_pos ) )
	{
		ent2 = ent1;
		ent2_moveto_pos = ent1_moveto_pos;
	}

	while ( 1 )
	{
		ent1_current_pos = ent1.origin;
		etn2_current_pos = ent2.origin;

		if ( ent1_current_pos == ent1_moveto_pos && etn2_current_pos == ent2_moveto_pos )
			break;

		wait 0.05;
	}
}

// return test of min max bound on xy plane, self = entity being tested - bool
isInbound( bounding_box )
{

	assertex( isdefined( self ) && isdefined( self.origin ), "Fail! Can not test bounds with the entity called on" );

	// the box isn't working on the angle that I have so I'm going to do it by a sphere shape.. -Nate 
	//special case cause I don't know if a sphere will break other levels.
	//hey look you can make it do spheres, nobody is doing that now.
	//if( level.script == "plaza" || level.script == "highrise_test" )
		//return isInBoundingSpere( bounding_box );

	v_x = self.origin[ 0 ];
	v_y = self.origin[ 1 ];

	min_x = bounding_box[ 0 ];
	max_x = bounding_box[ 1 ];
	min_y = bounding_box[ 2 ];
	max_y = bounding_box[ 3 ];

	return ( v_x >= min_x && v_x <= max_x && v_y >= min_y && v_y <= max_y );
}

isInBoundingSpere( bounding_box )
{
	v_x = self.origin[ 0 ];
	v_y = self.origin[ 1 ];
	min_x = bounding_box[ 0 ];
	max_x = bounding_box[ 1 ];
	min_y = bounding_box[ 2 ];
	max_y = bounding_box[ 3 ];
	
	mid_x = (min_x + max_x )/2;
	mid_y = (min_y + max_y )/2;
	radius = abs( Distance( (min_x,min_y,0 ), (mid_x,mid_y,0)) );
	return( abs( distance( (v_x,v_y,0), (mid_x,mid_y,0) ) )  < radius );
}

waittill_or_timeout( msg, timer )
{
	self endon( msg );
	wait( timer );
}

elevator_get_dvar_int( dvar, def )
{
	return int( elevator_get_dvar( dvar, def ) );
}

elevator_get_dvar( dvar, def )
{
	if ( getdvar( dvar ) != "" )
		return getdvarfloat( dvar );
	else
	{
		setdvar( dvar, def );
		return def;
	}
}

elevator_debug()
{
	if ( !level.elevator_debug )
		return;

	//print3d(<origin>, <text>, <color>, <alpha>, <scale>, <duration> )

	while ( 1 )
	{
		// simple debug info:

		if ( level.elevator_debug != 2 )
			continue;

		// full debug info:

		// print all parts for all elevators
		foreach ( i, elevator in level.elevators )
		{
			mainframe = elevator get_housing_mainframe();
			musak_model = elevator get_housing_musak_model();

			print3d( musak_model.origin, "[e" + i + "]musak_origin", ( 0.75, 0.75, 1.0 ), 1, 0.25, 20 );
			print3d( mainframe.origin, "[e" + i + "]mainframe", ( 0.75, 0.75, 1.0 ), 1, 0.25, 20 );
			print3d( elevator.e[ "housing" ][ "left_door" ].origin, "[e" + i + "]left door", ( 0.75, 0.75, 1.0 ), 1, 0.25, 20 );
			print3d( elevator.e[ "housing" ][ "right_door" ].origin, "[e" + i + "]right door", ( 0.75, 0.75, 1.0 ), 1, 0.25, 20 );
			print3d( elevator.e[ "housing" ][ "door_closed_pos" ], "[e" + i + "]->|<-", ( 0.75, 0.75, 1.0 ), 1, 0.25, 20 );
			print3d( elevator.e[ "housing" ][ "inside_trigger" ].origin, "[e" + i + "]USE", ( 0.75, 0.75, 1.0 ), 1, 0.25, 20 );

			foreach ( j, eFloor in elevator.e[ "outer_doorset" ] )
			{
				print3d( eFloor[ "left_door" ].origin + ( 0, 0, 8 ), "[e" + i + "][f" + j + "]left door", ( 0.75, 1.0, 0.75 ), 1, 0.25, 20 );
				print3d( eFloor[ "right_door" ].origin + ( 0, 0, 8 ), "[e" + i + "][f" + j + "]right door", ( 0.75, 1.0, 0.75 ), 1, 0.25, 20 );
				print3d( eFloor[ "door_closed_pos" ] + ( 0, 0, 8 ), "[e" + i + "][f" + j + "]->|<-", ( 0.75, 1.0, 0.75 ), 1, 0.25, 20 );
				print3d( elevator.e[ "floor" + j + "_pos" ] + ( 0, 0, 8 ), "[e" + i + "][f" + j + "]stop", ( 1.0, 0.75, 0.75 ), 1, 0.25, 20 );
			}
		}

		// per button
		foreach ( callbutton in level.elevator_callbuttons )
		{
			print3d( callbutton.origin, "linked to:", ( 0.75, 0.75, 1.0 ), 1, 0.25, 20 );

			// per floor
			foreach ( f_idx, eFloor in callbutton.e )
			{
				// per elevator linked
				printoffset = 0;
				foreach ( e_idx, eLinked in eFloor )
				{
					printoffset++ ;
					// e_idx is used to offset print
					print_pos = callbutton.origin + ( 0, 0, ( printoffset ) * ( -4 ) );
					print3d( print_pos, "[f" + f_idx + "][e" + eLinked.e[ "id" ] + "]", ( 0.75, 0.75, 1.0 ), 1, 0.25, 20 );
				}
			}
		}

		wait 0.05;
	}
}