Back to Index Page generated: Dec 20, 2024, 7:22:09 AM

Expansion Hired Guns

Content

Warnings

  1. Unknown key 'upload_date' at https://wiki.alioth.net/img_auth.php/c/ca/Oolite.oxp.Thargoid.HiredGuns.oxz!manifest.plist

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description A chance to agree a contract for two escort ships to accompany you on your next journey. A chance to agree a contract for two escort ships to accompany you on your next journey.
Identifier oolite.oxp.Thargoid.HiredGuns oolite.oxp.Thargoid.HiredGuns
Title Hired Guns Hired Guns
Category Weapons Weapons
Author Thargoid Thargoid
Version 2.02 2.02
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Information URL http://wiki.alioth.net/index.php/Hired_Guns_OXP n/a
Download URL https://wiki.alioth.net/img_auth.php/c/ca/Oolite.oxp.Thargoid.HiredGuns.oxz http://wiki.alioth.net/img_auth.php/c/ca/Oolite.oxp.Thargoid.HiredGuns.oxz
License Creative Commons Attribution - Non-Commercial - Share Alike 3.0 license with clauses - see readme file Creative Commons Attribution - Non-Commercial - Share Alike 3.0 license with clauses - see readme file
File Size n/a
Upload date 1610873473

Documentation

Also read http://wiki.alioth.net/index.php/Hired%20Guns

Hired Guns v2 ReadMe & License.txt

==================================================================

Hired Guns OXP by Thargoid, modified by UK_Eliter

[Nearly all of what follows in this file was written by Thargoid.]

==================================================================

A small OXP that selectively offers the player a chance to hire a pair of escort ships for a single journey (from main station in one system to the main station of another one, via whatever route). The ships will follow you through space, and will fight along side you against any alien or hostile vessels that you may encounter. 

The type of ship available as an escort will depend on the tech level of the system, and better ships command a higher hiring fee.

--------------------------------------------------------------

Conditions of the hire contract (small print):

* Escort will only be offered to commanders of a good standing and with a clean criminal record.
* This is not a babysitting service, only commanders of at least Average rank will be offered contract terms.
* Availability of escort ships for hire is not guaranteed, although the chances are better for more lawful government systems.
* This service only available at the main system station.
* Once hired, escorts will serve until you reach the station aegis of any other main station than the one you hired them from.
* Jumping into a new galaxy will void this contract, any hired escorts will not follow.
* The type of ship available as an escort will depend on the tech level of the system they are hired in.
* The escorts will work with the commander under their orders (unless they deem them to be illegal or immoral). 
* Any combat kills and bounty are transferred to the hirer, as is responsibility for any legal ramifications that may arise.

--------------------------------------------------------------

License:

This OXP is released under the Creative Commons Attribution - Non-Commercial - Share Alike 3.0 license with the following clauses:

* Whilst you are free (and encouraged) to re-use any of the scripting, models or texturing in this OXP, the usage must be distinct from that within this OXP. Unique identifiers such as (but not limited to) unique shipdata.plist entity keys, mission variables, script names (this.name), equipment identity strings (EQ_), description list arrays and entity roles must not be re-used without prior agreement. Basically if it's unique or would identify or overwrite anything in the original OXP, then you may not re-use it (for obvious compatibility reasons).
* rebundling of this OXP within another distribution is permitted as long as it is unchanged. The following derivates however are permitted and except from the above:
	* the conversion of files between XML and openStep.
	* the merging of files with other files of the same type from other OXPs.
* The license information (either as this file or merged into a larger one) must be included in the OXP.
* Even though it is not compulsory, if you are re-using any sizable or recognisable piece of this OXP, please let me know :) (where 'me' now means: UK_Eliter).

--------------------------------------------------------------

Instructions:

Unzip the file, and then move the folder "Hired Guns 1.26.oxp" to the AddOns directory of your Oolite installation. Then start the game up whilst holding down the shift key (until the spinning Cobra Mk III screen appears).

--------------------------------------------------------------

Version history:

09/04/2009 - Version 1.00, Initial release.
01/09/2009 - Version 1.10, script tweak to be compatible with Oolite 1.73.
13/04/2010 - Version 1.20, script change to use 1.74 code (now incompatible with <1.74) - ships are yellow on scanner.
05/07/2010 - Version 1.21, small script tweak to prevent breaking constrictor mission if escort kills it.
19/07/2010 - Version 1.22, another small adjustment to make escorts ignore derelicts (to stop salvage missile misunderstandings).
05/09/2010 - Version 1.23, added check during combat for targets ejecting. If so (if target becomes a derelict) then break off the combat.
13/02/2011 - Version 1.24, removal of upper limit, to allow running with 1.75.
19/02/2011 - Version 1.25, lollipop tweak.
29/01/2012 - Version 1.26, stop player cheating by using HG's to shoot inanimate objects.
21/09/2018 - Version 1.3: implemented attempts to reduce the frequency at which the escorts kill themselves via collision.
09/05/2019 - Version 2.0: small bug fixes; the escorts now behave more intelligently; other, miscellaneous small improvements. This version of the expansion pack requires version >= 1.79 of Oolite.

--------------------------------------------------------------

Acknowledgements:

The escort AI is based on the private escort hire ship AI from UPS courier OXP, by Eric Walch.
UK_Eliter thanks phkb and rustem for the code and advice that they provided.

Equipment

Name Visible Cost [deci-credits] Tech-Level
Hire Escorts yes 15000 4+
Hire Escorts yes 7500 4+
Hire Escorts yes 10000 4+

Ships

Name
Hired Escort
Hired Escort
Hired Escort

Models

This expansion declares no models. This may be related to warnings.

Scripts

Path
Scripts/hiredGuns_escort.js
/*
=================================
HEADER
=================================
*/

this.name					= "hiredGuns_escort";
this.author					= "Thargoid";
this.copyright				= "Creative Commons: attribution, non-commercial, sharealike.";
this.description			= "AI-actions script for the escorts to the player ship";
this.version				= "1.1";

/*
============================
Start of wrapper for JSHINT
============================
*/

(
	function (){


/*
============================
DIRECTIVE
============================
*/

"use strict";

/*
========
GLOBALS
========
*/

this.debug = false; 					// There is a 'this.debug' also in hiredGuns_system.js.

var consoleDebugMessages;
if (this.debug === true) {
	this.logging = true;
	consoleDebugMessages = true;
}
else {
	consoleDebugMessages = false;		// This does need setting to something, even when debugging is off.
}


/*
=========
FUNCTIONS
=========
*/

/*
--------------
Debugging
--------------
*/

this.$dbg = function(msg)
{
	log(this.name,"DEBUG: " + msg);
};


/*
--------------
Event handlers
--------------
*/

this.shipSpawned = function ()
{
	if (this.debug) {
		this.ship.reportAIMessages = true;
		this.$dbg("Debugging ON");
	}

	// Initialise an important global variable
	this.shipStats = {};

	// Assign 'sensitivity' and heat insulation to the ship.
	var r = Math.random();
	if (this.ship.hasRole("hiredGuns_escortHigh")) {
		if (r < 0.3) {
			this.shipPrudence = 1.25;
		}
		else if (r < 0.6) {
			this.shipPrudence = 1.2;
		}
		else {
			this.shipPrudence = 1.15;
		}
		this.ship.heatInsulation = 2.5; // Speeds of the escorts mean they will get hot.
	}
	else {
		if (r < 0.2) {
			this.shipPrudence = 1.2;
		}
		else if (r < 0.4) {
			this.shipPrudence = 1.05;
		}
		else {
			this.shipPrudence = 1;
		}
		this.ship.heatInsulation = 2;
	}
	if (this.debug) { this.$dbg("heatInsulation = " + this.ship.heatInsulation); }
	delete this.shipSpawned;
};

this.shipTargetDestroyed = function(target)
{
	// Just in case an escort kills the constrictor, let's not break the mission for the player...
	if (target.primaryRole == "constrictor" && missionVariables.conhunt && missionVariables.conhunt == "STAGE_1") {
		missionVariables.conhunt = "CONSTRICTOR_DESTROYED";
	}
	if (target.isRock || target.isBoulder || target.isCargo || target.isDerelict || target.isWeapon) {
		return;
	}
	player.score += 1;
	player.credits += target.bounty;
	player.consoleMessage("Escort kill - " + target.name + " : " + target.bounty + "₢ awarded.", 5);
	log("Escort kill - " + target.name + " : " + target.bounty);
};

this.shipDied = function(whom,why)
{
	if (whom && whom.isValid) {
		player.commsMessage("Escort terminated.", 6);
	}
	missionVariables.hiredGuns_count -= 1;
};


/*
------------------------
Temperature AND altitude
------------------------
*/

this.$checkTemperatureOrAltitude = function ()
{
	if (this.debug) { this.$dbg("checkTemperatureOrAltitude"); }
	// Anything to do?
	if (
		! this.ship || ! this.ship.isValid || (player.ship && player.ship.isValid && player.ship.docked === true)
		) {
		if (this.debug) { this.$dbg("Nothing to do"); }
		this.ship.reactToAIMessage("PROCEED");
		return;
	}
	this.$setShipStats();
	if (Math.random() < 0.5) {
		this.$checkTemperature();
	}
	else {
		this.$checkAltitude();
	}
};

/*
----------------------
Temperature
----------------------
*/

// This function or its children must tell the AI to STALL or FLEE or else PROCEED.
// Needs this.$setShipStats() to have been called already.
this.$checkTemperature = function ()
{
	if (this.debug) { this.$dbg("checkTemperature"); }
	var escortTemperature = this.ship.temperature;
	if (this.debug) { this.$dbg("temperature_timed: escortTemperature = " + escortTemperature); }
	if (escortTemperature < 0.6) {
		if (this.debug) { this.$dbg("temperature - below threshold"); }
		this.ship.reactToAIMessage("PROCEED");
		return;
	}
	if (escortTemperature > 0.85) { 
		this.$temperature_veryHigh();
	}
	else { this.$temperature_high(); }
};

this.$temperature_veryHigh = function ()
{
	var isHeadingInto;

	if (this.shipStats === null) {
		isHeadingInto = false;
	}
	else {
		isHeadingInto = this.shipStats.isHeadingInto;
	}

	var r = Math.random() * this.shipPrudence;

	if (this.debug) { this.$dbg("temperature_veryHigh"); }

	if (isHeadingInto === true) {
		if (this.debug) { this.$dbg("Heading is TOWARDS body; action is .."); }
		if (r < 0.1) {
			if (this.debug) { this.$dbg(".. none"); }
			this.ship.reactToAIMessage("PROCEED");
		} else if (r < 0.35) {
			if (this.debug) { this.$dbg(".. message"); }
			this.$temperature_veryHigh_messaging();
			this.ship.reactToAIMessage("PROCEED");
		}
		else if (r < 0.6) {
			if (this.debug) { this.$dbg("..flee"); }
			this.$flee();
		}
		else {
			if (this.debug) { this.$dbg("..message and flee"); }
			this.$flee();
			this.$temperature_veryHigh_messaging();
		}
	}
	else {
		if (this.debug) { this.$dbg("Heading is AWAY from body; action is .."); }
		if (r < 0.1) {
			if (this.debug) { this.$dbg(".. none"); }
			this.ship.reactToAIMessage("PROCEED");
		} else if (r < 0.35) {
			if (this.debug) { this.$dbg(".. message"); }
			this.$temperature_veryHigh_messaging();
			this.ship.reactToAIMessage("PROCEED");
		}
		else if (r < 0.6) {
			if (this.debug) { this.$dbg("..flee"); }
			this.$flee();
		}
		else {
			if (this.debug) { this.$dbg("..message and flee"); }
			this.$flee();
			this.$temperature_veryHigh_messaging();
		}
	}
};

this.$temperature_high = function ()
{
	var isHeadingInto;

	if (this.shipStats === null) {
		isHeadingInto = false;
	}
	else {
		isHeadingInto = this.shipStats.isHeadingInto;
	}

	var r = Math.random() * this.shipPrudence;

	if (this.debug) { this.$dbg(".. temperature_high"); }

	if (isHeadingInto === true) {
		if (this.debug) { this.$dbg("Heading is TOWARDS body; action is .."); }
		if (r < 0.2) {
			if (this.debug) { this.$dbg(".. none"); }
			this.ship.reactToAIMessage("PROCEED");
		} else if (r < 0.3) {
			// Do message only
			this.$temperature_high_messaging();
			if (this.debug) { this.$dbg(".. message"); }
			this.ship.reactToAIMessage("PROCEED");
		}
		else if (r < 0.45) {
			if (this.debug) { this.$dbg(".. stall"); }
			this.ship.reactToAIMessage("STALL");
		}
		else {
			if (this.debug) { this.$dbg("message and stall"); }
			this.$temperature_high_messaging();
			this.ship.reactToAIMessage("STALL");
		}
	}
	else {
		if (this.debug) { this.$dbg("Heading is AWAY from body; action is .."); }
		if (r < 0.6) {
			if (this.debug) { this.$dbg(".. none"); }
			this.ship.reactToAIMessage("PROCEED");
		} else {
			this.$temperature_high_messaging();
			if (this.debug) { this.$dbg(".. message"); }
			this.ship.reactToAIMessage("PROCEED");
		}
	}
};


/*
----------------------
Altitude
----------------------
*/

// This function or its children must tell the AI to STALL or FLEE or else PROCEED.
// Needs this.$setShipStats() to have been called already (but check that variable here).
this.$checkAltitude = function ()
{
	if (this.debug) { this.$dbg("checkAltitude"); }
	if (this.shipStats === null) {
		if (this.debug) { this.$dbg(".. no celestial bodies"); }
		this.ship.reactToAIMessage("PROCEED");
		return;
	}
	if (this.shipStats.altitude < 8150) { this.$altitude_veryLow(); }
	else if (this.shipStats.altitude < 17500) { this.$altitude_low(); }
	else {
		if (this.debug) { this.$dbg(".. altitude above thresholds i.e. is OK"); }
		this.ship.reactToAIMessage("PROCEED");
	}
};

this.$altitude_veryLow = function ()
{
	var isHeadingInto = this.shipStats.isHeadingInto;
	var r = Math.random() * this.shipPrudence;

	if (this.debug) { this.$dbg(".. altitude_veryLow"); }
	
	if (isHeadingInto === true) {
		if (this.debug) { this.$dbg("Heading is TOWARDS body; action is .."); }
		if (r < 0.06) {
			if (this.debug) { this.$dbg(".. none"); }
			this.ship.reactToAIMessage("PROCEED");
		}
		else if (r < 0.25) {
			this.$altitude_low_messaging();
			if (this.debug) { this.$dbg(".. send message"); }
			this.ship.reactToAIMessage("PROCEED");
		}
		else if (r < 0.5) {
			if (this.debug) { this.$dbg(".. stall"); }
			this.ship.reactToAIMessage("STALL");
		}
		else if (r < 0.85) {
			// Flee with message
			if (this.debug) { this.$dbg(".. message and flee"); }
			this.$flee();
			this.$altitude_low_messaging();
		}
		else {
			// Flee without message
			if (this.debug) { this.$dbg(".. flee"); }
			this.$flee();
		}
	}
	else {
		if (this.debug) { this.$dbg("Heading is NOT towards body"); }
		if (r < 0.15) {
			if (this.debug) { this.$dbg(".. none"); }
			this.ship.reactToAIMessage("PROCEED");
		}
		else if (r < 0.45) {
			this.$altitude_low_messaging();
			if (this.debug) { this.$dbg(".. send message"); }
			this.ship.reactToAIMessage("PROCEED");
		}
		else if (r < 0.9) {
			// Flee with message
			if (this.debug) { this.$dbg(".. message and flee"); }
			this.$flee();
			this.$altitude_low_messaging();
		}
		else {
			// Flee without message
			if (this.debug) { this.$dbg(".. flee"); }
			this.$flee();
		}
	}
};

this.$altitude_low = function ()
{
	var isHeadingInto = this.shipStats.isHeadingInto;
	var r = Math.random() * this.shipPrudence;

	if (this.debug) { this.$dbg(".. altitude_low"); }

	// r = 1; // TEST
	if (isHeadingInto === true) {
		if (this.debug) { this.$dbg("Heading is TOWARDS body; action is .."); }
		if (r < 0.15) {
			if (this.debug) { this.$dbg(".. none"); }
			this.ship.reactToAIMessage("PROCEED");
			return;
		} else if (r < 0.3) {
			// Do message only
			this.$altitude_low_messaging();
			if (this.debug) { this.$dbg(".. message"); }
			this.ship.reactToAIMessage("PROCEED");
		}
		else if (r < 0.45) {
			if (this.debug) { this.$dbg(".. stall"); }
			this.ship.reactToAIMessage("STALL");
		}
		else {
			this.$altitude_low_messaging();
			if (this.debug) { this.$dbg("message and stall"); }
			this.ship.reactToAIMessage("STALL");
		}
	}
	else {
		if (this.debug) { this.$dbg("Heading is NOT towards body; action is .."); }
		if (r < 0.6) {
			if (this.debug) { this.$dbg(".. none"); }
			this.ship.reactToAIMessage("PROCEED");
		} else {
			this.$altitude_low_messaging();
			if (this.debug) { this.$dbg(".. message"); }
			this.ship.reactToAIMessage("PROCEED");
		}
	}
};


/*
----------------------
Altitude and temperature
(lower-level routines)
----------------------
*/

this.$altitudeOverPlanetaryBody = function $altitudeOverPlanetaryBody(body, ship)
{
	var shippos = ship.position;
	if (!shippos) return -1;
	return shippos.distanceTo(body) - body.radius - ship.collisionRadius;
};

// Gets:
// the escort ship's distance from nearest celestial body;
// what type of body that nearest one is;
// whether the *player* is heading into or away from the body.
this.$setShipStats = function ()
{
	var alt;
	var bodyType = null;
	var closest = null;
	var deviation = 1;
	var ent = [];
	var isHeadingInto = false;
	var ship = this.ship;
	var smallest = -1;
	var l;

	if (system.isInterstellarSpace === false && system.sun) ent.push(system.sun);
	ent = ent.concat(system.planets);
	l = ent.length;
	if (l === 0) {
		this.shipStats = null;
		if (this.debug) {
			this.$dbg("Are no celestial bodies");
		}
	}
	else {
		for (var i = 0; i < l; i++) {
			alt = this.$altitudeOverPlanetaryBody(ent[i], ship);
			if (smallest === -1 || (alt != -1 && alt < smallest)) { smallest = alt; closest = ent[i]; }
		}
		if (closest) {
			bodyType = closest.toString().substring(1, 4);
			deviation = player.ship.vectorForward.angleTo(closest.position.subtract(player.ship.position));
			if ((deviation < 0.2) && (player.ship.speed > 0)) {
				isHeadingInto = true;
			}
		}

		// Set globals
		this.shipStats.altitude = smallest;
		this.shipStats.body = closest;
		this.shipStats.bodyType = bodyType;
		this.shipStats.isHeadingInto = isHeadingInto;

		if (this.debug) {
			this.$dbg("this.shipStats.altitude = "+this.shipStats.altitude);
			this.$dbg("this.shipStats.body = "+this.shipStats.body);
			this.$dbg("this.shipStats.bodyType = "+this.shipStats.bodyType);
			this.$dbg("this.shipStats.isHeadingInto = "+this.shipStats.isHeadingInto);
		}
	}
};


/*
----------------------
Altitude & temperature
- messaging
----------------------
*/

this.$altitude_low_messaging = function ()
{
	var		msg_main;
	var		msg_salutation;
	var		msg;
	var		picker;

	msg_salutation = this.$msg_getSalutation();

	// Determine main message
	picker = Math.ceil(Math.random() * 40);
	switch (picker) {
		case 1:		msg_main = "Aren't we a bit low here?"; break;
		case 2:		msg_main = "Are you sure about this?"; break;
		case 3:		msg_main = "We are quite close to the surface!"; break;
		case 4:		msg_main = "Pull up?"; break;
		case 5:		msg_main = "Altitude low!"; break;
		case 6:		msg_main = "Low altitude!"; break;
		case 7:		msg_main = "I can't land on that!"; break;
		case 8:		msg_main = "Too low!"; break;
		case 9:		msg_main = "Too low?"; break;
		case 10:	msg_main = "We're getting a bit low."; break;
		case 11:	msg_main = "We're rather low."; break;
		case 12:	msg_main = "I'm worried."; break;
		case 13:	msg_main = "Aren't we rather low?"; break;
		case 14:	msg_main = "Are you entirely sure about this?"; break;
		case 15:	msg_main = "I am not so sure about this."; break;
		case 16:	msg_main = "I suggest pulling up."; break;
		case 17:	msg_main = "Altitude low."; break;
		case 18:	msg_main = "Altitude low!"; break;
		case 19:	msg_main = "Altitude very low!"; break;
		case 20:	msg_main = "Altitude low!"; break;
		case 21:	msg_main = "Low altitude!"; break;
		case 22:	msg_main = "Low altitude!"; break;
		case 23:	msg_main = "Do we want to make friends with the ground?"; break;
		case 24:	msg_main = "I can't land on that!"; break;
		case 25:	msg_main = "Too low!"; break;
		case 26:	msg_main = "Pull up!"; break;
		case 27:	msg_main = "I don't like this."; break;
		case 28:	msg_main = "Are you trying to land?"; break;
		case 29:	msg_main = "I feel we are a bit close."; break;
		case 30:	msg_main = "This is cutting it a bit fine."; break;
		default:	msg_main = "Altitude is low!"; break;
	}
	this.$msg_assembleAndIssue(msg_salutation,msg_main);
};

this.$temperature_veryHigh_messaging = function ()
{
	var		msg_main;
	var		msg_salutation;
	var		msg;
	var		picker;

	msg_salutation = this.$msg_getSalutation();

	// Determine main message
	picker = Math.ceil(Math.random() * 27);
	switch (picker) {
		case 1:		msg_main = "Aren't we getting very hot?"; break;
		case 2:		msg_main = "Are you entirely sure about this?"; break;
		case 3:		msg_main = "I am not at all sure about this."; break;
		case 4:		msg_main = "You could fry an egg on my ship!"; break;
		case 6:		msg_main = "Temperature high!"; break;
		case 7:		msg_main = "High temperature!"; break;
		case 8:		msg_main = "Too hot!"; break;
		case 9:		msg_main = "I am getting very hot!"; break;
		case 10:	msg_main = "I'm getting grilled!"; break;
		case 11:	msg_main = "Do we want to make cook ourselves?"; break;
		case 12:	msg_main = "I don't have enough temperature shielding!"; break;
		case 13:	msg_main = "Too hot!"; break;
		case 14:	msg_main = "Too hot!"; break;
		case 15:	msg_main = "Let's get out of here!"; break;
		case 16:	msg_main = "I don't like this."; break;
		case 17:	msg_main = "Temperature high!"; break;
		case 18:	msg_main = "I'm going to explode!"; break;
		case 19:	msg_main = "I'm worried."; break;
		case 20:	msg_main = "This is cutting it rather fine."; break;
		default:	msg_main = "Temperature very high!"; break;
	}
	this.$msg_assembleAndIssue(msg_salutation,msg_main);
};

this.$temperature_high_messaging = function ()
{
	var		msg_main;
	var		msg_salutation;
	var		msg;
	var		picker;

	if (this.debug) {
		this.$dbg("temperature_high_messaging");
	}

	msg_salutation = this.$msg_getSalutation();

	// Determine main message
	picker = Math.ceil(Math.random() * 20);
	switch (picker) {
		case 1:		msg_main = "Aren't we getting a bit hot here?"; break;
		case 2:		msg_main = "Are you sure about this?"; break;
		case 3:		msg_main = "This is toasty!"; break;
		case 4:		msg_main = "You might want to go somewhere cooler!"; break;
		case 5:		msg_main = "Temperature high!"; break;
		case 6:		msg_main = "The temperature is high!"; break;
		case 7:		msg_main = "My temperature shielding isn't that great."; break;
		case 8:		msg_main = "Is this in the contract?"; break;
		case 9:		msg_main = "High temperature!"; break;
		case 10:	msg_main = "Are you watching the temperature?"; break;
		case 11:	msg_main = "Too hot?"; break;
		case 12:	msg_main = "Should we get out of here?"; break;
		case 13:	msg_main = "We're getting a bit hot."; break;
		case 14:	msg_main = "Er, it's a bit hot."; break;
		case 15:	msg_main = "It's hot here!"; break;
		default:	msg_main = "High temperature!"; break;
	}
	this.$msg_assembleAndIssue(msg_salutation,msg_main);
};

/*
----------------------
Altitude & temperature
- Fleeing actions
----------------------
*/

this.$flee = function ()
{
	if (! this.ship || ! this.ship.isValid) { return; }
	if (this.shipStats === null) {
		var position = new Vector3D(ps);
		var x = Math.random() * 5000; var y = Math.random() * 5000; var z = Math.random() * 5000;
		position = this.ship.position.add([x, y, z]);
		this.ship.destination = position;
	}
	else {
		this.ship.destination = this.$positionAwayFromEntity(this.ship,this.shipStats.body);
	}
	this.ship.reactToAIMessage("FLEE");
};

// This function is called by the AI.
this.$flee_stage2 = function ()
{
	if (! this.ship || ! this.ship.isValid) { return; }
	// var range = 15000 + Math.ceil(Math.random() * 10000);
	// this.ship.desiredRange = this.shipStats.body.radius + this.shipStats.altitude + range;
	// this.ship.target = null;

	// NB: desiredRange is from the point being fled *to*; see also $positionAwayFromEntity.
	if (this.shipStats.bodyType === "Sun") {
		// SUN
		this.ship.desiredRange = Math.ceil(Math.random() * 10000);
		this.ship.desiredSpeed = this.ship.maxSpeed * 7;
	} else
	{
		// PLANET
		this.ship.desiredRange = 16000;
		// If fleeing planet, need not go as far away as when fleeing sun.
		// Also, if send ship too far away, it might use injectors to get back, and crash.
		if (Math.random() < 0.7) {
			this.ship.desiredSpeed = this.ship.maxSpeed * 0.5;
		} else {
			this.ship.desiredSpeed = this.ship.maxSpeed;
		}
		// Less need with planet for speed and also high speed in atmosphere raises heat.
	}
	this.ship.performFlyToRangeFromDestination();
};

// returns a position that's the same distance as between the ship and the entity, but in the opposite direction
// (and with some randomisation of position). 
this.$positionAwayFromEntity = function $positionAwayFromEntity(ship, entity) {
	return Vector3D.interpolate(ship.position.add(Vector3D.random(10E3)), entity.position, -1);
};


/*
--------------
AI, Misc.
--------------
*/

this.$locatePlayer = function ()
{
	this.playerArray = system.shipsWithPrimaryRole("player");
	if (this.playerArray.length > 0) {
		this.ship.target = this.playerArray[0];
		this.ship.reactToAIMessage("PLAYER_FOUND");
	}
};

this.$checkPlayerDistance = function ()
{
	if (!player.ship){ return; }
	this.playerDistance = this.ship.position.distanceTo(player.ship.position);
	// If the escort is more than 2 scanner ranges from the player, move it just off-scanner.
	if (this.playerDistance > 51200) {
		var playerDirection = player.ship.position.subtract(this.ship.position).direction(); 
		var newPosition = player.ship.position.subtract(playerDirection.multiply(30000));
		this.ship.position = newPosition;
		this.ship.reactToAIMessage("PLAYER_MID");
		return;
	}

	if (this.playerDistance < 25600) {
		this.ship.reactToAIMessage("PLAYER_NEAR");
	}
	else {
		if (this.playerDistance > 38400 || player.ship.speed > 2000) {
			this.ship.reactToAIMessage("PLAYER_FAR");
		}
		else {
			this.ship.reactToAIMessage("PLAYER_MID");
		}
	}
};

this.$combatCheck = function ()
{
	if (this.ship.target && this.ship.target.isDerelict) {
		this.ship.target = null;
		this.ship.reactToAIMessage("TARGET_EJECTED");
	}
	this.playerDistance = this.ship.position.distanceTo(player.ship.position);
	// If the escort is more than 2 scanner ranges from the player, break off combat
	if (this.playerDistance > 51200) {
		this.ship.AIState = "LOCATE_PLAYER";
	}
	else {
		this.ship.reactToAIMessage("ENEMY_FIRE");
	}
};

this.$isHostileToPlayer = function(entity) 
{
	return (entity.isThargoid || (entity.isShip && entity.target && entity.target == player.ship && entity.hasHostileTarget && !entity.isDerelict));
};

this.$findPlayerHostiles = function ()
{
	if (this.ship.target == player.ship && player.ship.target && player.ship.target.bounty > 2) {
		this.ship.target = player.ship.target;
		this.ship.reactToAIMessage("HOSTILE_FOUND");
	}
	else {
		var targets = system.filteredEntities(this, this.$isHostileToPlayer, this.ship, this.ship.scannerRange);
		if (targets.length > 0) {
			this.ship.target = targets[0];
			this.ship.reactToAIMessage("HOSTILE_FOUND");
		}
		else {
			this.ship.reactToAIMessage("NO_HOSTILE_FOUND");
		}
	}
};

this.$fireCheck = function ()
{
	if ((this.ship.target.hasRole("hiredGuns_escort")) || (player.ship.target == this.ship && this.ship.target.isPlayer)) {
		this.ship.reactToAIMessage("FRIENDLY_FIRE");
	}
	else {
		this.ship.reactToAIMessage("ENEMY_FIRE");
	}
};

this.$switchID = function ()
{
	this.ship.switchAI("route1patrolAI.plist");
	this.ship.displayName = "Bounty Hunter";
	this.ship.primaryRole = "hunter";
	log("Hired escort switched to bounty hunter role");
};

/*
---------------
Misc messaging
---------------
*/

this.$greetPlayer_probably = function ()
{
	if (this.debug) { this.$greetPlayer_core(); return ; }
	if (Math.random() < 0.9) { this.$greetPlayer_core(); }
};

this.$greetPlayer_perhaps = function ()
{
	if (this.debug) { this.$greetPlayer_core(); return ; }
	if (Math.random() < 0.6) { this.$greetPlayer_core(); }
};

this.$greetPlayer_core = function ()
{
	var chance_interstellar = 0.7;
	var chance_stellar = 0.7;
	var length;
	var	msg;
	var	msg_main;
	var	msg_salutation;
	var	picker;
	var isInterstellarSpace;

	if (system.isInterstellarSpace === true) {
		isInterstellarSpace = true;
	} else {
		isInterstellarSpace = false;
	}

	if (isInterstellarSpace === true && Math.random() < 0.7) {
		// *Interstellar space*

		msg_salutation = this.$msg_getSalutation();

		// Determine main message
		picker = Math.ceil(Math.random() * 20);
		switch (picker) {
			case 1:		msg_main = "Er, where to?"; break;
			case 2:		msg_main = "Are you sure about this?"; break;
			case 3:		msg_main = "Er, Thargoids?"; break;
			case 4:		msg_main = "I don't like the look of this."; break;
			case 5:		msg_main = "This could get hairy .."; break;
			case 6:		msg_main = "Welcome to interstellar space!"; break;
			case 7:		msg_main = "So this is .. where exactly?"; break;
			case 8:		msg_main = "Is this in the contract?"; break;
			case 9:		msg_main = "This is NOT in the contract."; break;
			case 10:	msg_main = "Should we get out of here?"; break;
			case 11:	msg_main = "Oh dear."; break;
			case 12:	msg_main = "Uh oh."; break;
			case 13:	msg_main = "Where you go, we follow."; break;
			case 14:	msg_main = "We're in interstellar space!"; break;
			case 15:	msg_main = "I want to home."; break;
			case 16:	msg_main = "Shall we get out of here?"; break;
			default:	msg_main = "Ready."; break;
		}
	}
	else {
		// *Normal* space (or just possibly interstellar)
		// Create message - using mostly Thargoids's original materials.

		msg_salutation = this.$msg_getSalutation();

		// Determine main message
		picker = Math.ceil(Math.random() * 11);
		switch (picker) {
			case 1:		msg_main = "Reporting for duty."; break;
			case 2:		msg_main = "Reporting for duty."; break;
			case 3:		msg_main = "Forming up on your six."; break;
			case 4:		msg_main = "Forming up on your six."; break;
			case 5:		msg_main = "Where to?"; break;
			case 6:		msg_main = "Where to?"; break;
			case 7:		msg_main = "Ready."; break;
			case 8:		msg_main = "Hello."; break;
			default:	msg_main = "Greetings."; break;
		}
	}
	this.$msg_assembleAndIssue (msg_salutation,msg_main);
};


/*
--------------------
Low-level messaging
-------------------
*/

this.$str_deCapitalizeFirstLetter = function(string) {
	if (string.charAt(0) === "I") {
		return string;
	}
	else {
		return string.charAt(0).toLowerCase() + string.slice(1);
	}
};

this.$str_prependSalutation = function(msg_salutation,msg_main)
{
	return msg_salutation + ", " + this.$str_deCapitalizeFirstLetter(msg_main);
};

this.$str_appendSalutation = function(msg_salutation,msg_main)
{
	var length = msg_main.length;
	var msg_lastCharOfMain = msg_main.charAt(length - 1);
	return msg_main.replace(/.$/,", ") + msg_salutation + msg_lastCharOfMain;
};

this.$msg_getSalutation = function ()
{
	var	msg_salutation;

	var r = Math.ceil(Math.random() * 10);
	if (r < 3) { msg_salutation = "NONE"; } else {
		switch (r) {
			case 3:		msg_salutation = "Commander"; break;
			case 4:		msg_salutation = "Commander"; break;
			case 5:		msg_salutation = "Captain"; break;
			case 6:		msg_salutation = "Sir"; break;
			case 7:		msg_salutation = "Chief"; break;
			case 8:		msg_salutation = "Chief"; break;
			default:	msg_salutation = "Boss"; break;
		}
	}
	return msg_salutation;
};


this.$msg_assembleAndIssue = function (msg_salutation,msg_main)
{
	var msg;

	switch (msg_salutation) {
		case "NONE":
			msg = msg_main;
			break;
		default:
			if ( Math.random() < 0.5 ) {
				msg = this.$str_prependSalutation(msg_salutation,msg_main);
			} else {
				msg = this.$str_appendSalutation(msg_salutation,msg_main);
			}
			break;
	}
	if (this.debug) {
		this.$dbg("msg_salutation = " + msg_salutation);
		this.$dbg("msg_main = " + msg_main);
		this.$dbg("msg = " + msg);
	}
	// Issue message
	this.ship.commsMessage(msg, player.ship);
};

/*
=========================
End of wrapper for JSHINT
=========================
*/

}).call(this);


// EOF
Scripts/hiredGuns_system.js
/*
=================================
HEADER
=================================
*/

this.name					= "hiredGuns_system";
this.author					= "Thargoid";
this.copyright				= "Creative Commons: attribution, non-commercial, sharealike.";
this.description			= "Set up the player escorts when bought";
this.version				= "1.1";


/*
===========================
Start of wrapper for JSHINT 
============================
*/

(
	function(){


/*
===========================
DIRECTIVE
===========================
*/

"use strict";


/*
===========================
GLOBALS
===========================
*/

this.debug = false;				// There is a 'this.debug' also in hiredGuns_system.js.

if ( this.debug === true ) {
	this.logging = true;
}



/*
===========================
FUNCTIONS
===========================
*/

/*
--------------
Debugging
--------------
*/

this.$dbg = function(msg)
{
	log(this.name,"DEBUG: " + msg);
};


this.startUp = function()
{
	missionVariables.hiredGuns_chance = Math.random() + (system.government * 0.05);
	// That value above is set also in this.shipExitedWitchspace.
	if ( this.debug ) {
		this.$dbg("Debugging ON");
	}
};

this.shipWillDockWithStation = function(station)
{
	if (!station.isMainStation) {
		return;
	}

	// Have we gone anywhere?
	if (system.ID == missionVariables.hiredGuns_launchSystem) {
		return;
	}

	var escortArray = system.shipsWithRole("hiredGuns_escort");
	if (escortArray.length > 0) {
		for (var i = 0; i < escortArray.length; i++) {
			escortArray[i].remove();
		}
	}
};

this.shipWillEnterWitchspace = function()
{
	var escortArray = system.shipsWithRole("hiredGuns_escort");
	if (escortArray.length > 0) {
		for (var i = 0; i < escortArray.length; i++) {
			escortArray[i].remove();
			missionVariables.hiredGuns_count += 1;
		}
	}
};

this.alertConditionChanged = function()
{
	if (player.alertCondition == 3 && player.alertHostiles && missionVariables.hiredGuns_purchased) {
		var escortArray = system.shipsWithRole("hiredGuns_escort", player.ship, 35000);
		if (escortArray.length > 0)
		{
			for (var i = 0; i < escortArray.length; i++) { 
				escortArray[i].AIState = ("FIND_PLAYER_HOSTILES");
			}
		}
	}
};

this.shipExitedWitchspace = function()
{
	if (missionVariables.hiredGuns_count > 0) {
		this.escorts = system.addShips(missionVariables.hiredGuns_role, missionVariables.hiredGuns_count, player.ship.position, 20000);
		for (var i = 0; i < missionVariables.hiredGuns_count; i++) {
			this.escorts[i].scannerDisplayColor1 = "yellowColor";
			this.escorts[i].scannerDisplayColor2 = "magentaColor";
		}
	}
	missionVariables.hiredGuns_chance = Math.random() + (system.government * 0.05);
	missionVariables.hiredGuns_launchSystem = null; // in case we jump out and back to the same system.
	if ( this.debug && system.isInterstellarSpace ) {
		this.$dbg("In interstellar space.");
	}
};

this.playerEnteredNewGalaxy = function(galaxyNumber)
{
	var escortArray = system.shipsWithRole("hiredGuns_escort");
	if (escortArray.length > 0) {
		for (var i = 0; i < escortArray.length; i++) {
			escortArray[i].remove();
		}
	}
	missionVariables.hiredGuns_count = 0;
	missionVariables.hiredGuns_role = null;
	missionVariables.hiredGuns_purchased = null;
	missionVariables.hiredGuns_launchSystem = null;
};

this.shipWillLaunchFromStation = function(station)
{
	if (station.isMainStation && system.countShipsWithRole("hiredGuns_escort") == 0 && missionVariables.hiredGuns_count > 0 && missionVariables.hiredGuns_role) {
		this.escorts = system.addShips(missionVariables.hiredGuns_role, missionVariables.hiredGuns_count, player.ship.position, 20000);
		for (var i = 0; i < missionVariables.hiredGuns_count; i++) { 
			this.escorts[i].scannerDisplayColor1 = "yellowColor";
			this.escorts[i].scannerDisplayColor2 = "magentaColor";
			this.escorts[i].AIState = "INITIALISE_FROM_STATION";
		}
		this.removeEq();
	}
};

this.shipEnteredStationAegis = function(station)
{
	if (!this.ship || !this.ship.isValid) { return; }
	this.ship.addCollisionException(station);
	 // Only main stations have aegis, but just to be safe:
	if (!station.isMainStation) {
		return;
	}

	// Have we gone anywhere?
	if (system.ID == missionVariables.hiredGuns_launchSystem) {
		return;
	}

	if ( this.debug ) {
		this.$dbg("Mission accomplished - removing escorts.");
	}

	var escortArray = system.shipsWithRole("hiredGuns_escort");
	if (escortArray.length > 0) {
		for (var i = 0; i < escortArray.length; i++) { 
			escortArray[i].AIState = ("MISSION_ACCOMPLISHED");
		}
	}
	missionVariables.hiredGuns_count = 0;
	missionVariables.hiredGuns_role = null;
	missionVariables.hiredGuns_purchased = null;
};

this.playerBoughtEquipment = function(equipment)
{
	switch(equipment) {
		case "EQ_HIREDGUN_LOW":
		{
			missionVariables.hiredGuns_role = "hiredGuns_escortLow";
			this.admin();
			player.ship.removeEquipment("EQ_HIREDGUN_LOW");
			break;
		}
		case "EQ_HIREDGUN_MID":
		{
			missionVariables.hiredGuns_role = "hiredGuns_escortMid";
			this.admin();
			player.ship.removeEquipment("EQ_HIREDGUN_MID");
			break;
		}
		case "EQ_HIREDGUN_HIGH":
		{
			missionVariables.hiredGuns_role = "hiredGuns_escortHigh";
			this.admin();
			player.ship.removeEquipment("EQ_HIREDGUN_HIGH");
			// break;
		}
	}
};

this.guiScreenChanged = function(newScreen, oldScreen)
{
	if (oldScreen == "GUI_SCREEN_EQUIP_SHIP") {
		this.removeEq();
	}
};

this.removeEq = function()
{
	if (player.ship.equipmentStatus("EQ_HIREDGUN_LOW") != "EQUIPMENT_UNAVAILABLE") {
		player.ship.removeEquipment("EQ_HIREDGUN_LOW");
		return;
	}
	if (player.ship.equipmentStatus("EQ_HIREDGUN_MID") != "EQUIPMENT_UNAVAILABLE") {
		player.ship.removeEquipment("EQ_HIREDGUN_MID");
		return;
	}
	if (player.ship.equipmentStatus("EQ_HIREDGUN_HIGH") != "EQUIPMENT_UNAVAILABLE") {
		player.ship.removeEquipment("EQ_HIREDGUN_HIGH");
		return;
	}
};

this.admin = function()
{
	missionVariables.hiredGuns_count = 2;
	missionVariables.hiredGuns_launchSystem = system.ID;
	missionVariables.hiredGuns_purchased = true;
};


/*
=========================
End of wrapper for JSHINT
=========================
*/

}).call(this);


// EOF