#include "scripts/common.txt"
#include "scripts/BP_common.txt"

//Original SpawnParams
//(0) Target ID -32768..32767 (sound id lookup)
//(1) Trigger Anim 0..255  (delay(TurokFloorMoverLowerChangeWaitRaise), rotate speed(TurokFloorMoverLowerOnce))
//(2) Health -32768..32767 (delay to reset position)
//(4) Attach Chance 0..255 (move speed)
//(7) Params 1 -128..127 (distance to move)

//BP SpawnParams
//(0) Target ID -32768..32767 (sound id lookup)
//(2) Health -32768..32767 (Target Yaw)
//(7) Params 1 -128..127 (time it takes to move in seconds * 0.25)
//(6) Params 2 -128..127 (start delay time * 0.25)
//(4) Attack Chance 0..255 (return delay time (0 = never returns) * 0.25)
//Dead Height/Step Height (float) (target sector/water height position)
//(3) Max Regenerations -32768..32767 (flags)
//Wall Width/Radius (float) (actor target z position)
//Height (float) (Arg6 target value)


//Common Flags
//Sector: 				3		(BP_MF_ACTOR_HEIGHT | BP_MF_SECTOR_HEIGHT)
//One Shot Sector:		11		(BP_MF_ACTOR_HEIGHT | BP_MF_SECTOR_HEIGHT | BP_MF_ONE_SHOT)
//Water:				5		(BP_MF_ACTOR_HEIGHT | BP_MF_WATER_HEIGHT)
//One Shot Water:		13		(BP_MF_ACTOR_HEIGHT | BP_MF_WATER_HEIGHT | BP_MF_ONE_SHOT)
//Blocker: 				129		(BP_MF_ACTOR_HEIGHT | BP_MF_BLOCKER)
//One Shot Blocker: 	137		(BP_MF_ACTOR_HEIGHT | BP_MF_BLOCKER | BP_MF_ONE_SHOT)

//8789 - water platform, 4689 - water platform with sync

//32109876543210 //129
//00000000100011

//actor
//-3496.959961 //from
//-20.480000 //to

//sector
//-20.480000 //from
//3461.119873 //to




//flag bits (16 bits) - default value of 3 (BP_MF_ACTOR_HEIGHT | BP_MF_SECTOR_HEIGHT)
const int BP_MF_ACTOR_HEIGHT 	= 1 << 0; //Change Actor Z Position
const int BP_MF_SECTOR_HEIGHT 	= 1 << 1; //Change Sector Height
const int BP_MF_WATER_HEIGHT 	= 1 << 2; //Change Sector Water Height
const int BP_MF_ONE_SHOT 		= 1 << 3; //Can only activate once
const int BP_MF_START_MOVING 	= 1 << 4; //Starts active moving OnBeginLevel
const int BP_MF_START_UP 		= 1 << 5; //Starts in up state
const int BP_MF_PERPETUAL 		= 1 << 6; //On move finish will start moving again
const int BP_MF_BLOCKER 		= 1 << 7; //Unblock sector when start moving up and block when start moving down
const int BP_MF_WATER 			= 1 << 8; //change area to water when start moving up and no water when done moving down
const int BP_MF_NOPERSIST		= 1 << 9; //Never tries to mark the persistent bit
const int BP_MF_DAMAGE			= 1 << 10; //when in up state (not while moving) will have damage area. If down state won't have damage area.
const int BP_MF_ONGROUND		= 1 << 11; //only activated if player is on the ground
const int BP_MF_SYNC			= 1 << 12; //sync state to closest platform(X/Y only) that does not have sync flag OnWake
const int BP_MF_AREAARG			= 1 << 13; //Change Area Arg 6
//const int BP_MF_PROTATE		= 1 << 14; //rotate player when mover rotates

//state flags (32 bits)
const int BP_MSF_UP = 1 << 0;
const int BP_MSF_MOVING = 1 << 1;

array<kActor@> floorMovers;

//============================================================================================================================
class BP_FloorMover : ScriptObject
{
    kActor @self;
	float lerpTime;
	float moveTime;
	float delayTime;
	float startDelay;
	float returnDelay;
	int soundID;
	float startActorHeight;
	float startSectorHeight;
	float startWaterHeight;
	float targetSectorHeight;
	float targetActorHeight;
	float targetYaw;
	float targetWaterHeight;
	float startYaw;
	float startArgHeight;
	float targetArgHeight;
	int flags;
	int state;
	bool beginPersist;
	bool isAsleep;
	BP_FloorMover @syncMover;
	//------------------------------------------------------------------------------------------------------------------------
    BP_FloorMover(kActor @actor)
    {
        @self = actor;
		state = 0;
		lerpTime = 0.0f;
		moveTime = 0.0f;
		startDelay = 0.0f;
		returnDelay = 0.0f;
		soundID = 0;
		startYaw = 0.0f;
		targetYaw = 0.0f;
		flags = 0;
		startActorHeight = 0.0f;
		startSectorHeight = 0.0f;
		startWaterHeight = 0.0f;
		startArgHeight = 0.0f;
		targetSectorHeight = 0.0f;
		targetActorHeight = 0.0f;
		targetWaterHeight = 0.0f;
		targetArgHeight = 0.0f;
		startArgHeight = 0.0f;
		targetArgHeight = 0.0f;
		beginPersist = false;
		@syncMover = null;
		isAsleep = false;
	}
	//------------------------------------------------------------------------------------------------------------------------
	void OnSleep()
	{
		isAsleep = true;
	}
	//------------------------------------------------------------------------------------------------------------------------
	void OnWake()
	{
		isAsleep = false;
		if (IsSyncing())
		{
			Sync();
		}
	}
	//------------------------------------------------------------------------------------------------------------------------
	bool IsSyncing()
	{
		return (flags & BP_MF_SYNC != 0 && syncMover !is null);
	}
	//------------------------------------------------------------------------------------------------------------------------
	void Sync()
	{
		state = syncMover.state;
		lerpTime = syncMover.lerpTime;
		delayTime = syncMover.delayTime;
	}
	//------------------------------------------------------------------------------------------------------------------------
	void OnPostBeginLevel()
	{
		//if sync flag is on then get platforms that are near (a certain distance x/y only) and find the first platform that doesn't have sync and is the closest
		if (flags & BP_MF_SYNC != 0)
		{
			float closestDist = 9999999.0f;
			BP_FloorMover @closestMover = null;
			for (int i = 0; i < int(floorMovers.length()); i++)
			{
				kActor @actor = @floorMovers[i];
				BP_FloorMover @mover = cast<BP_FloorMover@>(actor.ScriptObject().obj);
				if (mover.flags & BP_MF_SYNC == 0)
				{
					float distance = self.Origin().Distance(kVec3(actor.Origin().x, actor.Origin().y, self.Origin().z));
					if (distance < closestDist)
					{
						closestDist = distance;
						@closestMover = @mover;
					}
				}
			}
			@syncMover = @closestMover;
		}
	}
	//------------------------------------------------------------------------------------------------------------------------
	void OnEndLevel()
	{
		floorMovers.resize(0);
	}
	//------------------------------------------------------------------------------------------------------------------------
    void OnBeginLevel()
    {
		floorMovers.insertLast(@self);
		
		self.Flags() &= ~(AF_HIDDEN|AF_DISABLED);
		flags = self.SpawnParams(3);
		moveTime = float(self.SpawnParams(7)) * 0.25f;
		startDelay = float(self.SpawnParams(6)) * 0.25f;
		returnDelay = float(self.SpawnParams(4)) * 0.25f;
		soundID = self.SpawnParams(0);
		beginPersist = self.IsPersistentMarked();
				
		if (flags & BP_MF_START_UP != 0)
		{
			//reverse the start and target heights
			startSectorHeight = self.StepHeight();
			targetSectorHeight = self.FloorHeight();
			startYaw = float(self.SpawnParams(2));
			targetYaw = Math::Rad2Deg(self.Yaw());
			startActorHeight = self.WallRadius();
			targetActorHeight = self.Origin().z;
			startWaterHeight = self.StepHeight();
			targetWaterHeight = self.GetWaterHeight();
			startArgHeight = self.Height();
			targetArgHeight = float(World.GetAreaArg(self.AreaID(), 5));
			
			SetHeights(startSectorHeight, startActorHeight, startWaterHeight, startArgHeight);
			SetRotation(startYaw);
		}
		else
		{
			startSectorHeight = self.FloorHeight();
			targetSectorHeight = self.StepHeight();
			startYaw = Math::Rad2Deg(self.Yaw());
			targetYaw = float(self.SpawnParams(2));
			startActorHeight = self.Origin().z;
			targetActorHeight = self.WallRadius();
			startWaterHeight = self.GetWaterHeight();
			targetWaterHeight = targetSectorHeight;
			startArgHeight = float(World.GetAreaArg(self.AreaID(), 5));
			targetArgHeight = self.Height();
		}
		
		// if (self.TID() == 9999)
		// {
			// Sys.Print("moveTime: " + moveTime);
			// Sys.Print("startDelay: " + startDelay);
			// Sys.Print("returnDelay: " + returnDelay);
			// Sys.Print("soundID: " + soundID);
			// Sys.Print("startYaw: " + startYaw);
			// Sys.Print("targetYaw: " + targetYaw);
			// Sys.Print("flags: " + flags);
			// Sys.Print("startActorHeight: " + startActorHeight);
			// Sys.Print("targetActorHeight: " + targetActorHeight);
			// Sys.Print("startSectorHeight: " + startSectorHeight);
			// Sys.Print("targetSectorHeight: " + targetSectorHeight);		
			// Sys.Print("startWaterHeight: " + startWaterHeight);
			// Sys.Print("targetWaterHeight: " + targetWaterHeight);		
			// Sys.Print("startArgHeight: " + startArgHeight);
			// Sys.Print("targetArgHeight: " + targetArgHeight);		
		// }

        if (flags & BP_MF_START_MOVING != 0)
		{
			MoveUpStart();
		}
    }
	//------------------------------------------------------------------------------------------------------------------------
    void OnRestore()
    {
		if (!beginPersist)
		{
			return;
		}
		
		SetToUp();
				
		if (flags & BP_MF_ONE_SHOT == 0)
		{
			self.Flags() |= AF_ACTIVATED;
		}
	}
	//------------------------------------------------------------------------------------------------------------------------
    void OnSpawn()
    {
    }
	//------------------------------------------------------------------------------------------------------------------------
    void OnActivate()
    {
		if (flags & BP_MF_ONGROUND != 0 && !Player.Actor().OnGround())
		{
			self.Flags() &= ~AF_ACTIVATED;
			return;
		}
		
		if (flags & BP_MF_ONE_SHOT == 0)
		{
			self.Flags() &= ~AF_ACTIVATED;
		}
		
		if (state & BP_MSF_MOVING != 0)
		{
			return;
		}
		
		if (self.IsPersistentMarked() && (returnDelay <= 0.0f && flags & BP_MF_NOPERSIST == 0))
		{
			return;
		}
		
		// if ((state & BP_MSF_MOVING != 0) && ((state & BP_MSF_UP != 0 && flags & BP_MF_ACTIVE_ON_UP == 0) ||
											// (state & BP_MSF_UP == 0 && flags & BP_MF_ACTIVE_ON_DOWN == 0)))
		// {
			// return;
		// }
		// Sys.Print("on activate - isMoving: " + (state & BP_MSF_MOVING != 0) + " Up: " + (state & BP_MSF_UP != 0) + " fActiveOnUP: " + (flags & BP_MF_ACTIVE_ON_UP != 0) + " fActiveOnDOWN: " + (flags & BP_MF_ACTIVE_ON_DOWN != 0));
		
		if (moveTime > 0.0f)
		{
			if (state & BP_MSF_UP != 0)
			{
				MoveDownStart();
			}
			else
			{
				MoveUpStart();
			}
		}
		else
		{
			if (state & BP_MSF_UP != 0)
			{
				MoveDownFinish();
			}
			else
			{
				MoveUpFinish();
			}
		}
	}
	//------------------------------------------------------------------------------------------------------------------------
	void BlockArea()
	{
		if (flags & BP_MF_BLOCKER != 0)
		{
			::BlockArea(true, self);
		}
	}
	//------------------------------------------------------------------------------------------------------------------------
	void UnblockArea()
	{
		if (flags & BP_MF_BLOCKER != 0)
		{
			::BlockArea(false, self);
		}
	}
	//------------------------------------------------------------------------------------------------------------------------
	void SetWaterArea(bool active)
	{
		int areaID = self.AreaID();
		if (flags & BP_MF_WATER != 0 && areaID >= 0)
		{
			World.ChangeAreaFlag(areaID, AAF_WATER, active);
		}
	}
	//------------------------------------------------------------------------------------------------------------------------
	void SetDamageArea(bool active)
	{
		int areaID = self.AreaID();
		if (flags & BP_MF_DAMAGE != 0 && areaID >= 0)
		{
			World.ChangeAreaFlag(areaID, AAF_DAMAGE, active);
		}
	}
	//------------------------------------------------------------------------------------------------------------------------
	void PlaySound()
	{
		if (soundID > 0)
		{
			self.PlaySoundWithLookupID(soundID);
		}
	}
	//------------------------------------------------------------------------------------------------------------------------
	void AddShake()
	{
		if ((self.SpawnFlags1() & 0x40000000) != 0) //1 << 30
		{
			AddShakePoint(self, moveTime);
		}
	}
	//------------------------------------------------------------------------------------------------------------------------
    void SetToUp()
    {
		state |= BP_MSF_UP;
		state &= ~BP_MSF_MOVING;
		SetHeights(targetSectorHeight, targetActorHeight, targetWaterHeight, targetArgHeight);
		SetRotation(targetYaw);
		UnblockArea();
		SetWaterArea(true);
		SetDamageArea(true);
		SetPersistentBit(false);
	}
	//------------------------------------------------------------------------------------------------------------------------
	void MoveUpStart()
	{
		state |= BP_MSF_UP | BP_MSF_MOVING;
		lerpTime = 0.0f;
		SetHeights(startSectorHeight, startActorHeight, startWaterHeight, startArgHeight);
		SetRotation(0.0f);
		delayTime = startDelay;
		if (returnDelay <= 0.0f)
		{
			SetPersistentBit(false);
		}
		
		if (startDelay <= 0.0f)
		{
			OnUpPostDelay();
		}
	}
	//------------------------------------------------------------------------------------------------------------------------
	void MoveUpFinish()
	{
		state |= BP_MSF_UP;
		state &= ~BP_MSF_MOVING;
		SetHeights(targetSectorHeight, targetActorHeight, targetWaterHeight, targetArgHeight);
		SetRotation(targetYaw);
		UnblockArea();
		SetWaterArea(true);
		SetDamageArea(true);
		self.StopSound();
		
		if (returnDelay > 0.0f || flags & BP_MF_PERPETUAL != 0)
		{
			MoveDownStart();
		}
		else
		{
			SetPersistentBit(false);
		}
	}
	//------------------------------------------------------------------------------------------------------------------------
	void OnUpPostDelay()
	{
		UnblockArea();
		SetWaterArea(true);
		SetDamageArea(false);
		PlaySound();
		AddShake();
	}
	//------------------------------------------------------------------------------------------------------------------------
	void OnDownPostDelay()
	{
		BlockArea();
		SetWaterArea(true);
		SetDamageArea(false);
		PlaySound();
		AddShake();
	}
	//------------------------------------------------------------------------------------------------------------------------
	void MoveDownStart()
	{
		state &= ~BP_MSF_UP;
		state |= BP_MSF_MOVING;
		SetPersistentBit(true);
		lerpTime = 0.0f;
		delayTime = returnDelay;
		SetHeights(targetSectorHeight, targetActorHeight, targetWaterHeight, targetArgHeight);
		SetRotation(targetYaw);
		if (returnDelay <= 0.0f)
		{
			OnDownPostDelay();
		}
	}
	//------------------------------------------------------------------------------------------------------------------------
	void MoveDownFinish()
	{
		state &= ~(BP_MSF_UP|BP_MSF_MOVING);
		SetPersistentBit(true);
		SetHeights(startSectorHeight, startActorHeight, startWaterHeight, startArgHeight);
		SetRotation(0.0f);
		BlockArea();
		SetWaterArea(false);
		SetDamageArea(false);
		self.StopSound();
		if (flags & BP_MF_PERPETUAL != 0)
		{
			MoveUpStart();
		}
	}
	//------------------------------------------------------------------------------------------------------------------------
	void SetPersistentBit(bool clear)
	{
		if (flags & BP_MF_NOPERSIST == 0)
		{
			self.MarkPersistentBit(clear);
		}
	}
	//------------------------------------------------------------------------------------------------------------------------
	void SetRotation(float angle)
	{
		self.Yaw() = Math::Deg2Rad(startYaw + angle);
	}
	//------------------------------------------------------------------------------------------------------------------------
	void SetHeights(float sectorHeight, float actorHeight, float waterHeight, float argHeight)
	{
		if (flags & BP_MF_ACTOR_HEIGHT != 0)
		{
			self.Origin().z = actorHeight;
		}
		
		int sectorID = self.SectorIndex();
		if (flags & BP_MF_SECTOR_HEIGHT != 0 && sectorID >= 0)
		{
			World.ChangeSectorHeight(sectorID, sectorHeight);
		}
		
		int areaID = self.AreaID();
		if (areaID >= 0)
		{
			if (flags & BP_MF_WATER_HEIGHT != 0)
			{
				World.ChangeAreaWaterHeight(areaID, waterHeight);
			}
			
			if (flags & BP_MF_AREAARG != 0)
			{
				World.ChangeAreaArg(areaID, 5, int16(argHeight));
			}
		}
		
		// if (self.TID() == 9999)
		// {
			// Game.PrintLine("waterHeight: " + waterHeight + " argHeight: " + argHeight, 0);
		// }
				
	}
	//------------------------------------------------------------------------------------------------------------------------
    void OnTick()
    {
		if (IsSyncing())
		{
			Sync();
		}

		// if (syncMover !is null && self.GameTicks() % 20 == 0)
		// {
			// syncMover.self.RunFxEvent("Mantis_GlowBlue");
		// }

		if (delayTime > 0.0f)
		{
			delayTime -= GAME_DELTA_TIME;
			if (delayTime <= 0.0f)
			{
				if (state & BP_MSF_UP != 0)
				{
					OnUpPostDelay();
				}
				else
				{
					OnDownPostDelay();
				}
			}
			return;
		}
		
		//if moving and can move
		if (state & BP_MSF_MOVING != 0 && moveTime > 0.0f)
		{
			lerpTime += GAME_DELTA_TIME;
			float time = lerpTime / moveTime;
			float sectorHeight;
			float actorHeight;
			float waterHeight;
			float argHeight;
			float yaw;
			if (state & BP_MSF_UP != 0)
			{
				sectorHeight = Math::SmoothStepBP(startSectorHeight, targetSectorHeight, time);
				actorHeight = Math::SmoothStepBP(startActorHeight, targetActorHeight, time);
				waterHeight = Math::SmoothStepBP(startWaterHeight, targetWaterHeight, time);
				argHeight = Math::SmoothStepBP(startArgHeight, targetArgHeight, time);
				yaw = Math::SmoothStepBP(0.0f, targetYaw, time);
			}
			else
			{
				sectorHeight = Math::SmoothStepBP(targetSectorHeight, startSectorHeight, time);
				actorHeight = Math::SmoothStepBP(targetActorHeight, startActorHeight, time);
				waterHeight = Math::SmoothStepBP(targetWaterHeight, startWaterHeight, time);
				argHeight = Math::SmoothStepBP(targetArgHeight, startArgHeight, time);
				yaw = Math::SmoothStepBP(targetYaw, 0.0f, time);
			}
			SetHeights(sectorHeight, actorHeight, waterHeight, argHeight);
			SetRotation(yaw);
			if (time >= 1.0f)
			{
				if (state & BP_MSF_UP != 0)
				{
					MoveUpFinish();
				}
				else
				{
					MoveDownFinish();
				}
			}
		}
    }
	//------------------------------------------------------------------------------------------------------------------------
}
//============================================================================================================================
class TurokFloorMover : ScriptObject
{
    kActor  @self;
    float   distance;
    float   delayTime;
    float   currentTime;
    float   lerpTime;
    float   moveSpeed;
    float   diffHeight;
    float   destHeight;
    float   moveAmount;
    bool    bPlaySound;
    uint    stateFlags;
    
    TurokFloorMover(kActor @actor)
    {
        @self = actor;
        distance = 0;
        delayTime = 0;
        currentTime = 0;
        lerpTime = 0;
        moveSpeed = 0;
        destHeight = 0;
        moveAmount = 0;
        stateFlags = 0;
        bPlaySound = false;
    }
    
    /*
    ==============================================================
    SetMoveProperties
    ==============================================================
    */
    
    void SetMoveProperties(void)
    {
        if(self.SpawnParams(4) > 0)
        {
            moveSpeed = float(self.SpawnParams(4));
        }
        else
        {
            moveSpeed = 1.0f;
        }
        
        switch(self.Type())
        {
        case AT_DYNAMIC_FLOOR_1X:
        case AT_DYNAMIC_FLOOR_PERPETUAL:
            moveAmount = float(self.SpawnParams(7)) * GAME_SCALE + destHeight;
            break;
            
        default:
            moveAmount = float(self.SpawnParams(7) * 5) * GAME_SCALE + destHeight;
            break;
        }
    }
    
    /*
    ==============================================================
    ChangeAreaType
    ==============================================================
    */
    
    void ChangeAreaType(void)
    {
        if(self.SectorIndex() <= -1)
        {
            return;
        }
        
        int flag = (self.SpawnFlags2() >> 28) & 1;
        
        if(self.GetFloorHeight() >= self.StepHeight())
        {
            World.ChangeAreaFlag(self.AreaID(), (flag != 0) ? AAF_TELEPORT : AAF_DAMAGE, false);
        }
        else
        {
            World.ChangeAreaFlag(self.AreaID(), (flag != 0) ? AAF_TELEPORT : AAF_DAMAGE, true);
        }
    }
    
    /*
    ==============================================================
    MoveFloor
    ==============================================================
    */
    
    void MoveFloor(void)
    {
        float t = (Math::Cos((1.0f - lerpTime) * Math::pi) + 1.0f) * 0.5f;
        
        self.Origin().z = (moveAmount - destHeight) * t + destHeight;
		if (self.Type() != 6061)
		{
			World.ChangeSectorHeight(self.SectorIndex(), self.Origin().z - destHeight + distance);
		}
    }
    
    /*
    ==============================================================
    LowerFloor
    ==============================================================
    */
    
    void LowerFloor(void)
    {
        int sound;
        
        SetMoveProperties();
        
        sound = self.SpawnParams(0);
        
        if(sound > 0 && lerpTime < 1.0f)
        {
            if(sound != 561 || lerpTime <= 0.75f)
            {
                bPlaySound = false;
            }
            else
            {
                sound = 0;
                
                if(!bPlaySound)
                {
                    bPlaySound = true;
                    self.PlaySound("sounds/shaders/generic_177.ksnd");
                }
            }
            
            if(sound > 0)
            {
                self.PlaySoundWithLookupID(sound);
            }
        }
        
        float amt = GAME_DELTA_TIME / moveSpeed;
        lerpTime += amt;
        
        if(lerpTime >= 1.0f)
        {
            stateFlags &= ~TFF_MOVING;
            stateFlags |= TFF_WAITING;
            lerpTime = 1.0f;
            delayTime = float(self.SpawnParams(2) * 15);
            
            self.StopSound();
        }
        
        MoveFloor();
    }
    
    /*
    ==============================================================
    RaiseFloor
    ==============================================================
    */
    
    void RaiseFloor(void)
    {
        int sound;
        
        SetMoveProperties();
        
        sound = self.SpawnParams(0);
        
        if(sound > 0)
        {
            if(sound != 561 || lerpTime <= 0.75f)
            {
                bPlaySound = false;
            }
            else
            {
                sound = 0;
                
                if(!bPlaySound)
                {
                    bPlaySound = true;
                    self.PlaySound("sounds/shaders/generic_177.ksnd");
                }
            }
            
            if(sound > 0)
            {
                self.PlaySoundWithLookupID(sound);
            }
        }
        
        float amt = GAME_DELTA_TIME / moveSpeed;
        lerpTime -= amt;
        
        if(lerpTime <= 0.0f)
        {
            stateFlags &= ~TFF_ACTIVATED;
            self.Flags() &= ~AF_ACTIVATED;
            lerpTime = 0.0f;
            
            // clear persistant data
            self.MarkPersistentBit(true);
            
            // stop looping sounds
            self.StopSound();
        }
        
        MoveFloor();
    }
    
    /*
    ==============================================================
    DelayWait
    ==============================================================
    */
    
    void DelayWait(void)
    {
        if(delayTime <= 0.0f)
        {
            return;
        }
        
        delayTime -= GAME_FRAME_TIME;
            
        if(delayTime <= 0.0f)
        {
            delayTime = 0;
            stateFlags &= ~TFF_WAITING;
        }
    }
    
    /*
    ==============================================================
    OnBeginLevel
    ==============================================================
    */
    
    void OnBeginLevel(void)
    {
        self.Flags() &= ~(AF_HIDDEN|AF_DISABLED);
    }
    
    /*
    ==============================================================
    OnSpawn
    ==============================================================
    */
    
    void OnSpawn(void)
    {
    }
    
    /*
    ==============================================================
    OnTick
    ==============================================================
    */
    
    void OnTick(void)
    {
    }
    
    /*
    ==============================================================
    OnRestore
    ==============================================================
    */
    
    void OnRestore(void)
    {
        stateFlags |= TFF_ACTIVATED|TFF_MOVING;
        lerpTime = 1.0f;
        destHeight = self.Origin().z;
        distance = self.FloorHeight();
    }
};

//-----------------------------------------------------------------------------
//
// LowerOnce Mover
//
//-----------------------------------------------------------------------------

/*
==============================================================
TurokFloorMoverLowerOnce
==============================================================
*/

class TurokFloorMoverLowerOnce : TurokFloorMover
{
    float initialAngle;
    
    TurokFloorMoverLowerOnce(kActor @actor)
    {
        super(actor);
        initialAngle = 0;
    }
    
    /*
    ==============================================================
    Rotate
    ==============================================================
    */
    
    void Rotate(void)
    {
        float diff;
        float oldYaw;
        float speed;
        
        oldYaw = self.Yaw();
        speed = float(self.SpawnParams(1)) * 2.0f;
        
        self.Yaw() = initialAngle - (self.Origin().z - destHeight) / GAME_SCALE * -(Math::pi * 2.0f) / speed;
        diff = self.Yaw() - oldYaw;
        
        int sec1 = Player.Actor().SectorIndex();
        int sec2 = self.SectorIndex();
        
        if(sec1 >= 0 && sec2 >= 0 && Player.Actor().AreaID() == self.AreaID())
        {
            if(Player.Actor().OnGround())
            {
                // TODO
            }
        }
    }
    
    /*
    ==============================================================
    OnTick
    ==============================================================
    */
    
    void OnTick(void)
    {
        if(self.SectorIndex() <= -1)
        {
            return;
        }
        
        if((stateFlags & TFF_ACTIVATED) == 0)
        {
            return;
        }
        
        if((stateFlags & (TFF_MOVING|TFF_WAITING)) == (TFF_MOVING|TFF_WAITING))
        {
            int sec1 = Player.Actor().SectorIndex();
            int sec2 = self.SectorIndex();
            
            if(sec1 >= 0 && sec2 >= 0 && Player.Actor().AreaID() == self.AreaID())
            {
                delayTime -= GAME_FRAME_TIME;
                
                if(delayTime <= 0.0f)
                {
                    stateFlags &= ~TFF_ACTIVATED;
                }
            }
            else
            {
                stateFlags &= ~TFF_ACTIVATED;
            }
        }
        else
        {
            if((stateFlags & TFF_MOVING) != 0)
            {
                LowerFloor();
                
                if(self.Type() == AT_DYNAMIC_FLOOR_LOWER_AND_ROTATE)
                {
                    Rotate();
                }
            }
            else
            {
                if((stateFlags & TFF_WAITING) != 0)
                {
                    DelayWait();
                }
                else
                {
                    RaiseFloor();
                    
                    if(self.Type() == AT_DYNAMIC_FLOOR_LOWER_AND_ROTATE)
                    {
                        Rotate();
                    }
                    
                    if((stateFlags & TFF_ACTIVATED) == 0)
                    {
                        stateFlags |= (TFF_MOVING|TFF_WAITING|TFF_ACTIVATED);
                        delayTime = float(self.SpawnParams(2) * 15);
                    }
                }
            }
        }
    }
    
    /*
    ==============================================================
    OnActivate
    ==============================================================
    */
    
    void OnActivate(void)
    {
        if(self.SectorIndex() <= -1)
        {
            return;
        }
        
        if(self.SpawnParams(2) > 0)
        {
            self.Flags() &= ~AF_ACTIVATED;
        }
        
        if((stateFlags & TFF_ACTIVATED) == 0)
        {
            stateFlags &= ~TFF_WAITING;
            stateFlags |= (TFF_MOVING|TFF_ACTIVATED);
            
            // store persistant data as being started
            self.MarkPersistentBit(false);
            
            lerpTime        = 0.0f;
            currentTime     = 0.0f;
            initialAngle    = self.Yaw();
            destHeight      = self.Origin().z;
            distance        = self.FloorHeight();
            
            if((self.SpawnFlags1() & 0x40000000) != 0)
            {
                kActor @actor = ActorFactory.Spawn("QuakeSource", 0, 0, 0, 0, self.SectorIndex());
                TurokQuakeSource @quake;
        
                if(actor is null)
                {
                    return;
                }

                @quake = cast<TurokQuakeSource@>(actor.ScriptObject().obj);
                
                if(quake is null)
                {
                    return;
                }

                quake.SetupShake(self.Origin(), 1.125f * GAME_SCALE, -0.1048f, float(self.SpawnParams(4)));
            }
        }
    }
};

//-----------------------------------------------------------------------------
//
// LowerChangeWaitRaise Mover
//
//-----------------------------------------------------------------------------

/*
==============================================================
TurokFloorMoverLowerChangeWaitRaise
==============================================================
*/

class TurokFloorMoverLowerChangeWaitRaise : TurokFloorMover
{
    TurokFloorMoverLowerChangeWaitRaise(kActor @actor)
    {
        super(actor);
    }
    
    /*
    ==============================================================
    OnTick
    ==============================================================
    */
    
    void OnTick(void)
    {
        if(self.SectorIndex() <= -1)
        {
            return;
        }
        
        if((stateFlags & TFF_ACTIVATED) == 0)
        {
            return;
        }
        
        ChangeAreaType();
        
        switch(stateFlags & (TFF_MOVING|TFF_WAITING))
        {
        case TFF_WAITING:
            delayTime -= GAME_FRAME_TIME;
            if(delayTime <= 0.0f)
            {
                stateFlags &= ~TFF_WAITING;
                stateFlags |= TFF_MOVING;
            }
            break;
            
        case TFF_MOVING:
            LowerFloor();
            if((stateFlags & TFF_WAITING) != 0)
            {
                stateFlags |= (TFF_MOVING|TFF_WAITING);
                delayTime   = float(self.SpawnParams(1) * 15);
            }
            break;
            
        case (TFF_MOVING|TFF_WAITING):
            if(delayTime > 0.0f)
            {
                delayTime -= GAME_FRAME_TIME;
            }
            else
            {
                RaiseFloor();
                
                if((stateFlags & TFF_ACTIVATED) == 0)
                {
                    World.ChangeAreaFlag(self.AreaID(), AAF_EVENT, true);
                }
            }
            break;
        }
    }
    
    /*
    ==============================================================
    OnActivate
    ==============================================================
    */
    
    void OnActivate(void)
    {
        if(self.SectorIndex() <= -1)
        {
            return;
        }
        
        if((stateFlags & TFF_ACTIVATED) == 0)
        {
            stateFlags &= ~TFF_MOVING;
            stateFlags |= (TFF_ACTIVATED|TFF_WAITING);
            
            lerpTime    = 0.0f;
            currentTime = 0.0f;
            delayTime   = float(self.SpawnParams(2) * 15);
            destHeight  = self.Origin().z;
            distance    = self.FloorHeight();
        }
    }
};

//-----------------------------------------------------------------------------
//
// Perpetual Mover
//
//-----------------------------------------------------------------------------

/*
==============================================================
TurokFloorMoverPerpetual
==============================================================
*/

class TurokFloorMoverPerpetual : TurokFloorMover
{
    TurokFloorMoverPerpetual(kActor @actor)
    {
        super(actor);
    }
    
    /*
    ==============================================================
    OnTick
    ==============================================================
    */
    
    void OnTick(void)
    {
        if(self.SectorIndex() <= -1)
        {
            return;
        }
        
        if((stateFlags & TFF_ACTIVATED) == 0)
        {
            return;
        }
        
        if((stateFlags & TFF_MOVING) != 0)
        {
            LowerFloor();
            
            if((stateFlags & TFF_WAITING) != 0)
            {
                stateFlags &= ~TFF_WAITING;
            }
        }
        else
        {
            RaiseFloor();
            
            if((stateFlags & TFF_ACTIVATED) == 0)
            {
                stateFlags |= (TFF_MOVING|TFF_ACTIVATED);
            }
        }
    }
    
    /*
    ==============================================================
    OnActivate
    ==============================================================
    */
    
    void OnActivate(void)
    {
        if(self.SectorIndex() <= -1)
        {
            return;
        }
        
        if((stateFlags & TFF_ACTIVATED) == 0)
        {
            stateFlags &= ~TFF_WAITING;
            stateFlags |= (TFF_MOVING|TFF_ACTIVATED);
            
            lerpTime    = 0.0f;
            destHeight  = self.Origin().z;
            distance    = self.FloorHeight();
            
            self.MarkPersistentBit(false);
        }
    }
    
    /*
    ==============================================================
    OnRestore
    ==============================================================
    */
    
    void OnRestore(void)
    {
        self.Flags() |= AF_IGNORESOUNDEVENTS;
        
        stateFlags &= ~TFF_WAITING;
        stateFlags |= (TFF_MOVING|TFF_ACTIVATED);
        
        lerpTime = 0.0f;
        destHeight = self.Origin().z;
        distance = self.FloorHeight();
    }
};
