Back to Index Page generated: May 8, 2024, 6:16:03 AM

Expansion Deep Horizon Advanced Navigation Computer

Content

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Adds a primeable piece of equipment that improves interstellar navigation in the form of fuel savings. Adds a primeable piece of equipment that improves interstellar navigation in the form of fuel savings.
Identifier oolite.oxp.CmdCheyd.DH_AdvancedNavigationComputer oolite.oxp.CmdCheyd.DH_AdvancedNavigationComputer
Title Deep Horizon Advanced Navigation Computer Deep Horizon Advanced Navigation Computer
Category Equipment Equipment
Author Cmd. Cheyd, PhantorGorth Cmd. Cheyd, PhantorGorth
Version 1.0.5 1.0.5
Tags navigation navigation
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Information URL http://www.aegidian.org/bb/viewtopic.php?f=4&t=13526 n/a
Download URL https://wiki.alioth.net/img_auth.php/b/bd/Deep_Horizon_Advanced_Navigation_Computer.oxz n/a
License CC-BY-NC-SA 3.0 CC-BY-NC-SA 3.0
File Size n/a
Upload date 1626941204

Documentation

Also read http://wiki.alioth.net/index.php/Deep%20Horizon%20Advanced%20Navigation%20Computer

ReadMe.txt

======================================================
Deep Horizons - Advance Navigation Computer is an Add-on for Oolite by Giles Williams

Author: Cmd. Cheyd (Blake Deakins), Phantor Gorth (Paul Cooper)
Contributors: Svengali and Submersible, phkb (Nick Rogers)
Created on: 8/21/2011
Version: 1.0.3 (Release)
======================================================

Features:
Adds a prime-able piece of equipment that improves interstellar navigation in the form of fuel savings.

Installation:
Place the folder 'Deep Horizons - Advance Navigation Computer.oxp' inside the Oolite\AddOns folder.

Credits:
To Svengali for the original smooth orientation change function.
To submersible for rebuilding my original model for the Navigation Frame after I fubar'd it.
To the Oolite Community for feedback and inspiration.
=======================================================

IMPORTANT NOTE:
Some large, slow-moving ships (such as the Anaconda) may not be able to complete reorientation before completion of the normal hyperspace countdown timer.  This is a known issue.  We decided that accelerating the reorientation any more than current would ruin the illusion of size of such large vessels.  So instead, we are allowing this 'bug' to exist for this release, and may address it in a future release of this OXP.

Version 1.0.1: Updated to work with ANA navigation plotting introduced in 1.82. 
Version 1.0.2: A couple of small bug fixes for "item is null" errors. Adjusted function names to remove them from the global namespace.
Version 1.0.3: Jump markers were not being removed if jump countdown was aborted by the player in some situations.
Version 1.0.4: ANC will now disengage if auto-docking is started during a jump countdown.
Version 1.0.5: Making sure Far Point Markers will not have a pilot attached.

Equipment

Name Visible Cost [deci-credits] Tech-Level
Advanced Navigation Computer yes 18240 10+

Ships

Name
Farpoint Jump Marker

Models

This expansion declares no models.

Scripts

Path
Scripts/Deep_Horizon_Adv_Nav_Comp_Equipment.js
"use strict";
this.name = "Deep_Horizon_Adv_Nav_Comp_Equipment";
this.authors	= "Cmd. Cheyd (Blake Deakins) and PhantorGorth (Paul Cooper)";
this.contributors = "Svengali";
this.description = "Adds a prime-able piece of equipment that improves interstellar navigation in the form of fuel savings.";
this.version = "1.0.0";
this.copyright = "� Creative Commons Attribution-Noncommercial-ShareAlike 3.0 Unported license (Modified)";

//*************************************************************************************************************
//************************************           Event Functions           ************************************
//*************************************************************************************************************

this.activated = function () {
	// If there is already a coutdown running
	if (player.ship.status == "STATUS_WITCHSPACE_COUNTDOWN") {
		// Was the countdown triggered by DH-ANC?
		if (worldScripts["Deep_Horizon_Adv_Nav_Comp"].usedANC == true) {
			// Yes - Cancel the ANC countdown with notice and end processing.
			worldScripts["Deep_Horizon_Adv_Nav_Comp"]._cancelANC(false);
			return;
		} else {
			// No - use the ANC cancellation function to kill the countdown silently.
			worldScripts["Deep_Horizon_Adv_Nav_Comp"]._cancelANC(true);
			player.consoleMessage("Prior hyperspace initiation cancelled by Adv. Nav Computer calcuations.", 4);
		}
	}
	// No countdown previously running - Set flag that ANC is being used and activate the equipment via the world script
	worldScripts["Deep_Horizon_Adv_Nav_Comp"].usedANC = true;
	worldScripts["Deep_Horizon_Adv_Nav_Comp"]._ancActivated();
}

this.equipmentDestroyed = this.equipmentDamanged = function (equipment) {
	// Let the world script handle.
	worldScripts["Deep_Horizon_Adv_Nav_Comp"]._ancDamaged();
}


Scripts/Deep_Horizon_Adv_Nav_Comp_World.js
"use strict";
this.name = "Deep_Horizon_Adv_Nav_Comp";
this.authors	= "Cmd. Cheyd (Blake Deakins) and PhantorGorth (Paul Cooper)";
this.contributors = "Svengali, Nick Rogers";
this.description = "Adds a prime-able piece of equipment that improves interstellar navigation in the form of fuel savings.";
this.copyright = "� Creative Commons Attribution-Noncommercial-ShareAlike 3.0 Unported license (Modified)";

/*
	***************************************************************************************************************
	**  A significant portion of the code contained herein was based on code originally written and contributed by
	**	Svengali.  Without his contribution, this OXP would not have been possible.  Thanks Olli!
	***************************************************************************************************************
*/

//*************************************************************************************************************
//************************************         Variable Declaration        ************************************
//*************************************************************************************************************
this.$jumpDistance = 0;
this.usedANC = false;

this.$lastSource = -1;

//*************************************************************************************************************
//************************************           Event Functions           ************************************
//*************************************************************************************************************
this.startUp = function() {
	// get instance
	this.myFCB = new CheydsReorient();
	this.systemDone = false;
	this.loopHere = false;
}

this.shipWillLaunchFromStation = function () {
	this._systemSetup();
	if (this.systemDone) return;
	this.loopHere = false;
	this.entryVectorCorrect = false;
	this.$lastSource = system.ID;
}

this.shipWillEnterWitchspace = function(cause) {
	this.systemDone = false;
	this.myFCB.fcbRemove(1);
	if (cause == "wormhole") {
		this.playerCancelledJumpCountdown();
		return;
	} else if (cause =="standard jump" && this.usedANC) {
		// Calculate distance about to be jumped.  Use this after the jump for fuel waste calculations
		var playerTarget = this._playerTargetSystem();
		this.$jumpDistance = Number(System.infoForSystem(galaxyNumber,system.ID).distanceToSystem(System.infoForSystem(galaxyNumber,playerTarget)).toFixed(1));
		this.entryVector = player.ship.heading.angleTo(this.$jumpMarker.position.subtract(player.ship.position).direction());
	}
}

this.shipExitedWitchspace = function() {
	if (system.ID != -1) this.$lastSource = system.ID;
	// If the ANC was used, calculate distance travelled and reward fuel savings.
	this._systemSetup();
	this.loopHere = false;
	if (!this.usedANC) return;
	this.usedANC = false;
	if ((this.$jumpDistance > 1.0) && this.entryVectorCorrect) {
		var fuelSaved = Number((this.$jumpDistance*0.1).toFixed(1));
		player.ship.fuel += fuelSaved;
		player.consoleMessage("Fuel savings due to optimized routing: " + fuelSaved, 4);
		this.entryVectorCorrect = false;
	} else if (this.$jumpDistance < 1.0) {
		player.consoleMessage("Jump distance was too short to achieve appreciable fuel savings.",4);
	} else {
		player.consoleMessage("Entry vector across witchspace event horizon meniscus too far off calculated.  Fuel savings negated.",4);
	}
}

this.shipWillExitWitchspace = function () {
	if (this.entryVector < 0.010) this.entryVectorCorrect = true;
	this._stopNavFrameCallback();
	if (this.$navFrameVE) this.$navFrameVE.remove();
	if (this.$navStarVE) this.$navStarVE.remove();
	if (this.navigationTimer) {
		this.navigationTimer.stop();
		delete this.navigationTimer;
	}
}

this.shipWillDockWithStation = this.shipDied = function() {
	// cleaning up to avoid trouble
	this.usedANC = false;
	this._stopNavFrameCallback();
	delete this.navFrameCallbackID;
	if (this.$navFrameVE) this.$navFrameVE.remove();
	if (this.$navStarVE) this.$navStarVE.remove();
	if (this.myFCB) this.myFCB.fcbRemove(1);
	if (this.navigationTimer) {
		this.navigationTimer.stop();
		delete this.navigationTimer;
	}
	this.loopHere = false;
}

this.playerStartedJumpCountdown = function(type, seconds) {
	if (!this.usedANC) {
		if (type === "standard") {
			if (worldScripts["Deep_Horizon_Emer_Jump_Initiator"]) {
				if (worldScripts["Deep_Horizon_Emer_Jump_Initiator"].$usedEWI) return;
				else {
					this._spawnDHINavVEs();
					this._reorientPlayer();
				}
			} else {
				this._spawnDHINavVEs();
				this._reorientPlayer();
			}
		}
	} else {
		if (this.navigationTimer && this.loopHere) {
			this._cancelANC(false);
			this.loopHere = false;
		}
		if (type == "standard" && seconds == player.ship.hyperspaceSpinTime*2) this.loopHere = true;
		else if (type == "standard" && seconds != player.ship.hyperspaceSpinTime*2) {
			this._cancelANC(false);
			this._reorientPlayer();
		}
	}
}

this.playerCancelledJumpCountdown = this.playerStartedAutoPilot = function() {
	//If the ANC is running, kill it.
	if (this.usedANC) this._cancelANC(true);
	else {
		this.usedANC = false;
		this._stopNavFrameCallback();
		if (this.$navFrameVE) this.$navFrameVE.remove();
		if (this.$navStarVE) this.$navStarVE.remove();
		if (this.myFCB) this.myFCB.fcbRemove(1);
		if (this.navigationTimer) {
			this.navigationTimer.stop();
			delete this.navigationTimer;
		}
		this.loopHere = false;
	}
}

this.playerJumpFailed = function(reason) {
	//If the ANC is running, kill it.
	this.usedANC = false;
	this._stopNavFrameCallback();
	delete this.navFrameCallbackID;
	if (this.$navFrameVE) this.$navFrameVE.remove();
	if (this.$navStarVE) this.$navStarVE.remove();
	if (this.myFCB) this.myFCB.fcbRemove(1);
	if (this.navigationTimer) {
		this.navigationTimer.stop();
		delete this.navigationTimer;
	}
	this.loopHere = false;
}

//*************************************************************************************************************
//************************************            OXP Functions            ************************************
//*************************************************************************************************************

//-------------------------------------------------------------------------------------------------------------
// returns the player's target system (1.80) or the next jump to their target system (1.82)
this._playerTargetSystem = function() {
	if (player.ship.hasOwnProperty("nextSystem")) return player.ship.nextSystem;

	var p = player.ship;
	var target = p.targetSystem;

	var last = system.ID;
	if (system.ID == -1) {
		last = this.$lastSource;
	}

	if (oolite.compareVersion("1.81") < 0) {
		// in 1.81 or greater, the target system could be more than 7 ly away. It becomes, essentially, the final destination.
		// there could be multiple interim stop points between the current system and the target system.
		// the only way to get this info is to recreate a route using the same logic as entered on the ANA, and pick item 1
		// from the list. That should be the next destination in the list.
		var myRoute = System.infoForSystem(global.galaxyNumber, last).routeToSystem(System.infoForSystem(global.galaxyNumber, target), p.routeMode);
		if (myRoute) {
			target = myRoute.route[1];
		}
	}

	return target;
}


this._ancActivated = function () {
	var playerTarget = this._playerTargetSystem();
	if (!playerTarget || playerTarget == system.ID) {
		// Local system is selected.  Halt.
		player.consoleMessage("Local system selected.  Navigation calculations unnecessary.  Shutting down.", 4);
		return;
	}
	if (System.infoForSystem(galaxyNumber,system.ID).distanceToSystem(System.infoForSystem(galaxyNumber,playerTarget)) > player.ship.fuel) {
		// Insufficient Fuel.  Halt.
		player.consoleMessage("Insufficient fuel to reach destination system.  Navigation calculations unnecessary.  Shutting down.", 4);
		return;
	}
	if (!this.navigationTimer) {
		// Proceed with ANC-style long-count jump.
		this._spawnDHINavVEs();
		//Pause to simulate calculations...
		var delay = Math.floor((system.scrambledPseudoRandomNumber (24784 + playerTarget)*100)/33);
		this.navigationTimer = new Timer(this, this._reorientPlayer, 3+delay);
	} else {
		// Cancel the ANC.
		this._cancelANC(false);
	}
	return;
}

this._initiateJump = function () {
	var self = worldScripts["Deep_Horizon_Adv_Nav_Comp"];
	if (player.ship.status === "STATUS_IN_FLIGHT") {
		self.usedANC = true;
		player.ship.beginHyperspaceCountdown(player.ship.hyperspaceSpinTime*2);
	} else self.usedANC = false;
}

this._spawnDHINavVEs = function () {
	// Select farpointMarker that matches the destination system.
	var playerTarget = this._playerTargetSystem();
	for (var i = this.$farpointMarkers.length; i > 0; i--) {
		if (playerTarget == this.$farpointMarkers[i-1].systemID) {
			this.$jumpMarker = this.$farpointMarkers[i-1]
		}
	}
	player.consoleMessage("Locking onto " + System.systemNameForID(playerTarget) + " system witchpoint nav beacon.", 4);
	// Spawn Visual Effect and store reference to it
	this.$navFrameVE = system.addVisualEffect("deephorizon_navframe",this._vectoredPositionToTarget(this.$jumpMarker.position,player.ship.collisionRadius + 5000));
	this.$navStarVE = system.addVisualEffect("deephorizon_navstar",this._vectoredPositionToTarget(this.$jumpMarker.position,player.ship.collisionRadius + 50000));
	// Orient the VE toward the player ship
	this.$navFrameVE.scale(0.66);
	this.$navStarVE.scale(3.30-this.$jumpMarker.distanceToSystem*.27);
	var tex = this.$jumpMarker.systemID%4;
	switch (tex) {
		case 1:
			this.$navStarVE.setMaterials({"dhi_navstar.png":{"textures":["dhi_navstar2.png"],"fragment_shader":"dhi_anc_jumpstar.fragment","emission_map":"dhi_navstar2.png","uniforms":{"uColorMap":{"type":"texture","value":"0"},"uSpecIntensity":"shaderFloat1","uSpecColor":"shaderVector1"},"vertex_shader":"dhi_anc_jumpstar.vertex"}});
			break;
		case 2:
			this.$navStarVE.setMaterials({"dhi_navstar.png":{"textures":["dhi_navstar3.png"],"fragment_shader":"dhi_anc_jumpstar.fragment","emission_map":"dhi_navstar3.png","uniforms":{"uColorMap":{"type":"texture","value":"0"},"uSpecIntensity":"shaderFloat1","uSpecColor":"shaderVector1"},"vertex_shader":"dhi_anc_jumpstar.vertex"}});
			break;
		case 3:
			this.$navStarVE.setMaterials({"dhi_navstar.png":{"textures":["dhi_navstar4.png"],"fragment_shader":"dhi_anc_jumpstar.fragment","emission_map":"dhi_navstar4.png","uniforms":{"uColorMap":{"type":"texture","value":"0"},"uSpecIntensity":"shaderFloat1","uSpecColor":"shaderVector1"},"vertex_shader":"dhi_anc_jumpstar.vertex"}});
			break;
	}
	this._reorientVEToPlayer (this.$navFrameVE);
	this._reorientVEToPlayer (this.$navStarVE);
	this.navFrameCallbackID = addFrameCallback(this._positionNavVisualEffects.bind(this));
}

this._positionNavVisualEffects = function () {
	this._reorientVEToPlayer (this.$navFrameVE);
	this._reorientVEToPlayer (this.$navStarVE);
	this.$navFrameVE.position = this._vectoredPositionToTarget(this.$jumpMarker.position,player.ship.collisionRadius + 5000);
	this.$navStarVE.position = this._vectoredPositionToTarget(this.$jumpMarker.position,player.ship.collisionRadius + 10000 );
	this.$navStarVE.shaderVector1 = this.$jumpMarker.uSpecColor;
	this.$navStarVE.shaderFloat1 = this.$jumpMarker.uSpecIntensity;
	return;
}

this._stopNavFrameCallback = function() {
	if(!this.navFrameCallbackID) return;
	removeFrameCallback(this.navFrameCallbackID);
	delete this.navFrameCallbackID;
	return;
};

this._reorientPlayer = function _reorientPlayer() {
	//Calculation of the roll and Pitch accelerations and decelerations based on variations from a Cobra Mk3.
	var rollAcc = (0.2) * (player.ship.maxThrust / 30) / (player.ship.mass / 185580.015625) / ((player.ship.boundingBox.x + player.ship.boundingBox.y) / 160);
	if (rollAcc > 1) rollAcc = 1;
	else if (rollAcc < 0.28) rollAcc = 0.28;
	var pitchAcc = rollAcc * (160/95) * ((player.ship.boundingBox.x + player.ship.boundingBox.y) / 160) / ((player.ship.boundingBox.y + player.ship.boundingBox.z) / 95);
	if (pitchAcc > 1) pitchAcc = 1;
	else if (pitchAcc < 0.255) pitchAcc = 0.255;
	var rollDec = rollAcc / 2;
	var pitchDec = pitchAcc / 2;
	//Turn the player to the appropriate buoy for the selected system.ID
	this.myFCB.fcbFace(player.ship,[[1,this.$jumpMarker],[0,this.$jumpMarker]],0,[rollAcc,pitchAcc],[rollDec,pitchDec],this._initiateJump);
	player.consoleMessage (System.systemNameForID(this._playerTargetSystem()) + " system beacon acquired.  Reorienting ship for vectored system departure\n",4);
	return;
}

this._reorientVEToPlayer = function (navVE) {
	var orient = this._lookAtRotate (player.ship.position.subtract(navVE.position),player.ship.orientation.vectorUp());
	if (orient === null) orient = this._lookAtRotate(player.ship.position.subtract(navVE.position),player.ship.orientation.vectorForward().multiply(-1));
	navVE.orientation = orient;
}


this._orthoNormalise = function(a, b)
{
	//Returns a normalised vector that is in the plane of "ab" and is at 90� to "a" in the same half plane as "b".
	var a2 = a.direction();
	var b2 = b.direction();
	var c = a.cross(b).cross(a);

	return c.direction();
}

this._lookAtRotateEuler = function(theta, phi, psi)
{
	//Returns a Quaternion that matches a rotational transformation that is described by the Euler angles.
	//Uses Y axis at the basis axis for theta and the Z axis is used to measure phi from.
	var q = new Quaternion(1, 0, 0, 0);

	var theta2 = (Math.PI/2) - theta;

	q = q.rotateZ(psi);
	q = q.rotateX(theta2);
	q = q.rotateY(phi);

	return q;
}

this._lookAtRotate = function(forward, up)
{
	//returns an orientation quaternion given a Forward vector and an Up vector (the Forward and Up do not need to be
	//orthonormal but they can not be parallel or anti-parallel.)
	if (forward.direction().cross(up.direction()).magnitude() < 0.01) return null; // Return null if Forward and Up are parallel or anti-parallel or nearly so.
	var f = forward.direction();
	var u = this._orthoNormalise(f, up);

	var v = new Vector3D(0,1,0); //Uses Y axis at the basis axis for theta and the Z axis is used to measure phi from for finding the Euler angles.
	var u2 = this._orthoNormalise(f, v);

	var sign = -u2.cross(u).dot(f);
	var sign = sign && sign / Math.abs(sign);

	var psi = sign * u2.angleTo(u);

	var z = new Vector3D(0,0,1);
	var h = new Vector3D(f.x,0,f.z);

	sign = -z.cross(h).y;
	sign = sign && sign / Math.abs(sign);

	var phi = sign * z.angleTo(h);

	sign = f.y;
	sign = sign && sign / Math.abs(sign);

	var theta = (Math.PI/2) - (sign * h.angleTo(f));

	return this._lookAtRotateEuler(theta, phi, psi);
}

this._vectoredPositionToTarget = function (targetPosVector, distance) {
	var v = targetPosVector.subtract(player.ship.position).direction();
	v = v.multiply(distance);
	v = v.add(player.ship.position);
	return v;
}

this._cancelANC = function(silent) {
	if (!silent) player.consoleMessage("Advanced Navigation calculations cancelled.", 4);
	this._stopNavFrameCallback();
	if (this.$navFrameVE) this.$navFrameVE.remove();
	if (this.$navStarVE) this.$navStarVE.remove();
	if (this.navigationTimer) {
		this.navigationTimer.stop();
		delete this.navigationTimer;
	}
	player.ship.cancelHyperspaceCountdown();
	if (this.myFCB) this.myFCB.fcbRemove(1);
	this.loopHere = false;
	this.usedANC = false;
	return;
}

this._ancDamaged = function() {
	if ((equipment == "EQ_ADV_NAV_COMP" || equipment == "EQ_ADVANCED_NAVIGATIONAL_ARRAY") && this.navigationTimer) {
		this._cancelANC(false);
	}
	return;
}

this._systemSetup = function () {
	this.$farpointMarkers = new Array();
	// Determine how many farpoint markers are needed
	this.$localSystems = System.infoForSystem(galaxyNumber,system.ID).systemsInRange(7);
	this.$spawnCount = this.$localSystems.length;
	// For each in-range system spawn a farpoint marker
	this.$farpointMarkers = system.addShips("DHI_farpoint_marker",this.$spawnCount,player.ship.position,5000000);
	//Assign a matching system.ID as an additional value to each buoy
	for (var i = this.$farpointMarkers.length; i > 0; i--) {
		var fpm = this.$farpointMarkers[i-1];
		fpm.systemID = this.$localSystems[i-1].systemID;
		fpm.galCoordinates = this.$localSystems[i-1].coordinates;
		fpm.name = this.$localSystems[i-1].name;
		fpm.distanceToSystem = System.infoForSystem(galaxyNumber,system.ID).distanceToSystem(this.$localSystems[i-1]);
		fpm.uSpecColor = this._selectColor(this.$localSystems[i-1].systemID,i);
		fpm.uSpecIntensity = 0.5;
		fpm.position = this.positionFarpointMarker(fpm,50000000);
	}
	this.systemDone = true;
	return;
}

//-------------------------------------------------------------------------------------------------------------
this.$anc_shipLaunchedEscapePod = function $anc_shipLaunchedEscapePod(pod, passengers) {
	pod.remove(true);
}

this._selectColor = function (sysID,i) {
	var color = System.infoForSystem(galaxyNumber,sysID).sun_color;
	if (typeof(color) === 'undefined') {
		var c = Math.floor(system.scrambledPseudoRandomNumber(785331-i)*7);
		switch (c) {
			case 0:		color = "magentaColor";	break;
			case 1:		color = "redColor"; 	break;
			case 2:		color = "orangeColor";	break;
			case 3:		color = "yellowColor";	break;
			case 4:		color = "whiteColor";	break;
			case 5:		color = "cyanColor";	break;
			default:	color = "blueColor";	break;
		}
	}
	switch (color) {
		case "magentaColor":	var colorVec = [1,0,1];	break;
		case "redColor":		var colorVec = [1,0,0];	break;
		case "orangeColor": 	var colorVec = [1,0.5,0];	break;
		case "yellowColor": 	var colorVec = [1,1,0];	break;
		case "whiteColor": 		var colorVec = [1,1,1];	break;
		case "cyanColor": 		var colorVec = [0,1,1];	break;
		case "blueColor": 		var colorVec = [0,0,1];	break;
		default:				colorVec = [1,1,1];
	}
	return colorVec;
}

this.positionFarpointMarker = function positionFarpointMarker(marker,distance)
{
	if (marker.galCoordinates == null) return null;

	var t = marker.galCoordinates;
	var s = System.infoForSystem(galaxyNumber, system.ID).coordinates;

	var tV = t.subtract(s);

	var u = system.scrambledPseudoRandomNumber(34567346);
	var v = system.scrambledPseudoRandomNumber(431976567);
	var w = system.scrambledPseudoRandomNumber(9834674);

	var theta = Math.acos(2 * u - 1);
	var phi = 2 * Math.PI * v;
	var psi = 2 * Math.PI * w;

	var v1 = new Vector3D(Math.sin(theta) * Math.sin(phi), Math.sin(theta) * Math.cos(phi), Math.cos(theta));
	var v2 = new Vector3D(0, 0, 1); //straight up
	var v3 = v2.cross(v1);
	v3 = v3.direction(); // normalize (should be normalized anyway)

	var q1 = new Quaternion(1,0,0,0);
	q1 = q1.rotate(v3, theta).normalize();
	var v4 = tV.rotateBy(q1).direction(); // rotate target vector tV so that the coordinate space has pole moved from straight up to v1 (theta, phi) (spherical coords)
	var q2 = new Quaternion(1,0,0,0);
	q2 = q2.rotate(v1, psi).normalize();
	v4 = v4.rotateBy(q2).direction(); // rotate about the new pole v1 by psi

	return v4.multiply(distance).add(player.ship.position);
}

this._anc_sum = function(arr) {
	var total = 0;
	for(var a = 0; a < arr.length; a++) { total += arr[a]; };
	return total
}

this._anc_time_taken = function(angle, max_ang_vel, dampa, dampb)
{
	var ret_val = [0, 0, 0];

	ret_val[0] = (angle / max_ang_vel) + ((dampa + dampb) * 0.5);
	ret_val[1] = dampa;
	ret_val[2] = dampb;

	if (ret_val[0] < (dampa + dampb))
	{
		var m = Math.sqrt((2 * angle)/(max_ang_vel * (dampa + dampb)));
		ret_val[0] = m * (dampa + dampb);
		ret_val[1] *= m;
		ret_val[2] *= m;
	}

	return ret_val;
}


//*************************************************************************************************************
//************************************            Lib Functions            ************************************
//*************************************************************************************************************
this.CheydsReorient = function CheydsReorient(){this._root = null;}
CheydsReorient.prototype = {
	constructor: CheydsReorient,
	cheyds_fcb: 0,
	cheyds_fcbs: [],
	cheyds_doAutoRemove: true,
	cheyds_autoRemove: function(mode){
		if(mode){
			if(!this.cheyds_autoRemoveTimer) this.cheyds_autoRemoveTimer = new Timer(this,this.fcbRemove,0,3);
			else if(!this.cheyds_autoRemoveTimer.isRunning) this.cheyds_autoRemoveTimer.start();
		} else if(this.cheyds_autoRemoveTimer) this.cheyds_autoRemoveTimer.stop();
	},

	// Face target
	//	ent			Entity. Required. To be reoriented.
	//	tar			Entity. Required. Target entity.
	//	recall		Boolean. Recall settings. If set ignores dampb and starts over when duration over.
	//	angAccA		Number. Angular acceleration array
	//	angDecA		Number. Angular deceleration array 
	fcbFace: function(ent,tarVA,recall,angAccA,angDecA,call_back){
		if(!ent || !ent.isValid) return;
		var croStep = 0,ccl_sum = 0,ccl_ent = ent,ccl_tarVA = tarVA,ccl_tar = tarVA[0],ccl_recall = recall,ccl_angAccA = angAccA,ccl_angDecA = angDecA,ccl_angAcc,ccl_angDec,ccl_dampa,ccl_dampb;
		var ccl_last, initialAngle, aimAngle, alpha, reset = 0, max_ang_vel = 0, targetVector;
		var cheyds_fcbs_ref, absSecs, totalSecs = 0;
		var finalCallbackCalled = false, ccl_callback = call_back;
		var lastOrientation, lastUpAngle;

		this.cheyds_fcb = addFrameCallback(function(delta){
			if(!delta || !ccl_ent || !ccl_ent.isValid) return;

			if (ccl_sum == 0) {

				lastOrientation = ccl_ent.orientation;
				lastUpAngle = ccl_ent.orientation.vectorUp().angleTo(worldScripts["Deep_Horizon_Adv_Nav_Comp"]._orthoNormalise(ccl_ent.heading, ccl_tar[1].position.subtract(ccl_ent.position)));

				if ((!ccl_recall) && (ccl_tarVA.length > 0) && (croStep < ccl_tarVA.length)) {
					ccl_tar = ccl_tarVA[croStep];
				}

				if (ccl_tar[0] == 0) {
					max_ang_vel = ccl_ent.maxPitch;
					targetVector = ccl_tar[1].position.subtract(ccl_ent.position).direction();
					if (reset == 0) initialAngle = ccl_ent.heading.angleTo(targetVector);
				}
				else {
					max_ang_vel = ccl_ent.maxRoll;
					targetVector = worldScripts["Deep_Horizon_Adv_Nav_Comp"]._orthoNormalise(ccl_ent.heading, ccl_tar[1].position.subtract(ccl_ent.position));
					if (reset == 0) initialAngle = ccl_ent.orientation.vectorUp().angleTo(targetVector);
				}

				if(ccl_angAccA.length > 0) if (croStep < ccl_angAccA.length) { ccl_angAcc = ccl_angAccA[croStep]; } else { ccl_angAcc = ccl_angAccA[ccl_angAccA.length - 1]; }
				if(ccl_angDecA.length > 0) if (croStep < ccl_angDecA.length) { ccl_angDec = ccl_angDecA[croStep]; } else { ccl_angDec = ccl_angDecA[ccl_angDecA.length - 1]; }

				ccl_dampa = max_ang_vel / ccl_angAcc;
				ccl_dampb = max_ang_vel / ccl_angDec;

				let ret_val = worldScripts["Deep_Horizon_Adv_Nav_Comp"]._anc_time_taken(initialAngle, max_ang_vel, ccl_dampa, ccl_dampb);
				ccl_last = ret_val[0];
				log ("Reorient Function", "ccl_last = " + ccl_last + ".");
				ccl_dampa = ret_val[1];
				ccl_dampb = ret_val[2];

				totalSecs += ccl_last;
				if (croStep == (ccl_tarVA.length -1)) cheyds_fcbs_ref[1] = absSecs + totalSecs;

				alpha = ccl_last/(ccl_last - (ccl_dampa + ccl_dampb) / 2);


			}
			ccl_sum += delta;

			if(ccl_sum>=ccl_last){
				if(ccl_recall) ccl_sum = 0;
				else if (croStep < (ccl_tarVA.length-1)) {
					croStep += 1;
					ccl_sum = 0;
					reset = 0;

					return;
				}
				else if ((croStep == (ccl_tarVA.length - 1)) && (!finalCallbackCalled)) {
					finalCallbackCalled = true;
					ccl_callback();
				}
				else return;
			}

			if (ccl_tar[0] == 0) {
				let currentForward = ccl_ent.heading;
				let currentForwardAngle = currentForward.angleTo(targetVector);
				let currentUpAngle = ccl_ent.orientation.vectorUp().angleTo(targetVector);
				if (ccl_sum < ccl_dampa) aimAngle = (1 - (alpha*(ccl_last/ccl_dampa)*(ccl_sum/ccl_last)*(ccl_sum/ccl_last)/2))*initialAngle;
				else if (ccl_sum < (ccl_last - ccl_dampb)) aimAngle = (1 - ((alpha*ccl_dampa/ccl_last/2) + alpha*(ccl_sum-ccl_dampa)/ccl_last))*initialAngle;
				else if (ccl_sum < ccl_last) aimAngle = (alpha*(ccl_last/ccl_dampb)*(ccl_last-ccl_sum)/ccl_last*(ccl_last-ccl_sum)/ccl_last/2)*initialAngle;
				else aimAngle = 0;
				let angle = currentForwardAngle - aimAngle;
				let cross = lastOrientation.vectorForward().cross(targetVector).direction();
				ccl_ent.orientation = worldScripts["Deep_Horizon_Adv_Nav_Comp"]._lookAtRotate(ccl_ent.orientation.rotate(cross,-angle).vectorForward(), lastOrientation.vectorUp());
				lastOrientation = ccl_ent.orientation;
				lastUpAngle = currentUpAngle;
			}
			else {
				let currentForward = ccl_ent.heading;
				let currentForwardAngle = currentForward.angleTo(targetVector);
				let currentUpAngle = ccl_ent.orientation.vectorUp().angleTo(targetVector);
				if (ccl_sum < ccl_dampa) aimAngle = (1 - (alpha*(ccl_last/ccl_dampa)*(ccl_sum/ccl_last)*(ccl_sum/ccl_last)/2))*initialAngle;
				else if (ccl_sum < (ccl_last - ccl_dampb)) aimAngle = (1 - ((alpha*ccl_dampa/ccl_last/2) + alpha*(ccl_sum-ccl_dampa)/ccl_last))*initialAngle;
				else if (ccl_sum < ccl_last) aimAngle = (alpha*(ccl_last/ccl_dampb)*(ccl_last-ccl_sum)/ccl_last*(ccl_last-ccl_sum)/ccl_last/2)*initialAngle;
				else aimAngle = 0;
				let angle = currentUpAngle - aimAngle;
				let cross = lastOrientation.vectorUp().cross(targetVector).direction();
				ccl_ent.orientation = worldScripts["Deep_Horizon_Adv_Nav_Comp"]._lookAtRotate(lastOrientation.vectorForward(), ccl_ent.orientation.rotate(cross,-angle).vectorUp());
				lastOrientation = ccl_ent.orientation;
				lastUpAngle = currentUpAngle;
			}

			reset = 1;
			return;
		});
		absSecs = clock.absoluteSeconds;
		if(ccl_recall) this.cheyds_fcbs.push([this.cheyds_fcb,absSecs + 0xFFFFFF]);
		else {
			this.cheyds_fcbs.push([this.cheyds_fcb,absSecs + 0xFFFFFF]);
			cheyds_fcbs_ref = this.cheyds_fcbs[this.cheyds_fcbs.length - 1];
			if(this.cheyds_doAutoRemove) this.cheyds_autoRemove(1);
		}
		return;
	},
	// Check fcbs and log them. 
	fcbCheckAll: function(){
		if(!this.cheyds_fcbs.length) return(false);
		for(let i=0;i<this.cheyds_fcbs.length;i++){
			if(isValidFrameCallback(this.cheyds_fcbs[i][0])) log("fcbCheck","valid i:"+i+" fcb:"+this.cheyds_fcbs[i][0]+" typeof:"+typeof(this.cheyds_fcbs[i][0])+" lasts:"+(clock.absoluteSeconds-this.cheyds_fcbs[i][1]));
			else log("fcbCheck","invalid i:"+i);
		}
		return(true);
	},
	// Clean fcbs.
	//	all		Boolean. Clean all fcbs. 
	fcbRemove: function fcbRemove(all){
		if(!this.cheyds_fcbs.length) return(false);
		var checked=0;
		for(let i=0;i<this.cheyds_fcbs.length;i++){
			if(isValidFrameCallback(this.cheyds_fcbs[i][0])){
				if(all) removeFrameCallback(this.cheyds_fcbs[i][0]);
				else if(this.cheyds_doAutoRemove && clock.absoluteSeconds>this.cheyds_fcbs[i][1]){
					removeFrameCallback(this.cheyds_fcbs[i][0]);
					checked++;
				}
			} else checked++;
		}
		if(all || this.cheyds_fcbs.length===checked){
			this.cheyds_fcb = 0;
			this.cheyds_fcbs = [];
			this.cheyds_autoRemove();
		}
		return(true);
	}
};