| Scripts/mwa_main.js | "use strict";
this.name = "ManualWitchspaceAlignment";
this.author = "phkb";
this.copyright = "2017 phkb";
this.description = "Reimplements the DHI AHC but with a manual alignment procedure";
this.licence = "CC BY-NC-SA 3.0";
/*
	TODO:
		work out better occlusion calc for stations
*/
this._markers = []; // array of markers representing jump points to all local systems
this._running = false; // used to work out when the spawn routine is running
this._alignAccuracy = 0.18; // used to control how accurately the player needs to align their ship to the nav beacon
this._jumpThrottle = 0.2;
// higher number means less accuracy
this._heldTarget = null;
this._colorList = ["blue", "amber", "green", "pink", "purple", "white"];
this._color = 0;
this._userOverride = false;
this._nova = false;
this._cancelled = false;
this._fcb;
this._libSettings = {
	Name: this.name,
	Display: expandDescription("[mwa_config_display]"),
	Alias: expandDescription("[mwa_config_alias]"),
	Alive: "_libSettings",
	Bool: {
		B0: {
			Name: "_userOverride",
			Def: false,
			Desc: expandDescription("[mwa_config_useroverride]")
		},
		Info: expandDescription("[mwa_config_bool_info]")
	},
	SInt: {
		S0: {Name: "_color", Def: 0, Min: 0, Max: 5, Desc: expandDescription("[mwa_config_color]") },
		S1: {Name: "_jumpThrottle", Def: 0.2, Min: 0, Max: 1, Float:true, Desc: expandDescription("[mwa_config_throttle]") },
		Info: expandDescription("[mwa_config_sint_info]")
	},
};
this._trueValues = ["yes", "1", 1, "true", true];
//-------------------------------------------------------------------------------------------------------------
this.startUp = function () {
	if (worldScripts.Lib_Config) {
		if (missionVariables.MWA_UserOverride) this._userOverride = (this._trueValues.indexOf(missionVariables.MWA_UserOverride) >= 0 ? true : false);
		if (missionVariables.MWA_Color) this._color = parseInt(missionVariables.MWA_Color);
		if (missionVariables.MWA_JumpThrottle) this._jumpThrottle = parseFloat(missionVariables.MWA_JumpThrottle);
	}
	// make sure we don't get in the way of the ANC
	if (worldScripts.Deep_Horizon_Adv_Nav_Comp) {
		// turn off ws events in ANC and control them from here
		var anc = worldScripts.Deep_Horizon_Adv_Nav_Comp;
		anc.shipWillEnterWitchspace_hold = anc.shipWillEnterWitchspace;
		delete anc.shipWillEnterWitchspace;
		anc.shipWillExitWitchspace_hold = anc.shipWillExitWitchspace;
		delete anc.shipWillExitWitchspace;
		anc.playerStartedJumpCountdown_hold = anc.playerStartedJumpCountdown;
		delete anc.playerStartedJumpCountdown;
	}
	if (worldScripts.BGS) {
		player.ship.script._BGS = true;
		// we're taking charge of BGS's playerStartedJumpCountdown routine so we can make sure
		// it's only called when we actually start the countdown (ie when aligned);
		worldScripts.BGS.$mwa_playerStartedJumpCountdown = worldScripts.BGS.playerStartedJumpCountdown;
		delete worldScripts.BGS.playerStartedJumpCountdown;
	}
}
//-------------------------------------------------------------------------------------------------------------
this.startUpComplete = function () {
	if (worldScripts.Lib_Config) worldScripts.Lib_Config._registerSet(this._libSettings);
	this.$systemSetup();
	// bug fix to prevent countdown to zero from having its timer garbage collected if the jump starts/stops frequently
	if (worldScripts.countdown_to_zero) {
		worldScripts.countdown_to_zero.playerStartedJumpCountdown = function(type, seconds) {
			if (this.$hyperTimer && this.$hyperTimer.isRunning === true) {
				this.$hyperTimer.stop();
			}
			if (type === "standard") {
				missionVariables["countdown_to_zero"] = seconds;
				this.$hyperTimer = new Timer(this,this._hyperDriveCountdown,0,1);
			}
			else if (type === "galactic") {
				missionVariables["countdown_to_zero"] = seconds - 1;
				this.$hyperTimer = new Timer(this,this._hyperDriveCountdown,1,1);
			}
		}
	}
}
//-------------------------------------------------------------------------------------------------------------
this.shipWillDockWithStation = function (station) {
	this.$stopNavFrameCallback(true);
}
//-------------------------------------------------------------------------------------------------------------
this.shipWillLaunchFromStation = function () {
	this._running = false;
	var ps = player.ship.script;
	ps.$checkForAlignment = this.$checkForAlignment;
	ps.$shipIsAligned = this.$shipIsAligned;
	ps.$jumpIsOccluded = this.$jumpIsOccluded;
	ps.$entityType = this.$entityType;
	ps.$reorientVEToPlayer = this.$reorientVEToPlayer;
	ps.$vectoredPositionToTarget = this.$vectoredPositionToTarget;
	ps.$lookAtRotate = this.$lookAtRotate;
	ps.$orthoNormalise = this.$orthoNormalise;
	ps.$lookAtRotateEuler = this.$lookAtRotateEuler;
	ps.$displayBasicPointerMessage = this.$displayBasicPointerMessage;
	ps.$positionNavVisualEffects = this.$positionNavVisualEffects;
	ps.$performCancel = this.$performCancel;
	ps._checkingAlignment = false;
	ps._jumpStarted = false;
	ps._BGSStarted = false;
	ps._jumpMarker = null;
	ps._alignCount = 0;
	ps._occludedCount = 0;
	ps._alignWarning = false;
	ps._occludedWarning = false;
	ps._override = false;
	ps._navFrameVE = null;
	ps._navStarVE = null;
	ps._basicCompassFrameCount = 0;
	ps._alignAccuracy = this._alignAccuracy;
	ps._jumpThrottle = this._jumpThrottle;
	
	this._cancelled = false;
	var anc = worldScripts.Deep_Horizon_Adv_Nav_Comp;
	ps._usingANC = false;
	if (anc) {
		if (player.ship.equipmentStatus("EQ_ADV_NAV_COMP") === "EQUIPMENT_OK") {
			// turn on anc, turn off mwa
			anc.shipWillEnterWitchspace = anc.shipWillEnterWitchspace_hold;
			anc.shipWillExitWitchspace = anc.shipWillExitWitchspace_hold;
			anc.playerStartedJumpCountdown = anc.playerStartedJumpCountdown_hold;
			ps._usingANC = true;
		} else {
			delete anc.shipWillEnterWitchspace;
			delete anc.shipWillExitWitchspace;
			delete anc.playerStartedJumpCountdown;
		}
	}
}
//-------------------------------------------------------------------------------------------------------------
this.shipDied = function(whom, why) {
	this.$alternateStopNavFrameCallback(false);
}
//-------------------------------------------------------------------------------------------------------------
this.playerWillSaveGame = function () {
	missionVariables.MWA_UserOverride = this._userOverride;
	missionVariables.MWA_Color = this._color;
	missionVariables.MWA_JumpThrottle = this._jumpThrottle;
}
//-------------------------------------------------------------------------------------------------------------
this.guiScreenChanged = function (to, from) {
	var p = player.ship;
	if (p.isInSpace === false) return;
	if ((to === "GUI_SCREEN_LONG_RANGE_CHART" || to === "GUI_SCREEN_SHORT_RANGE_CHART") && this._heldTarget == null) this._heldTarget = p.nextSystem;
	if ((from === "GUI_SCREEN_LONG_RANGE_CHART" || from === "GUI_SCREEN_SHORT_RANGE_CHART") &&
		(to != "GUI_SCREEN_LONG_RANGE_CHART" && to != "GUI_SCREEN_SHORT_RANGE_CHART") && this._heldTarget != p.nextSystem) {
		// player changed destination system
		if (p.script._navFrameCallbackID && isValidFrameCallback(p.script._navFrameCallbackID)) {
			player.consoleMessage(expandDescription("[witch-jump-cancelled]"), 5);
			p.cancelHyperspaceCountdown();
			this.$stopNavFrameCallback(false);
			this._cancelled = false;
		}
	}
}
//-------------------------------------------------------------------------------------------------------------
this.shipLaunchedEscapePod = function(escapePod) {
	this.$alternateStopNavFrameCallback(false);
}
//-------------------------------------------------------------------------------------------------------------
this.shipExitedWitchspace = function () {
	this.$systemSetup();
	player.ship.script._checkingAlignment = false;
	if (this._nova === true) {
		player.ship.takeInternalDamage();
		this._nova = false;
	}
}
//-------------------------------------------------------------------------------------------------------------
this.shipWillEnterWitchspace = function (cause, destination) {
	this._heldTarget = null;
	delete this.compassTargetChanged;
	delete this.playerStartedAutoPilot;
	//delete this.playerCancelledJumpCountdown;
	this._cancelled = false;
	this.$stopNavFrameCallback(false);
}
//-------------------------------------------------------------------------------------------------------------
this.playerStartedJumpCountdown = function (type, seconds) {
	if (this._cancelled == true) {
		// jump was cancelled during a playerStartedJumpCountdown worldscript
		this._cancelled = false;
		return;
	}
	var ps = player.ship.script;
	// check for galactic jump or if the anc is in play, or the system is going nova - don't do anything for these
	if (type === "galactic" || ps._usingANC === true || (system.sun && system.sun.hasGoneNova)) {
		if (ps._BGS) worldScripts.BGS.$mwa_playerStartedJumpCountdown(type, seconds);
		if (system.sun && system.sun.hasGoneNova) {
			this._nova = true;
			player.consoleMessage(expandDescription("[witch-jump-nova]"));
		}
		return;
	}
	if (!ps._navFrameCallbackID) {
		ps._basicCompass = false;
		if (system.isInterstellarSpace === true || player.ship.hasEquipmentProviding("EQ_ADVANCED_COMPASS") === false) {
			ps._basicCompass = true;
		}
		this.$spawnJBNavVEs();
	} else if (ps._override === false) {
		player.ship.cancelHyperspaceCountdown();
		//this.$stopNavFrameCallback(true);
		player.consoleMessage(expandDescription("[witch-user-abort]"));
	}
}
//-------------------------------------------------------------------------------------------------------------
this.playerStartedAutoPilot_hide = function () {
	player.consoleMessage(expandDescription("[witch-user-abort]"));
	player.ship.cancelHyperspaceCountdown();
	this.$stopNavFrameCallback(true);
	this._cancelled = false;
}
//-------------------------------------------------------------------------------------------------------------
this.compassTargetChanged_hide = function (whom, mode) {
	player.consoleMessage(expandDescription("[witch-user-abort]"));
	delete player.ship.script._oldTarget;
	this.$stopNavFrameCallback(true);
	player.ship.cancelHyperspaceCountdown();
	this._cancelled = false;
}
//-------------------------------------------------------------------------------------------------------------
this.playerCancelledJumpCountdown = function () {
	var ps = player.ship.script;
	// set cancelled = true if we got here before the playerStartedJumpCountdown function was executed
	// usually because some other OXP cancelled the jump in it's own playerStartedJumpCountdown function.
	if (ps._jumpMarker == null) this._cancelled = true;
	ps._BGSStarted = false;
	if (ps._override === true) return;
	if (ps._usingANC === true) return;
	this.$stopNavFrameCallback(true);
}
//-------------------------------------------------------------------------------------------------------------
this.playerJumpFailed = function (reason) {
	this.$stopNavFrameCallback(true);
}
//-------------------------------------------------------------------------------------------------------------
this.$spawnJBNavVEs = function $spawnJBNavVEs() {
	if (this._running === true) return;
	this._running = true;
	var p = player.ship;
	var ps = p.script;
	// Select marker that matches the destination system.
	var playerTarget = p.nextSystem;
	for (var i = 0; i < this._markers.length; i++) {
		if (playerTarget === this._markers[i].systemID) {
			ps._jumpMarker = this._markers[i];
			break;
		}
	}
	if (ps._jumpMarker == null) {
		log(this.name, expandDescription("[mwa_error_notfound]", {sysname: System.systemNameForID(playerTarget)}));
		this._running = false;
		return;
	}
	player.consoleMessage(expandDescription("[witch-nav-acquired]", {sysname: System.systemNameForID(playerTarget)}));
	ps._jumpMarker.beaconCode = "witch-destination-icon";
	ps._jumpMarker.beaconLabel = expandDescription("[witch-beacon-label]", {sysname:ps._jumpMarker.name});
	ps._jumpStarted = true;
	// start the framecount at its recycle point so an alignment check will happen next frame
	ps._frameCount = 0.4;
	// Spawn Visual Effect and store reference to it
	if (this._color > 5 || this._color < 0) this._color = 0; // default to blue if incorrectly set
	ps._navFrameVE = system.addVisualEffect("jumpbeacon_navframe_" + this._colorList[this._color], ps.$vectoredPositionToTarget(ps._jumpMarker.position, p.collisionRadius + 5000));
	ps._navStarVE = system.addVisualEffect("jumpbeacon_navstar", ps.$vectoredPositionToTarget(ps._jumpMarker.position, p.collisionRadius + 50000));
	// Orient the VE toward the player ship
	ps._navFrameVE.scale(0.66);
	ps._navStarVE.scale(3.30 - ps._jumpMarker.distanceToSystem * 0.27);
	var tex = ps._jumpMarker.systemID % 4;
	switch (tex) {
		case 1:
			ps._navStarVE.setMaterials({
				"jumpbeacon_navstar.png": {
					"textures": ["jumpbeacon_navstar2.png"],
					"fragment_shader": "jumpbeacon_jumpstar.fragment",
					"emission_map": "jumpbeacon_navstar2.png",
					"uniforms": {
						"uColorMap": {
							"type": "texture",
							"value": "0"
						},
						"uSpecIntensity": "shaderFloat1",
						"uSpecColor": "shaderVector1"
					},
					"vertex_shader": "jumpbeacon_jumpstar.vertex"
				}
			});
			break;
		case 2:
			ps._navStarVE.setMaterials({
				"jumpbeacon_navstar.png": {
					"textures": ["jumpbeacon_navstar3.png"],
					"fragment_shader": "jumpbeacon_jumpstar.fragment",
					"emission_map": "jumpbeacon_navstar3.png",
					"uniforms": {
						"uColorMap": {
							"type": "texture",
							"value": "0"
						},
						"uSpecIntensity": "shaderFloat1",
						"uSpecColor": "shaderVector1"
					},
					"vertex_shader": "jumpbeacon_jumpstar.vertex"
				}
			});
			break;
		case 3:
			ps._navStarVE.setMaterials({
				"jumpbeacon_navstar.png": {
					"textures": ["jumpbeacon_navstar4.png"],
					"fragment_shader": "jumpbeacon_jumpstar.fragment",
					"emission_map": "jumpbeacon_navstar4.png",
					"uniforms": {
						"uColorMap": {
							"type": "texture",
							"value": "0"
						},
						"uSpecIntensity": "shaderFloat1",
						"uSpecColor": "shaderVector1"
					},
					"vertex_shader": "jumpbeacon_jumpstar.vertex"
				}
			});
			break;
	}
	ps._navFrameCallbackID = addFrameCallback(ps.$positionNavVisualEffects.bind(ps));
	worldScripts.ManualWitchspaceAlignment._fcb = ps._navFrameCallbackID;
	this._running = false;
}
//-------------------------------------------------------------------------------------------------------------
this.$stopNavFrameCallback = function $stopNavFrameCallback(cancelled) {
	var p = player.ship;
	var ps = p.script;
	if (ps._navFrameCallbackID && isValidFrameCallback(ps._navFrameCallbackID)) {
		removeFrameCallback(ps._navFrameCallbackID);
	}
	delete ps._navFrameCallbackID;
	delete this.compassTargetChanged;
	delete this.playerStartedAutoPilot;
	//delete this.playerCancelledJumpCountdown;
	if (ps._jumpMarker) {
		ps._jumpMarker.beaconCode = "";
		ps._jumpMarker.beaconLabel = "";
		ps._jumpMarker = null;
		if (ps._oldTarget && ps._oldTarget.isValid) {
			p.compassTarget = ps._oldTarget;
		}
		delete ps._oldTarget;
	}
	if (ps._navFrameVE != null) {
		ps._navFrameVE.remove();
		ps._navFrameVE = null;
	}
	if (ps._navStarVE != null) {
		ps._navStarVE.remove();
		ps._navStarVE = null;
	}
	ps._jumpStarted = false;
	ps._occludedCount = 0;
	ps._occludedWarning = false;
	ps._alignWarning = false;
	ps._alignCount = 0;
	ps._override = false;
	ps._lastHeading = null;
	if (ps._BGS && ps._usingANC === false && cancelled === true) {
		ps._BGSStarted = false;
		worldScripts.BGS.playerCancelledJumpCountdown();
		worldScripts.BGS._clrTimer(1);
	}
	this._running = false;
}
//-------------------------------------------------------------------------------------------------------------
this.$alternateStopNavFrameCallback = function $alternateStopNavFrameCallback() {
	if (this.fcb && isValidFrameCallback(this._fcb)) {
		removeFrameCallback(this._fcb);
	}
	var ve = system.allVisualEffects;
	for (var i = ve.count; i >= 0; i--) {
		if (ve[i].dataKey.indexOf("jumpbeacon") >= 0) {
			ve[i].remove();
		}
	}
}
//-------------------------------------------------------------------------------------------------------------
this.$systemSetup = function $systemSetup() {
	this._markers.length = 0;
	// 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._markers = system.addShips("jump_marker", this._spawnCount, player.ship.position, 5000000);
	//Assign a matching system.ID as an additional value to each buoy
	for (var i = 0; i < this._markers.length; i++) {
		var mkr = this._markers[i];
		mkr.systemID = this._localSystems[i].systemID;
		mkr.galCoordinates = this._localSystems[i].coordinates;
		mkr.name = this._localSystems[i].name;
		mkr.distanceToSystem = System.infoForSystem(galaxyNumber, system.ID).distanceToSystem(this._localSystems[i]);
		mkr.uSpecColor = this.$selectColor(this._localSystems[i].systemID, i);
		mkr.uSpecIntensity = 0.5;
		mkr.position = this.$positionJumpBeaconMarker(mkr, 100000000);
	}
}
//-------------------------------------------------------------------------------------------------------------
this.$positionJumpBeaconMarker = function $positionJumpBeaconMarker(marker, distance) {
	if (marker.galCoordinates == null) return null;
	var t = marker.galCoordinates;
	var s = System.infoForSystem(galaxyNumber, system.ID).coordinates;
	// special case: if target system has same coords as current system, force them to be different
	if (t.distanceTo(s) == 0) t.z += 1;
	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.$selectColor = function $selectColor(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:
			var colorVec = [1, 1, 1];
	}
	return colorVec;
}
//-------------------------------------------------------------------------------------------------------------
// all the following functions are attached to player.ship.script to improve performance
//-------------------------------------------------------------------------------------------------------------
// main frame callback routine
this.$positionNavVisualEffects = function $positionNavVisualEffects(delta) {
	if (delta === 0) return;
	var p = this.ship;
	// make sure we have a valid ship to work with
	if (!p || !p.position || !p.vectorForward) {
		worldScripts.ManualWitchspaceAlignment.$alternateStopNavFrameCallback(false);
		return;
	}
	var ps = this;
	// make sure we have valid entities to work with, otherwise cancel the fcb
	if (ps._navFrameVE.isValid === false || ps._navStarVE.isValid === false) {
		if (ps._navFrameCallbackID && isValidFrameCallback(ps._navFrameCallbackID)) {
			removeFrameCallback(ps._navFrameCallbackID);
		}
		delete ps._navFrameCallbackID;
		return;
	}
	ps.$reorientVEToPlayer(ps._navFrameVE);
	ps.$reorientVEToPlayer(ps._navStarVE);
	ps._navFrameVE.position = ps.$vectoredPositionToTarget(ps._jumpMarker.position, p.collisionRadius + 5000);
	ps._navStarVE.position = ps.$vectoredPositionToTarget(ps._jumpMarker.position, p.collisionRadius + 10000);
	ps._navStarVE.shaderVector1 = ps._jumpMarker.uSpecColor;
	ps._navStarVE.shaderFloat1 = ps._jumpMarker.uSpecIntensity;
	ps._frameCount += delta;
	if (ps._frameCount > 0.5) {
		ps._frameCount = 0;
		ps.$checkForAlignment();
	}
	// for non-compass directions, we need to give a console message pointer to the beacon
	if (ps._basicCompass === true) {
		ps._basicCompassFrameCount += delta;
		if (ps._basicCompassFrameCount > 2) {
			ps._basicCompassFrameCount = 0;
			ps.$displayBasicPointerMessage();
		}
	}
}
//-------------------------------------------------------------------------------------------------------------
this.$checkForAlignment = function $checkForAlignment() {
	//var startDate = new Date();
	// make sure we don't run over ourselves
	var ps = this;
	if (ps._checkingAlignment === true) return;
	ps._checkingAlignment = true;
	var p = this.ship;
	if (p.maxPitch - Math.abs(p.pitch) < 0.0001) {
		ps._checkingAlignment = false;
		return;
	}
	var ws = worldScripts.ManualWitchspaceAlignment;
	// set the compass to the nav jump marker, and kick in our functions to monitor when the 
	// compass target changes, and when the player cancels the jump
	if (ps._basicCompass === false && ps._jumpMarker && p.compassTarget != ps._jumpMarker) {
		ps._oldTarget = p.compassTarget;
		p.compassTarget = ps._jumpMarker;
		ws.compassTargetChanged = ws.compassTargetChanged_hide;
		ws.playerStartedAutoPilot = ws.playerStartedAutoPilot_hide;
	}
	var alignedOK = ps.$shipIsAligned();
	var speedOK = (p.speed >= p.maxSpeed * ps._jumpThrottle);
	if (alignedOK && speedOK) {
		p.scriptedMisjump = ((Math.random() * 0.18) < (p._deviation - 0.02));
		ps._alignWarning = false;
		ps._lastHeading = p.heading;
		if (ps._jumpStarted === false) {
			ps._jumpStarted = true;
			if (ps._cancelTimer && ps._cancelTimer.isRunning) ps._cancelTimer.stop();
			ps._override = true;
			p.beginHyperspaceCountdown(p.hyperspaceSpinTime);
			ps._override = false;
		}
		if (ps._BGS && ps._BGSStarted === false) {
			ps._BGSStarted = true;
			worldScripts.BGS.$mwa_playerStartedJumpCountdown("standard", p.hyperspaceSpinTime);
		}
	} else {
		if (ps._jumpStarted === true) {
			ps._jumpStarted = false;
			// do the actual cancellation through a separate timer, rather than the framecallback
			// sometimes getting timeout errors when run through the fcb
			if (!ps._cancelTimer || !ps._cancelTimer.isRunning) {
				ps._cancelTimer = new Timer(ps, ps.$performCancel, 0.25, 0);
			}
		}
		ps._alignCount += 1;
		if (ps._alignCount >= 15 && ps._occludedWarning === false) ps._alignWarning = false;
		if (ps._alignWarning === false) {
			ps._alignWarning = true;
			ps._alignCount = 0;
			var key = "";
			if (alignedOK && !speedOK) {
				key = "[witch-speed]";
			} else if (!alignedOK && !speedOK) {
				key = "[witch-speed-unaligned]";
			} else {
				key = "[witch-unaligned]";
			}
			player.consoleMessage(expandDescription(key, {value: parseInt(ps._jumpThrottle * 100)}), 4);
		}
	}
	ps._checkingAlignment = false;
	//log(this.name, "checkForAlignment complete in ms: " + (new Date().getTime() - startDate.getTime()));
}
//-------------------------------------------------------------------------------------------------------------
this.$performCancel = function $performCancel() {
	var p = player.ship;
	p.script._override = true;
	p.cancelHyperspaceCountdown();
	p.script._override = false;
}
//-------------------------------------------------------------------------------------------------------------
this.$vectoredPositionToTarget = function $vectoredPositionToTarget(targetPosVector, distance) {
	if (!this.ship || !this.ship.position) return null;
	var v = targetPosVector.subtract(this.ship.position).direction();
	v = v.multiply(distance);
	v = v.add(this.ship.position);
	return v;
}
//-------------------------------------------------------------------------------------------------------------
this.$reorientVEToPlayer = function $reorientVEToPlayer(navVE) {
	if (!this.ship || !this.ship.position) return;
	var orient = this.$lookAtRotate(this.ship.position.subtract(navVE.position), this.ship.orientation.vectorUp());
	if (orient === null) orient = this.$lookAtRotate(this.ship.position.subtract(navVE.position), this.ship.orientation.vectorForward().multiply(-1));
	navVE.orientation = orient;
}
//-------------------------------------------------------------------------------------------------------------
this.$lookAtRotate = function $lookAtRotate(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.$orthoNormalise = function $orthoNormalise(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 $lookAtRotateEuler(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.$shipIsAligned = function $shipIsAligned() {
	//var startDate = new Date();
	var p = this.ship;
	var target = this._jumpMarker;
	if (!target) return false;
	var deviation = p.vectorForward.angleTo(target.position.subtract(p.position));
	p._deviation = deviation;
	if (deviation < this._alignAccuracy) {
		// check for occlusion
		/*if (p.script._useCheckCourseFunction === true) {
			var ent = p.checkCourseToPosition(target.position);
			if (ent && (ent.isPlanet || ent.isSun || ent.isStation)) {
				this._occludedCount += 1;
				if (this._occludedCount >= 15 && this._occludedWarning === true) this._occludedWarning = false;
				if (this._occludedWarning === false) {
					this._occludedCount = 0;
					player.consoleMessage(expandDescription("[witch-occluded]", {
						entity: this.$entityType(ent)
					}), 4);
					this._occludedWarning = true;
				}
				return false;
			}
		} else {*/
			var entities = [].concat(system.planets).concat(system.sun).concat(system.stations);
			var occluded = false;
			if (entities.length > 0) {
				for (var i = 0; i < entities.length; i++) {
					if (entities[i] && this.$jumpIsOccluded(entities[i], target) === true) {
						this._occludedCount += 1;
						if (this._occludedCount >= 15 && this._occludedWarning === true) this._occludedWarning = false;
						if (this._occludedWarning === false) {
							this._occludedCount = 0;
							player.consoleMessage(expandDescription("[witch-occluded]", {
								entity: this.$entityType(entities[i])
							}), 4);
							this._occludedWarning = true;
						}
						//log(this.name, "align check complete in ms: " + (new Date().getTime() - startDate.getTime()));
						return false;
					}
				}
			}
		//}
		//log(this.name, "align check complete in ms: " + (new Date().getTime() - startDate.getTime()));
		return true;
	} else {
		//log(this.name, "align check complete in ms: " + (new Date().getTime() - startDate.getTime()));
		return false;
	}
}
//-------------------------------------------------------------------------------------------------------------
// with thanks to spara for the calculation
this.$jumpIsOccluded_alt = function $jumpIsOccluded_alt(bodyEntity, targetEntity) {
	//if (this._jumpStarted === true && this._lastHeading && this.ship.heading.dot(this._lastHeading) > 0.99) return false;
	var vStellarBody = bodyEntity.position;
	var rStellarBody = bodyEntity.radius;
	// i'm reusing this routine for stations, but it's not ideal for non-spherical entities
	if (bodyEntity.isStation) {
		var box = bodyEntity.boundingBox;
		rStellarBody = (box.x > box.y && box.x > box.z ? box.x :
			(box.y > box.x && box.y > box.z ? box.y : box.z)) / 2;
	}
	if (isNaN(rStellarBody) === true) return false;
	var vTarget = targetEntity.position;
	var vPlayerShip = player.ship;
	var vPlayerToStellar = vStellarBody.subtract(vPlayerShip);
	var vPlayerToTarget = vTarget.subtract(vPlayerShip);
	var dPlayerToStellar = vPlayerToStellar.magnitude();
	var dPlayerToTarget = vPlayerToTarget.magnitude();
	if (dPlayerToStellar < dPlayerToTarget) {
		var aStellar = Math.asin(rStellarBody / dPlayerToStellar);
		var aTarget = vPlayerToStellar.angleTo(vPlayerToTarget);
		if (aStellar > aTarget) {
			return true;
		} else {
			return false;
		}
	}
}
//-------------------------------------------------------------------------------------------------------------
// with thanks to spara for the calculation
this.$jumpIsOccluded = function $jumpIsOccluded(bodyEntity, targetEntity) {
	//if (this._jumpStarted === true && this._lastHeading && this.ship.heading.dot(this._lastHeading) > 0.99) return false;
	//var startDate = new Date();
	//var vStellarBody = bodyEntity.position;
	var rStellarBody = bodyEntity.radius;
	// i'm reusing this routine for stations, but it's not ideal for non-spherical entities (I think)
	if (bodyEntity.isStation) {
		var box = bodyEntity.boundingBox;
		rStellarBody = (box.x > box.y && box.x > box.z ? box.x :
			(box.y > box.x && box.y > box.z ? box.y : box.z)) / 2;
	}
	if (isNaN(rStellarBody) === true) return false;
	var vTarget = targetEntity.position;
	var vPlayerShip = this.ship.position;
	var vPlayerToTarget = vTarget.subtract(vPlayerShip);
	var dPlayerToStellar = vPlayerShip.distanceTo(bodyEntity);
	var dPlayerToTarget = vPlayerToTarget.magnitude();
	if (dPlayerToStellar < dPlayerToTarget) {
		var checkVal = Math.sqrt(Math.pow(dPlayerToStellar, 2) - Math.pow(rStellarBody, 2));
		if (isNaN(checkVal) === false) {
			var vTest = vPlayerShip.add(vPlayerToTarget.direction().multiply(checkVal));
			var dTest = vTest.distanceTo(bodyEntity);
			if (rStellarBody > dTest) {
				//log(this.name, "occlusion check complete in ms: " + (new Date().getTime() - startDate.getTime()));
				return true;
			} else {
				//log(this.name, "occlusion check complete in ms: " + (new Date().getTime() - startDate.getTime()));
				return false;
			}
		} else {
			return true;
		}
	} else {
		//log(this.name, "occlusion check complete in ms: " + (new Date().getTime() - startDate.getTime()));
		return false;
	}
}
//-------------------------------------------------------------------------------------------------------------
this.$entityType = function $entityType(entity) {
	if (entity.isPlanet) return "planet";
	if (entity.isSun) return "sun";
	return "station";
}
//-------------------------------------------------------------------------------------------------------------
this.$displayBasicPointerMessage = function $displayBasicPointerMessage() {
	var p = this.ship;
	var target = this._jumpMarker;
	var output = "";
	var f_dev = p.vectorForward.angleTo(target.position.subtract(p));
	var r_dev = p.vectorRight.angleTo(target.position.subtract(p));
	var u_dev = p.vectorUp.angleTo(target.position.subtract(p));
	var s = "";
	// > 1.56 means opposite side (3.12 exact opposite)
	// so, f_dev < alignAccuracy -- aligned
	if (f_dev < this._alignAccuracy) s = "X";
	if (f_dev >= this._alignAccuracy) {
		if (r_dev > 1.69) s += "left ";
		if (u_dev < 1.45) {
			s += (s === "" ? "" : "and ") + "up ";
		} else if (u_dev > 1.69) {
			s += (s === "" ? "" : "and ") + "down ";
		}
		if (r_dev < 1.45) s += (s === "" ? "" : "and ") + "right ";
		if (f_dev > 1.57) s += " (aft)";
	}
	if (s !== "X") {
		output += "Align: " + s;
		player.consoleMessage(output, 2);
	}
} |