| Scripts/st_equipment.js | "use strict";
this.name	     = "Synchronised_Torus_Controller";
this.version     = "1.0";    // 2015-12-14
this.description = "equipment script for torus drive synchronisation";
this.author	     = "Fritz G.";   
this.copyright	 = "© 2015 Fritz G.";
this.licence	 = "CC BY-NC-SA 4.0";
//------------------------------------------------------------------------------------------------------------
// Credits to Capt. Murphy for his idea to implement torus synchronisation in his Escort Contracts OXP.
//------------------------------------------------------------------------------------------------------------
// This OXP is based on an idea I had when escorting slow freighters. Because NPX ships have no torus drive,
// this can take very long, if you want to follow them all the way from witchpoint to station.
// Later I discovered Escort Contracts OXP, using "torus synchronisation" to speed up the game.
// This of course works only for the ship you have agreed to escort, but using the same basic technique
// (simulating torus drive by using a high velocity) I was able to make this available for all ships 
// that are underway in a straight line to a distant destination. That could be traders, bounty hunters,
// vipers or even pirates or assassins on their way to the station, the witchpoint or the sun. 
// In theory this should work with all ships regardless of role, including OXP ships. Only Thargoids are 
// excluded but they usually attack and dont't fly to a distant destination. 
// 
// Because the player shouldn't be able to change the destination of a NPC ship, the direction of torus 
// driving (and its speed) is defined by the NPC. The torus drive stops if the destination is reached or  
// if either the player or the mother is mass locked. 
//------------------------------------------------------------------------------------------------------------
// configuration (values taken from Escort Contracts v1.7.1)
this.$st_minDist     	= 500;	// minimum distance to NPC ship in game meters
this.$st_maxDist     	= 2500;	// maximum distance to NPC ship in game meters
this.$st_headingAlign  	= 0.98;	// precision needed for heading alignment (1 is exact (impossible!), 0.9 is too easy and looks odd) 
this.$st_timerInterval  = 0.75; // The Escort Contracts AI checks once per second, but a little more often can't do damage.
//------------------------------------------------------------------------------------------------------------
this.$st_timerRunning = false;  
this.$st_npc = null;           // will hold the ship object of the npc 
this.$st_nbEscorts = 0;        // number of npc escorts 
// for saving scanner properties of targeted npc ship
this.$st_scanClass = ""; 	   	 
this.$st_scanColour1 = null;
this.$st_scanColour2 = null;
this.$st_scanDescription = null;
// sound
this.$st_beep = null;  
this.$st_beepSounds = [];      // array for storing the different sounds 
// Key "n" pressed for starting or stopping torus synchronisation.
this.activated = function ()
{
    if (!this.$st_timerRunning) this.$st_init(); // initialize timer and sounds
	if (worldScripts["Synchronised_Torus"].$st_torusEngaged) 
	{
		// stop synchronised torus 
        this.$st_stopTorus(1);
		this.$st_message("Synchronised torus stopped.", 2);
	}
	else
	{	
		// try to start synchronised torus 
		// first check the conditions (NPC targeted, correct distance and heading, NPC destination, mass locking)
		if (this.$st_checkConditions(1))
		{
			this.$st_message("Synchronised torus drive engaged.", 1);
			// Because the core game doesn't feature torus drives for npc ships, we have to simulate 
			// it by a high velocity. The factor 32 is the same as used for the standard torus drive.
			// Synchronised torus speed is defined by the npc ship because it is the leader.
			// For exact synchronisation of velocities the timer interval is too long, so use frame callback.	
			this.$st_frameCallback = addFrameCallback(this.$st_matchVelocity.bind(this));   
			this.$st_npc.velocity = this.$st_npc.heading.multiply(this.$st_npc.maxSpeed * 32); 
			// velocity will be synchronised in this.$st_matchVelocity(), but if we dont't do it here, the npc  
			// will start up to 1/60s (frame rate) earlier, and this is visible because of the very high speed.
			player.ship.velocity = this.$st_npc.velocity;
			if (this.$st_nbEscorts > 0)  this.$st_setEscortVelocity(this.$st_npc.velocity);
			worldScripts["Synchronised_Torus"].$st_torusEngaged = true;
			this.$st_timer.start();      
		}
		else
		{
			// Initialisation failed, reset the npc scanner class.
			this.$st_resetScanClass();
		}	
	}	
};
// Key "b" pressed, show distance to target waypoint.
this.mode = function () 
{
    if (!this.$st_timerRunning) this.$st_init(); // initialize sounds (and timer)
	var pst = player.ship.target;
	if (!pst)
	{
		this.$st_message("No target selected.", 2);
	}		
	else if (!pst.isPiloted || pst.isStation)
	{	
		this.$st_message("Unsuitable target.", 2);
	}
	else
	{
		this.$st_message("Distance to waypoint: " + Math.round(pst.position.distanceTo(pst.destination)/1000) + " km.", 0);
	}		
};
// Check if conditions allow starting or continuing torus synchronisation. 
// Function called on activation (n key) (1) and in timer function (2).
this.$st_checkConditions = function(calledBy)
{
	var ps = player.ship;
    if (calledBy === 1)
    {
		// Checks only needed when starting (conditions shouldn't change after starting).
		// First we have to check if something is targeted,
		if (!ps.target) 
		{
			this.$st_message("No target selected.", 2);
			return false;
		}		
		this.$st_npc = ps.target;
		this.$st_nbEscorts = this.$st_npc.escortGroup.count - 1;  		
		// Exclude unsuitable "ships"
		// An Escort Contracts mother is unsuitable because the results could be unpredictable...
		// Usually the mother starts synchronised torus driving anyway if it is possible.
		// Escape capsules are piloted but don't have a torus drive.
		if (!this.$st_npc.isPiloted || this.$st_npc.isThargoid || this.$st_npc.isStation || this.$st_npc.primaryRole === "ec_mother" || this.$st_npc.primaryRole === "escape-capsule") 
		{
			this.$st_message("Unsuitable target.", 2);
			return false;
		}		
		// Is the target an escort?
		if (this.$st_npc.owner != null)			
        {
			// We could automatically target the leader. I tried it and it works, but it seemed too confusing.
			// this.$st_message("Target was an escort, targeting leader.",0);
			// ps.target = this.$st_npc.owner;
			this.$st_message("Target is an escort, please target the leader.", 2);
			return false;
		}	
		// Does the target move?
		if (this.$st_npc.speed === 0)			
        {
			this.$st_message("Target is stationary.", 2);
			return false;
		}	
		// Check distance between ships. 
		if(this.$st_npc.position.distanceTo(ps.position) > this.$st_maxDist)
		{
			this.$st_message("Distance to target must not exceed " + this.$st_maxDist + " m.", 2);
			return false;
		}			
		else if(this.$st_npc.position.distanceTo(ps.position) < this.$st_minDist)
		{
			this.$st_message("Distance to target must be at least " + this.$st_minDist + " m.", 2);
			return false;
		}			
		// Check if escort ships are aligned correctly.
		if (this.$st_nbEscorts > 0 && !this.$st_escortAligned(this.$st_npc.heading))
		{
			this.$st_message("Escort not aligned properly.", 2);
			return false;
		}			
		// Check for planet and star mass locks
		// The reason for doing it this way is that player.alertMassLocked can't be used at 
		// this stage, even if these lines would stand behind the change of npc scan class.
        if (this.st_checkForPlanetMassLock(ps))
        {
			this.$st_message("Mass-locked by planet.", 2);
			return false;
		}	
        else if (this.st_checkForSunMassLock(ps))
        {
			this.$st_message("Mass-locked by sun.", 2);
			return false;
		}	
		
		// To prevent the player being mass-locked we have to give the npc the scan class "rock". 
		// $st_setScanClass() will also turn the scanner lollipop to flashing yellow and magenta (like in Escort Contracts).
		// Side effects:
		// - The player status will change from yellow to green if no other ships are present
		// - The legal status will not be shown during torus driving when using the Scanner Targeting Enhancement.
		// Escorts, if present, will be "rocked" too, but they don't get a different colour.
		this.$st_setScanClass(); 
    }
    // The following conditions are checked continuously during torus driving
	// Torus synchronisation makes only sense over larger distances, so the distance to  
	// destination of npc ship must be outside scanner range.
	var distance = this.$st_npc.position.distanceTo(this.$st_npc.destination); 			
	if (distance < ps.scannerRange)			
	{
		if (calledBy === 1) 
			this.$st_message("Next waypoint too close (" + Math.round(distance/1000) + " km).", 2);
		else
			this.$st_message("Waypoint reached.", 2);
		return false;
	}	
	else if (calledBy === 2)
	{
		// show (remaining) distance in scanner targeting description 
		this.$st_npc.scanDescription  = "waypoint: " + Math.round(distance/1000) + " km";
		
	}		
	// The player and in some cases the npc can change heading (but not velocity) 
	// during torus driving, so this has to be checked continuously. 
	if(this.$st_npc.heading.dot(ps.heading) < this.$st_headingAlign)
	{
		this.$st_message("Heading mismatch.", 2);
		return false;
	}			
	// Check for player mass lock (check returns true when done immediately after changing the npc ship(s) to rock).
	if (calledBy != 1 && player.alertMassLocked) 
	{
		this.$st_message("Mass-locked.", 2);
		return false;
	}	
	// Check for npc mass lock. 
	var targets = system.filteredEntities(this, st_checkForMassLock, this.$st_npc, this.$st_npc.scannerRange);
	if (targets.length > 0) 
	{
		this.$st_message("Target mass-locked.", 2);
		return false;
	}	
	// Check for npc planet and star mass locks
	if (this.st_checkForPlanetMassLock(this.$st_npc))
	{
		this.$st_message("Target mass-locked by planet.", 2);
		return false;
	}	
	else if (this.st_checkForSunMassLock(this.$st_npc))
	{
		this.$st_message("Target mass-locked by sun.", 2);
		return false;
	}	
	// If player engages normal torus drive or injectors, synchronisation will be stopped.
	// Player will be mass locked by npc ship shortly after this. 
	// Note: Torus driving does _not_ change speed, but using injectors does!
	if (ps.torusEngaged || ps.speed > ps.maxSpeed) 
	{
		return false;
	}	
	// Everything is (still) ok  
	return true;
};	
// Check for entities other than the player mass-locking the npc.
this.st_checkForMassLock = function(entity) 
{
	return ((entity.scanClass === "CLASS_NEUTRAL" || entity.scanClass === "CLASS_MILITARY" || entity.scanClass === "CLASS_POLICE" || entity.scanClass === "CLASS_THARGOID" || entity.scanClass === "CLASS_STATION") && !entity.isPlayer);
};
// Check mass-lock distance for sun 
this.st_checkForSunMassLock = function(ship) 
{
	return (ship.position.distanceTo(system.sun.position) < system.sun.radius * 1.4142136);
};
// Check mass-lock distance for planets 
this.st_checkForPlanetMassLock = function(ship) 
{
    for (var i = 0; i < system.planets.length; i++)	
	{
		if (ship.position.distanceTo(system.planets[i].position) < system.planets[i].radius + Math.max(system.planets[i].radius, 25600))
		{
			return true;
		}				
	}
	return false;
};
// Stop torus, called by pressing n key (1) or by timer function (2). 
this.$st_stopTorus = function(calledBy)
{
	this.$st_timer.stop();
	worldScripts["Synchronised_Torus"].$st_torusEngaged = false;      
    // Stop velocity synchronisation.
	this.$st_removeFrameCallback(); 	
	// Bring ships back to normal speed.
	this.$st_npc.velocity = this.$st_npc.thrustVector; 
	player.ship.velocity = player.ship.thrustVector; 
	if (this.$st_nbEscorts > 0) this.$st_setEscortVelocity(null);
    // Reset scan class and lollipop colours.	
	this.$st_resetScanClass(); 
};	
// Set velocity of escort ships or restore to normal. 
this.$st_setEscortVelocity = function(v)	
{
	for (var i = 0; i < this.$st_nbEscorts; i++)
	{	
		if (v) 
			this.$st_npc.escorts[i].velocity = v;
		else
			this.$st_npc.escorts[i].velocity = this.$st_npc.escorts[i].thrustVector;
	}
};	
// Check if all escort ships are aligned correctly. 
this.$st_escortAligned = function(heading)	
{
	for (var i = 0; i < this.$st_nbEscorts; i++)
	{	
		if(this.$st_npc.escorts[i].heading.dot(heading) < this.$st_headingAlign)  
			return false;
	}
	return true;
};	
// Set scan class to prevent mass locking.
this.$st_setScanClass = function()
{
	if (this.$st_npc)
	{	
		// Save for resetting. It can be assumed that escorts have the same class.	
		// We should save the colours too because these could have been changed by an OXP.
		// Scan description will be used to show remaining distance to waypoint.
		this.$st_scanClass   = this.$st_npc.scanClass;  
		this.$st_scanColour1 = this.$st_npc.scannerDisplayColor1;
		this.$st_scanColour2 = this.$st_npc.scannerDisplayColor2;
		this.$st_scanDescription = this.$st_npc.scanDescription;
		this.$st_npc.scanClass = "CLASS_ROCK"; 		
		this.$st_npc.scannerDisplayColor1 =	"yellowColor";	// same colours are used in Escort Contracts
		this.$st_npc.scannerDisplayColor2 =	"magentaColor";		
		for (var i = 0; i < this.$st_nbEscorts; i++)
		{	
			this.$st_npc.escorts[i].scanClass = "CLASS_ROCK"; 		
			// The escorts should not flash. Note: The saved colour values are usually null, 
			// so using them here will turn the escorts white, the default for scan class "rock".
			if (this.$st_scanClass === "CLASS_POLICE" || this.$st_scanClass === "CLASS_MILITARY")
			{
				this.$st_npc.escorts[i].scannerDisplayColor1 =	"purpleColor";	
				this.$st_npc.escorts[i].scannerDisplayColor2 =	"purpleColor";		
			}		
			else
			{
				this.$st_npc.escorts[i].scannerDisplayColor1 =	"yellowColor";	
				this.$st_npc.escorts[i].scannerDisplayColor2 =	"yellowColor";		
			}		
		}
	}	
};
// Reset scan class after stopping torus driving or after a failed starting attempt.
this.$st_resetScanClass = function()
{
	// only reset if it has been changed already
	if (this.$st_npc && this.$st_scanClass != "")
	{	
		this.$st_npc.scanClass = this.$st_scanClass; 		
		this.$st_npc.scannerDisplayColor1 =	this.$st_scanColour1; 		
		this.$st_npc.scannerDisplayColor2 =	this.$st_scanColour2;		
		this.$st_npc.scanDescription = this.$st_scanDescription;
		for (var i = 0; i < this.$st_nbEscorts; i++)
		{	
			this.$st_npc.escorts[i].scanClass = this.$st_scanClass; 		
			this.$st_npc.escorts[i].scannerDisplayColor1 = this.$st_scanColour1;
			this.$st_npc.escorts[i].scannerDisplayColor2 = this.$st_scanColour2;
		}
		this.$st_scanClass = "";
	}	
};
// Timer function, checks if conditions are still ok.
this.$st_torusDriving = function()
{
	if (!this.$st_checkConditions(2))
	{
		this.$st_stopTorus(2);
	}
	else
	{
		// Velocity reduces slowly, so we have to keep it up.
		this.$st_npc.velocity = this.$st_npc.heading.multiply(this.$st_npc.maxSpeed * 32); 
		player.ship.velocity = this.$st_npc.velocity;
		if (this.$st_nbEscorts > 0) this.$st_setEscortVelocity(this.$st_npc.velocity);	
	}		
};
	
// Frame callback function for adjusting velocities continuously. 
this.$st_matchVelocity = function() 
{
	player.ship.velocity = this.$st_npc.velocity;
	if (this.$st_nbEscorts > 0) this.$st_setEscortVelocity(this.$st_npc.velocity);	
};	
// Remove frame callback function. 
this.$st_removeFrameCallback = function() 
{
	if (this.$st_frameCallback)
	{
		removeFrameCallback(this.$st_frameCallback);
		delete this.$st_frameCallback;
	}
};
// Console message with no sound (0), beep (1), or boop (2).
this.$st_message = function(msg, beep)
{
	if (beep > 0)
	{
		this.$st_beep.sound = this.$st_beepSounds[beep];
		this.$st_beep.play();
	}	
	player.consoleMessage(msg);
};	
// Initialise timer and sound.
this.$st_init = function()
{
	// initialise timer
	this.$st_timer =  new Timer(this, this.$st_torusDriving, -1, this.$st_timerInterval);  
	this.$st_timerRunning = true;
	// initialise beep sounds
	this.$st_beepSounds[1] = Sound.load("beep.ogg");   // on
	this.$st_beepSounds[2] = Sound.load("boop.ogg");   // off, interrupted, not possible
	this.$st_beep = new SoundSource;  
	// this.$st_beep.sound = this.$st_beepSounds[1];  
	this.$st_beep.volume = 1;  
	this.$st_beep.loop = false;  
};	
 |