/*
**
*/
#if defined _AESTHETIC_included
 #endinput
#endif
#define _AESTHETIC_included
#include <basic>
#include <vscripts>
#define CELL_SIZE 128.0

methodmap Chess < Basic
{
	public Chess(int greenStart, int greenEnd, int purpleStart, int purpleEnd)
	{
		Basic myclass = new Basic();
		myclass.SetFloat("fGreenX", 2.0);
		myclass.SetFloat("fGreenY", 6.0);
		myclass.SetFloat("fPurpleX", 4.0);
		myclass.SetFloat("fPurpleY", 1.0);
		myclass.SetInt("iGreenStart", greenStart);
		myclass.SetInt("iGreenEnd", greenEnd);
		myclass.SetInt("iPurpleStart", purpleStart);
		myclass.SetInt("iPurpleEnd", purpleEnd);
		myclass.SetBool("bDead", false);
		return view_as<Chess>(myclass);
	}

	property bool bDead
	{
		public get()
		{
			return this.GetBool("bDead");
		}
		public set(bool val)
		{
			this.SetBool("bDead", val);
		}
	}

	property float greenX
	{
		public get()
		{
			return this.GetFloat("fGreenX");
		}
		public set(float val)
		{
			this.SetFloat("fGreenX", val);
		}
	}

	property float greenY
	{
		public get()
		{
			return this.GetFloat("fGreenY");
		}
		public set(float val)
		{
			this.SetFloat("fGreenY", val);
		}
	}

	property float purpleX
	{
		public get()
		{
			return this.GetFloat("fPurpleX");
		}
		public set(float val)
		{
			this.SetFloat("fPurpleX", val);
		}
	}

	property float purpleY
	{
		public get()
		{
			return this.GetFloat("fPurpleY");
		}
		public set(float val)
		{
			this.SetFloat("fPurpleY", val);
		}
	}

	property int greenStart
	{
		public get()
		{
			return this.GetInt("iGreenStart");
		}
	}

	property int greenEnd
	{
		public get()
		{
			return this.GetInt("iGreenEnd");
		}
	}

	property int purpleStart
	{
		public get()
		{
			return this.GetInt("iPurpleStart");
		}
	}

	property int purpleEnd
	{
		public get()
		{
			return this.GetInt("iPurpleEnd");
		}
	}

	public bool Check(float x, float y)
	{
		return (-1.0 < x && x < 8.0 && -1.0 < y && y < 8.0 && !((x == this.greenX && y == this.greenY) || (x == this.purpleX && y == this.purpleY)));
	}

	public void UpdateGreen(float dx, float dy)
	{
		this.greenX += dx;
		this.greenY += dy;
	}

	public void UpdateGreenEnd(float dx, float dy)
	{
		float currentPos[3];
		GetOrigin(this.greenEnd, currentPos);
		float moveVector[3] =  { 0.0, ... };
		moveVector[0] = dx * CELL_SIZE;
		moveVector[1] = dy * CELL_SIZE;
		float tmp[3];
		AddVectors(currentPos, moveVector, tmp);
		SetOrigin(this.greenEnd, tmp);
	}

	public void MoveGreen(float dx, float dy)
	{
		if (this.Check(this.greenX + dx, this.greenY + dy))
		{
			this.UpdateGreen(dx, dy);
			this.UpdateGreenEnd(dx, dy);
		}
	}

	public void MoveStartToEnd(int start, int end)
	{
		float e_Orig[3];
		GetOrigin(end, e_Orig);
		SetOrigin(start, e_Orig);
	}

	public void UpdatePurple(float dx, float dy)
	{
		this.purpleX += dx;
		this.purpleY += dy;
	}

	public void UpdatePurpleEnd(float dx, float dy)
	{
		float currentPos[3];
		GetOrigin(this.purpleEnd, currentPos);
		float moveVector[3] =  { 0.0, ... };
		moveVector[0] = dx * CELL_SIZE;
		moveVector[1] = dy * CELL_SIZE;
		float tmp[3];
		AddVectors(currentPos, moveVector, tmp);
		SetOrigin(this.purpleEnd, tmp);
	}

	public void MovePurple(float dx, float dy)
	{
		if (this.Check(this.purpleX + dx, this.purpleY + dy))
		{
			this.UpdatePurple(dx, dy);
			this.UpdatePurpleEnd(dx, dy);
		}
	}

	public void CheckInLove()
	{
		if(this.bDead)
			return;
		float dx = this.greenX - this.purpleX;
		float dy = this.greenY - this.purpleY;
		if (dx < 0.0) dx *= -1.0;
		if (dy < 0.0) dy *= -1.0;
		if (dx + dy == 1.0)
		{
			this.bDead = true;
			EntFire("TotalTrigger", "FireUser2", "", "0.0", -1);
		}
	}
}

const int DEFAULT_RETARGET_TICKS = 200;
const float DEFAULT_MAX_VEL = 16.0;
const float DEFAULT_MAX_ACC = 0.1;
const float DEFAULT_RANGE = 100000.0;
const float DEFAULT_PER_FRAME_MAX_VEL_DELTA = 0.0003333; // 2.0 / (120.0 * 50.0);
const float DEFAULT_PER_FRAME_MAX_ACC_DELTA = 0.0000125; // 0.075 / (120.0 * 50.0);
#define PI 3.14159

public bool SphereCollideWithWorld(const float[3] orig, const float radius, float degree, float[3] buffer, int entity)
{

	float inc = PI / degree;
	float  phiStop = PI - inc;

	float minDist = 1.0;
	for (float phi = 0.0; phi < PI; phi += inc)
	{
        // Avoid computing the full circle on the edge cases
        float theta = 0.0;
        if (phi == 0.0 || phi == phiStop)
            theta = PI - inc;

        float cp = Cosine(phi);
        float sp = Sine(phi);
        for (; theta < 2 * PI; theta += inc)
        {
            float ct = Cosine(theta);
            float st = Sine(theta);
            float vec[3];
            vec[0] = ct * sp;
            vec[1] = st * sp;
            vec[2] = cp;
            ScaleVector(vec, radius);

            float end[3];
            AddVectors(orig, vec, end);
            float dist = TraceLine(orig, end, entity);
            if ( 0 < dist && dist < minDist)
            {
            	ScaleVector(vec, dist);
            	AddVectors(orig, vec, buffer);
            	return true;
            }
        }
    }
    return false;
}

methodmap Eye < Basic
{
	public Eye(int entity)
	{
		Basic myclass = new Basic();
		myclass.SetInt("iEntity", entity);
		myclass.SetInt("iTarget", -1);
		myclass.SetFloat("fBossRadius", 72.0);
		myclass.SetInt("iBossTeamTarget", 3);
		myclass.SetInt("iRetargetTicks", 0);
		myclass.SetFloat("fMaxVel", DEFAULT_MAX_VEL);
		myclass.SetFloat("fMaxAcc", DEFAULT_MAX_ACC);
		myclass.SetVector("vVec", { 0.0, 0.0, 0.0 } );
		myclass.SetVector("vAcc", { 0.0, 0.0, 0.0 } );
		myclass.SetFloat("fSavedVel", 0.0);
		myclass.SetFloat("fSavedAcc", 0.0);
		return view_as<Eye>(myclass);
	}

	property int entity
	{
		public get()
		{
			return this.GetInt("iEntity");
		}
	}

	property int target
	{
		public get()
		{
			return this.GetInt("iTarget");
		}
		public set(int val)
		{
			this.SetInt("iTarget", val);
		}
	}

	property float bossRadius
	{
		public get()
		{
			return this.GetFloat("fBossRadius");
		}
		public set(float val)
		{
			this.SetFloat("fBossRadius", val);
		}
	}

	property int bossTeamTarget
	{
		public get()
		{
			return this.GetInt("iBossTeamTarget");
		}
		public set(int val)
		{
			this.SetInt("iBossTeamTarget", val);
		}
	}

	property int retargetTicks
	{
		public get()
		{
			return this.GetInt("iRetargetTicks");
		}
		public set(int val)
		{
			this.SetInt("iRetargetTicks", val);
		}
	}

	property float maxVel
	{
		public get()
		{
			return this.GetFloat("fMaxVel");
		}
		public set(float val)
		{
			this.SetFloat("fMaxVel", val);
		}
	}

	property float maxAcc
	{
		public get()
		{
			return this.GetFloat("fMaxAcc");
		}
		public set(float val)
		{
			this.SetFloat("fMaxAcc", val);
		}
	}

	property float savedVel
	{
		public get()
		{
			return this.GetFloat("fSavedVel");
		}
		public set(float val)
		{
			this.SetFloat("fSavedVel", val);
		}
	}

	property float savedAcc
	{
		public get()
		{
			return this.GetFloat("fSavedAcc");
		}
		public set(float val)
		{
			this.SetFloat("fSavedAcc", val);
		}
	}

	public void GetVel(float[3] vel)
	{
		this.GetVector("vVel", vel);
	}

	public void SetVel(const float[3] vel)
	{
		this.SetVector("vVel", vel);
	}

	public void GetAcc(float[3] acc)
	{
		this.GetVector("vAcc", acc);
	}

	public void SetAcc(const float[3] acc)
	{
		this.SetVector("vAcc", acc);
	}

	public void Retarget()
	{

    	// Start the gig
    	this.retargetTicks = 0;
    	int tempPlayer = -1;
    	int currentPlayer = -1;
    	int checkedPlayers = 0;
    	float distance = DEFAULT_RANGE;

    	// While there is no target or
    	// players found as potential targets aren't humans
    	while (checkedPlayers < MaxClients) {
    	    currentPlayer = FindEntityByClassname(currentPlayer, "player");

    	    // Only humans are valid
    	    if(currentPlayer != -1 &&
    	       IsPlayerAlive(currentPlayer) &&
    	       GetClientTeam(currentPlayer) == this.bossTeamTarget)
    	    {
    	        float tmp[3], s_orig[3], p_orig[3];
    	        GetOrigin(this.entity, s_orig);
    	        GetOrigin(currentPlayer, p_orig);
    	        SubtractVectors(s_orig, p_orig, tmp);
    	        float length = GetVectorLength(tmp);

    	        // Make sure we're close enough
    	        if (length < distance)
    	        {
    	            tempPlayer = currentPlayer;
    	            distance = length;
    	        }
    	    }
    	    // Make sure we don't stay here forever
    	    checkedPlayers++;
    	}

    	// Try the last bet if no player was found
    	this.target = tempPlayer;

	}

	public void Stop()
	{
    	// Save the values
    	this.savedVel = this.maxVel;
    	this.savedAcc = this.maxAcc;

    	// Block movement (with a bit of velocity to avoid spinning)
    	this.maxVel = 0.01;
    	this.maxAcc = 0.0001;
	}

	public void AllowMovement()
	{
    	// Set either default or saved vals
    	if (this.savedVel == 0.0) this.maxVel = DEFAULT_MAX_VEL;
    	else this.maxVel = this.savedVel;

    	if (this.savedAcc == 0.0) this.maxAcc = DEFAULT_MAX_ACC;
    	else this.maxAcc = this.savedAcc;

    	// Reset saved values
    	this.savedVel = 0.0;
    	this.savedAcc = 0.0;
	}

	public void FakePhysicsMovement()
	{

    	// Get the center and end point
    	float cnt[3];
    	GetOrigin(this.entity, cnt);

    	// Make the boss face the target
    	float tmp[3];
    	this.GetVel(tmp);
    	SetForwardVector(this.entity, tmp);

    	// If there is an impact, bounce
    	float collPoint[3];
    	if (SphereCollideWithWorld(cnt, this.bossRadius, 5.0, collPoint, this.entity))
    	{

    		float vec[3];
    		SubtractVectors(cnt, collPoint, vec);
    		float norm[3], norm_copy[3];
    		NormalizeVector(vec, norm);
    		norm_copy[0] = norm[0];
    		norm_copy[1] = norm[1];
    		norm_copy[2] = norm[2];
    		// Reflect velocity in a hacky way (not plane-accurate)
    		float nn[3];
    		NormalizeVector(norm, nn);
    		this.GetVel(tmp);
    		float dot = GetVectorDotProduct(tmp, nn);
    		ScaleVector(norm_copy, 2 * dot);
    		SubtractVectors(tmp, norm_copy, tmp);
    		this.SetVel(tmp);
    		EntFireByIndex(this.entity, "FireUser1", "", "0.0", this.target);
    		// Move as far as we can
    		ScaleVector(norm, this.bossRadius);
    		AddVectors(cnt, norm, tmp);
    		SetOrigin(this.entity, tmp);
    	}
    	else
    	{

    		this.GetVel(tmp);
    		AddVectors(cnt, tmp, tmp);
    		SetOrigin(this.entity, tmp);
    	}
    }

	public void Move()
	{

    	// If there is no target or the target is gone/dead, retarget
    	if (this.target == -1 ||
    	    !IsValidEntity(this.target) ||
    	    !IsPlayerAlive(this.target) ||
    	    this.retargetTicks++ >= DEFAULT_RETARGET_TICKS) this.Retarget();

    	// Perform the update with the data from the previous moment
    	float vel[3], tmp[3], acc[3];
    	this.GetVel(vel);
    	this.GetAcc(acc);
    	AddVectors(vel, acc, tmp);
    	this.SetVel(tmp);
    	float length = GetVectorLength(tmp);
    	if (length > this.maxVel)
    	{
    		ScaleVector(tmp, this.maxVel / length);
    		this.SetVel(tmp);
    	}
    	this.SetAcc( { 0.0, 0.0, 0.0 } );

    	// Apply the higher accuracy movement deltas
    	this.maxVel += DEFAULT_PER_FRAME_MAX_VEL_DELTA;
    	this.maxAcc += DEFAULT_PER_FRAME_MAX_ACC_DELTA;

    	// Try to move, finally
    	this.FakePhysicsMovement();

    	// Keep going if no target's there still
    	if (this.target == -1) return;

    	// Try moving towards our target if it exists, otherwise don't

    	float tgtPos[3];
    	GetOrigin(this.target, tgtPos);
    	float bssPos[3];
    	GetOrigin(this.entity, bssPos);
    	float dir[3];
    	SubtractVectors(tgtPos, bssPos, dir);

    	// Compute the length for later and normalize it if needed

    	length = GetVectorLength(dir);
    	if (length > this.maxVel)
    		ScaleVector(dir, this.maxVel / length);

    	// Compute the acceleration

    	this.GetVel(vel);
    	SubtractVectors(dir, vel, acc);
    	length = GetVectorLength(acc);
    	if (length > this.maxAcc)
    	{
    		ScaleVector(acc, this.maxAcc / length);
    		this.SetAcc(acc);
    	}

	}
}