/*
 * This file is avatar code for controlling a
 *	dragonfly.
 *
 * I actually stripped this out of a world I'm
 *  working on, so beware the comments and some of the
 *  code may be nonsensical with that surrounding
 *	context missing...
 *
 * -Brandyn
 */



if (context == remoteAvatarContext) {

//================== Config =========================
authorUrl = "http://sifter.org/~brandyn/"

debug = false;

//================== Utilities =========================

//------------------ Misc -------------------------

// More efficient to pre-build these:
gravityVec = Vector(0, -32, 0);
upVec      = Vector(0,   1, 0);
zeroVec    = Vector(0,   0, 0);

function random(min, max)
{
	return Math.random() * (max-min) + min;
}


//-------------------- Dragonfly ------------------

function initDragonfly()
{
	var w;

	dragonfly = mySubWorld;

	dragonfly.wings = [];

	for(w=0; w<4; w++)
		dragonfly.wings[w] = SubWorld("dragonflyWing"+(w+1)+".aer");

	dragonfly.yaw = 0;

	dragonfly.acceleration  = zeroVec;	// Unused
	dragonfly.velocity      = zeroVec;	// Unused
	dragonfly.acceleration2 = zeroVec;	// Rolling our own instead...
	dragonfly.velocity2     = zeroVec;

	// Init the wings:
	for (w=0; w<4; w++) {
		var wing;

		wing = dragonfly.wings[w];

		wing.add();	// Add it to the world

		wing.transparent = true;
		wing.transform   = dragonfly.transform;	// Starting location
	}


	/*
	 * This is the "AI" that decides where to move
	 *	the "control stick".  The answer is left
	 *	in the Vector this.jerk -- jerk is the
	 *	derivitive of the acceleration, similar
	 *	to the control stick in a helicopter.
	 *
	 * The "input", in addition to the current accel.,
	 *	velocity, and pos., (and delt) is this.targetPos (Vector)
	 *	which tells the	AI where we want to be.
	 */
	dragonfly.computeJerk = function(delt)
	{
		var amax, vmax, jmax, pmax;

		/*
		 * Our prefered maximum acceleration, velocity, and jerk,
		 *	as well as our maximum distance (pmax) to our target
		 *	beyond which we don't fly any faster to get there:
		 */
		amax  = 32.;
		vmax  = 20.;
		jmax  = amax;
		pmax  = 10.;

		/*
		 * To get a handle on an overall approach, let's
		 *	examine the "simple" case that we wish simply
		 *	to come to a stop.
		 *
		 * The obvious solution is, perhaps, to set J = -kV,
		 *	i.e., to move our control stick in the opposite
		 *	direction of motion.
		 *
		 * Unfortunately, solving the differential equation
		 *	which that implies tells us that V = sin(w*t)
		 *	(where w = sqrt(k)), or, in other words, we'll
		 *	never actually come to rest but instead will
		 *	keep overshooting.  This is classic first-order
		 *	behavior of someone trying to learn to fly a
		 *	helicopter...
		 *
		 * What we want instead is a nice exponential decay,
		 *	which, if we solve in the other direction to tell
		 *	us what J to use, has the counter-intuitive property
		 *	that:
		 *
		 *	J = k^2 * V
		 *
		 * I.e., here we are actually moving J *toward* the
		 *	direction of motion.  The reason this makes sense
		 *	is because it is assumed we are *already decelerating*
		 *	and so what we are doing with J is gently letting
		 *	off the breaks so that when we come to a stop, we
		 *	are also flying level.
		 *
		 * Given the above J, the full solution for V is:
		 *
		 *  V = m*exp(-kt) + n*exp(kt)
		 *
		 * I.e., if everything is set up properly at the start,
		 *	then n is zero and we come to rest.  Otherwise, we
		 *	miss the mark and the positive feedback of J = k^2 * V
		 *	sends us off into space.
		 *
		 * Note that the initial conditions, Vo and Ao, can be
		 *	expressed as:
		 *
		 * Vo = m+n, and Ao = k(n-m)
		 *
		 * Since our goal is to have n = 0, we want:
		 *
		 *	A = -kV
		 *
		 * in order for V to decay to 0.  So, we can add in
		 *	an error-correcting term:
		 *
		 *	J -= K(kV + A)   for some "correction rate" K > 0
		 *
		 * which will push A toward -kV (and will have no
		 *	additional effect once it's there).
		 *
		 * Combining the equations, we have
		 *
		 *  J = k(k-K)V - KA
		 *
		 * From which, in addition to k = -A/V, we can
		 *	infer an approximation to K from Jmax (the
		 *  maximum jerk we wish to allow):
		 *
		 * K = k + Jmax/Amax = Amax/Vmax + Jmax/Amax
		 *
		 * The result after substituting and simplifying is:
		 *
		 *  J = -Jmax(V/Vmax + A/Amax) - (Amax/Vmax)A
		 *
		 * This has the nice intuitive property of J being
		 *	negatively proportional to both V and A, each
		 *	(also intuitively) scaled by the inverse of their
		 *	maximums, and then the last term adds extra
		 *	negative-feedback on A which drains the energy
		 *	of the sin wave that would otherwise ensue.
		 *
		 * Note that if our target velocity were non-zero, the
		 *	only necessary substitution would be (V-Vt) for (V) in
		 *	the above equation.
		 *
		 * Of course, we can also write the above equation as:
		 *
		 *  J = -(Jmax/Vmax)V - (Jmax/Amax + Amax/Vmax)A
		 */
		this.jerk =
			this.velocity2.scale(-jmax/vmax).subtract(
			this.acceleration2.scale(jmax/amax + amax/vmax));

		/*
		 * Since we plan to come to a stop at targetPos, then it
		 *	would be nice to hit our target fairly accurately,
		 *	and not to over or under shoot.  So... how do
		 *	we come to a stop at just the right place?
		 *
		 * Rather than trying to solve a similar equation
		 *	for position as we did for velocity (third-order
		 *	differential equation == yucky!), we can simply
		 *	project forward in time and see where we would
		 *	end up if we allowed the above jerk to bring us
		 *	to a stop starting right now.
		 *
		 * Since we know V = m*exp(-k*t), we can call this
		 *	t=0, and V=Vo, so m = Vo, and the change in
		 *	position -- the integral of V from now until
		 *	infinity (remember we're coming to a stop)--is
		 *	simply:
		 *
		 *  P = Po + V/k, or, approximately: P = Po + V*Vmax/Amax
		 *
		 * (I threw in a factor of 1.5 because it was overshooting,
		 *	probably a cumulative effect of all the "approximately"s
		 *	strewn about.)
		 */
		var pos;

		pos = this.position.add(this.velocity2.scale(vmax/amax * 1.5));

		/* 
		 * Now "pos" is where we're going to end up if we
		 *	try to stop right now.  So, we can just subtract
		 *	that from our target position, and treat the
		 *	difference as a nudge to get us where we want
		 *	to be.
		 */
		delp = this.targetPos.subtract(pos);

		/*
		 * Beyond a certain distance, pmax, we only care
		 *	about the direction, not any additional distance:
		 */
		if (delp.length > pmax)
			delp = delp.scale(pmax/delp.length);

		/*
		 * Now let's crank the control to get us there:
		 */
		this.jerk = this.jerk.add(
			delp.scale(jmax/pmax));
	}


	/*
	 * The timestep function controls the
	 *	tilt of the dragonfly to fly us
	 *	toward this.targetPos.
	 */
	dragonfly.timestep = function(now, delt)
	{
		var thrust, w;

		/*
		 * Cap delt in case of studder:
		 */
		if (delt > .2)
			delt = .2;

		/*
		 * We'll enhance the physics with a little air
		 *	friction (this ultimately determines how
		 *	much we'll be tilted into the direction
		 *	of flight once we reach a steady speed):
		 */
		this.velocity2 = this.velocity2.scale(Math.pow(.8, delt));

		/*
		 * This is the "AI" that decides where to move
		 *	the "control stick".  The answer is left
		 *	in the Vector this.jerk -- jerk is the
		 *	derivitive of the acceleration.
		 *
		 * The "input", in addition to the current accel,
		 *	velocity, and pos, is this.targetPos (Vector)
		 *	which tells the AI where we want to be.
		 */
		this.computeJerk(delt);

		/*
		 * I'm doing my own physics here rather than using
		 *	the built in ones (acceleration, velocity) since
		 *	it's not clear whether the physics engine is running
		 *	off the same 'delt' as is used here (in the current,
		 *	beta version of Atmosphere, anyway) when the frame
		 *	times are varying from frame to frame, and the "AI"
		 *	might get confused by this.
		 */
		this.acceleration2 = this.acceleration2.add(this.jerk.         scale(delt));
		this.velocity2     = this.    velocity2.add(this.acceleration2.scale(delt));
		this.position      = this.     position.add(this.velocity2.    scale(delt));

		/*
		 * In reverse of supposed causation, we'll
		 *	update the body position to reflect the
		 *	acceleration and velocity:
		 *
		 * For starters, let's yaw into the wind:
		 */
		this.yaw -= this.velocity2.dot(this.orientation.mapAxis(0)) * delt;

		/*
		 * (If we're near enough our target position, let's
		 *	turn toward the target yaw)
		 */
		if (this.position.subtract(this.targetPos).length < 1) {

			/*
			 * Ick, what a mess for such a trivial thing.
			 * There's got to be a better way...
			 */
			var delyaw;

			while (this.yaw < 0)
				this.yaw += 3.14159*2.;
	
			this.yaw %= 3.14159*2.;

			delyaw = this.targetYaw - this.yaw;

			while (delyaw < -3.14159)
				delyaw += 3.14159*2.;

			if (delyaw > 3.14159)
				delyaw -= 3.14159*2.;

			if (delyaw > .5)
				delyaw = .5;
			else if (delyaw < -.5)
				delyaw = -.5;

			this.yaw += delyaw * delt;

			/*
			 * This hack bumps us around randomly so we
			 *	don't just hover perfectly still (which
			 *	looks a wee unnatural):
			 */
			if (this.position.subtract(this.targetPos).length < .05)
				this.targetPos = this.targetPosTrue.add(
						Vector(random(-.5,.5), random(0.,.5), random(-.5,.5)));
		}

		/*
		 * Next, note that our true relative acceleration must
		 *	include gravity (i.e., this is proportional to
		 *	the thrust vector our wings must be generating):
		 */
		thrust = this.acceleration2.subtract(gravityVec);

		/*
		 * When we have sound, we'll use this as the volume:
		 */
		this.soundVolume = (thrust.length - 22.)*(1./13.);
		if (this.soundVolume < 0)
			this.soundVolume = 0;
		if (this.soundVolume > 1)
			this.soundVolume = 1;

		/*
		 * Assuming our wings mostly generate lift straight
		 *	down (relative to our body), we need to tilt our
		 *	body so Up points in the thrust direction.
		 *
		 * So, what we want is the Rotation from straight
		 *	up to thrust (hmm.. there really should be a
		 *  built-in function to do this...):
		 */
		var cross;
		cross = upVec.cross(thrust.normalized);
		if (cross.length < 0.01)
			this.orientation = Rotation('y', this.yaw);
		else
			this.orientation = Rotation(cross, Math.asin(cross.length), 'y', this.yaw);

		/*
		 * This just flaps the wings after transforming
		 *	them to the dragonfly's body's transform:
		 */
		for (w=0; w<4; w++) {
			var wing, scaleFlap;
			wing = dragonfly.wings[w];

			//scaleFlap = thrust.length*(1./250.);
			scaleFlap = this.soundVolume * .2;

			if ((w-1)&2)
				scaleFlap = -scaleFlap;

			wing.transform = Transform(dragonfly.transform, Rotation('z', Math.cos(now*40)*scaleFlap));
		}
	}

	/*
	 * When the chat servers tells us to move...
	 */
	dragonfly.suggestLocation = function(loc) {

		var zaxis;

		this.targetPos     = loc.translation;
		this.targetPosTrue = loc.translation;
		// (Just so we can randomly permute targetPos without
		//  forgetting what the True targetPos is.)

		zaxis = loc.rotation.mapAxis(2);
		this.targetYaw = Math.atan2(zaxis.x, zaxis.z);
	}

	dragonfly.suggestLocation(dragonfly.transform);

	addAnimator(dragonfly);
}

initDragonfly();


} else {
	// Local-side script

	atmo = Button("Atmo Resource Page").add();
	atmo.onClick = function() { launchURL("http://sifter.org/atmo/"); }
}

