/*

	oolite-priorityai.js

	Priority-based Javascript AI library


	Oolite
	Copyright © 2004-2013 Giles C Williams and contributors

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	as published by the Free Software Foundation; either version 2
	of the License, or (at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
	MA 02110-1301, USA.

*/

"use strict";

/* AI Library */
this.name = "oolite-libPriorityAI";
this.copyright		= "© 2008-2013 the Oolite team.";
this.author = "cim";


/* Constructor */

this.PriorityAIController = function(ship)
{
	// the ship property must be read-only
	Object.defineProperty(this,	"ship", {
		value: ship,
		writable: false,
		enumerable: true,
		configurable: true
	});

	this.__cache = {}; // short-term cache
	this.__ltcache = {}; // long-term cache
	this.__ltcachestart = clock.adjustedSeconds+60;
	this.scannerRange = this.ship.scannerRange; // cached
	this.ship.AIScript.oolite_intership = {};
	this.ship.AIScript.oolite_priorityai = this;
	var activeHandlers = [];
	var handlerCache = {};
	var priorityList = null;
	var parameters = {};
	var lastCommSent = 0;
	var lastCommHeard = 0;
	var commsRole = "generic";
	var commsPersonality = "generic";
	var waypointgenerator = null;
	this.playerRole = this.playerRoleAssessment();
	
	/* Cache variables used by utility functions */
	var condmet = true;

	/* Private utility functions. Cannot be called from external code */

	// event handlers which must not be overridden
	function _handlerAIAwoken()
	{
		if (this.ship)
		{
			_reconsider.call(this);
		}
	}


	function _handlerShipDied()
	{
		this.cleanup();
	}


	/* Considers a priority list, potentially recursively */
	function _reconsiderList(priorities) {
		var logging = this.getParameter("oolite_flag_behaviourLogging");
		var pl = priorities.length;
		if (pl == 0)
		{
			log(this.name,"AI '"+this.ship.AIScript.name+"' for ship "+this.ship+" had a branch with no entries. This may be caused by a template function not being executed during priority set up.");
		}
		if (logging)
		{
			log(this.ship.name,"Considering branch with "+pl+" entries");
		}
		for (var i = 0; i < pl; i++)
		{
			if (logging)
			{
				if (priorities[i].label) 
				{
					log(this.ship.name,"Considering: "+priorities[i].label);
				}
				else
				{
					log(this.ship.name,"Considering: entry "+i);
				}
			}
			// always call the preconfiguration function at this point
			// to set up condition parameters
			if (priorities[i].preconfiguration)
			{
				priorities[i].preconfiguration.call(this);
			}
			// allow inverted conditions
			condmet = true;
			if (priorities[i].notcondition)
			{
				 condmet = !priorities[i].notcondition.call(this);
			}
			else if (priorities[i].condition)
			{
				condmet = priorities[i].condition.call(this);
			}
			// absent condition is always true
			if (condmet)
			{
				if (logging)
				{
					log(this.ship.name,"Conditions met");
				}

				// always call the configuration function at this point
				if (priorities[i].configuration)
				{
					priorities[i].configuration.call(this);
				}
				// this is what we're doing
				if (priorities[i].behaviour) 
				{
					if (logging)
					{
						log(this.ship.name,"Executing behaviour");
					}

					if (priorities[i].reconsider) 
					{
						_resetReconsideration.call(this,priorities[i].reconsider);
					}
					return priorities[i].behaviour;
				}
				// otherwise this is what we might be doing
				if (priorities[i].truebranch)
				{
					if (logging)
					{
						log(this.ship.name,"Entering truebranch");
					}

					var branch = _reconsiderList.call(this,priorities[i].truebranch);
					if (branch != null)
					{
						return branch;
					}
					// otherwise nothing in the branch was usable, so move on
				}
			}
			else
			{
				if (priorities[i].falsebranch)
				{
					if (logging)
					{
						log(this.ship.name,"Entering falsebranch");
					}

					var branch = _reconsiderList.call(this,priorities[i].falsebranch);
					if (branch != null)
					{
						return branch;
					}
					// otherwise nothing in the branch was usable, so move on
				}
			}
		}
		if (this.getParameter(logging))
		{
			log(this.ship.name,"Exiting branch");
		}

		return null; // nothing in the list is usable, so return
	};


	/* Only call this from aiAwoken to avoid loops */
	function _reconsider() {
		if (!this.ship || !this.ship.isValid || !this.ship.isInSpace)
		{
			return;
		}
		this.__cache = {}; // clear short-term cache
		// maybe clear long-term cache
		if (this.__ltcachestart < clock.adjustedSeconds)
		{
			this.__ltcache = {};
			this.__ltcachestart = clock.adjustedSeconds + 60;
		}
		if (!this.__ltcache.oolite_nearestStation || !this.__ltcache.oolite_nearestStation.isValid)
		{
			this.__ltcache.oolite_nearestStation = this.ship.findNearestStation();
		}
		var newBehaviour = _reconsiderList.call(this,priorityList);
		if (newBehaviour == null) {
			log(this.name,"AI '"+this.ship.AIScript.name+"' for ship "+this.ship+" had all priorities fail. All priority based AIs should end with an unconditional entry.");
			return false;
		}

		if (this.getParameter("oolite_flag_behaviourLogging") && this.getParameter("oolite_flag_behaviourLoggingVerbose"))
		{
			log(this.ship.name,newBehaviour);
		}
		newBehaviour.call(this);
		return true;
	};


	/* Resets the reconsideration timer. (Can't make it later) */
	function _resetReconsideration(delay)
	{
		if (this.ship)
		{
			var newwake = clock.adjustedSeconds + delay;
			if (this.ship.AIScriptWakeTime > newwake || this.ship.AIScriptWakeTime == 0)
			{
				this.ship.AIScriptWakeTime = newwake;
			}
		}
	};


	/* ****************** General AI functions. ************** */
	
	/* These privileged functions interface with the private functions
	 * and variables. Do not override them. */

	/* The simple implementation of this function requires a lot of
	 * creation and destruction of function objects, which aggravates
	 * the garbage collector. We avoid this where possible by keeping
	 * a cache (pre-binding) of the handler objects, and only
	 * replacing those which have changed. This requires the functions
	 * to be stored where possible as this. variables (or
	 * this.prototype. variables) */
	this.applyHandlers = function(handlers)
	{
		/* This handler must always exist for a priority AI, and must
		 * be set here. */
		handlers.aiAwoken = _handlerAIAwoken;
		/* This handler must always exist for a priority AI, and must
		 * be set here. */
		handlers.shipDied = _handlerShipDied;

		// step 1: go through activeHandlers, and delete those
		// functions from this.ship.AIScript that aren't in the new
		// handler list
		for (var i=activeHandlers.length-1; i >= 0 ; i--)
		{
			if (handlerCache[activeHandlers[i]] != handlers[activeHandlers[i]])
			{
				delete this.ship.AIScript[activeHandlers[i]];
				delete handlerCache[activeHandlers[i]];
			}
		}

		// step 2: go through the keys in handlers and put those handlers
		// into this.ship.AIScript and the keys into activeHandlers
		activeHandlers = Object.keys(handlers);
		for (var i=activeHandlers.length-1; i >= 0 ; i--)
		{
			// unset or not deleted in step 1
			if (!this.ship.AIScript[activeHandlers[i]])
			{
				handlerCache[activeHandlers[i]] = handlers[activeHandlers[i]];
				if(handlers[activeHandlers[i]])
				{
					this.ship.AIScript[activeHandlers[i]] = handlers[activeHandlers[i]].bind(this);
				}
				else
				{
					log(this.name,"AI '"+this.ship.AIScript.name+"' for ship "+this.ship+" had an invalid entry for handler "+activeHandlers[i]+". Skipped.");
				}
			}
		}

	}


	/* Do not call this directly. It is called automatically on ship death. Deliberately not documented. */
	this.cleanup = function()
	{
		// stop timers
		var etimer = this.getParameter("oolite_internal_ecmtimer");
		if (etimer && etimer.isRunning)
		{
			etimer.stop();
		}

		// break links to disconnect this from GC roots a little sooner
		delete this.ship.AIScript.oolite_priorityai;
		this.applyHandlers({});
		this.ship.AIScriptWakeTime = 0;
		delete this.ship.AIScript.aiAwoken;
		Object.defineProperty(this,	"ship", {
			value: ship,
			writable: true,
			enumerable: true,
			configurable: true
		});
		delete this.ship;
		delete this.parameters; // might contain entities
	}


	this.clearHandlers = function()
	{
		// delete all handlers to allow rebinding
		activeHandlers = Object.keys(handlerCache);
		for (var i=0; i < activeHandlers.length ; i++)
		{
			delete this.ship.AIScript[activeHandlers[i]];
		}
		handlerCache = {};
		delete this.ship.AIScript.aiAwoken;
		delete this.ship.AIScript.shipDied;
	}


	this.communicate = function(key,params,priority)
	{
		if (!worldScripts["oolite-libPriorityAI"].$commsAllowed)
		{
			// comms temporarily disabled
			return;
		}
		if (priority > 1)
		{
			var send = clock.adjustedSeconds - lastCommSent;
			if (priority == 2)
			{
				if (send < 10)
				{
					return;
				}
			}
			else
			{
				var recv = clock.adjustedSeconds - lastCommHeard;
				if (priority == 3)
				{
					if (recv < 10 || send < 10)
					{
						return;
					}
				}
				else
				{
					if (recv < 60 || send < 60)
					{
						return;
					}
				}
			}
		}
		var template = worldScripts["oolite-libPriorityAI"]._getCommunication(commsRole,commsPersonality,key);
		if (template != "")
		{
			if (params && params.isShip)
			{
				params = this.entityCommsParams(params);
			}
			if (template instanceof Function)
			{
				var message = template(key,params);
			}
			else
			{
				var message = expandDescription(template,params);
			}
			if (message != "")
			{
				this.ship.commsMessage(message);
				lastCommSent = clock.adjustedSeconds;
			}
			else
			{
				// this is for debugging: ordinarily this is legitimate
//				log(this.name,"Empty message for "+key);
			}
		}
	}


	this.communicationsPersonality = function()
	{
		return commsPersonality;
	}


	this.communicationsRole = function()
	{
		return commsRole;
	}


	this.fireECM = function()
	{
		if (this.getParameter("oolite_internal_ecmtimer"))
		{
			return;
		}
		// give a chance for missiles fired close to their target to
		// hit before it can use ECM
		var delay = (12-this.ship.accuracy)/4; // 0.5 - 4.25
		var etimer = new Timer(this,function() { 
			if (this.ship) 
			{
				this.ship.fireECM()
				this.setParameter("oolite_internal_ecmtimer",null);
			}
		}.bind(this),delay);
		this.setParameter("oolite_internal_ecmtimer",etimer);
	}


	this.getParameter = function(key)
	{
		if (key in parameters)
		{
			return parameters[key];
		}
		return null;
	}


	this.getWaypointGenerator = function()
	{
		return waypointgenerator;
	}


	this.noteCommsHeard = function()
	{
		lastCommHeard = clock.adjustedSeconds;
	}


	/* Requests reconsideration of behaviour ahead of schedule. */
	this.reconsiderNow = function() 
	{
		if (this.ship && this.ship.accuracy !== undefined)
		{
			// 0.1 - 1.6 seconds depending on accuracy
			_resetReconsideration.call(this,0.1+((10-this.ship.accuracy)/10));
		}
	}


	this.reconsiderIn = function(delta)
	{
		if (delta >= 0.1)
		{
			_resetReconsideration.call(this,delta);
		}
	}


	this.setCommunicationsRole = function(role)
	{
		commsRole = role;
		// If personality is generic, or doesn't exist in this role,
		// pick a new one from the allowed list.
		var cps = worldScripts["oolite-libPriorityAI"]._getCommunicationPersonalities(role);
		
		if (commsPersonality == "generic" || cps.indexOf(commsPersonality) == -1)
		{
			/* Allow group leaders to optionally set their own
			 * personality on ships in their group with the same
			 * role. This bit is for setting a follower's
			 * personality. */
			var pmatch = this.getParameter("oolite_personalityMatchesLeader");
			if (pmatch && Math.random() < pmatch)
			{
				if (this.ship.group)
				{
					var l = this.ship.group.leader;
					if (l && l != this.ship && l.AIScript.oolite_priorityai)
					{

						if (l.AIScript.oolite_priorityai.communicationsRole() == role)
						{
							var lp = l.AIScript.oolite_priorityai.communicationsPersonality();
							if (lp != "generic")
							{
								this.setCommunicationsPersonality(lp);
								return;
							}
						}
					}
				}
			}
			
			if (cps.length > 0)
			{
				if (cps.length == 1)
				{
					this.setCommunicationsPersonality(cps[0]);
				}
				else
				{
					var cpx = this.ship.entityPersonality % cps.length;
					if (cps[cpx] != "generic")
					{
						this.setCommunicationsPersonality(cps[cpx]);
					}
					else 
					{
						var cpx2 = this.ship.entityPersonality % (cps.length-1);
						if (cpx2 >= cpx)
						{
							++cpx2; // don't pick the same as cpx
						}
						this.setCommunicationsPersonality(cps[cpx2]);
					}
				}
			}
			/* Set personalities of followers */
			if (pmatch && this.ship.group && this.ship.group.leader == this.ship)
			{
				var fs = this.ship.group.ships;
				for (var i=fs.length-1;i>=0;--i)
				{
					if (fs[i] != this.ship && Math.random() < pmatch && fs[i].AIScript.oolite_priorityai)
					{
						if (fs[i].AIScript.oolite_priorityai.communicationsRole() == role)
						{
							fs[i].AIScript.oolite_priorityai.setCommunicationsPersonality(commsPersonality);
						}
					}
				}
			}
		}
	}


	this.setCommunicationsPersonality = function(personality)
	{
		commsPersonality = personality;
	}

	// parameters created by Oolite must always be prefixed oolite_
	this.setParameter = function(key, value)
	{
		parameters[key] = value;
	}

	this.setPriorities = function(priorities,delay) 
	{
		priorityList = priorities;
		this.clearHandlers();
		this.applyHandlers({});
		if (delay && delay > 0)
		{
			_resetReconsideration.call(this,delay);
		}
		else
		{
			_resetReconsideration.call(this,Math.random());
		}
	}


	// set the waypoint generator function
	this.setWaypointGenerator = function(value)
	{
		waypointgenerator = value;
	}


}; // end object constructor

/* Object prototype */
PriorityAIController.prototype.constructor = PriorityAIController;
PriorityAIController.prototype.name = this.name;

/* ****************** AI utility functions. ************** */

/* These functions provide standard checks for consistency in
 * conditions and other functions. */

PriorityAIController.prototype.allied = function(ship1,ship2)
{
	if (!ship1.isShip || !ship2.isShip)
	{
		return false;
	}
	// ships in same group
	var g1 = ship1.group;
	if (g1 && g1.containsShip(ship2))
	{
		return true;
	}
	if (g1 && g1.leader)
	{
		// ship1 is escort of ship in same group as ship2
		if (g1.leader.group && g1.leader.group.containsShip(ship2))
		{
			return true;
		}
	}
	// or in reverse, ship2 is the escort
	var g2 = ship2.group;
	if (g2 && g2.leader)
	{
		// ship2 is escort of ship in same group as ship1
		if (g2.leader.group && g2.leader.group.containsShip(ship1))
		{
			return true;
		}
	}
	// ship1 is escort of a ship, ship2 is escort of a ship, both
	// those ships are in the same group
	if (g1 && g2 && g1.leader && g2.leader && g1.leader.group && g1.leader.group.containsShip(g2.leader))
	{
		return true;
	}

	// all thargoids are allied with each other
	// all police are allied with each other
	if (ship1.scanClass == "CLASS_THARGOID" || ship1.scanClass == "CLASS_POLICE")
	{
		if (ship1.scanClass == ship2.scanClass)
		{
			return true;
		}
	}
	// main and other galcop stations allied with police
	if (ship1.isStation && (ship1.allegiance == "galcop" || ship1.isMainStation) && ship2.scanClass == "CLASS_POLICE")
	{
		return true;
	}
	if (ship2.isStation && (ship2.allegiance == "galcop" || ship2.isMainStation) && ship1.scanClass == "CLASS_POLICE")
	{
		return true;
	}
	// Okay, these ships really do have nothing to do with each other...
	return false;
}


PriorityAIController.prototype.broadcastAttackMessage = function(target,code,priority)
{
	var msgcode = "oolite_"+code+"Attack";
	var scan = target.scanClass;
	if (scan == "CLASS_THARGOID") {
		msgcode += "Thargoid";
	} else if (scan == "CLASS_ROCK" || scan == "CLASS_CARGO" || scan == "CLASS_BUOY" || scan == "CLASS_MISSILE" || scan == "CLASS_MINE") {
		msgcode += "Inanimate";
	}
	this.communicate(msgcode,target,priority);
}


PriorityAIController.prototype.broadcastDistressMessage = function()
{
	if (this.__ltcache.oolite_sentDistressMessage)
	{
		return;
	}
	this.__ltcache.oolite_sentDistressMessage = true;
	this.ship.broadcastDistressMessage();
	if (this.ship.AIPrimaryAggressor)
	{
		this.communicate("oolite_makeDistressCall",this.ship.AIPrimaryAggressor,2);
	}
}


PriorityAIController.prototype.checkScannerWithPredicate = function(predicate)
{
	var scan = this.getParameter("oolite_scanResults");
	if (scan == null || predicate == null)
	{
		return false;
	}
	// if current target matches, use that
	if (this.ship.target && predicate.call(this,this.ship.target))
	{
		this.setParameter("oolite_scanResultSpecific",this.ship.target);
		return true;
	}
	var sl = scan.length; 
	// use a random offset so if several ships make the same scan
	// they don't all pick the same target
	var offset = Math.floor(Math.random()*sl);
	for (var i = 0 ; i < sl ; i++)
	{
		var io = (i+offset)%sl;
		if (predicate.call(this,scan[io]))
		{
			// stops ships near the witchpoint beginning an attack on the player
			// before they've even got ship control back
			if (scan[io].status != "STATUS_EXITING_WITCHSPACE")
			{
				this.setParameter("oolite_scanResultSpecific",scan[io]);
				return true;
			}
			else
			{
				// something's coming out; reconsider sooner
				this.reconsiderIn(5);
			}
		}
	}
	return false;
}


PriorityAIController.prototype.cruiseSpeed = function()
{
	if (this.__ltcache.oolite_cruiseSpeed)
	{
		return this.__ltcache.oolite_cruiseSpeed;
	}
	var cruise = this.ship.maxSpeed * 0.8;
	var ignore = this.ship.maxSpeed / 4;
	var grouped = false;
	if (this.ship.group)
	{
		var gs = this.ship.group.ships;
		if (gs.length > 1) 
		{
			grouped = true;
		}
		for (var i = gs.length-1 ; i >= 0 ; i--)
		{
			var spd = gs[i].maxSpeed;
			if (spd >= ignore && cruise > spd)
			{	
				cruise = spd*0.95;
			}
		}
	}
	if (this.ship.escortGroup)
	{
		var gs = this.ship.escortGroup.ships;
		if (gs.length > 1) 
		{
			grouped = true;
		}
		for (var i = gs.length-1 ; i >= 0 ; i--)
		{
			var spd = gs[i].maxSpeed;
			if (spd >= ignore && cruise > spd)
			{
				cruise = spd;
			}
		}
	}
	if (!grouped)
	{
		// not in a group, so don't need to slow down for others to catch up
		cruise = this.ship.maxSpeed;
	}
	this.__ltcache.oolite_cruiseSpeed = cruise;
	return cruise;
}


PriorityAIController.prototype.distance = function(entity)
{
	if (!entity)
	{
		return 0;
	}
	if (this.__cache.oolite_position === undefined)
	{
		this.__cache.oolite_position = this.ship.position;
	}
	return this.__cache.oolite_position.distanceTo(entity);
}


// gets a standard comms params object
PriorityAIController.prototype.entityCommsParams = function(entity)
{
	var params = {};
	if (entity)
	{
		if (entity.isShip)
		{
			// TODO: extend the ship object so more precise names can be
			// returned?
			params["oolite_entityClass"] = entity.shipClassName; 
			if (entity.shipUniqueName != "")
			{
				params["oolite_entityName"] = entity.shipUniqueName;
			}
			else
			{
				params["oolite_entityName"] = entity.displayName;
			}
			if (!entity.isPlayer)
			{
				var crew = entity.crew;
				if (entity.crew && entity.crew.length > 0)
				{
					params["oolite_entityCrew"] = entity.crew[0].name;
				}
				else
				{
					params["oolite_entityCrew"] = params["oolite_entityName"];
				}
			} 
			else
			{
				params["oolite_entityCrew"] = player.name;
			}
		}
		else if (entity.name)
		{
			params["oolite_entityClass"] = entity.name;
			params["oolite_entityName"] = entity.name;
			params["oolite_entityCrew"] = entity.name;
		}
	}
	return params;
}


PriorityAIController.prototype.fineThreshold = function()
{
	if (!this.__ltcache.oolite_fineThreshold)
	{
		this.__ltcache.oolite_fineThreshold = 50 - (system.info.government * 6);
	}
	return this.__ltcache.oolite_fineThreshold;
}


// May need to move this and hostileStation to native code for efficiency
PriorityAIController.prototype.friendlyStation = function(station)
{
	if (!station || !station.isValid || !station.isInSpace)
	{
		return false;
	}
	// home station always friendly unless actually shooting at you
	if (station != this.__ltcache.oolite_homeStation)
	{
		var allegiance = this.stationAllegiance(station);
		// thargoid stations unfriendly to non-thargoid and vice versa
		if (allegiance == "thargoid" && this.ship.scanClass != "CLASS_THARGOID")
		{
			return false;
		}
		if (allegiance != "thargoid" && this.ship.scanClass == "CLASS_THARGOID")
		{
			return false;
		}
		// hunter stations attack any ship without bounty
		if (allegiance == "hunter" && this.ship.bounty > 0)
		{
			return false;
		}
		// galcop stations likely to be hostile to certain ships
		if (allegiance == "galcop" && (this.ship.bounty > this.fineThreshold() || this.ship.isPirate))
		{
			return false;
		}
		// pirate stations hostile to bounty-free ships
		if (allegiance == "pirate" && (this.ship.bounty == 0 || this.shipInRoleCategory(this.ship,"oolite-pirate-victim")))
		{
			return false;
		}
		// pirates won't dock at neutral stations
		if (allegiance == "neutral" && this.ship.isPirate)
		{
			return false;
		}
		// restricted+private stations never count as friendly: AI must use custom routines
		if (allegiance == "restricted" || allegiance == "private")
		{
			return false;
		}
	}
	return (station.target != this.ship || !station.hasHostileTarget);
}


PriorityAIController.prototype.homeStation = function() 
{
	if (this.__ltcache.oolite_homeStation === null)
	{
		return null;
	}
	if (this.__ltcache.oolite_homeStation !== undefined && this.__ltcache.oolite_homeStation.isValid)
	{
		return this.__ltcache.oolite_homeStation;
	}
	// home station might be the owner of the ship, or might just
	// be a group member
	if (this.ship.owner && this.ship.owner.isStation && this.friendlyStation(this.ship.owner))
	{
		this.__ltcache.oolite_homeStation = this.ship.owner;
		return this.ship.owner;
	}
	if (this.ship.group)
	{
		var gs = this.ship.group.ships;
		for (var i = gs.length-1 ; i >= 0  ; i--)
		{
			if (gs[i] != this.ship && gs[i].isStation && this.friendlyStation(gs[i]))
			{
				this.__ltcache.oolite_homeStation = gs[i];
				return gs[i];
			}
		}
	}
	this.__ltcache.oolite_homeStation = null;
	return null;
}


// this is mostly, but not entirely, a mirror of friendlyStation to
// get certain things (e.g. pirates) to work, unfortunately, it can't
// be an exact negation
PriorityAIController.prototype.hostileStation = function(station)
{
	if (!station || !station.isValid || !station.isInSpace)
	{
		return false;
	}
	// home station not hostile unless actually shooting at you
	if (station != this.__ltcache.oolite_homeStation)
	{

		var allegiance = this.stationAllegiance(station);
		// thargoid stations unfriendly to non-thargoid and vice versa
		if (allegiance == "thargoid" && this.ship.scanClass != "CLASS_THARGOID")
		{
			return true;
		}
		if (allegiance != "thargoid" && this.ship.scanClass == "CLASS_THARGOID")
		{
			return true;
		}
		// hunter stations attack any ship without bounty
		if (allegiance == "hunter" && this.ship.bounty > 0)
		{
			return true;
		}
		// galcop stations likely to be hostile to certain ships
		if (allegiance == "galcop" && (this.ship.bounty > this.fineThreshold() || this.ship.isPirate))
		{
			return true;
		}
		// pirate stations hostile to bounty-free ships
		if (allegiance == "pirate" && (this.ship.bounty == 0 || this.shipInRoleCategory(this.ship,"oolite-pirate-victim")))
		{
			return true;
		}
		// neutral, chaotic and private stations don't count as unfriendly
		// restricted stations should always be considered unfriendly
		if (allegiance == "restricted")
		{
			return true;
		}
		
	}
	return (station.target == this.ship && station.hasHostileTarget);
}


PriorityAIController.prototype.ignorePlayerFriendlyFire = function()
{
	var whom = player.ship;
	if (whom.target == this.ship)
	{
		return false; // was probably intentional
	}
	if (this.getParameter("oolite_lastAssist") == whom)
	{
		// player has helped this ship in this fight so is probably on the same side.
		if (Math.random() < 0.5)
		{
			// don't forgive too often
			this.setParameter("oolite_lastAssist",null);
		}
		return true;
	}
	// don't trust ships with opposite legal status
	if ((this.ship.bounty==0)==(whom.bounty==0))
	{
		// player could have meant to do that
		if (!this.getParameter("oolite_playerFriendlyFireAlready"))
		{
			var friendlyRoles = this.getParameter("oolite_friendlyRoles");
			if (Array.isArray(friendlyRoles))
			{
				for (var i=friendlyRoles.length-1;i>=0;i--)
				{
					if (this.shipInRoleCategory(whom,friendlyRoles[i]))
					{
						// only allow one!
						this.setParameter("oolite_playerFriendlyFireAlready",true);
						return true;
					}
				}
			}
		}
	}
	return false;
}


PriorityAIController.prototype.isAggressive = function(ship)
{
	if (ship && ship.isPlayer)
	{
		return !ship.isFleeing;
	}
	return ship && ship.hasHostileTarget && !ship.isFleeing && !ship.isDerelict;
}


PriorityAIController.prototype.isEscaping = function(ship)
{
	if (this.getParameter("oolite_flag_continueUnlikelyPursuits") != null)
	{
		return false;
	}
	return !this.isAggressive(ship) && this.distance(ship) > 15000 && ship.speed > this.ship.maxSpeed && ship.speed > this.ship.speed;
}


PriorityAIController.prototype.isFighting = function(ship)
{
	if (ship.isPlayer)
	{
		return !ship.isFleeing; // have to assume aggressive
	}
	return ship && ship.target && ship.hasHostileTarget;
}


/* Call just before switching target to a more serious threat, whom is
 * the more serious threat */
PriorityAIController.prototype.noteDistraction = function(whom)
{
	if (this.ship.target)
	{
		if (this.ship.target.script && this.ship.target.script.shipAttackerDistracted)
		{
			this.ship.target.script.shipAttackerDistracted(whom);
		}
		if (this.ship.target.AIScript && this.ship.target.AIScript.shipAttackerDistracted)
		{
			this.ship.target.AIScript.shipAttackerDistracted(whom);
		}
	}
}


PriorityAIController.prototype.oddsAssessment = function()
{
	if (this.__cache.oolite_oddsAssessment)
	{
		return this.__cache.oolite_oddsAssessment;
	}
	var target = this.ship.target;
	if (!target)
	{
		return 10;
	}
	var us = 0;
	var them = 0;
	var i = 0;
	var ship;
	us += this.threatAssessment(this.ship,true)
	var group;
	if ((group = this.ship.group))
	{
		var gs = group.ships;
		for (i = gs.length-1; i >= 0 ; i--)
		{
			ship = gs[i]
			if (ship != this.ship && ship.position.distanceTo(target) < this.scannerRange)
			{
				us += this.threatAssessment(ship,true);
			}
		}
		if (group.leader && group.leader.group != group)
		{
			gs = group.leader.group.ships;
			// don't want escorts running off early
			for (i = gs.length-1; i >= 0 ; i--)
			{
				ship = gs[i];
				if (ship != this.ship && ship.position.distanceTo(target) < this.scannerRange)
				{
					us += this.threatAssessment(ship,true);
				}
			}
		}
	}
	var egroup;
	if ((egroup = this.ship.escortGroup) && egroup != this.ship.group) 
	{
		var gs = egroup.ships;
		for (i = gs.length-1 ; i >= 0 ; i--)
		{
			ship = gs[i];
			if (ship != this.ship && ship.position.distanceTo(target) < this.scannerRange)
			{
				us += this.threatAssessment(ship,true);
			}
		}
	}

	// If the ship is in combat, it's almost certainly assessing a
	// ship which is in combat with it. If the ship is not in combat,
	// use the (usually smaller) non-combat assessment to encourage
	// pile-ons
	var full = this.conditionInCombat();

	them += this.threatAssessment(target,full)
	if (target.group)
	{
		var gs = target.group.ships;
		for (i = gs.length - 1 ; i >= 0 ; i--)
		{
			ship = gs[i]
			if (ship != target && this.distance(ship) < this.scannerRange)
			{
				them += this.threatAssessment(ship,full);
			}
		}
	}
	if (target.escortGroup && target.escortGroup != target.group) 
	{
		var gs = target.escortGroup.ships;
		for (i = gs.length - 1 ; i >= 0 ; i--)
		{
			ship = gs[i]
			if (ship != target && this.distance(ship) < this.scannerRange)
			{
				them += this.threatAssessment(ship,full);
			}
		}
	}
	this.__cache.oolite_oddsAssessment = us/them;		
	return this.__cache.oolite_oddsAssessment;
}


PriorityAIController.prototype.playerRoleAssessment = function()
{
	/* For the player, we pick one entry of their role array at
	 * random when first asked, then preserve it. Group members
	 * will take the role their group leader has set, and keep it
	 * until they get a group leader with different opinions. */
	var role = null;
	// grab role assessment from current group leader
	var leader = null;
	if (this.ship.group && (leader = this.ship.group.leader) && leader.AIScript.oolite_intership)
	{
		// if leader hasn't decided on a role, make them do so
		if (leader.AIScript.oolite_intership.oolite_player_role === undefined)
		{
			leader.AIScript.oolite_intership.oolite_player_role = player.roleWeights[Math.floor(Math.random()*player.roleWeights.length)];
		}
		role = leader.AIScript.oolite_intership.oolite_player_role;
		// save leader's decision
		this.ship.AIScript.oolite_intership.oolite_player_role = role;
	}
	else
	// group leader does not exist or does not have useful AI
	{ 
		// already decided what the player's role is
		if (this.ship.AIScript.oolite_intership.oolite_player_role !== undefined)
		{
			role = this.ship.AIScript.oolite_intership.oolite_player_role;
		}
		else
		{
			role = player.roleWeights[Math.floor(Math.random()*player.roleWeights.length)];
			this.ship.AIScript.oolite_intership.oolite_player_role = role; // save decision
		}
	}
	this.playerRole = role;
}


/* Be very careful with 'passon' parameter to avoid infinite loops */
PriorityAIController.prototype.respondToThargoids = function(whom,passon)
{
	if (this.getParameter("oolite_flag_noSpecialThargoidReaction") != null)
	{
		return false;
	}
	// non-thargoid being attacked by thargoid
	if (this.ship.target && this.ship.target.scanClass != "CLASS_THARGOID")
	{
		if (passon)
		{
			this.noteDistraction(whom);
		}
		this.ship.target = whom; // thargoid gets priority
		if (passon)
		{
			this.ship.requestHelpFromGroup(); // tell the rest!
			this.communicate("oolite_thargoidAttack",whom,2);
		}
	}
	var dts = this.ship.defenseTargets;
	for (var i = 0; i < dts.length ; i++)
	{
		if (dts[i].scanClass != "CLASS_THARGOID" && dts[i].scanClass != "CLASS_MISSILE" && dts[i].scanClass != "CLASS_MINE")
		{
			// safe: dts is a copy of the real data
			this.ship.removeDefenseTarget(dts[i]);
		}
	}
	return true;
}


PriorityAIController.prototype.setWitchspaceRouteTo = function(dest) 
{
	if (dest === null || dest < 0)
	{
		return this.configurationSelectWitchspaceDestination();
	}
	if (dest == system.ID)
	{
		this.setParameter("oolite_witchspaceDestination",-1);
		return;
	}
	var info = System.infoForSystem(galaxyNumber,dest);
	if (system.info.distanceToSystem(info) < this.ship.fuel)
	{
		this.setParameter("oolite_witchspaceDestination",dest);
		this.communicate("oolite_selectedWitchspaceDestination",{"oolite_witchspaceDestination":info.name},4);

		return;
	}
	else
	{
		var route = system.info.routeToSystem(info);
		if (!route)
		{
			this.setParameter("oolite_witchspaceDestination",-1);
			return;
		}
		var next = route.route[1];
		if (system.info.distanceToSystem(System.infoForSystem(galaxyNumber,next)) < this.ship.fuel)
		{
			this.setParameter("oolite_witchspaceDestination",next);
			this.communicate("oolite_selectedWitchspaceDestination",{"oolite_witchspaceDestination":System.infoForSystem(galaxyNumber,next).name},4);
			return;
		}
		this.setParameter("oolite_witchspaceDestination",null);
	}
}


PriorityAIController.prototype.shipHasRiskyContracts = function(ship) 
{
	var cs = ship.parcels;
	for (var i = cs.length-1; i >= 0 ; i--)
	{
		if (cs[i].risk == 1 && Math.random() < 0.1)
		{
			return true;
		} 
		if (cs[i].risk == 2 && Math.random() < 0.5)
		{
			return true;
		}
	}
	cs = ship.passengers;
	for (i = cs.length-1; i >= 0 ; i--)
	{
		if (cs[i].risk == 1 && Math.random() < 0.1)
		{
			return true;
		} 
		if (cs[i].risk == 2 && Math.random() < 0.5)
		{
			return true;
		}
	}
	return false;
}



/* Check role category membership allowing for player role assessment */
PriorityAIController.prototype.shipInRoleCategory = function(ship,category) 
{
	if (ship.isPlayer)
	{
		// recheck every so often in case we change groups
		if (this.__ltcache.oolite_shipInRoleCategory === undefined)
		{
			this.__ltcache.oolite_shipInRoleCategory = 1;
			this.playerRoleAssessment();
		}
		return Ship.roleIsInCategory(this.playerRole,category);
	}
	else  // NPCs are easier!
	{
		return Ship.roleIsInCategory(ship.primaryRole,category);
	}
}


PriorityAIController.prototype.stationAllegiance = function(station)
{
	if (station.allegiance)
	{
		return station.allegiance;
	}
	else
	{
		var allegiance = "neutral";

		if (station.isMainStation)
		{
			allegiance = "galcop";
		}
		else if (station.scanClass == "CLASS_THARGOID")
		{
			allegiance = "thargoid";
		}
		else if (station.scanClass == "CLASS_MILITARY" || station.scanClass == "CLASS_POLICE")
		{
			allegiance = "hunter";
		}
		else if (station.bounty > 0)
		{
			allegiance = "pirate";
		}
		else
		{
			var ses = station.subEntities;
			for (var i = 0; i < ses.length ; i++)
			{
				if (ses[i].isTurret)
				{
					allegiance = "hunter";
					break;
				}
			}
		}
		if (allegiance == "neutral" && system.mainStation && system.mainStation.position.distanceTo(station) < 51200)
		{
			allegiance = "galcop"; // neutral stations in aegis
		}
		// cache default value
		station.allegiance = allegiance;
		return allegiance;
	}
}


PriorityAIController.prototype.threatAssessment = function(ship,full)
{
	if (ship.isStation && this.getParameter("oolite_flag_fightsNearHostileStations"))
	{
		return 1; // mostly ignore stations in assessment
	}
	var ta = worldScripts["oolite-libPriorityAI"]._threatAssessment(ship,full);
	if (!full && ship.isPlayer && ship.alertCondition < 3 && this.playerRole != "player-unknown")
	{
		// we haven't already added on the player's skill bonus, but
		// the player is somewhat known
		ta += Math.pow(player.score,0.33)/10;
	}
	return ta;
}

/* ****************** Condition functions ************** */

/* Conditions. Any function which returns true or false can be used as
 * a condition. They do not have to be part of the AI library, but
 * several common conditions are provided here. */


/*** Combat-related conditions ***/


PriorityAIController.prototype.conditionCascadeDetected = function()
{
	var cpos = this.getParameter("oolite_cascadeDetected");
	if (cpos != null)
	{
		if (this.distance(cpos) < this.scannerRange)
		{
			return true;
		}
		this.setParameter("oolite_cascadeDetected",null);
	}
	return false;
}


PriorityAIController.prototype.conditionCombatOddsTerrible = function()
{
	if (this.getParameter("oolite_flag_surrendersEarly"))
	{
		return this.oddsAssessment() < 0.65;
	}
	else
	{
		return this.oddsAssessment() < 0.375;
	}
}


PriorityAIController.prototype.conditionCombatOddsBad = function()
{
	if (this.getParameter("oolite_flag_surrendersLate"))
	{
		return this.oddsAssessment() < 0.375;
	}
	else
	{
		return this.oddsAssessment() < 0.65;
	}
}


PriorityAIController.prototype.conditionCombatOddsGood = function()
{
	return this.oddsAssessment() >= 1.2;
}


PriorityAIController.prototype.conditionCombatOddsExcellent = function()
{
	return this.oddsAssessment() >= 2.5;
}


// group has taken too many losses
PriorityAIController.prototype.conditionGroupAttritionReached = function()
{
	var group;
	if (!(group = this.ship.group))
	{
		return false;
	}
	var cgp = this.getParameter("oolite_groupPower");
	if (!cgp)
	{
		this.setParameter("oolite_groupPower",group.count);
		return false;
	}
	if (group.count > cgp)
	{
		this.setParameter("oolite_groupPower",group.count);
		return false;
	}
	return group.count < cgp*0.75;
}


PriorityAIController.prototype.conditionGroupSuppliesLow = function()
{
	var group;
	if (!(group = this.ship.group))
	{
		return this.ship.damageAssessment() > 0;
	}
	var assessment = 0;
	var gs = group.ships;
	for (var i = gs.length-1; i >= 0; i--)
	{
		assessment += gs[i].damageAssessment();
	}
	return (assessment > gs.length / 2); // over half ships have low supplies
}


PriorityAIController.prototype.conditionInCombat = function()
{
	/* Overrides normal considerations - if fleeing, make the ship
	 * flee faster to the wormhole. Taking a further hit may change
	 * the target anyway, but this should prevent trying to actually
	 * attack a wormhole */
	if (this.ship.target && this.ship.target.isWormhole)
	{
		// but don't cache in case target changes later in evaluation
		return false;
	}

	if (this.__cache.oolite_conditionInCombat !== undefined)
	{
		return this.__cache.oolite_conditionInCombat;
	}
	this.__cache.oolite_conditionInCombat = (this.ship.alertCondition==3);
	if (!this.__cache.oolite_conditionInCombat)
	{
		delete this.ship.AIScript.oolite_intership.cargodemandpaid;
	}
	return this.__cache.oolite_conditionInCombat;
}

/* Ships being attacked are firing back */
PriorityAIController.prototype.conditionInCombatWithHostiles = function()
{
	if (this.isFighting(this.ship) && this.isAggressive(this.ship.target))
	{
		return true;
	}
	var dts = this.ship.defenseTargets;
	for (var i=dts.length-1; i >= 0; i--)
	{
		if (this.isAggressive(dts[i]) && this.distance(dts[i]) < this.scannerRange)
		{
			return true;
		}
		else
		{
			// this is safe to do mid-loop as dts is a copy of the
			// actual defense target list
			this.ship.removeDefenseTarget(dts[i]);
		}
	}
	if (this.ship.group != null)
	{
		var gs = this.ship.group.ships;
		for (var i = gs.length-1 ; i >= 0 ; i--)
		{
			if (this.isFighting(gs[i]) && this.isAggressive(gs[i].target))
			{
				return true;
			}
		}
	}
	if (this.ship.escortGroup != null)
	{
		var gs = this.ship.escortGroup.ships;
		for (var i = gs.length-1 ; i >= 0 ; i--)
		{
			if (this.isFighting(gs[i]) && this.isAggressive(gs[i].target))
			{
				return true;
			}
		}
	}
	
	delete this.ship.AIScript.oolite_intership.cargodemandpaid;
	return false;
}


PriorityAIController.prototype.conditionLosingCombat = function()
{
	var cascade = this.getParameter("oolite_cascadeDetected");
	if (cascade != null)
	{
		if (cascade.distanceTo(this.ship) < this.scannerRange)
		{
			return true;
		}
		else
		{
			this.setParameter("oolite_cascadeDetected",null);
		}
	}
	if (!this.conditionInCombat()) 
	{
		this.setParameter("oolite_lastFleeing",null);
		return false;
	}
	var en = this.ship.energy;
	var maxen = this.ship.maxEnergy; 
	if (en == maxen)
	{
		// forget previous defeats
		if (!this.conditionCombatOddsTerrible())
		{
			this.setParameter("oolite_lastFleeing",null);
		}
	}
	if (this.getParameter("oolite_flag_fleesPreemptively") && this.ship.fuel > 0 && this.ship.hasEquipmentProviding("EQ_FUEL_INJECTION"))
	{
		// ships of this behaviour will run away from anything if they
		// still have fuel
		return true;
	}
	
	var lastThreat = this.getParameter("oolite_lastFleeing");
	if (lastThreat != null && this.distance(lastThreat) < this.scannerRange * 1.25)
	{
		// the thing that attacked us is still nearby
		return true;
	}
	if (en * 4 < maxen)
	{
		// TODO: adjust threshold based on group odds
		return true; // losing if less than 1/4 energy
	}
	var dts = this.ship.defenseTargets;
	for (var i = dts.length-1 ; i >= 0 ; i--)
	{
		if (dts[i].scanClass == "CLASS_MISSILE" && dts[i].target == this.ship)
		{
			this.ship.target = dts[i]; // specifically flee the missile
			return true;
		}
		if (dts[i].scanClass == "CLASS_MINE" && this.distance(dts[i]) < this.scannerRange)
		{
			return true;
		}
	}
	// if we've dumped cargo or the group leader has, then we're losing
	if (this.ship.AIScript.oolite_intership.cargodemandpaid)
	{
		return true;
	}
	if (this.ship.group && this.ship.group.leader && this.ship.group.leader.AIScript.oolite_intership && this.ship.group.leader.AIScript.oolite_intership.cargodemandpaid)
	{
		return true;
	}
	if (en * 2 < maxen)
	{
		if (this.conditionCombatOddsBad())
		{
			// outnumbered; losing earlier
			return true;
		}
	}
	if (this.conditionCombatOddsTerrible())
	{
		if (!this.ship.isFleeing)
		{
			if (this.ship.group && this.ship.group.leader && this.ship.group.leader == this.ship)
			{
				this.communicate("oolite_groupIsOutnumbered",{},2);
			}
			else
			{
				this.communicate("oolite_groupIsOutnumbered",{},4);
			}
		}
		// badly outnumbered; losing
		return true;
	}

	if (!this.getParameter("oolite_flag_fightsNearHostileStations"))
	{
		if (this.__ltcache.oolite_nearestStation && this.__ltcache.oolite_nearestStation.isValid && this.distance(this.__ltcache.oolite_nearestStation) < 51200 && this.hostileStation(this.__ltcache.oolite_nearestStation))
		{
			// if there is a hostile station nearby, probably best to leave
			return true;
		}
	}

	return false; // not losing yet
}


PriorityAIController.prototype.conditionMothershipInCombat = function()
{
	if (this.ship.group)
	{
		var leader = this.ship.group.leader;
		if (leader && leader != this.ship)
		{
			if (this.distance(leader) > this.scannerRange)
			{
				return false; // can't tell
			}
			if (this.isFighting(leader))
			{
				return true;
			}
			var ltarget = leader.target;
			if (ltarget && ltarget.target == leader && ltarget.hasHostileTarget)
			{
				return true;
			}
			var dts = leader.defenseTargets;
			for (var i = dts.length-1 ; i >= 0 ; i--)
			{
				if (dts[i].target == leader && dts[i].hasHostileTarget)
				{
					return true;
				}
			}
			return false;
		}
	}
	// no mothership
	return false;
}


PriorityAIController.prototype.conditionMothershipIsAttacking = function()
{
	if (this.ship.group && this.ship.group.leader != this.ship)
	{
		var leader = this.ship.group.leader;
		if (leader && leader.target && this.isFighting(leader) && this.distance(leader.target) < this.scannerRange)
		{
			return true;
		}
	}
	return false;
}

// as MothershipIsAttacking, but leader.target must be aggressive
PriorityAIController.prototype.conditionMothershipIsAttackingHostileTarget = function()
{
	if (this.ship.group && this.ship.group.leader != this.ship)
	{
		var leader = this.ship.group.leader;
		if (leader && leader.target && this.isFighting(leader) && this.isAggressive(leader.target) && this.distance(leader.target) < this.scannerRange)
		{
			return true;
		}
	}
	return false;
}

PriorityAIController.prototype.conditionMothershipUnderAttack = function()
{
	if (this.ship.group && this.ship.group.leader != this.ship)
	{
		var leader = this.ship.group.leader;
		if (leader && leader.target && leader.target.target == leader && leader.target.hasHostileTarget && this.distance(leader.target) < this.scannerRange)
		{
			return true;
		}
		var dts = leader.defenseTargets;
		for (var i = 0 ; i < dts.length ; i++)
		{
			if (dts[i].target == leader && dts[i].hasHostileTarget && this.distance(dts[i]) < this.scannerRange)
			{
				return true;
			}
		}
		return false;
	}
	else
	{
		return false;
	}
}


PriorityAIController.prototype.conditionSuppliesLow = function()
{
	if (this.__ltcache.oolite_conditionSuppliesLow !== undefined)
	{
		return this.__ltcache.oolite_conditionSuppliesLow;
	}
	this.__ltcache.oolite_conditionSuppliesLow = (this.ship.damageAssessment() > 0);
	return this.__ltcache.oolite_conditionSuppliesLow;
}


/*** Navigation-related conditions ***/


PriorityAIController.prototype.conditionCanWitchspaceOnRoute = function()
{
	if (!this.ship.hasHyperspaceMotor)
	{
		return false;
	}
	var dest = this.getParameter("oolite_witchspaceDestination");
	if (dest === null || dest == -1)
	{
		return false;
	}
	return (system.info.distanceToSystem(System.infoForSystem(galaxyNumber,dest)) <= this.ship.fuel);
}


PriorityAIController.prototype.conditionCanWitchspaceOut = function()
{
	if (!this.ship.hasHyperspaceMotor)
	{
		return false;
	}
	return (system.info.systemsInRange(this.ship.fuel).length > 0);
}


PriorityAIController.prototype.conditionFriendlyStationExists = function()
{
	if (this.__cache.oolite_friendlyStationExists !== undefined)
	{
		return this.__cache.oolite_friendlyStationExists;
	}
	var stations = system.stations;
	for (var i = 0 ; i < stations.length ; i++)
	{
		var station = stations[i];
		if (this.friendlyStation(station))
		{
			this.__cache.oolite_friendlyStationExists = true;
			return true;
		}
	}
	this.__cache.oolite_friendlyStationExists = false;
	return false;
}

PriorityAIController.prototype.conditionFriendlyStationNearby = function()
{
	if (!this.__ltcache.oolite_nearestStation)
	{
		return false;
	}
	return this.friendlyStation(this.__ltcache.oolite_nearestStation) && this.distance(this.__ltcache.oolite_nearestStation) < this.scannerRange;
}


PriorityAIController.prototype.conditionGroupIsSeparated = function()
{
	var group = this.ship.group;
	if (!group)
	{
		return;
	}
	var leader;
	if (group.leader)
	{
		leader = group.leader;
	}
	else
	{
		leader = group.ships[0];
	}

	if (leader.isStation)
	{
		// can get 2x as far from station
		return (this.distance(leader) > this.scannerRange * 2);
	}
	else
	{
		return (this.distance(leader) > this.scannerRange);
	}
}


PriorityAIController.prototype.conditionHasSelectedPlanet = function()
{
	var planet = this.getParameter("oolite_selectedPlanet");
	if (planet && (!planet.isValid || !planet.isPlanet))
	{
		this.setParameter("oolite_selectedPlanet",null);
		return false;
	}
	return planet != null;
}


PriorityAIController.prototype.conditionHasSelectedStation = function()
{
	var station = this.getParameter("oolite_selectedStation");
	if (station && (!station.isValid || !station.isStation))
	{
		this.setParameter("oolite_selectedStation",null);
		return false;
	}
	return station != null;
}



PriorityAIController.prototype.conditionHomeStationExists = function()
{
	return (this.homeStation() != null);
}


PriorityAIController.prototype.conditionHomeStationNearby = function()
{
	var home = this.homeStation();
	if (home == null)
	{
		return false;
	}
	return this.distance(home) < this.scannerRange;
}


PriorityAIController.prototype.conditionHostileStationNearby = function()
{
	if (!this.__ltcache.oolite_nearestStation)
	{
		return false;
	}
	return this.hostileStation(this.__ltcache.oolite_nearestStation) && this.distance(this.__ltcache.oolite_nearestStation) < 51200;
}


PriorityAIController.prototype.conditionInInterstellarSpace = function()
{
	return system.isInterstellarSpace;
}


PriorityAIController.prototype.conditionInNovaSpace = function()
{
	return system.sun && (system.sun.hasGoneNova || system.sun.isGoingNova);
}


PriorityAIController.prototype.conditionMainPlanetNearby = function()
{
	if (!system.mainPlanet)
	{
		return false;
	}
	if (this.distance(system.mainPlanet) < system.mainPlanet.radius * 4)
	{
		return true;
	}
	return false;
}


PriorityAIController.prototype.conditionNearDestination = function()
{
	return (this.distance(this.ship.destination) < this.ship.desiredRange);
}


PriorityAIController.prototype.conditionPlayerNearby = function()
{
	return this.distance(player.ship) < this.scannerRange;
}


PriorityAIController.prototype.conditionReadyToSunskim = function()
{
	return (system.sun && this.distance(system.sun) < system.sun.radius * 1.15);
}


PriorityAIController.prototype.conditionSelectedStationNearby = function()
{
	var station = this.getParameter("oolite_selectedStation");
	if (station && this.distance(station) < this.scannerRange)
	{
		return true;
	}
	return false;
}

PriorityAIController.prototype.conditionSelectedStationNearMainPlanet = function()
{
	if (!system.mainPlanet)
	{
		return false;
	}
	var station = this.getParameter("oolite_selectedStation");
	if (station && station.position.distanceTo(system.mainPlanet) < system.mainPlanet.radius * 4)
	{
		return true;
	}
	return false;
}


PriorityAIController.prototype.conditionStationNearby = function()
{
	if (!this.__ltcache.oolite_nearestStation || !this.__ltcache.oolite_nearestStation.isValid)
	{
		return false;
	}
	return this.distance(this.__ltcache.oolite_nearestStation) < this.scannerRange*2;
}


PriorityAIController.prototype.conditionSunskimPossible = function()
{
	return (system.sun && 
			!system.sun.hasGoneNova && 
			!system.sun.isGoingNova && 
			this.ship.fuel < 7 && 
			this.ship.hasEquipmentProviding("EQ_FUEL_SCOOPS") &&
			(this.ship.heatInsulation > 1000/this.ship.maxSpeed || this.ship.heatInsulation >= 12));
}


PriorityAIController.prototype.conditionWormholeNearby = function()
{
	var holes = system.wormholes;
	for (var i=holes.length-1; i >= 0 ; i--)
	{
		var hole = holes[i];
		if (hole.expiryTime > clock.adjustedSeconds && this.distance(hole) < this.scannerRange)
		{
			this.__cache.oolite_wormholeNearby = hole;
			return true;
		}
	}
	return false;
}


/*** Pirate conditions ***/


PriorityAIController.prototype.conditionCargoDemandsMet = function()
{
	if (!this.getParameter("oolite_flag_watchForCargo"))
	{
		log(this.name,"AI '"+this.ship.AIScript.name+"' for ship "+this.ship+" is asking if cargo demands are met but has not set 'oolite_flag_watchForCargo'");
		return true;
	}
	var seen = this.getParameter("oolite_cargoDropped");
	if (seen != null)
	{
		var recorder = null;
		var demand = 0;
		if (this.ship.group)
		{
			if (this.ship.group.leader && this.ship.group.leader.AIScript.oolite_intership && this.ship.group.leader.AIScript.oolite_intership.cargodemanded > 0)
			{
				if (this.ship.group.leader.AIScript.oolite_intership.cargodemandmet)
				{
					return true;
				}
				recorder = this.ship.group.leader;
				demand = this.ship.group.leader.AIScript.oolite_intership.cargodemanded;
			}
			else if (this.ship.group.ships[0].AIScript.oolite_intership && this.ship.group.ships[0].AIScript.oolite_intership.cargodemanded > 0)

			{
				demand = this.ship.group.ships[0].AIScript.oolite_intership.cargodemanded;							
				if (this.ship.group.ships[0].AIScript.oolite_intership.cargodemandmet)
				{
					return true;
				}
				recorder = this.ship.group.ships[0];
			}
		}
		else
		{
			if (this.ship.AIScript.oolite_intership.cargodemanded > 0)
			{
				if (this.ship.AIScript.oolite_intership.cargodemandmet)
				{
					return true;
				}
				demand = this.ship.AIScript.oolite_intership.cargodemanded;
				recorder = this.ship;
			}
		}

		if (demand == 0)
		{
			return true; // no demand made
		}
		if (demand <= seen)
		{
			recorder.AIScript.oolite_intership.cargodemandmet = true;
			return true;
		}
	}
	return false;
}


PriorityAIController.prototype.conditionGroupHasEnoughLoot = function()
{
	var used = 0;
	var available = 0;
	if (!this.ship.group)
	{
		used = this.ship.cargoSpaceUsed;
		if (this.ship.hasEquipmentProviding("EQ_CARGO_SCOOPS"))
		{
			available = this.ship.cargoSpaceAvailable;
		}
	}
	else
	{
		var gs = this.ship.group.ships;
		for (var i = gs.length-1; i >= 0 ; i--)
		{
			used += gs[i].cargoSpaceUsed;
			if (gs[i].hasEquipmentProviding("EQ_CARGO_SCOOPS"))
			{
				available += gs[i].cargoSpaceAvailable;
			}
		}
	}
	var threshold = 0.33; // normally retreat at 2/3 hold
	if (this.conditionGroupAttritionReached())
	{
		threshold += 0.25; // if losing ships, take off a 1/4 of hold space
	}
	if (this.conditionGroupSuppliesLow())
	{
		threshold += 0.25; // if running out of supplies, take off a 1/4 of hold space
	} 

	if (available < (available+used)*threshold || available == 0)
	{
		return true;
	}
	return false;
}


PriorityAIController.prototype.conditionPiratesCanBePaidOff = function()
{
	if (this.ship.AIScript.oolite_intership.cargodemandpaid)
	{
		return false;
	}
	// TODO: need some way for the player to set this
	if (!this.ship.AIScript.oolite_intership.cargodemand)
	{
		return false;
	}
	if (this.ship.cargoSpaceUsed < this.ship.AIScript.oolite_intership.cargodemand)
	{
		return false;
	}
	return true;
}


/*** Scanner conditions ***/


PriorityAIController.prototype.conditionScannerContainsAssassinationTarget = function()
{
	return this.checkScannerWithPredicate(function(s) { 
		return s.primaryRole == "escape-capsule"; 
	});
}


PriorityAIController.prototype.conditionScannerContainsCleanShip = function()
{
	return this.checkScannerWithPredicate(function(s) { 
		return (s.scanClass == "CLASS_NEUTRAL" || s.scanClass == "CLASS_POLICE") && s.bounty == 0; 
	});
}


PriorityAIController.prototype.conditionScannerContainsCourier = function()
{
	return (this.checkScannerWithPredicate(function(s) { 
		return (this.shipInRoleCategory(s,"oolite-courier")) || (s.isPlayer && this.shipHasRiskyContracts(s));
	}));
}


PriorityAIController.prototype.conditionScannerContainsEscapePods = function()
{
	if (!this.conditionCanScoopCargo())
	{
		return false;
	}
	return this.checkScannerWithPredicate(function(s) { 
		return  s.primaryRole == "escape-capsule" && s.isInSpace && s.scanClass == "CLASS_CARGO" && s.velocity.magnitude() < this.ship.maxSpeed; 
	});
}


PriorityAIController.prototype.conditionScannerContainsFineableOffender = function()
{
	return this.checkScannerWithPredicate(function(s) { 
		var threshold = this.fineThreshold();
		return s.isInSpace && s.bounty <= threshold && s.bounty > 0 && !s.markedForFines && (s.scanClass == "CLASS_NEUTRAL" || s.isPlayer) && !s.isDerelict; 
	});
}


PriorityAIController.prototype.conditionScannerContainsFugitive = function()
{
	return this.checkScannerWithPredicate(function(s) { 
		return s.isInSpace && s.bounty > 50 && s.scanClass != "CLASS_CARGO" && s.scanClass != "CLASS_ROCK" && s.scanClass != "CLASS_BUOY"; 
	});
}

PriorityAIController.prototype.conditionScannerContainsHuntableOffender = function()
{
	return this.checkScannerWithPredicate(function(s) { 
		var threshold = this.fineThreshold() / 2;
		return s.isInSpace && s.bounty > threshold && s.scanClass != "CLASS_CARGO" && s.scanClass != "CLASS_ROCK" && s.scanClass != "CLASS_BUOY"; 
	});
}


PriorityAIController.prototype.conditionScannerContainsSeriousOffender = function()
{
	return this.checkScannerWithPredicate(function(s) { 
		var threshold = this.fineThreshold();
		return s.isInSpace && s.bounty > threshold && s.scanClass != "CLASS_CARGO" && s.scanClass != "CLASS_ROCK" && s.scanClass != "CLASS_BUOY"; 
	});
}


PriorityAIController.prototype.conditionScannerContainsHunters = function()
{
	return this.checkScannerWithPredicate(function(s) { 
		return (s.primaryRole && this.shipInRoleCategory(s,"oolite-bounty-hunter")) || s.scanClass == "CLASS_POLICE" || (s.isStation && s.isMainStation);
	});
}


PriorityAIController.prototype.conditionScannerContainsLoneVictim = function()
{
	var scan = this.getParameter("oolite_scanResults");
	var others = 0;
	var target = null;
	for (var i = scan.length-1 ; i >= 0 ; i--)
	{
		if (!this.allied(this.ship,scan[i]))
		{
			if (this.shipInRoleCategory(scan[i],"oolite-pirate-victim") && scan[i].cargoSpaceCapacity > 0)
			{
				target = scan[i];
			}
			others++;
		}
	}
	if (others == 1)
	{
		this.setParameter("oolite_scanResultSpecific",target);
		return true;
	}
	return false;
}


PriorityAIController.prototype.conditionScannerContainsMiningOpportunity = function()
{
	// if hold full, no
	if (!this.conditionCanScoopCargo())
	{
		return false;
	}
	// need a mining laser, and for now a forward one
	if (!this.ship.forwardWeapon == "EQ_WEAPON_MINING_LASER")
	{
		return false;
	}
	return this.conditionScannerContainsRocks();
}


PriorityAIController.prototype.conditionScannerContainsNonThargoid = function()
{
	var prioritytargets = this.checkScannerWithPredicate(function(s) { 
		return s.scanClass != "CLASS_THARGOID" && s.scanClass != "CLASS_ROCK" && s.scanClass != "CLASS_BUOY" && s.scanClass != "CLASS_CARGO";
	});
	if (prioritytargets) 
	{
		return true;
	}
	return this.checkScannerWithPredicate(function(s) { 
		return s.scanClass != "CLASS_THARGOID";
	});
}


PriorityAIController.prototype.conditionScannerContainsPirateLeader = function()
{
	return this.checkScannerWithPredicate(function(s) { 
		return s.group && s.group.leader == s && this.shipInRoleCategory(s,"oolite-pirate-leader");
	});
}


PriorityAIController.prototype.conditionScannerContainsPirateVictims = function()
{
	var lpv = this.getParameter("oolite_lastPirateVictim");
	return this.checkScannerWithPredicate(function(s) { 
		// is a pirate victim
		// can carry cargo
		// hasn't already paid up
		return s != lpv && this.shipInRoleCategory(s,"oolite-pirate-victim") && s.cargoSpaceCapacity > 0 && (!s.AIScript || !s.AIScript.oolite_intership || !s.AIScript.oolite_intership.cargodemandpaid);
	});
}


PriorityAIController.prototype.conditionScannerContainsReadyThargoidMothership = function()
{
	return this.checkScannerWithPredicate(function(s) { 
		return s.hasRole("thargoid-mothership") && (!s.escortGroup || s.escortGroup.count <= 16);
	});
}


PriorityAIController.prototype.conditionScannerContainsRocks = function()
{
	var scan1 = this.checkScannerWithPredicate(function(s) { 
		return s.isInSpace && s.isBoulder;
	});
	if (scan1)
	{
		return true;
	}
	// no boulders, what about asteroids?
	return this.checkScannerWithPredicate(function(s) { 
		return s.isInSpace && s.isMinable;
	});
}


PriorityAIController.prototype.conditionScannerContainsSalvage = function()
{
	return this.checkScannerWithPredicate(function(s) { 
		return s.isInSpace && s.scanClass == "CLASS_CARGO" && s.commodity != null;
	});
}


PriorityAIController.prototype.conditionScannerContainsSalvageForGroup = function()
{
	if (!this.__ltcache.oolite_conditionScannerContainsSalvageForGroup)
	{
		var maxspeed = 0;
		if (this.conditionCanScoopCargo())
		{
			maxspeed = this.ship.maxSpeed;
		}
		if (this.ship.group)
		{
			var gs = this.ship.group.ships;
			for (var i = gs.length-1; i >= 0 ; i--)
			{
				if (gs[i].cargoSpaceAvailable > 0 && gs[i].hasEquipmentProviding("EQ_CARGO_SCOOPS") && gs[i].maxSpeed > maxspeed)
				{
					maxspeed = gs[i].maxSpeed;
				}
			}
		}
		this.__ltcache.oolite_conditionScannerContainsSalvageForGroup = maxspeed;
	}
	return this.checkScannerWithPredicate(function(s) { 
		return s.isInSpace && s.scanClass == "CLASS_CARGO" && s.commodity != null && s.velocity.magnitude() < this.__ltcache.oolite_conditionScannerContainsSalvageForGroup; 
	});
}


PriorityAIController.prototype.conditionScannerContainsSalvageForMe = function()
{
	if (!this.conditionCanScoopCargo())
	{
		return false;
	}
	return this.checkScannerWithPredicate(function(s) { 
		return s.isInSpace && s.scanClass == "CLASS_CARGO" && s.commodity != null && s.velocity.magnitude() < this.ship.maxSpeed; 
	});
}


PriorityAIController.prototype.conditionScannerContainsShipAttackingPirate = function()
{
	return this.checkScannerWithPredicate(function(s) { 
		return s.target && s.hasHostileTarget && s.target.isPirate;
	});
}


PriorityAIController.prototype.conditionScannerContainsShipNeedingEscort = function()
{
	if (this.ship.bounty == 0)
	{
		return this.checkScannerWithPredicate(function(s) { 
			return s.scanClass == this.ship.scanClass && s.bounty == 0 && (!s.escortGroup || s.escortGroup.count <= s.maxEscorts);
		});
	}
	else
	{
		return this.checkScannerWithPredicate(function(s) { 
			return s.scanClass == this.ship.scanClass && s.bounty > 0 && (!s.escortGroup || s.escortGroup.count <= s.maxEscorts);
		});
	}
}


PriorityAIController.prototype.conditionScannerContainsSuspiciousShip = function()
{
	return this.checkScannerWithPredicate(function(s) { 
		return (s.primaryRole && this.shipInRoleCategory(s,"oolite-police-dislike"));
	});
}


PriorityAIController.prototype.conditionScannerContainsThargoidMothership = function()
{
	return this.checkScannerWithPredicate(function(s) { 
		return s.hasRole("thargoid-mothership");
	});
}


PriorityAIController.prototype.conditionScannerContainsTraderEnemy = function()
{
	return this.checkScannerWithPredicate(function(s) { 
		return (s.primaryRole && this.shipInRoleCategory(s,"oolite-trader-dislike"));
	});
}


PriorityAIController.prototype.conditionScannerContainsTraderSmugglerEnemy = function()
{
	return this.checkScannerWithPredicate(function(s) { 
		return (s.primaryRole && this.shipInRoleCategory(s,"oolite-smuggler-dislike"));
	});
}


PriorityAIController.prototype.conditionScannerContainsUnspreadMissile = function()
{
	if (!this.getParameter("oolite_flag_autoSpreadMissiles"))
	{
		return false;
	}
	return this.checkScannerWithPredicate(function(s) { 
		var target = this.ship.target;
		return (s.scanClass == "CLASS_MISSILE") && s.target == target && s.owner == this.ship.owner && this.distance(s) < 500 && this.distance(target) > s.position.distanceTo(target);
	});
}


/*** State conditions ***/


PriorityAIController.prototype.conditionAllEscortsInFlight = function()
{
	if (!this.ship.escortGroup)
	{
		return true; // there are no escorts not in flight
	}
	var gs = this.ship.escortGroup.ships;
	for (var i = gs.length-1 ; i >= 0 ; i--)
	{
		if (gs[i].status != "STATUS_IN_FLIGHT")
		{
			return false;
		}
	}
	// if just exited witchspace, escorts might not have rejoined escort
	// group yet.
	if (!this.ship.group)
	{
		return true; 
	}
	var gs = this.ship.group.ships;
	for (var i = gs.length-1 ; i >= 0 ; i--)
	{
		if (gs[i].status != "STATUS_IN_FLIGHT")
		{
			return false;
		}
	}

	return true;
}

PriorityAIController.prototype.conditionCanScoopCargo = function()
{
	if (this.__cache.oolite_conditionCanScoopCargo !== undefined)
	{
		return this.__cache.oolite_conditionCanScoopCargo;
	}
	if (this.ship.cargoSpaceAvailable == 0 || !this.ship.hasEquipmentProviding("EQ_CARGO_SCOOPS"))
	{
		this.__cache.oolite_conditionCanScoopCargo = false;
		return false;
	}
	this.__cache.oolite_conditionCanScoopCargo = true;
	return true;
}


PriorityAIController.prototype.conditionCargoIsProfitableHere = function()
{
	// only consider these values if the ship has a route defined
	if (this.ship.homeSystem != this.ship.destinationSystem)
	{
		// cargo is always considered profitable in the designated
		// destination system (assume they have a prepared buyer)
		if (this.ship.destinationSystem > -1 && this.ship.destinationSystem == system.ID)
		{
			return true;
		}
		// cargo is never considered profitable in the designated source
		// system (or you could get ships launching and immediately
		// redocking)
		if (this.ship.homeSystem > -1 && this.ship.homeSystem == system.ID)
		{
			return false;
		}
		// and allow ships to be given multi-system trips if wanted
		if (this.getParameter("oolite_flag_noDockingUntilDestination"))
		{
			return false;
		}
	}

	if (!system.mainStation)
	{
		return false;
	}

	if (this.__ltcache.oolite_conditionCargoIsProfitableHere == undefined)
	{
		if (this.ship.cargoSpaceUsed == 0)
		{
			this.__ltcache.oolite_conditionCargoIsProfitableHere = false;
		}
		else
		{
			var cargo = this.ship.cargoList;
			var profit = 0;
			var multiplier = (system.info.economy <= 3)?-1:1;
			var market = system.mainStation.market;
			for (var i = cargo.length-1 ; i >= 0 ; i--)
			{
				var commodity = cargo[i].commodity;
				var quantity = cargo[i].quantity;
				var adjust = market[commodity].marketEcoAdjustPrice * multiplier * quantity / market[commodity].marketMaskPrice;
				profit += adjust;
			}
			
			this.__ltcache.oolite_conditionCargoIsProfitableHere = (profit >= 0);
		}
	}
	return this.__ltcache.oolite_conditionCargoIsProfitableHere;
}


PriorityAIController.prototype.conditionCoinFlip = function()
{
	return (Math.random() < 0.5);
}



PriorityAIController.prototype.conditionGroupLeaderIsStation = function()
{
	return (this.ship.group && this.ship.group.leader && this.ship.group.leader.isStation);
}


PriorityAIController.prototype.conditionHasInterceptCoordinates = function()
{
	return (this.getParameter("oolite_interceptCoordinates") != null);
}


PriorityAIController.prototype.conditionHasMothership = function()
{
	return (this.ship.group && this.ship.group.leader && this.ship.group.leader != this.ship);
}


PriorityAIController.prototype.conditionHasNonThargoidTarget = function()
{
	return (this.ship.target && this.ship.target.scanClass != "CLASS_THARGOID");
}


PriorityAIController.prototype.conditionHasReceivedDistressCall = function()
{
	var aggressor = this.getParameter("oolite_distressAggressor");
	var sender = this.getParameter("oolite_distressSender");
	var ts = this.getParameter("oolite_distressTimestamp");

	if (aggressor == null || !aggressor.isInSpace || sender == null || !sender.isInSpace || this.distance(sender) > this.scannerRange || ts+30 < clock.adjustedSeconds)
	{
		// no, or it has expired
		this.setParameter("oolite_distressAggressor",null);
		this.setParameter("oolite_distressSender",null);
		this.setParameter("oolite_distressTimestamp",null);
		return false;
	}
	return true;
}


PriorityAIController.prototype.conditionHasRememberedTarget = function()
{
	var rt = this.getParameter("oolite_rememberedTarget");

	if (rt != null && (rt.isInSpace || rt.status == "STATUS_ENTERING_WITCHSPACE"))
	{
		return true;
	} 
	else
	{
		this.setParameter("oolite_rememberedTarget",null);
		return false;
	}
}


PriorityAIController.prototype.conditionHasTarget = function()
{
	return this.ship.target != null;
}


PriorityAIController.prototype.conditionHasWaypoint = function()
{
	return this.getParameter("oolite_waypoint") != null;
}


PriorityAIController.prototype.conditionIsActiveThargon = function()
{
	return this.ship.scanClass == "CLASS_THARGOID" && this.ship.hasRole("EQ_THARGON");
}


PriorityAIController.prototype.conditionIsEscorting = function()
{
	if (!this.ship.group || !this.ship.group.leader || this.ship.group.leader == this.ship)
	{
		return false;
	}
	var leader = this.ship.group.leader;
	if (leader.escortGroup && leader.escortGroup.containsShip(this.ship))
	{
		if (leader.status == "STATUS_ENTERING_WITCHSPACE")
		{
			var hole = this.getParameter("oolite_witchspaceWormhole");
			if (hole == null || hole.expiryTime < clock.seconds)
			{
				// has been left behind
				this.configurationLeaveEscortGroup();
				this.setParameter("oolite_witchspaceWormhole",null);
				return false;
			}
		}
		return true;
	}
	return false;
}


PriorityAIController.prototype.conditionIsGroupLeader = function()
{
	if (!this.ship.group)
	{
		return true;
	}
	return (this.ship.group.leader == this.ship);
}


PriorityAIController.prototype.conditionMissileNeedsLaunchEvasion = function()
{
	return (this.getParameter("oolite_flag_launchAdjustMissile") != null);
}


PriorityAIController.prototype.conditionMissileOutOfFuel = function()
{
	var range = 30000; // 30 km default
	if (this.ship.scriptInfo.oolite_missile_range)
	{
		range = this.ship.scriptInfo.oolite_missile_range;
	}
	return range < this.ship.distanceTravelled;
}


PriorityAIController.prototype.conditionPatrolIsOver = function()
{
	/* patrol is over after 200km, or if supplies are low after 20km
	 * 20km to prevent patrol being over on launch if a ship is set up
	 * to always have low supplies on creation */
	var plength = this.getParameter("oolite_patrolLength");
	var pdist = plength ? plength : 200000;

	return this.ship.distanceTravelled > pdist || (this.ship.distanceTravelled > 20000 && this.conditionSuppliesLow());
}


PriorityAIController.prototype.conditionWitchspaceEntryRequested = function()
{
	return (this.getParameter("oolite_witchspaceWormhole") != null);
}




/* ****************** Behaviour functions ************** */

/* Behaviours. Behaviours are effectively a state definition,
 * defining a set of events and responses. They are aided in this
 * by the 'responses', which mean that the event handlers for the
 * behaviour within the definition can itself be templated.  */



PriorityAIController.prototype.behaviourApproachDestination = function()
{
	var handlers = {};
	this.responsesAddStandard(handlers);

	handlers.shipAchievedDesiredRange = this.responseComponent_standard_shipAchievedDesiredRange;

	var waypoints = this.getParameter("oolite_waypoints");
	if (waypoints != null)
	{
		this.ship.destination = waypoints[waypoints.length-1];
		this.ship.desiredRange = 100;
	}
	var blocker = this.ship.checkCourseToDestination();
	if (blocker)
	{
		if (blocker.isPlanet || blocker.isSun)
		{
			// the selected planet can't block
			if (blocker.isSun || this.getParameter("oolite_selectedPlanet") != blocker)
			{
				var dist = this.distance(blocker);
				if (dist < blocker.radius * 1.3)
				{
					if (waypoints == null)
					{
						waypoints = [];
					}
					waypoints.push(this.ship.position.subtract(blocker.position.subtract(this.ship.position)));
					this.ship.destination = waypoints[waypoints.length-1];
					this.ship.desiredRange = 100;
				}
				else if (this.distance(blocker) < blocker.radius * 3)
				{
					if (waypoints == null)
					{
						waypoints = [];
					}
					waypoints.push(this.ship.getSafeCourseToDestination());
					this.ship.destination = waypoints[waypoints.length-1];
					this.ship.desiredRange = 100;
				}
			}
		}
		else if (blocker.isShip)
		{
			if (this.distance(blocker) < this.scannerRange)
			{
				if (!blocker.group || !blocker.group.leader == this.ship)
				{
					// our own escorts are not a blocker!
					if (waypoints == null)
					{
						waypoints = [];
					}
					waypoints.push(this.ship.getSafeCourseToDestination());
					this.ship.destination = waypoints[waypoints.length-1];
					this.ship.desiredRange = 100;
				}
			}
		}
	}
	this.setParameter("oolite_waypoints",waypoints);
	this.applyHandlers(handlers);
	this.ship.performFlyToRangeFromDestination();
}


PriorityAIController.prototype.behaviourAssassinateCurrentTarget = function()
{
	var params = this.entityCommsParams(this.ship.target);
	if (this.ship.target.isPlayer)
	{
		params["oolite_entityContracts"] = worldScripts["oolite-contracts-helpers"]._getClientName();
	}
	else
	{
		params["oolite_entityContracts"] = expandDescription("%N ")+expandDescription("[nom]");
	}
	this.communicate("oolite_beginningAssassination",params,3);
	this.behaviourCommenceAttackOnCurrentTarget();
}


PriorityAIController.prototype.behaviourAvoidCascadeExplosion = function()
{
	var handlers = {};
	this.responsesAddStandard(handlers);
	this.applyHandlers(handlers);

	var cascade = this.getParameter("oolite_cascadeDetected");
	if (cascade != null)
	{
		if (cascade.distanceTo(this.ship) < this.scannerRange)
		{
			if (this.ship.defenseTargets.length > 0 && this.ship.defenseTargets[0].scanClass == "CLASS_MINE")
			{
				// if the mine is still visible, conventional fleeing works
				this.communicate("oolite_quiriumCascade",{},3);
				this.ship.target = this.ship.defenseTargets[0];
				this.ship.desiredRange = 30000;
				this.ship.performFlee();
				return;
			}
			else
			{
				if (this.ship.destination != cascade)
				{
					this.communicate("oolite_quiriumCascade",{},3);
				}
				this.ship.destination = cascade;
				this.ship.desiredRange = 30000;
				this.ship.desiredSpeed = 10*this.ship.maxSpeed;
				this.ship.performFlyToRangeFromDestination();
				return;
			}
		}
		else
		{
			this.setParameter("oolite_cascadeDetected",null);
		}
	}
}


PriorityAIController.prototype.behaviourBecomeInactiveThargon = function()
{
	this.applyHandlers({});
	this.ship.scanClass = "CLASS_CARGO";
	this.ship.target = null;
	this.ship.clearDefenseTargets();
	if (this.ship.group)
	{
		this.ship.group.removeShip(this.ship);
		this.ship.group = null;
	}
	if (this.ship.escortGroup)
	{
		this.ship.escortGroup.removeShip(this.ship);
	}
	this.ship.desiredSpeed = 0;
	this.ship.performStop();
	var nearby = this.ship.checkScanner(true);
	for (var i = 0 ; i < nearby.length ; i++)
	{
		var ship = nearby[i];
		if (ship.target == this.ship && !ship.isPlayer && ship.hasHostileTarget)
		{
			ship.target = null;
		}
		ship.removeDefenseTarget(this.ship);
	}
}


PriorityAIController.prototype.behaviourCollectSalvage = function()
{
	var handlers = {};
	this.responsesAddStandard(handlers);
	handlers.shipScoopedOther = this.responseComponent_standard_shipScoopedOther;
	this.applyHandlers(handlers);
	this.ship.performCollect();
}


PriorityAIController.prototype.behaviourCommenceAttackOnCurrentTarget = function()
{
	this.communicate("oolite_beginningFight",this.ship.target,3);
	this.behaviourDestroyCurrentTarget();
}


PriorityAIController.prototype.behaviourDestroyCurrentTarget = function()
{
	this.setParameter("oolite_witchspaceEntry",null);

	var handlers = {};
	this.responsesAddStandard(handlers);
	this.applyHandlers(handlers);

	if (this.getParameter("oolite_flag_noSpecialThargoidReaction") != null)
	{
		if (this.ship.scanClass != "CLASS_THARGOID" && this.ship.target.scanClass != "CLASS_THARGOID" && this.ship.target.target.scanClass == "CLASS_THARGOID")
		{
			this.respondToThargoids(this.ship.target.target,true);
			this.ship.performAttack();
			return;
		}
	}

	
	/* This doesn't work: ships which are removed from the list
	 * because they're unreachable then just end up being reselected the
	 * next time the ship scans for targets. */
	/*
	if (this.getParameter("oolite_flag_continueUnlikelyPursuits") == null)
	{
		if (this.ship.target)
		{
			if (this.isEscaping(this.ship.target))
			{
				this.ship.removeDefenseTarget(this.ship.target);
				this.ship.target = null;
			}
		}
	}
	*/

	if (this.ship.target)
	{
		if (!this.ship.hasHostileTarget)
		{
			// entering attack mode
			this.broadcastAttackMessage(this.ship.target,"beginning",3);
			this.ship.requestHelpFromGroup();
		}
		else 
		{
			this.broadcastAttackMessage(this.ship.target,"continuing",4);
		}
	}
	this.ship.performAttack();
}


// NOTE: this does not, and should not, check whether the station is friendly
PriorityAIController.prototype.behaviourDockWithStation = function()
{
	var station = this.getParameter("oolite_dockingStation");
	this.ship.target = station;
	var handlers = {};
	this.responsesAddStandard(handlers);
	this.responsesAddDocking(handlers);
	this.ship.requestDockingInstructions();
	if (!this.ship.dockingInstructions)
	{
		this.ship.performIdle();
		this.reconsiderNow();
		return;
	}
	switch (this.ship.dockingInstructions.ai_message)
	{
	case "TOO_BIG_TO_DOCK":
	case "DOCKING_REFUSED":
		this.setParameter("oolite_dockingStation",null);
		this.ship.target = null;
		this.reconsiderNow();
		break;
	case "HOLD_POSITION":
		this.communicate("oolite_dockingWait",{},4);
		this.ship.destination = station.position;
		this.ship.performFaceDestination();
		// and will reconsider in a little bit
		break;
	case "TRY_AGAIN_LATER":
		if (this.distance(station) < 5000)
		{
			if (this.__ltcache.oolite_dockingclearpos)
			{
				this.ship.destination = this.__ltcache.oolite_dockingclearpos;
			}
			else
			{
				this.ship.destination = station.position.add(Vector3D.randomDirection(8000+station.collisionRadius));
				this.__ltcache.oolite_dockingclearpos = this.ship.destination;
			}
		}
		this.ship.performFlyToRangeFromDestination();
		break;
	case "APPROACH_COORDINATES":
		if (this.ship.escortGroup && this.ship.escortGroup.count > 1)
		{
			// docking clearance has been granted - can now release escorts
			if (this.ship.dockingInstructions.docking_stage >= 2)
			{
				this.communicate("oolite_dockEscorts",{},3);
				this.ship.dockEscorts();
			}
		}
		// and fall through
	case "APPROACH":				
	case "BACK_OFF":
		this.ship.performFlyToRangeFromDestination();
		break;
	}
	this.applyHandlers(handlers);
}


PriorityAIController.prototype.behaviourEnterWitchspace = function()
{
	var handlers = {};
	this.responsesAddStandard(handlers);
	var wormhole = this.getParameter("oolite_witchspaceWormhole");
	if (wormhole && wormhole.expiryTime < clock.adjustedSeconds)
	{
		// the wormhole we were trying for has expired
		this.setParameter("oolite_witchspaceWormhole",null);
		if (this.ship.group && this.ship.group.leader && this.ship.group.leader.status == "STATUS_ENTERING_WITCHSPACE")
		{
			// left behind, so leave group
			this.ship.group.removeShip(this.ship);
			this.ship.group = null;
		}
	}
	else if (wormhole)
	{

		handlers.playerWillEnterWitchspace = this.responseComponent_trackPlayer_playerWillEnterWitchspace;
		this.ship.destination = wormhole.position;
		this.ship.desiredRange = 0;
		this.ship.desiredSpeed = this.ship.maxSpeed;
		this.ship.performFlyToRangeFromDestination();
		this.applyHandlers(handlers);
		return;
	}

	var destID = this.getParameter("oolite_witchspaceDestination");
	if (destID == null)
	{
		// look for wormholes out of here
		// no systems in range
		handlers.playerWillEnterWitchspace = this.responseComponent_trackPlayer_playerWillEnterWitchspace;
		this.applyHandlers(handlers);
		return;
	}
	else
	{
		handlers.shipWitchspaceBlocked = this.responseComponent_standard_shipWitchspaceBlocked;
		// set up the handlers before trying it
		this.applyHandlers(handlers);
		
		var entry = this.getParameter("oolite_witchspaceEntry");
		// wait for escorts to launch
		if (!this.conditionAllEscortsInFlight())
		{
			if (this.__ltcache.oolite_nearestStation && this.__ltcache.oolite_nearestStation.isValid && this.distance(this.__ltcache.oolite_nearestStation) < 15000)
			{
				var launchpos = this.ship.position.add(this.__ltcache.oolite_nearestStation.vectorForward.multiply(12000).add(this.__ltcache.oolite_nearestStation.vectorRight.multiply(30000)));
				this.ship.destination = launchpos;
			}
			else
			{
				this.ship.destination = this.ship.position.add(this.ship.vectorForward.multiply(300000));
			}

			this.ship.desiredRange = 10000;
			this.ship.desiredSpeed = this.cruiseSpeed();
			if (this.ship.checkCourseToDestination())
			{
				this.ship.destination = this.ship.getSafeCourseToDestination();
			}
			this.ship.performFlyToRangeFromDestination();

		}
		else if (entry != null && entry < clock.seconds)
		{
			// this should work
			var result = this.ship.exitSystem(destID);
			// if it doesn't, we'll get blocked
			if (result)
			{
				this.ship.notifyGroupOfWormhole();
				this.setParameter("oolite_witchspaceEntry",null);
			}
		}
		else
		{
			if (entry == null)
			{
				if ((this.ship.group && this.ship.group.count > 1) || (this.ship.escortGroup && this.ship.escortGroup.count > 1))
				{
					this.communicate("oolite_engageWitchspaceDriveGroup",{},4);
				} 
				else
				{
					this.communicate("oolite_engageWitchspaceDrive",{},4);
				}
				this.setParameter("oolite_witchspaceEntry",clock.seconds + 15);
			}

			if (this.__ltcache.oolite_nearestStation && this.__ltcache.oolite_nearestStation.isValid && this.distance(this.__ltcache.oolite_nearestStation) < 5000)
			{
				var launchpos = this.ship.position.add(this.__ltcache.oolite_nearestStation.vectorForward.multiply(8000).add(this.__ltcache.oolite_nearestStation.vectorRight.multiply(30000)));
				this.ship.destination = launchpos;
			}
			else
			{
				this.ship.destination = this.ship.position.add(this.ship.vectorForward.multiply(30000));
			}

			this.ship.desiredRange = 10000;
			this.ship.desiredSpeed = this.cruiseSpeed();
			if (this.ship.checkCourseToDestination())
			{
				this.ship.destination = this.ship.getSafeCourseToDestination();
			}
			this.ship.performFlyToRangeFromDestination();
		}
	}
}


PriorityAIController.prototype.behaviourEscortMothership = function()
{
	var handlers = {};
	if (this.ship.group.leader)
	{
		this.communicate("oolite_escortFormation",this.ship.group.leader,4);
	}

	this.responsesAddStandard(handlers);
	this.responsesAddEscort(handlers);
	this.applyHandlers(handlers);
	this.ship.desiredRange = 0;
	this.ship.performEscort();
}


PriorityAIController.prototype.behaviourFineCurrentTarget = function()
{
	var handlers = {};
	this.responsesAddStandard(handlers);
	this.applyHandlers(handlers);
	
	if (this.ship.scanClass == "CLASS_POLICE" && this.ship.target)
	{
		this.communicate("oolite_markForFines",this.ship.target,1);
		
		this.ship.markTargetForFines();
	}

	this.ship.performIdle();
}


PriorityAIController.prototype.behaviourFleeCombat = function()
{
	var handlers = {};
	this.responsesAddStandard(handlers);
	this.applyHandlers(handlers);

	var cascade = this.getParameter("oolite_cascadeDetected");
	if (cascade != null)
	{
		if (cascade.distanceTo(this.ship) < this.scannerRange)
		{
			if (this.ship.defenseTargets.length > 0 && this.ship.defenseTargets[0].scanClass == "CLASS_MINE")
			{
				// if the mine is still visible, conventional fleeing works
				this.ship.target = this.ship.defenseTargets[0];
				this.ship.desiredRange = 30000;
				this.ship.performFlee();
				return;
			}
			else
			{
				if (this.ship.destination != cascade)
				{
					this.communicate("oolite_quiriumCascade",{},4);
				}
				this.ship.destination = cascade;
				this.ship.desiredRange = 30000;
				this.ship.desiredSpeed = 10*this.ship.maxSpeed;
				this.ship.performFlyToRangeFromDestination();
				return;
			}
		}
		else
		{
			this.setParameter("oolite_cascadeDetected",null);
		}
	}
	if (!this.ship.target || this.distance(this.ship.target) > this.scannerRange * 1.25)
	{
		var aggressor = this.ship.AIPrimaryAggressor;
		if (aggressor && aggressor.isInSpace && this.distance(aggressor) < this.scannerRange)
		{
			this.ship.target = aggressor;
		}
		else
		{
			var dts = this.ship.defenseTargets;
			for (var i = 0 ; i < dts.length ; i++)
			{
				if (this.distance(dts[i]) < this.scannerRange && this.isFighting(dts[i]))
				{
					this.ship.target = dts[i];
					break;
				}
			}
		}
	}
	if (this.getParameter("oolite_lastFleeing") != null)
	{
		this.communicate("oolite_continueFleeing",this.ship.target,4);
	}
	else if (this.ship.energy < this.ship.maxEnergy / 4)
	{
		this.communicate("oolite_startFleeing",this.ship.target,3);
	}
	if (this.ship.target)
	{
		this.setParameter("oolite_lastFleeing",this.ship.target);
	}
	
	if (!this.__ltcache.oolite_considerWitchspaceFlee)
	{
		if (this.getParameter("oolite_flag_neverFleeToWitchspace") == null)
		{
			this.__ltcache.oolite_considerWitchspaceFlee = (this.ship.hasHyperspaceMotor && ((system.isInterstellarSpace && this.ship.fuel > 0) || (system.ID != this.ship.homeSystem && system.info.systemsInRange(this.ship.fuel).length > 0)))?1:-1;
		}
		else
		{
			this.__ltcache.oolite_considerWitchspaceFlee = -1;
		}
	}

	if (this.__ltcache.oolite_considerWitchspaceFlee == 1)
	{
		if (!this.__ltcache.oolite_witchspaceflee)
		{
			this.communicate("oolite_engageWitchspaceDriveFlee",{},2);
			this.__ltcache.oolite_witchspaceflee = clock.seconds + 15;
		}
		if (this.__ltcache.oolite_witchspaceflee < clock.seconds)
		{
			if (this.ship.exitSystem())
			{
				this.ship.notifyGroupOfWormhole();
				delete this.__ltcache.oolite_witchspaceflee;
			}
		}
	}
	if (this.ship.group && this.ship.group.leader && this.ship.group.leader != this.ship)
	{
		var leader = this.ship.group.leader;
		if (leader.status == "STATUS_ENTERING_WITCHSPACE")
		{
			var wormholes = system.wormholes;
			for (var i=0;i<wormholes.count;i++)
			{
				var wormhole = wormholes[i];
				if (wormhole.expiryTime > clock.seconds && wormhole.position.distanceTo(leader) < 100 && wormhole.position.distanceTo(this.ship) < this.scannerRange)
				{
					// if the leader has departed and the wormhole is
					// reachable, go for it!
					this.ship.destination = wormhole.position;
					this.ship.desiredSpeed = this.ship.maxSpeed * 7;
					this.ship.performFlyToRangeFromDestination();
					return;
				}
			}
		}
	}

	this.ship.desiredRange = this.scannerRange;
	this.ship.performFlee();
}


/* Follow a ship, including to witchspace */
PriorityAIController.prototype.behaviourFollowCurrentTarget = function()
{
	if (this.ship.target)
	{
		var rt = this.ship.target;
	}
	else
	{
		var rt = this.getParameter("oolite_rememberedTarget");
	}
	if (!rt) {
		return;
	}
	this.ship.destination = rt.position;

	if (rt.status == "STATUS_ENTERING_WITCHSPACE")
	{
		if (this.getParameter("oolite_flag_witchspacePursuit"))
		{
			var pos = rt.position;
			var ws = system.wormholes;
			// most likely to be most recent
			for (var i=ws.length-1; i>=0; i--)
			{
				if (ws[i].position.distanceTo(pos) < 100)
				{
					this.setParameter("oolite_witchspaceWormhole",ws[i]);
					this.setParameter("oolite_rememberedTarget",null);
					break;
				}
			}

			this.ship.desiredRange = 0; // use wormhole
		}
		else
		{
			this.ship.destination = this.ship.position;
			this.ship.target = null;
			this.setParameter("oolite_rememberedTarget",null);
		}
	}
	else
	{
		this.setParameter("oolite_rememberedTarget",rt);
		this.ship.desiredRange = 500+Math.random()*1000;
	}
	this.ship.desiredSpeed = this.ship.maxSpeed;
	this.behaviourApproachDestination();
}


/* Follow the group leader in a less organised way than escorting them */
PriorityAIController.prototype.behaviourFollowGroupLeader = function()
{
	if (!this.ship.group || !this.ship.group.leader)
	{
		var handlers = {};
		this.responsesAddStandard(handlers);
		this.applyHandlers(handlers);
		this.ship.performIdle();
	}
	else
	{
		var gl = this.ship.group.leader;
		this.ship.destination = gl.position.add(gl.vectorForward.multiply(gl.speed*10));
		this.ship.desiredRange = 500+Math.random()*1000;
		this.ship.desiredSpeed = Math.min(this.ship.maxSpeed,gl.speed*1.5);
		this.behaviourApproachDestination();
	}
}


PriorityAIController.prototype.behaviourGuardTarget = function()
{
	if (!this.ship.target)
	{
		this.ship.destination = this.ship.position;						
	}
	else
	{
		this.ship.destination = this.ship.target.position;
	}
	this.ship.desiredSpeed = this.cruiseSpeed();
	this.ship.desiredRange = 2500;
	this.behaviourApproachDestination();
}


PriorityAIController.prototype.behaviourJoinTargetGroup = function()
{
	if (this.ship.target && this.ship.target.group)
	{
		this.ship.target.group.addShip(this.ship);
		this.ship.group = this.ship.target.group;
	}
	this.ship.performIdle();
}


PriorityAIController.prototype.behaviourLandOnPlanet = function()
{
	this.ship.desiredSpeed = this.ship.maxSpeed / 4;
	this.ship.performLandOnPlanet();
	this.ship.AIScriptWakeTime = 0; // cancel reconsiderations
	this.applyHandlers({}); // cancel interruptions
	this.communicate("oolite_landingOnPlanet",{},4);
}


PriorityAIController.prototype.behaviourLeaveVicinityOfDestination = function()
{
	this.ship.desiredRange = 60000;
	this.ship.desiredSpeed = this.ship.maxSpeed;
	this.communicate("oolite_leaveVicinity",this.ship.target,3);
	this.behaviourApproachDestination();
}


PriorityAIController.prototype.behaviourLeaveVicinityOfTarget = function()
{
	if (!this.ship.target)
	{
		this.reconsiderNow();
		return;
	}
	this.ship.destination = this.ship.target.position;
	this.ship.desiredRange = 27500;
	this.ship.desiredSpeed = this.ship.maxSpeed;
	this.communicate("oolite_leaveVicinity",this.ship.target,3);
	this.behaviourApproachDestination();
}


PriorityAIController.prototype.behaviourMineTarget = function()
{
	var handlers = {};
	this.responsesAddStandard(handlers);
	this.applyHandlers(handlers);
	this.communicate("oolite_mining",{},4);
	this.ship.performMining();
}


PriorityAIController.prototype.behaviourOfferToEscort = function()
{
	var handlers = {};
	this.responsesAddStandard(handlers);
	this.applyHandlers(handlers);
	
	var possible = this.getParameter("oolite_scanResultSpecific");
	if (possible == null)
	{
		this.reconsiderNow();
	}
	else
	{
		if (this.ship.offerToEscort(possible))
		{
			// accepted
			this.reconsiderNow();
		}
		// if rejected, wait for next scheduled reconsideration
	}
}


PriorityAIController.prototype.behaviourPayOffPirates = function()
{
	this.ship.dumpCargo(this.ship.AIScript.oolite_intership.cargodemand);
	this.communicate("oolite_agreeingToDumpCargo",{"oolite_demandSize":this.ship.AIScript.oolite_intership.cargodemand},1);
	delete this.ship.AIScript.oolite_intership.cargodemand;
	this.ship.AIScript.oolite_intership.cargodemandpaid = true;
	this.behaviourFleeCombat();
}


PriorityAIController.prototype.behaviourReconsider = function()
{
	var handlers = {};
	this.responsesAddStandard(handlers);
	this.applyHandlers(handlers);
	this.reconsiderNow();
}


// Separate behaviour to EscortMothership in case we want to change it later
// This is the one to catch up with a distant mothership
PriorityAIController.prototype.behaviourRejoinMothership = function()
{
	var handlers = {};
	this.responsesAddStandard(handlers);
	this.responsesAddEscort(handlers);
	this.applyHandlers(handlers);
	// to consider: should this behaviour use injectors if
	// possible? so few escorts have them that it's probably not
	// worth it.
	this.ship.desiredRange = 0;
	this.ship.performEscort();
}


PriorityAIController.prototype.behaviourRepelCurrentTarget = function()
{
	this.setParameter("oolite_witchspaceEntry",null);

	var handlers = {};
	this.responsesAddStandard(handlers);
	this.applyHandlers(handlers);
	var target = this.ship.target
	if (!target || !target.isValid || !target.isShip)
	{
		this.reconsiderNow();
		return;
	}

	if (this.getParameter("oolite_flag_noSpecialThargoidReaction") != null)
	{
		if (this.ship.scanClass != "CLASS_THARGOID" && target.scanClass != "CLASS_THARGOID" && target.target.scanClass == "CLASS_THARGOID")
		{
			this.respondToThargoids(target.target,true);
			this.ship.performAttack();
			return;
		}
	}

	if (!this.isAggressive(target))
	{
		// repelling succeeded
		if (this.ship.escortGroup)
		{
			// also tell escorts to stop attacking it
			for (var i = 0 ; i < this.ship.escortGroup.ships.length ; i++)
			{
				this.ship.escortGroup.ships[i].removeDefenseTarget(target);
				if (this.ship.escortGroup.ships[i].target == target)
				{
					this.ship.escortGroup.ships[i].target = null;
				}
			}
		}
		this.ship.removeDefenseTarget(target);
		this.ship.target = null;
	}
	else
	{
		if (!this.ship.hasHostileTarget)
		{
			// entering attack mode
			this.broadcastAttackMessage(this.ship.target,"beginning",3);
			this.ship.requestHelpFromGroup();
		}
		else if (this.ship.target)
		{
			this.broadcastAttackMessage(this.ship.target,"continuing",4);
		}
		if (this.ship.energy == this.ship.maxEnergy && this.getParameter("oolite_flag_escortsCoverRetreat") && this.ship.escortGroup.count > 1)
		{
			// if has escorts, and is not yet taking damage, run and let
			// the escorts take them on
			this.ship.performFlee();
			return;
		}
		this.ship.performAttack();
	}
}


/* Standard "help the innocent" distress call response. Perhaps
 * there should be a 'blood in the water' response available
 * too... */
PriorityAIController.prototype.behaviourRespondToDistressCall = function()
{
	var aggressor = this.getParameter("oolite_distressAggressor");
	var sender = this.getParameter("oolite_distressSender");
	if (aggressor && aggressor.isShip && sender && sender.isShip)
	{
		if (sender.bounty > aggressor.bounty)
		{
			var tmp = sender;
			sender = aggressor;
			aggressor = tmp;
		}
		if (this.distance(aggressor) < this.scannerRange)
		{
			this.ship.target = aggressor;
			this.ship.performAttack();
			this.reconsiderNow();
			this.communicate("oolite_distressResponseAggressor",aggressor,2);
		}
		else
		{ // we can't actually see what's attacking the sender yet
			this.ship.destination = sender.position;
			this.ship.desiredRange = 1000+sender.collisionRadius+this.ship.collisionRadius;
			this.ship.desiredSpeed = 7 * this.ship.maxSpeed; // use injectors if possible
			this.ship.performFlyToRangeFromDestination();
			// and when we next reconsider, hopefully the aggressor will be on the scanner
			this.communicate("oolite_distressResponseSender",sender,2);
		}
	}
	var handlers = {};
	this.responsesAddStandard(handlers);
	this.applyHandlers(handlers);
}


PriorityAIController.prototype.behaviourRobTarget = function()
{
	var demand = null;
	if (this.ship.group && this.ship.group.leader)
	{
		if (this.ship.group.leader.AIScript.oolite_intership && this.ship.group.leader.AIScript.oolite_intership.cargodemanded)
		{
			demand = this.ship.group.leader.AIScript.oolite_intership.cargodemanded;
		}
	}
	else
	{
		if (this.ship.AIScript.oolite_intership.cargodemanded)
		{
			demand = this.ship.AIScript.oolite_intership.cargodemanded;
		}
	}
	if (demand == null)
	{
		var target = this.ship.target;
		var hascargo = target.cargoSpaceCapacity; //cargoSpaceUsed?
		// blowing them up probably gets ~10%, so how much we feel
		// confident in demanding depends on how likely patrols
		// are to come along and interfere.
		demand = (hascargo/20);
		demand = demand * (1+Math.random()+(8-system.info.government)/8);
		// between 5% and 15% of cargo
		if (this.conditionCombatOddsExcellent())
		{
			// if we have overwhelming force, can get away with demanding more
			demand *= 1+Math.random();
			// between 5% and 30% of cargo
		}
		demand = Math.ceil(demand); // round it up so there's always at least 1

		var maxdemand = 0;
		var gc = 1;
		if (!this.ship.group)
		{
			if (this.ship.hasEquipmentProviding("EQ_CARGO_SCOOPS"))
			{
				maxdemand = this.ship.cargoSpaceAvailable;
			}
		}
		else
		{
			gc = this.ship.group.ships.length;
			for (var i = 0; i < gc ; i++)
			{
				var ship = this.ship.group.ships[i];
				if (ship.hasEquipmentProviding("EQ_CARGO_SCOOPS"))
				{
					maxdemand += ship.cargoSpaceAvailable;
				}
				else
				{
					gc--; // this ship can't help scoop
				}
			}
		}
		if (demand > maxdemand)
		{
			demand = maxdemand; // don't ask for more than we can carry
		}
		while (demand > gc * 5)
		{
			// asking for more than 5TC each probably means there
			// won't be time to pick it all up anyway
			demand = Math.ceil(demand/2);
		}
		if (demand < 2)
		{
			demand = 2;
		}

		/* Record our demand with the group leader */
		if (this.ship.group && this.ship.group.leader)
		{
			this.ship.group.leader.AIScript.oolite_intership.cargodemanded = demand;
		}
		else
		{
			this.ship.AIScript.oolite_intership.cargodemanded = demand;
		}
		/* Inform the victim of the demand, if possible */
		if (target.AIScript && target.AIScript.oolite_intership)
		{
			target.AIScript.oolite_intership.cargodemand = demand;
		}
		var commsparams = this.entityCommsParams(target);
		commsparams["oolite_demandSize"] = demand;
		this.ship.performAttack(); // must be before the comms message
		this.communicate("oolite_makePirateDemand",commsparams,1);
		this.ship.requestHelpFromGroup();
		// prevents choosing this ship twice in a row
		// either it beat us, or we just robbed it
		this.setParameter("oolite_lastPirateVictim",target);

		/*				}
						else
						{
						log(this.ship.displayName,"Already asked for "+demand); */
	}
	var handlers = {};
	this.responsesAddStandard(handlers);
	this.applyHandlers(handlers);
	this.ship.performAttack();
	this.ship.requestHelpFromGroup();
}


PriorityAIController.prototype.behaviourSunskim = function()
{
	var handlers = {};
	this.responsesAddStandard(handlers);
	this.responsesAddScooping(handlers);
	this.applyHandlers(handlers);
	this.ship.performFlyToRangeFromDestination();
}


PriorityAIController.prototype.behaviourTumble = function()
{
	this.applyHandlers({});
	this.ship.performTumble();
}


PriorityAIController.prototype.behaviourWaitHere = function()
{
	var handlers = {};
	this.responsesAddStandard(handlers);
	this.applyHandlers(handlers);
	this.ship.performStop();
}


/* Missile behaviours: have different standard handler sets */

PriorityAIController.prototype.behaviourMissileInterceptTarget = function()
{
	var handlers = {};
	this.responsesAddMissile(handlers);
	this.applyHandlers(handlers);
	if (this.ship.scriptInfo.oolite_missile_proximity)
	{
		this.ship.desiredRange = this.ship.scriptInfo.oolite_missile_proximity;
	}
	else
	{
		this.ship.desiredRange = 25;					
	}

	if (this.ship.target && !this.ship.target.isCloaked)
	{
		this.ship.performIntercept();
	}
	else
	{
		this.ship.performIdle();
	}
}

PriorityAIController.prototype.behaviourMissileInterceptCoordinates = function()
{
	var handlers = {};
	this.responsesAddMissile(handlers);
	this.applyHandlers(handlers);
	if (this.ship.scriptInfo.oolite_missile_proximity)
	{
		this.ship.desiredRange = this.ship.scriptInfo.oolite_missile_proximity;
	}
	else
	{
		this.ship.desiredRange = 25;					
	}
	var dest = this.getParameter("oolite_interceptCoordinates");
	if (dest == null)
	{
		return;
	}
	this.ship.destination = dest
	this.ship.desiredSpeed = this.ship.maxSpeed;
	this.ship.performFlyToRangeFromDestination();
	
	// if we have an intercept target, try to restore it
	var oldtarget = this.getParameter("oolite_interceptTarget");
	if (oldtarget && !oldtarget.isCloaked && oldtarget.isInSpace)
	{
		this.ship.target = oldtarget;
	}
}

PriorityAIController.prototype.behaviourMissileSelfDestruct = function() {
	this.ship.explode();
}



/* Station behaviours: have different standard handler sets */

PriorityAIController.prototype.behaviourStationLaunchDefenseShips = function() 
{
	if (system.sun && (system.sun.isGoingNova || system.sun.hasGoneNova))
	{
		return;
	}
	if (this.ship.target && this.isAggressive(this.ship.target))
	{
		if (this.ship.alertCondition < 3) 
		{
			if (this.ship.alertCondition == 1) this.ship.increaseAlertLevel();
			this.ship.increaseAlertLevel();
		}
		this.ship.launchDefenseShip();
		this.communicate("oolite_launchDefenseShips",this.ship.target,3);
		this.ship.requestHelpFromGroup();
	}
	else if (this.ship.alertCondition > 1)
	{
		this.ship.decreaseAlertLevel();
	}
	var handlers = {};
	this.responsesAddStation(handlers);
	this.applyHandlers(handlers);
}


PriorityAIController.prototype.behaviourStationLaunchMiner = function() 
{
	if (system.sun && (system.sun.isGoingNova || system.sun.hasGoneNova))
	{
		return;
	}
	if (this.ship.alertCondition > 1)
	{
		this.ship.decreaseAlertLevel();
	}
	var handlers = {};
	this.responsesAddStation(handlers);
	this.applyHandlers(handlers);
	if (this.ship.group)
	{
		for (var i = 0 ; i < this.ship.group.ships.length ; i++)
		{
			if (this.ship.group.ships[i].primaryRole == "miner")
			{
				// only one in flight at once
				return;
			}
		}
	}
	this.communicate("oolite_launchMiner",this.ship.target,3);
	this.ship.launchMiner();
}


PriorityAIController.prototype.behaviourStationLaunchPatrol = function() 
{
	if (system.sun && (system.sun.isGoingNova || system.sun.hasGoneNova))
	{
		return;
	}
	if (this.ship.alertCondition > 1)
	{
		this.ship.decreaseAlertLevel();
	}
	var handlers = {};
	this.responsesAddStation(handlers);
	this.applyHandlers(handlers);

	if (this.ship.group)
	{
		for (var i = 0 ; i < this.ship.group.ships.length ; i++)
		{
			if (this.ship.group.ships[i].primaryRole == this.getParameter("oolite_stationPatrolRole"))
			{
				// only one in flight at once
				return;
			}
		}
	}
	this.communicate("oolite_launchPatrol",this.ship.target,3);
	this.ship.launchPatrol();
}


PriorityAIController.prototype.behaviourStationLaunchSalvager = function() 
{
	if (system.sun && (system.sun.isGoingNova || system.sun.hasGoneNova))
	{
		return;
	}
	if (this.ship.alertCondition > 1)
	{
		this.ship.decreaseAlertLevel();
	}
	this.communicate("oolite_launchSalvager",this.ship.target,3);
	this.ship.launchScavenger();
	
	var handlers = {};
	this.responsesAddStation(handlers);
	this.applyHandlers(handlers);
}


PriorityAIController.prototype.behaviourStationManageTraffic = function() 
{
	var handlers = {};
	this.responsesAddStation(handlers);
	this.applyHandlers(handlers);
	// does nothing special in this state, just waits around being a station
}


PriorityAIController.prototype.behaviourStationRespondToDistressCall = function() 
{
	if (system.sun && (system.sun.isGoingNova || system.sun.hasGoneNova))
	{
		return;
	}
	var aggressor = this.getParameter("oolite_distressAggressor");
	var sender = this.getParameter("oolite_distressSender");
	if (sender.bounty > aggressor.bounty)
	{
		var tmp = sender;
		sender = aggressor;
		aggressor = tmp;
	}
	if (this.distance(aggressor) < this.scannerRange)
	{
		this.ship.target = aggressor;
		if (this.ship.alertCondition < 3) 
		{
			if (this.ship.alertCondition == 1) this.ship.increaseAlertLevel();
			this.ship.increaseAlertLevel();
		}
		this.ship.launchDefenseShip();
		this.communicate("oolite_distressResponseAggressor",aggressor,2);
		this.ship.requestHelpFromGroup();
	}
	else
	{
		this.communicate("oolite_distressResponseSender",sender,3);
	}

	var handlers = {};
	this.responsesAddStation(handlers);
	this.applyHandlers(handlers);
}


/* ****************** Configuration functions ************** */

/* Configurations. Configurations are set up actions for a behaviour
 * or behaviours. They can also be used on a fall-through conditional
 * to set parameters for later tests */

/*** Target acquisition configuration ***/

PriorityAIController.prototype.configurationAcquireCombatTarget = function()
{
	var target = this.ship.target;
	if (target && this.allied(this.ship,target))
	{
		// don't shoot at allies even if they have ended up as a target...
		this.ship.removeDefenseTarget(target);
		this.ship.target = null;
	}
	if (target && (target.scanClass == "CLASS_CARGO" || target.scanClass == "CLASS_BUOY"))
	{
		if (target.isShip)
		{
			this.ship.removeDefenseTarget(target);
		}
		this.ship.target = null;
	}
	/* Iff the ship does not currently have a target, select a new one
	 * from the defense target list. */
	if (target)
	{
		if (target.isInSpace)
		{
			return;
		}
		if (this.ship.target.isShip)
		{
			this.ship.removeDefenseTarget(target);
		}
		this.ship.target = null;
	}
	var dts = this.ship.defenseTargets
	var dtsl = dts.length; // we need to iterate up this time
	var scan = this.scannerRange;
	for (var i = 0; i < dtsl ; i++)
	{
		if (this.distance(dts[i]) < scan)
		{
			if (!dts[i].isCloaked)
			{
				this.ship.target = dts[i];
				return;
			}
		}
		else
		{
			this.ship.removeDefenseTarget(dts[i]);
		}
	}
	if (this.ship.group != null)
	{
		var gs = this.ship.group.ships;
		for (var i = gs.length-1 ; i >= 0 ; i--)
		{
			if (gs[i] != this.ship)
			{
				if (this.isFighting(gs[i]) && this.distance(gs[i].target) < scan && gs[i].target.isShip)
				{
					this.ship.target = gs[i].target;
					return;
				}
			}
		}
	}
	if (this.ship.escortGroup != null)
	{
		var gs = this.ship.escortGroup.ships;
		for (var i = gs.length-1 ; i >= 0 ; i--)
		{
			if (gs[i] != this.ship)
			{
				if (this.isFighting(gs[i]) && this.distance(gs[i].target) < scan && gs[i].target.isShip)
				{
					this.ship.target = gs[i].target;
					return;
				}
			}
		}
	}
}


PriorityAIController.prototype.configurationAcquireDefensiveEscortTarget = function()
{
	if (this.ship.target && this.allied(this.ship,this.ship.target))
	{
		// don't shoot at allies even if they have ended up as a target...
		this.ship.removeDefenseTarget(this.ship.target);
		this.ship.target = null;
	}
	/* Preserve current target if still fighting (leader can send help
	 * request if needed) */
	if (this.ship.target)
	{
		if (this.ship.target.isInSpace && this.isAggressive(this.ship.target))
		{
			return;
		}
		if (this.ship.target.isShip)
		{
			this.ship.removeDefenseTarget(this.ship.target);
		}
		this.ship.target = null;
	}

	if (this.ship.group && this.ship.group.leader)
	{
		var leader = this.ship.group.leader;
		if (this.isFighting(leader) && leader.target.target == leader && this.distance(leader.target) < this.scannerRange)
		{
			this.ship.target = leader.target;
		}
		else
		{
			var dts = leader.defenseTargets;
			for (var i = 0 ; i < dts.length ; i++)
			{
				if (dts[i].target == leader && this.isAggressive(dts[i]) && this.distance(dts[i]) < this.scannerRange)
				{
					if (!dts[i].isCloaked)
					{
						this.ship.target = dts[i];
					}
				}
			}
		}
	}
}


// TODO: reuse code from AcquireCombatTarget better
PriorityAIController.prototype.configurationAcquireHostileCombatTarget = function()
{
	if (this.ship.target && this.allied(this.ship,this.ship.target))
	{
		// don't shoot at allies even if they have ended up as a target...
		this.ship.removeDefenseTarget(this.ship.target);
		this.ship.target = null;
	}
	/* Iff the ship does not currently have a target, select a new one
	 * from the defense target list. */
	if (this.ship.target)
	{
		if (this.ship.target.isInSpace && this.isAggressive(this.ship.target))
		{
			return;
		}
		if (this.ship.target.isShip)
		{
			this.ship.removeDefenseTarget(this.ship.target);
		}
		this.ship.target = null;
	}
	var dts = this.ship.defenseTargets
	for (var i = 0; i < dts.length ; i++)
	{
		if (this.distance(dts[i]) < this.scannerRange && this.isAggressive(dts[i]))
		{
			if (!dts[i].isCloaked)
			{
				this.ship.target = dts[0];
				return;
			}
		}
	}
	if (this.ship.group != null)
	{
		var gs = this.ship.group.ships;
		for (var i = gs.length-1 ; i >= 0 ; i--)
		{
			if (gs[i] != this.ship)
			{
				if (this.isFighting(gs[i]) && this.distance(gs[i].target) < this.scannerRange && this.isAggressive(gs[i].target))
				{
					this.ship.target = gs[i].target;
					return;
				}
			}
		}
	}
	if (this.ship.escortGroup != null)
	{
		var gs = this.ship.escortGroup.ships;
		for (var i = gs.length-1 ; i >= 0 ; i--)
		{
			if (gs[i] != this.ship)
			{
				if (this.isFighting(gs[i]) && this.distance(gs[i].target) < this.scannerRange && this.isAggressive(gs[i].target))
				{
					this.ship.target = gs[i].target;
					return;
				}
			}
		}
	}
}


PriorityAIController.prototype.configurationAcquireOffensiveEscortTarget = function()
{
	if (this.ship.target && this.allied(this.ship,this.ship.target))
	{
		// don't shoot at allies even if they have ended up as a target...
		this.ship.removeDefenseTarget(this.ship.target);
		this.ship.target = null;
	}
	/* Preserve current target if still fighting */
	if (this.ship.target)
	{
		if (this.ship.target.isInSpace && this.isAggressive(this.ship.target))
		{
			return;
		}
		if (this.ship.target.isShip) {
			this.ship.removeDefenseTarget(this.ship.target);
		}
		this.ship.target = null;
	}

	if (this.ship.group && this.ship.group.leader)
	{ 
		var leader = this.ship.group.leader;
		var lt;
		if ((lt = leader.target) && lt.isShip && leader.hasHostileTarget)
		{
			if (this.distance(lt) < this.scannerRange)
			{
				if (!lt.isCloaked && lt.isShip)
				{
					this.ship.target = lt;
					this.ship.addDefenseTarget(lt);
				}
			}
		}
	}
}

PriorityAIController.prototype.configurationAcquirePlayerAsTarget = function()
{
	this.ship.target = player.ship;
}


PriorityAIController.prototype.configurationAcquireScannedTarget = function()
{
	this.ship.target = this.getParameter("oolite_scanResultSpecific");
}


PriorityAIController.prototype.configurationCheckScanner = function()
{
	if (this.getParameter("oolite_flag_scanIgnoresUnpowered") != null)
	{
		this.setParameter("oolite_scanResults",this.ship.checkScanner(true));
	}
	else
	{
		this.setParameter("oolite_scanResults",this.ship.checkScanner());
	}
	this.setParameter("oolite_scanResultSpecific",null);
}


/*** Navigation configuration ***/


PriorityAIController.prototype.configurationSelectPlanet = function()
{
	var possibles = system.planets;
	this.setParameter("oolite_selectedPlanet",possibles[Math.floor(Math.random()*possibles.length)]);
}


PriorityAIController.prototype.configurationSelectRandomTradeStation = function()
{
	var stations = system.stations;
	var chosenStation = null;
	if (this.ship.bounty == 0)
	{
		if (Math.random() < 0.9 && this.friendlyStation(system.mainStation))
		{
			this.setParameter("oolite_selectedStation",system.mainStation);
			this.communicate("oolite_selectedStation",system.mainStation,4);
			return;
		}
	} 
	else if (this.ship.bounty <= this.fineThreshold())
	{
		if (Math.random() < 0.5 && this.friendlyStation(system.mainStation))
		{
			this.setParameter("oolite_selectedStation",system.mainStation);
			this.communicate("oolite_selectedStation",system.mainStation,4);
			return;
		}
	}
	var friendlies = 0;
	for (var i = stations.length -1 ; i >= 0 ; i--)
	{
		var station = stations[i];
		if (this.friendlyStation(station))
		{
			friendlies++;
			// equivalent to filtering the list to only contain
			// friendlies, then picking a random element.
			if (Math.random() < 1/friendlies)
			{
				chosenStation = station;
			}
		}
	}
	this.setParameter("oolite_selectedStation",chosenStation);
	this.communicate("oolite_selectedStation",chosenStation,4);
}


PriorityAIController.prototype.configurationSelectShuttleDestination = function()
{
	var possibles = system.planets.concat(system.stations);
	var destinations1 = [];
	var destinations2 = [];
	for (var i = 0; i < possibles.length ; i++)
	{
		var possible = possibles[i];
		// travel at least a little way
		var distance = this.distance(possible);
		if (distance > possible.collisionRadius + 10000)
		{
			// must be friendly destination and not moving too fast
			if (possible.isPlanet || (this.friendlyStation(possible) && (possible.maxSpeed < this.ship.maxSpeed / 5)))
			{
				if (distance > system.mainPlanet.radius * 5)
				{
					destinations2.push(possible);
				}
				else
				{
					destinations1.push(possible);
				}
			}
		}
	}
	// no nearby destinations
	if (destinations1.length == 0)
	{
		destinations1 = destinations2;
	}
	// no destinations
	if (destinations1.length == 0)
	{
		return;
	}
	var destination = destinations1[Math.floor(Math.random()*destinations1.length)];
	if (destination.isPlanet)
	{
		this.setParameter("oolite_selectedPlanet",destination);
		this.setParameter("oolite_selectedStation",null);
	}
	else
	{
		this.setParameter("oolite_selectedStation",destination);
		this.communicate("oolite_selectedStation",destination,4);
		this.setParameter("oolite_selectedPlanet",null);
	}
}


PriorityAIController.prototype.configurationSelectWitchspaceDestination = function()
{
	if (!this.ship.hasHyperspaceMotor)
	{
		this.setParameter("oolite_witchspaceDestination",null);
		return;
	}
	var preselected = this.getParameter("oolite_witchspaceDestination");
	if (preselected != system.ID && system.info.distanceToSystem(System.infoForSystem(galaxyNumber,preselected)) <= this.ship.fuel)
	{
		// we've already got a destination
		return;
	}
	var possible = system.info.systemsInRange(this.ship.fuel);
	if (possible.length > 0)
	{
		var selected = possible[Math.floor(Math.random()*possible.length)];
		this.setParameter("oolite_witchspaceDestination",selected.systemID);
		this.communicate("oolite_selectedWitchspaceDestination",{"oolite_witchspaceDestination":selected.name},4);
	}
	else
	{
		this.setParameter("oolite_witchspaceDestination",null);
	}
}


PriorityAIController.prototype.configurationSelectWitchspaceDestinationInbound = function()
{
	if (this.ship.homeSystem == this.ship.destinationSystem)
	{
		return this.configurationSelectWitchspaceDestination();
	}
	this.setWitchspaceRouteTo(this.ship.homeSystem);
}


PriorityAIController.prototype.configurationSelectWitchspaceDestinationOutbound = function()
{
	if (this.ship.homeSystem == this.ship.destinationSystem)
	{
		return this.configurationSelectWitchspaceDestination();
	}
	this.setWitchspaceRouteTo(this.ship.destinationSystem);
}


/*** Destination configuration ***/


PriorityAIController.prototype.configurationMissileAdjustLaunch = function()
{
	// clear flag
	this.setParameter("oolite_flag_launchAdjustMissile",null);

	/* tilt down and accelerate - assumes missile is co-aligned to
	 * ship on launch, and launched from below the ship */
	this.ship.destination = this.ship.position.add(this.ship.vectorUp);
	this.ship.desiredRange = 100000;
	this.ship.desiredSpeed = this.ship.maxSpeed;
}


PriorityAIController.prototype.configurationMissileAdjustSpread = function()
{
	var near = this.getParameter("oolite_scanResultSpecific");
	if (!near)
	{
		this.ship.destination = this.ship.target.position;
		this.ship.desiredRange = 100;
		this.ship.desiredSpeed = this.ship.maxSpeed;
	}
	else
	{
		this.ship.destination = near.position.add(Vector3D.randomDirection(20));
		this.ship.desiredRange = 1000;
		this.ship.desiredSpeed = this.ship.maxSpeed;
	}
}


PriorityAIController.prototype.configurationSetDestinationToHomeStation = function()
{
	var home = this.homeStation();
	if (home != null)
	{
		this.ship.destination = home.position;
		this.ship.desiredRange = 15000;
		this.ship.desiredSpeed = this.cruiseSpeed();
	}
	else
	{
		this.ship.destination = this.ship.position;
		this.ship.desiredRange = 0;
	}
}


PriorityAIController.prototype.configurationSetDestinationToGroupLeader = function()
{
	if (!this.ship.group || !this.ship.group.leader)
	{
		this.ship.destination = this.ship.position;
	}
	else
	{
		this.ship.destination = this.ship.group.leader.position;
	}
	this.ship.desiredRange = 2000;
	this.ship.desiredSpeed = this.ship.maxSpeed;
}


PriorityAIController.prototype.configurationSetDestinationToMainPlanet = function()
{
	if (system.mainPlanet)
	{
		this.ship.destination = system.mainPlanet.position;
		this.ship.desiredRange = system.mainPlanet.radius * 3;
		this.ship.desiredSpeed = this.cruiseSpeed();
	}
}


PriorityAIController.prototype.configurationSetDestinationToMainStation = function()
{
	this.ship.destination = system.mainStation.position;
	this.ship.desiredRange = 15000;

	this.ship.desiredSpeed = this.cruiseSpeed();
}


PriorityAIController.prototype.configurationSetDestinationToNearestFriendlyStation = function()
{
	var stations = system.stations;
	var threshold = 1E16;
	var chosenStation = null;
	for (var i = 0 ; i < stations.length ; i++)
	{
		var station = stations[i];
		if (this.friendlyStation(station))
		{
			var distance = this.distance(station);
			if (distance < threshold)
			{
				threshold = distance;
				chosenStation = station;
			}
		}
	}
	if (chosenStation == null)
	{
		this.ship.destination = this.ship.position;
		this.ship.desiredRange = 0;
	}
	else
	{
		this.ship.destination = chosenStation.position;
		this.ship.desiredRange = 15000;
		this.ship.desiredSpeed = this.cruiseSpeed();
	}
}


PriorityAIController.prototype.configurationSetDestinationToNearestHostileStation = function()
{
	var stations = system.stations;
	var threshold = 1E16;
	var chosenStation = null;
	for (var i = 0 ; i < stations.length ; i++)
	{
		var station = stations[i];
		if (this.hostileStation(station))
		{
			var distance = this.distance(station);
			if (distance < threshold)
			{
				threshold = distance;
				chosenStation = station;
			}
		}
	}
	if (chosenStation == null)
	{
		this.ship.destination = this.ship.position;
		this.ship.desiredRange = 0;
	}
	else
	{
		this.ship.destination = chosenStation.position;
		this.ship.desiredRange = 15000;
		this.ship.desiredSpeed = this.cruiseSpeed();
	}
}


PriorityAIController.prototype.configurationSetDestinationToNearestStation = function()
{
	if (this.__ltcache.oolite_nearestStation && this.__ltcache.oolite_nearestStation.isValid)
	{
		this.ship.destination = this.__ltcache.oolite_nearestStation.position;
		this.ship.desiredRange = 15000;
		this.ship.desiredSpeed = this.cruiseSpeed();
	}
	else
	{
		this.ship.destination = this.ship.position;
		this.ship.desiredRange = 0;
	}
}

PriorityAIController.prototype.configurationSetDestinationToNearestWormhole = function()
{
	var dist = 1E16;
	var holes = system.wormholes;
	this.ship.desiredRange = 0;
	this.ship.desiredSpeed = this.ship.maxSpeed;
	for (var i=holes.length-1; i >= 0 ; i--)
	{
		var hole = holes[i];
		var hdist = this.distance(hole);
		if (hole.expiryTime > clock.adjustedSeconds && hdist < dist)
		{
			this.ship.destination = hole.position;
			dist = hdist;
		}
	}
	if (dist >= 1E15)
	{
		this.ship.destination = this.ship.position;
		this.ship.desiredRange = 1000;
	}
}


PriorityAIController.prototype.configurationSetDestinationToPirateLurk = function()
{
	var lurk = this.getParameter("oolite_pirateLurk");
	if (lurk != null)
	{
		this.ship.destination = lurk;
	}
	else
	{
		if (this.distance(system.sun) > system.sun.radius*3 && this.distance(system.mainPlanet) > system.mainPlanet.radius * 3)
		{
			var p = this.ship.position;
			// if already on a lane, stay on it
			if (p.z < (system.mainPlanet.position.z - system.mainPlanet.radius*2) && ((p.x * p.x) + (p.y * p.y)) < this.scannerRange * this.scannerRange * 4)
			{
				lurk = p;
			}
			else if (p.subtract(system.mainPlanet).direction().dot(p.subtract(system.sun).direction()) < -0.9)
			{
				lurk = p;
			}
			else if (p.direction().dot(system.sun.position.direction()) > 0.9)
			{
				lurk = p;
			}
		}
		if (lurk == null)
		{
			// not on a lane, or too close to a sun/planet
			var code;
			var choice = Math.random();
			if (choice < 0.7)
			{
				code = "LANE_WP";
			}
			else if (choice < 0.8)
			{
				code = "LANE_PS";
			}
			else if (choice < 0.9)
			{
				code = "LANE_WS";
			}
			else
			{
				code = "WITCHPOINT";
			}
			lurk = system.locationFromCode(code);
		}
		this.setParameter("oolite_pirateLurk",lurk);
	}
	this.ship.desiredRange = 1000;
	this.ship.desiredSpeed = this.cruiseSpeed();
}


PriorityAIController.prototype.configurationSetDestinationToScannedTarget = function()
{
	var ship = this.getParameter("oolite_scanResultSpecific");
	if (ship && ship.isShip)
	{
		this.ship.destination = ship.position;
		this.ship.desiredRange = 4000;
		this.ship.desiredSpeed = this.cruiseSpeed();
	}
}


PriorityAIController.prototype.configurationSetDestinationToSelectedPlanet = function()
{
	var planet = this.getParameter("oolite_selectedPlanet");
	if (planet)
	{
		this.ship.destination = planet.position;
		this.ship.desiredRange = planet.radius+100;
		this.ship.desiredSpeed = this.cruiseSpeed();
	}
}


PriorityAIController.prototype.configurationSetDestinationToSelectedStation = function()
{
	var station = this.getParameter("oolite_selectedStation");
	if (station)
	{
		this.ship.destination = station.position;
		this.ship.desiredRange = 15000;
		this.ship.desiredSpeed = this.cruiseSpeed();
	}
}


PriorityAIController.prototype.configurationSetDestinationToSunskimEnd = function()
{
	if (system.sun)
	{
		var direction = Vector3D.random().cross(this.ship.position.subtract(system.sun.position));
		// 2km parallel to local sun surface for every LY of fuel
		this.ship.destination = this.ship.position.add(direction.multiply(2000*(7-this.ship.fuel)));
		// max sunskim height is sqrt(4/3) radius 
		this.ship.desiredRange = 0;
		this.ship.desiredSpeed = this.ship.maxSpeed;
	}
}


PriorityAIController.prototype.configurationSetDestinationToSunskimStart = function()
{
	if (system.sun)
	{
		this.ship.destination = system.sun.position;
		// max sunskim height is sqrt(4/3) radius 
		this.ship.desiredRange = system.sun.radius * 1.125;
		this.ship.desiredSpeed = this.cruiseSpeed();
	}
}


PriorityAIController.prototype.configurationSetDestinationToWaypoint = function()
{
	if (this.getParameter("oolite_waypoint") != null && this.getParameter("oolite_waypointRange") != null)
	{
		this.ship.destination = this.getParameter("oolite_waypoint");
		this.ship.desiredRange = this.getParameter("oolite_waypointRange");
		this.ship.desiredSpeed = this.cruiseSpeed();
	}
}


PriorityAIController.prototype.configurationSetDestinationToWitchpoint = function()
{
	this.ship.destination = new Vector3D(0,0,0);
	this.ship.desiredRange = 10000;
	this.ship.desiredSpeed = this.cruiseSpeed();
}


PriorityAIController.prototype.configurationSetWaypoint = function()
{
	var gen = this.getWaypointGenerator();
	if(gen != null)
	{
		gen.call(this);
		this.configurationSetDestinationToWaypoint();
	}
}




/*** Docking configurations ***/



PriorityAIController.prototype.configurationSetNearbyFriendlyStationForDocking = function()
{
	if (!this.__ltcache.oolite_nearestStation)
	{
		return false;
	}
	if (this.friendlyStation(this.__ltcache.oolite_nearestStation))
	{
		if (this.distance(this.__ltcache.oolite_nearestStation) < this.scannerRange)
		{
			this.setParameter("oolite_dockingStation",this.__ltcache.oolite_nearestStation)
			return;
		}
	}
}


PriorityAIController.prototype.configurationSetHomeStationForDocking = function()
{
	var station = this.homeStation();
	if (station)
	{
		this.setParameter("oolite_dockingStation",station)
		return;
	}
}


PriorityAIController.prototype.configurationSetSelectedStationForDocking = function()
{
	this.setParameter("oolite_dockingStation",this.getParameter("oolite_selectedStation"));
}


/*** Miscellaneous configuration ***/


PriorityAIController.prototype.configurationAppointGroupLeader = function()
{
	if (this.ship.group && !this.ship.group.leader)
	{
		this.ship.group.leader = this.ship.group.ships[0];
		for (var i = 0 ; i < this.ship.group.ships.length ; i++)
		{
			if (this.ship.group.ships[i].hasHyperspaceMotor)
			{
				// bias towards jump-capable ships
				this.ship.group.leader = this.ship.group.ships[i];
				break;
			}
		}
		var leadrole = this.getParameter("oolite_leaderRole")
		if (leadrole != null)
		{
			this.ship.group.leader.primaryRole = leadrole;
		}
	}
}

PriorityAIController.prototype.configurationEscortGroupLeader = function()
{
	if (!this.ship.group || !this.ship.group.leader || this.ship.group.leader == this.ship)
	{
		return;
	}
	if (this.ship.group.leader.escortGroup && this.ship.group.leader.escortGroup.containsShip(this.ship))
	{
		return;
	}
	var escrole = this.getParameter("oolite_escortRole")
	if (escrole != null)
	{
		var oldrole = this.ship.primaryRole;
		this.ship.primaryRole = escrole;
		var accepted = this.ship.offerToEscort(this.ship.group.leader);
		if (!accepted)
		{
			this.ship.primaryRole = oldrole;
		}
	}
	
}


PriorityAIController.prototype.configurationForgetCargoDemand = function()
{
	/*				if (this.ship.group && this.ship.group.leader && this.ship.group.leader.AIScript.oolite_intership.cargodemanded)
					{
					delete this.ship.group.leader.AIScript.oolite_intership.cargodemanded;
					} */ // not sure about this, maybe not needed

	if (this.ship.AIScript.oolite_intership.cargodemanded)
	{
		delete this.ship.AIScript.oolite_intership.cargodemanded;
		delete this.ship.AIScript.oolite_intership.cargodemandmet;
		// and make the group lose the cargo count from the last demand
		if (this.ship.group)
		{
			for (var i = 0 ; i < this.ship.group.ships.length ; i++)
			{
				var ship = this.ship.group.ships[i];
				if (ship.AIScript && ship.AIScript.oolite_priorityai)
				{
					ship.AIScript.oolite_priorityai.setParameter("oolite_cargoDropped",0);
				}
			}
		}
	}
}


PriorityAIController.prototype.configurationLeaveEscortGroup = function()
{
	if (this.ship.group && this.ship.group.leader && this.ship.group.leader != this.ship && this.ship.group.leader.escortGroup && this.ship.group.leader.escortGroup.containsShip(this.ship))
	{
		this.ship.group.leader.escortGroup.removeShip(this.ship);
		if (this.ship.group)
		{
			this.ship.group.removeShip(this.ship);
			this.ship.group = null;
		}
	}
}


PriorityAIController.prototype.configurationLightsOff = function()
{
	this.ship.lightsActive = false;
}


PriorityAIController.prototype.configurationLightsOn = function()
{
	this.ship.lightsActive = true;
}


// remote controlled ships get same accuracy as lead ship
PriorityAIController.prototype.configurationSetRemoteControl = function()
{
	var group = this.ship.group;
	if (group && group.leader)
	{
		this.ship.accuracy = group.leader.accuracy;
	}
}


/*** Station configuration ***/

PriorityAIController.prototype.configurationStationReduceAlertLevel = function() 
{
	if (this.ship.alertCondition > 1)
	{
		this.ship.decreaseAlertLevel();
	}
}

PriorityAIController.prototype.configurationStationValidateTarget = function()
{
	if (this.ship.target)
	{
		if(this.distance(this.ship.target) > this.scannerRange)
		{
			// station behaviour does not generally validate target
			this.ship.target = null;
		}
	}
}

/* ****************** Response definition functions ************** */

/* Standard state-machine responses. These set up a set of standard
 * state machine responses where incoming events will cause reasonable
 * default behaviour and often force a reconsideration of
 * priorities. Many behaviours will need to supplement the standard
 * responses with additional definitions. */

PriorityAIController.prototype.responsesAddStandard = function(handlers) 
{
	handlers.approachingPlanetSurface = this.responseComponent_standard_approachingPlanetSurface;
	handlers.cargoDumpedNearby = this.responseComponent_standard_cargoDumpedNearby;
	handlers.cascadeWeaponDetected = this.responseComponent_standard_cascadeWeaponDetected;
	handlers.commsMessageReceived = this.responseComponent_standard_commsMessageReceived;
	handlers.distressMessageReceived = this.responseComponent_standard_distressMessageReceived;
	handlers.escortAccepted = this.responseComponent_standard_escortAccepted;
	handlers.helpRequestReceived = this.responseComponent_standard_helpRequestReceived;
	handlers.offenceCommittedNearby = this.responseComponent_standard_offenceCommittedNearby;
	handlers.shipAcceptedEscort = this.responseComponent_standard_shipAcceptedEscort;
	handlers.shipAttackedOther = this.responseComponent_standard_shipAttackedOther;
	handlers.shipAttackedWithMissile = this.responseComponent_standard_shipAttackedWithMissile;
	handlers.shipAttackerDistracted = this.responseComponent_standard_shipAttackerDistracted;
	handlers.shipBeingAttacked = this.responseComponent_standard_shipBeingAttacked;
	handlers.shipBeingAttackedUnsuccessfully = this.responseComponent_standard_shipBeingAttackedUnsuccessfully;
	handlers.shipFiredMissile = this.responseComponent_standard_shipFiredMissile;
	handlers.shipKilledOther = this.responseComponent_standard_shipKilledOther;
	handlers.shipLaunchedEscapePod = this.responseComponent_standard_shipLaunchedEscapePod;
	handlers.shipLaunchedFromStation = this.responseComponent_standard_shipLaunchedFromStation;
	handlers.shipWillEnterWormhole = this.responseComponent_standard_shipWillEnterWormhole;
	handlers.wormholeSuggested = this.responseComponent_standard_wormholeSuggested;

	// slightly different settings if pursuing to witchspace expected
	if (!this.getParameter("oolite_flag_witchspacePursuit"))
	{
		handlers.shipTargetLost = this.responseComponent_standard_shipTargetLost;
		handlers.playerWillEnterWitchspace = this.responseComponent_standard_playerWillEnterWitchspace;
	}
	else
	{
		handlers.playerWillEnterWitchspace = this.responseComponent_trackPlayer_playerWillEnterWitchspace;
		handlers.shipTargetLost = this.responseComponent_expectWitchspace_shipTargetLost;
	}

	// TODO: more event handlers
}

/* Additional handlers for use while docking */
PriorityAIController.prototype.responsesAddDocking = function(handlers) 
{
	handlers.stationWithdrewDockingClearance = this.responseComponent_docking_stationWithdrewDockingClearance;
	handlers.shipAchievedDesiredRange = this.responseComponent_docking_shipAchievedDesiredRange;
	handlers.shipAIFrustrated = this.responseComponent_docking_shipAIFrustrated;
}

/* Override of standard handlers for use while escorting */
PriorityAIController.prototype.responsesAddEscort = function(handlers) 
{
	handlers.helpRequestReceived = this.responseComponent_escort_helpRequestReceived;
	handlers.escortDock = this.responseComponent_escort_escortDock;
}

/* Additional handlers for scooping */
PriorityAIController.prototype.responsesAddScooping = function(handlers)
{
	handlers.shipAchievedDesiredRange = this.responseComponent_scooping_shipAchievedDesiredRange
	handlers.shipScoopedFuel = this.responseComponent_scooping_shipScoopedFuel;
}

// shorter list than before
PriorityAIController.prototype.responsesAddStation = function(handlers) 
{
	handlers.cascadeWeaponDetected = this.responseComponent_station_cascadeWeaponDetected;
	handlers.commsMessageReceived = this.responseComponent_station_commsMessageReceived;
	handlers.distressMessageReceived = this.responseComponent_station_distressMessageReceived;
	handlers.helpRequestReceived = this.responseComponent_station_helpRequestReceived;
	handlers.offenceCommittedNearby = this.responseComponent_station_offenceCommittedNearby;
	handlers.shipAttackedOther = this.responseComponent_station_shipAttackedOther;
	handlers.shipAttackedWithMissile = this.responseComponent_station_shipAttackedWithMissile;
	handlers.shipBeingAttacked = this.responseComponent_station_shipBeingAttacked;
	handlers.shipFiredMissile = this.responseComponent_station_shipFiredMissile;
	handlers.shipKilledOther = this.responseComponent_station_shipKilledOther;
	handlers.shipTargetLost = this.responseComponent_station_shipTargetLost;
}


PriorityAIController.prototype.responsesAddMissile = function(handlers) {
	handlers.commsMessageReceived = this.responseComponent_missile_commsMessageReceived;
	handlers.shipHitByECM = this.responseComponent_missile_shipHitByECM;
	handlers.shipTargetCloaked = this.responseComponent_missile_shipTargetCloaked;
	handlers.shipTargetLost = this.responseComponent_missile_shipTargetLost;
	handlers.shipAchievedDesiredRange = this.responseComponent_missile_shipAchievedDesiredRange;
}


/* ******************* Response components *********************** */

/* Response components. These are standard response component
 * functions which can be passed by reference to save on variable
 * destruction/creation */


PriorityAIController.prototype.responseComponent_standard_approachingPlanetSurface = function()
{
	if (this.getParameter("oolite_flag_allowPlanetaryLanding"))
	{
		this.ship.desiredSpeed = this.ship.maxSpeed / 4;
		this.ship.performLandOnPlanet();
		this.ship.AIScriptWakeTime = 0; // cancel reconsiderations
		this.applyHandlers({}); // cancel interruptions
		this.communicate("oolite_landingOnPlanet",{},4);
	}
	else
	{
		this.reconsiderNow();
	}
}


PriorityAIController.prototype.responseComponent_standard_cargoDumpedNearby = function(cargo,ship)
{
	if (this.getParameter("oolite_flag_watchForCargo"))
	{
		var previously = this.getParameter("oolite_cargoDropped");
		if (previously == null)
		{
			previously = 0;
		}
		previously++;
		this.setParameter("oolite_cargoDropped",previously);
	}
}


PriorityAIController.prototype.responseComponent_standard_cascadeWeaponDetected = function(weapon)
{
	this.ship.clearDefenseTargets();
	this.ship.addDefenseTarget(weapon);
	this.setParameter("oolite_cascadeDetected",weapon.position);
	this.ship.target = weapon;
	this.ship.performFlee();
	this.reconsiderNow();
}


PriorityAIController.prototype.responseComponent_standard_commsMessageReceived = function(message,sender)
{
	/* If the sender is hostile to us, and we're not obviously in
	 * combat, attack the sender: deals with pirate demand case */
	if (sender.target == this.ship && !this.ship.hasHostileTarget && sender.hasHostileTarget)
	{
		this.ship.target = sender;
		this.ship.performAttack();
		this.reconsiderNow();
	}
	this.noteCommsHeard();
}


PriorityAIController.prototype.responseComponent_standard_distressMessageReceived = function(aggressor, sender)
{
	if (this.getParameter("oolite_flag_listenForDistressCall") != true)
	{
		return;
	}
	if (aggressor.scanClass == "CLASS_POLICE" || (aggressor.isStation && aggressor.allegiance == "galcop"))
	{
		return;
	}
	if (this.ship.scanClass == "CLASS_POLICE" || (this.ship.isStation && this.ship.allegiance == "galcop"))
	{
		if (this.distance(aggressor) < this.scannerRange)
		{
			aggressor.bounty |= 8;
		}
	}
	this.setParameter("oolite_distressAggressor",aggressor);
	this.setParameter("oolite_distressSender",sender);
	this.setParameter("oolite_distressTimestamp",clock.adjustedSeconds);
	this.reconsiderNow();
}


PriorityAIController.prototype.responseComponent_standard_escortAccepted = function(escort)
{
	this.communicate("oolite_escortAccepted",escort,2);
}


// overridden for escorts
PriorityAIController.prototype.responseComponent_standard_helpRequestReceived = function(ally, enemy)
{
	if (!enemy.isShip)
	{
		return;
	}
	if (this.allied(this.ship,enemy))
	{
		return;
	}
	this.ship.addDefenseTarget(enemy);
	if (enemy.scanClass == "CLASS_MISSILE" && this.distance(enemy) < this.scannerRange && this.ship.hasEquipmentProviding("EQ_ECM"))
	{
		this.fireECM();
	}
	if (enemy.scanClass == "CLASS_THARGOID" && this.ship.scanClass != "CLASS_THARGOID" && (!this.ship.target || this.ship.target.scanClass != "CLASS_THARGOID"))
	{
		if (this.respondToThargoids(enemy,false))
		{
			this.reconsiderNow();
			return; // not in a combat mode
		}
	}

	if (!this.ship.hasHostileTarget)
	{
		// can't see the target
		if (this.distance(enemy) > this.scannerRange)
		{
			this.ship.destination = enemy.position;
			this.ship.desiredRange = this.scannerRange * 0.75;
			// rush to help on injectors if possible
			this.ship.desiredSpeed = this.ship.maxSpeed * 7;
			this.ship.performFlyToRangeFromDestination();
			// don't reconsider just yet
			return;
		}
		this.reconsiderNow();
		return; // not in a combat mode
	}
	if (ally.energy / ally.maxEnergy < this.ship.energy / this.ship.maxEnergy)
	{
		// not in worse shape than ally
		if (this.ship.target.target != ally && this.ship.target != ally.target)
		{
			// not already helping, go for it...
			this.communicate("oolite_startHelping",enemy,4);
			this.ship.target = enemy;
			this.reconsiderNow();
		}
	}
}


PriorityAIController.prototype.responseComponent_standard_offenceCommittedNearby = function(attacker, victim)
{
	if (this.ship == victim) return; // other handlers can get this one
	if (this.distance(attacker) > this.scannerRange) return; // can't mark what you can't see
	if (attacker.scanClass == "CLASS_POLICE") return; // ignored
	if (this.getParameter("oolite_flag_markOffenders")) 
	{
		if (attacker.bounty == 0 && victim.bounty == 0)
		{
			if ((this.shipInRoleCategory(victim,"oolite-police-dislike") && !this.shipInRoleCategory(attacker,"oolite-police-dislike")) ||
				(this.shipInRoleCategory(attacker,"oolite-police-like") && !this.shipInRoleCategory(victim,"oolite-police-like")))
			{
				if (victim.hasHostileTarget)
				{
					// they're both fighting; it's likely that the
					// attacker is fighting in self-defence; so swap them
					var tmp = victim;
					victim = attacker;
					attacker = tmp;
				}
			}
		}
		if (!attacker.isPlayer && attacker.target != victim)
		{
			// ignore friendly fire if they were aiming at a pirate/assassin
			if (attacker.bounty == 0 && attacker.target && attacker.target.isShip && this.shipInRoleCategory(attacker.target,"oolite-police-dislike"))
			{
				// but we might go after the pirate/assassin ourselves in a bit
				this.ship.addDefenseTarget(attacker.target);
				return;
			}
		}
		else if (attacker.isPlayer && this.ignorePlayerFriendlyFire())
		{
			this.communicate("oolite_friendlyFire",attacker,3);
			return;
		}

		if (attacker.bounty & 7 != 7)
		{
			this.communicate("oolite_offenceDetected",attacker,3);
		}
		else
		{
			this.communicate("oolite_offenceDetected",attacker,4);
		}
		attacker.setBounty(attacker.bounty | 7,"seen by police");
		if (attacker.isShip)
		{
			this.ship.addDefenseTarget(attacker);
		}
		this.reconsiderNow();
	}
}


PriorityAIController.prototype.responseComponent_standard_playerWillEnterWitchspace = function()
{
	var wormhole = this.getParameter("oolite_witchspaceWormhole");
	if (wormhole != null && wormhole.isWormhole)
	{
		this.ship.enterWormhole(wormhole);
	} 
	/* Given a chance to leave interstellar space, take it */
	else if (system.isInterstellarSpace && !this.getParameter("oolite_flag_likesInterstellarSpace") && this.distance(player.ship) < this.scannerRange)
	{
		this.ship.enterWormhole();
	}
}


PriorityAIController.prototype.responseComponent_standard_shipAcceptedEscort = function(mother)
{
	this.communicate("oolite_escortMotherAccepted",mother,2);
}


// not always applied
PriorityAIController.prototype.responseComponent_standard_shipAchievedDesiredRange = function() 
{
	var waypoints = this.getParameter("oolite_waypoints");
	if (waypoints != null)
	{
		if (waypoints.length > 0)
		{
			waypoints.pop();
			if (waypoints.length == 0)
			{
				waypoints = null;
			}
			this.setParameter("oolite_waypoints",waypoints);
		}
	}
	else
	{
		var patrol = this.getParameter("oolite_waypoint");
		if (patrol != null && this.ship.destination.distanceTo(patrol) < 1000+this.getParameter("oolite_waypointRange"))
		{
			// finished patrol to waypoint
			// clear route
			this.communicate("oolite_waypointReached",{},3);
			this.setParameter("oolite_waypoint",null);
			this.setParameter("oolite_waypointRange",null);
			if (this.getParameter("oolite_flag_patrolStation"))
			{
				if (this.ship.group)
				{
					var station = this.ship.group.leader;
					if (station != null && station.isStation)
					{
						this.communicate("oolite_patrolReportIn",station,4);
						this.ship.patrolReportIn(station);
					}
				}
			}
		}
	}
	this.reconsiderNow();
}


PriorityAIController.prototype.responseComponent_standard_shipAttackedOther = function(other)
{
	this.communicate("oolite_hitTarget",other,4);
}


PriorityAIController.prototype.responseComponent_standard_shipAttackedWithMissile = function(missile,whom)
{
	if (this.getParameter("oolite_flag_sendsDistressCalls"))
	{
		this.ship.AIPrimaryAggressor = whom;
		this.broadcastDistressMessage();
	}
	if (this.ship.hasEquipmentProviding("EQ_ECM"))
	{
		this.fireECM();
		this.ship.addDefenseTarget(missile);
		this.ship.addDefenseTarget(whom);
		// but don't usually reconsider immediately, because the ECM
		// will probably get it
		if (!this.__cache.oolite_conditionInCombat)
		{
			// however, if the missile is the start of an attack,
			// reconsider to start combat mode
			this.reconsiderNow();
		}
	}
	else
	{
		this.communicate("oolite_incomingMissile",whom,3);
		this.ship.addDefenseTarget(missile);
		this.ship.addDefenseTarget(whom);
		if (this.ship.target && this.ship.target.scanClass == "CLASS_MISSILE")
		{
			// keep fleeing first missile
			var tmp = this.ship.target;
			this.ship.target = missile;
			this.ship.requestHelpFromGroup(); // anyone got an ECM?
			this.ship.target = tmp;
		}
		else
		{
			this.ship.target = missile;
			this.ship.requestHelpFromGroup(); // anyone got an ECM?
		}
		this.reconsiderNow();
	}
}


PriorityAIController.prototype.responseComponent_standard_shipAttackerDistracted = function(whom)
{
	if (this.ship.scanClass != "CLASS_THARGOID" && whom.scanClass == "CLASS_THARGOID" && (!this.ship.target || this.ship.target.scanClass != "CLASS_THARGOID"))
	{
		// frying pan, fire
		if (this.respondToThargoids(whom,false))
		{
			this.reconsiderNow();
			return;
		}
	}

	var last = this.getParameter("oolite_lastAssist");
	if (last != whom)
	{
		if (whom.isPlayer)
		{
			this.communicate("oolite_thanksForHelp",whom,1);
		}
		else
		{
			this.communicate("oolite_thanksForHelp",whom,3);
		}
		if (this.ship.scanClass == "CLASS_POLICE")
		{
			if (whom.scanClass != "CLASS_POLICE" && whom.scanClass != "CLASS_THARGOID" && whom.bounty > 0)
			{
				whom.setBounty(whom.bounty*4/5,"assisting police");
			}
		}
		this.setParameter("oolite_lastAssist",whom);
	}
	this.reconsiderNow();
}


PriorityAIController.prototype.responseComponent_standard_shipBeingAttacked = function(whom)
{
	if (whom.target != this.ship)
	{
		if (!whom.isPlayer)
		{
			// was accidental
			if (this.allied(whom,this.ship))
			{
				this.communicate("oolite_friendlyFire",whom,3);
				// ignore it
				return;
			}
			// clean ships ignore friendly fire from police
			if (this.ship.bounty == 0 && whom.scanClass == "CLASS_POLICE")
			{
				this.communicate("oolite_friendlyFire",whom,3);
				return;
			}
			if (Math.random() > 0.1)
			{
				// usually ignore it anyway as we know they didn't mean to
				return;
			}
		}
		// only ignore the player's friendly fire if already in combat
		else if (this.conditionInCombat() && this.ignorePlayerFriendlyFire())
		{
			// send warning communication
			this.communicate("oolite_friendlyFire",whom,2);
			return;
		}
	}
	if (this.getParameter("oolite_flag_markOffenders"))
	{
		if (this.ship.scanClass == "CLASS_POLICE")
		{
			whom.setBounty(whom.bounty | 15,"attacked police");
		}
		else if (this.ship == system.mainStation)
		{
			whom.setBounty(whom.bounty | 63,"attacked main station");
		}
	}
	if (this.ship.target && !this.ship.hasHostileTarget && this.ship.target != this.ship.AIPrimaryAggressor)
	{
		// don't get confused and shoot the station!
		this.ship.target = null;
	}
	if (this.getParameter("oolite_flag_sendsDistressCalls"))
	{
		this.broadcastDistressMessage();
	}
	if (this.ship.isFleeing)
	{
		this.communicate("oolite_surrender",{},3);
		if (whom.isPlayer && this.ship.AIScript.oolite_intership.cargodemandpaid && this.ship.energy < 16)
		{
			/* Firing on surrendered traders means you're probably
			 * trying to kill them rather than rob them. Prefer
			 * replacing pirate roles. */
			if (!this.__ltcache.oolite_assassinPlayer)
			{
				this.__ltcache.oolite_assassinPlayer = true;
				var pws = player.roleWeights;
				var found = false;
				for (var i=pws.length-1;i>=0;i--)
				{
					if (pws[i] == "pirate")
					{
						player.setPlayerRole("assassin-player",i);
						found = true;
						break;
					}
				}
				if (!found)
				{
					player.setPlayerRole("assassin-player");
				}
			}
		}
	}
	if ((whom.scanClass == "CLASS_THARGOID") && (this.ship.scanClass != "CLASS_THARGOID") && (!this.ship.target || this.ship.target.scanClass != "CLASS_THARGOID"))
	{
		if (this.respondToThargoids(whom,true))
		{
			this.reconsiderNow();
			return;
		}
	}
	if (whom.scanClass != "CLASS_THARGOID" && this.ship.target && this.ship.target.scanClass == "CLASS_THARGOID")
	{
		// now is not a good time. Everything is friendly fire right now...
		return;
	}
	if (this.ship.defenseTargets.indexOf(whom) < 0)
	{
		this.communicate("oolite_newAssailiant",whom,3);
		this.ship.addDefenseTarget(whom);
		// for military laser reactions
		if (!this.ship.target && this.distance(whom) > this.scannerRange) 
		{
			this.ship.target = whom;
		}
	}
	else 
	{
		// else we know about this attacker already
		if (this.ship.energy * 4 < this.ship.maxEnergy)
		{
			this.communicate("oolite_attackLowEnergy",whom,2);
			// but at low energy still reconsider
			this.ship.requestHelpFromGroup();
		}
	}
	if (this.ship.hasHostileTarget)
	{
		if (!this.isAggressive(this.ship.target))
		{
			// if our current target is running away, switch targets
			this.noteDistraction(whom);
			this.ship.target = whom;
		}
		else if (this.ship.target.target != this.ship)
		{
			// if our current target isn't aiming at us
			if (Math.random() < 0.2)
			{
				// occasionally switch
				this.noteDistraction(whom);
				this.ship.target = whom;
			}
		}
		else
		{
			// tend to switch to the more dangerous one
			if (this.threatAssessment(whom,true) > this.threatAssessment(this.ship.target,true) * (1+Math.random()))
			{
				this.noteDistraction(whom);
				this.ship.target = whom;
			}
		}
	}
	// TODO: a rep for not accepting surrenders should have an effect here
	else if (whom.isPlayer && !this.ship.AIScript.oolite_intership.cargodemand && !this.ship.AIScript.oolite_intership.cargodemandpaid)
	{
		// don't need to check here: most AIs won't check if a cargo
		// demand exists, so setting it is harmless
		if (this.shipInRoleCategory(whom,"oolite-pirate"))
		{
			this.ship.AIScript.oolite_intership.cargodemand = Math.ceil(this.ship.cargoSpaceCapacity / 10);
		}
		else if (this.shipInRoleCategory(whom,"oolite-assassin"))
		{
			this.ship.AIScript.oolite_intership.cargodemand = Math.ceil(this.ship.cargoSpaceCapacity / 20); // worth a try
		}
		else
		{
			this.ship.AIScript.oolite_intership.cargodemand = Math.ceil(this.ship.cargoSpaceCapacity / 15);
		}
	}

	if (this.ship.escortGroup != null)
	{
		this.ship.requestHelpFromGroup();
	}
	if (!this.ship.hasHostileTarget && this.distance(whom) > this.scannerRange)
	{
		// deal with being shot at from outside scanner range
		this.ship.performAttack();
	}
	this.reconsiderNow();
}


PriorityAIController.prototype.responseComponent_standard_shipBeingAttackedUnsuccessfully = function(whom)
{
	if (this.getParameter("oolite_flag_sendsDistressCalls"))
	{
		this.ship.primaryAggressor = whom;
		this.broadcastDistressMessage();
	}
	if (this.ship.defenseTargets.indexOf(whom) < 0)
	{
		this.ship.addDefenseTarget(whom);
		this.reconsiderNow();
	}
	// TODO: a rep for not accepting surrenders should have an effect here
	if (!this.ship.hasHostileTarget && whom.isPlayer && !this.ship.AIScript.oolite_intership.cargodemand && !this.ship.AIScript.oolite_intership.cargodemandpaid)
	{
		this.ship.AIScript.oolite_intership.cargodemand = Math.ceil(this.ship.cargoSpaceCapacity / 15);
	}
	if (!this.ship.hasHostileTarget)
	{
		this.ship.target = whom;
		this.ship.performAttack();
		this.ship.requestHelpFromGroup();
	}
}


PriorityAIController.prototype.responseComponent_standard_shipFiredMissile = function(missile,target)
{
	// spread missiles out between targets
	if (this.ship.defenseTargets.length > 1)
	{
		this.ship.removeDefenseTarget(target);
		this.ship.target = null;
		this.reconsiderNow();
	}
	this.communicate("oolite_firedMissile",target,4);
}


PriorityAIController.prototype.responseComponent_standard_shipKilledOther = function(other)
{
	if (this.ship.target == other)
	{
		if (this.allied(this.ship,other))
		{
			this.communicate("oolite_killedAlly",other,3);
		}
		else if (other.crew && other.crew.length > 0)
		{
			this.communicate("oolite_killedTarget",other,2);
		}
		else
		{
			this.communicate("oolite_killedUncrewedTarget",other,3);
		}
	}
	else
	{
		if (this.allied(this.ship,other))
		{
			this.communicate("oolite_killedAlly",other,3);
		}
		else if (other.crew && other.crew.length > 0)
		{
			this.communicate("oolite_killedNonTarget",other,3);
		}
		else
		{
			this.communicate("oolite_killedUncrewedNonTarget",other,3);
		}
	}
}


PriorityAIController.prototype.responseComponent_standard_shipLaunchedEscapePod = function()
{
	this.communicate("oolite_eject",{},1);
	if (this.getParameter("oolite_flag_selfDestructAbandonedShip") == true)
	{
		if (!this.ship.script.__oolite_self_destruct)
		{
			this.ship.script.__oolite_self_destruct = new Timer(this.ship.script,function(){this.ship.explode()},10);
		}
	}
}


PriorityAIController.prototype.responseComponent_standard_shipLaunchedFromStation = function(station)
{
	// clear the station
	this.ship.destination = station.position;
	this.ship.desiredSpeed = this.cruiseSpeed();
	this.ship.desiredRange = 15000;
	this.ship.performFlyToRangeFromDestination();
}


PriorityAIController.prototype.responseComponent_standard_shipScoopedOther = function(other)
{
	if (other.commodity)
	{
		this.communicate("oolite_scoopedCargo",{"oolite_goodsDescription":displayNameForCommodity(other.commodity)},4);
	}
	this.setParameter("oolite_cargoDropped",null);
	this.reconsiderNow();
}


PriorityAIController.prototype.responseComponent_standard_shipTargetLost = function(target)
{
	this.reconsiderNow();
}


PriorityAIController.prototype.responseComponent_standard_shipWillEnterWormhole = function()
{
	this.setParameter("oolite_witchspaceWormhole",null);
	this.applyHandlers({});
}


PriorityAIController.prototype.responseComponent_standard_shipWitchspaceBlocked = function(blocker)
{
	this.communicate("oolite_witchspaceBlocked",blocker,3);
	if (blocker.isStation) {
		// fixed exitVector to stop problems with rotating stations
		var exitVector = blocker.vectorForward.direction().cross([0,0,1]);
		this.ship.destination = this.ship.position.add(blocker.vectorForward.multiply(12000).add(exitVector.multiply(40000)));
		this.ship.desiredRange = 10000;
	} else {
		this.ship.destination = blocker.position;
		this.ship.desiredRange = 30000;
	}
	this.ship.desiredSpeed = this.cruiseSpeed();
	this.ship.performFlyToRangeFromDestination();
	this.setParameter("oolite_witchspaceEntry",null);
	// no reconsidering yet
}


PriorityAIController.prototype.responseComponent_standard_wormholeSuggested = function(hole)
{
	this.ship.destination = hole.position;
	this.ship.desiredRange = 0;
	this.ship.desiredSpeed = this.ship.maxSpeed;
	this.ship.performFlyToRangeFromDestination();
	this.setParameter("oolite_witchspaceWormhole",hole);
	// wormhole suggestions include setting primary target
	// so unset it
	this.ship.target = null;
	// also clear defense targets to make it more likely that the ship
	// will reach the wormhole
	this.ship.clearDefenseTargets();
	// don't reconsider
}

/* Missile response components */

PriorityAIController.prototype.responseComponent_missile_commsMessageReceived = function(message)
{
	this.noteCommsHeard();
}


PriorityAIController.prototype.responseComponent_missile_shipHitByECM = function()
{
	if (this.ship.scriptInfo.oolite_missile_ecmResponse)
	{
		var fn = this.ship.scriptInfo.oolite_missile_ecmResponse;
		if (this.ship.AIScript[fn])
		{
			this.ship.AIScript[fn]();
			this.reconsiderNow();
			return;
		}
		if (this.ship.script[fn])
		{
			this.ship.script[fn]();
			this.reconsiderNow();
			return;
		}
	}

	/* This section for the hardheads should be an ECM
	 * response function, and that is used in the default
	 * shipdata.plist, but for compatibility with older OXPs
	 * it's also hardcoded here for now.
	 *
	 * OXPs wanting to overrule this for hardheads can set a
	 * response function to do so.
	 */
	if (this.ship.primaryRole == "EQ_HARDENED_MISSILE")
	{
		if (Math.random() < 0.1) //10% chance per pulse
		{
			if (Math.random() < 0.5)
			{
				// 50% chance responds by detonation
				this.ship.AIScript.shipAchievedDesiredRange();
				return;
			}
			// otherwise explode as normal below
		}
		else // 90% chance unaffected
		{
			return;
		}
	}

	this.ship.explode();
}


PriorityAIController.prototype.responseComponent_missile_shipTargetCloaked = function()
{
	this.setParameter("oolite_interceptCoordinates",this.ship.target.position);
	this.setParameter("oolite_interceptTarget",this.ship.target);
	// stops performIntercept sending AchievedDesiredRange
	this.ship.performIdle();
}


PriorityAIController.prototype.responseComponent_missile_shipTargetLost = function()
{
	this.reconsiderNow();
}


PriorityAIController.prototype.responseComponent_missile_shipAchievedDesiredRange = function()
{
	if (this.ship.scriptInfo.oolite_missile_detonation)
	{
		var fn = this.ship.scriptInfo.oolite_missile_detonation;
		if (this.ship.AIScript[fn])
		{
			this.ship.AIScript[fn]();
			this.reconsiderNow();
			return;
		}
		if (this.ship.script[fn])
		{
			this.ship.script[fn]();
			this.reconsiderNow();
			return;
		}
	}
	/* Defaults to standard missile settings, in case they're
	 * not specified in scriptInfo */
	var blastpower = 170;
	var blastradius = 32.5;
	var blastshaping = 0.25;
	if (this.ship.scriptInfo.oolite_missile_blastPower)
	{
		blastpower = this.ship.scriptInfo.oolite_missile_blastPower;
	}
	if (this.ship.scriptInfo.oolite_missile_blastRadius)
	{
		blastradius = this.ship.scriptInfo.oolite_missile_blastRadius;
	}
	if (this.ship.scriptInfo.oolite_missile_blastShaping)
	{
		blastshaping = this.ship.scriptInfo.oolite_missile_blastShaping;
	}
	this.ship.dealEnergyDamage(blastpower,blastradius,blastshaping);
	this.ship.explode();
}


/* Station response components */

PriorityAIController.prototype.responseComponent_station_commsMessageReceived = function(message)
{
	this.noteCommsHeard();
}


PriorityAIController.prototype.responseComponent_station_cascadeWeaponDetected = function(weapon)
{
	if (this.ship.alertCondition < 3) 
	{
		if (this.ship.alertCondition == 1) this.ship.increaseAlertLevel();
		this.ship.increaseAlertLevel();
	}
	this.reconsiderNow();
};


PriorityAIController.prototype.responseComponent_station_shipAttackedWithMissile = function(missile,whom)
{
	if (this.ship.alertCondition < 3) 
	{
		if (this.ship.alertCondition == 1) this.ship.increaseAlertLevel();
		this.ship.increaseAlertLevel();
	}
	if (this.ship.hasEquipmentProviding("EQ_ECM"))
	{
		this.fireECM();
		this.ship.addDefenseTarget(missile);
		this.ship.addDefenseTarget(whom);
		// but don't reconsider immediately
	}
	else
	{
		this.ship.addDefenseTarget(missile);
		this.ship.addDefenseTarget(whom);
		var tmp = this.ship.target;
		this.ship.target = whom;
		this.ship.requestHelpFromGroup();
		this.ship.target = tmp;
		this.reconsiderNow();
	}
};


PriorityAIController.prototype.responseComponent_station_shipBeingAttacked = function(whom)
{
	if (!whom) 
	{
		this.reconsiderNow();
		return;
	}
	if (whom.target != this.ship)
	{
		if (!whom.isPlayer)
		{
			// was accidental
			if (this.allied(whom,this.ship))
			{
				this.communicate("oolite_friendlyFire",whom,4);
				// ignore it
				return;
			}
			// allegiance=galcop case covered by this.allied
			if (whom.scanClass == "CLASS_POLICE" && (this.ship.allegiance == "hunter" || this.ship.allegiance == "neutral"))
			{
				this.communicate("oolite_friendlyFire",whom,4);
				// ignore it
				return;
			}
			if (Math.random() > 0.1)
			{
				// usually ignore it anyway
				return;
			}
		}
		else if (this.ship.alertCondition > 1 && this.ignorePlayerFriendlyFire())
		{
			// send warning communication
			this.communicate("oolite_friendlyFire",whom,2);
			return;
		}
	}
	if (this.ship.alertCondition < 3) 
	{
		if (this.ship.alertCondition == 1) this.ship.increaseAlertLevel();
		this.ship.increaseAlertLevel();
	}
	if (this.ship.defenseTargets.indexOf(whom) < 0)
	{
		this.ship.addDefenseTarget(whom);
		this.reconsiderNow();
	}
	else 
	{
		// else we know about this attacker already
		if (this.ship.energy * 4 < this.ship.maxEnergy)
		{
			// but at low energy still reconsider
			this.reconsiderNow();
			this.ship.requestHelpFromGroup();
		}
	}
	if (this.ship.hasHostileTarget)
	{
		if (!this.isAggressive(this.ship.target))
		{
			// if our current target is running away, switch targets
			this.ship.target = whom;
		}
		else if (this.ship.target.target != this.ship)
		{
			// if our current target isn't aiming at us
			if (Math.random() < 0.2)
			{
				// occasionally switch
				this.ship.target = whom;
			}
		}
	} else {
		// time to get one
		this.ship.target = whom;
		this.reconsiderNow();
	}
}


PriorityAIController.prototype.responseComponent_station_shipAttackedOther = function(other)
{
	this.communicate("oolite_hitTarget",other,4);
}


PriorityAIController.prototype.responseComponent_station_shipFiredMissile = function(missile,target)
{
	this.communicate("oolite_firedMissile",target,4);
}


PriorityAIController.prototype.responseComponent_station_shipKilledOther = function(other)
{
	// slightly lower message priorities than the ship version
	if (this.ship.target == other)
	{
		if (this.allied(this.ship,other))
		{
			this.communicate("oolite_killedAlly",other,3);
		}
		else if (other.crew && other.crew.length > 0)
		{
			this.communicate("oolite_killedTarget",other,3);
		}
		else
		{
			this.communicate("oolite_killedUncrewedTarget",other,4);
		}
	}
	else
	{
		if (this.allied(this.ship,other))
		{
			this.communicate("oolite_killedAlly",other,3);
		}
		else if (other.crew && other.crew.length > 0)
		{
			this.communicate("oolite_killedNonTarget",other,3);
		}
		else
		{
			this.communicate("oolite_killedUncrewedNonTarget",other,4);
		}
	}
}


PriorityAIController.prototype.responseComponent_station_shipTargetLost = function(target)
{
	this.reconsiderNow();
};


PriorityAIController.prototype.responseComponent_station_helpRequestReceived = function(ally, enemy)
{
	this.ship.addDefenseTarget(enemy);
	if (enemy.scanClass == "CLASS_MISSILE" && this.distance(enemy) < this.scannerRange && this.ship.hasEquipmentProviding("EQ_ECM"))
	{
		this.fireECM();
		return;
	}
	if (!this.ship.alertCondition == 3)
	{
		this.ship.target = enemy;
		this.reconsiderNow();
		return; // not in a combat mode
	}
	this.ship.target = enemy;
}


PriorityAIController.prototype.responseComponent_station_distressMessageReceived = function(aggressor, sender)
{
	if (this.getParameter("oolite_flag_listenForDistressCall") != true)
	{
		return;
	}
	if (aggressor.scanClass == "CLASS_POLICE" || (aggressor.isStation && aggressor.allegiance == "galcop"))
	{
		return;
	}
	if (this.ship.scanClass == "CLASS_POLICE" || (this.ship.isStation && this.ship.allegiance == "galcop"))
	{
		if (this.distance(aggressor) < this.scannerRange)
		{
			aggressor.setBounty(aggressor.bounty | 8,"attacked innocent");
		}
	}
	this.setParameter("oolite_distressAggressor",aggressor);
	this.setParameter("oolite_distressSender",sender);
	this.setParameter("oolite_distressTimestamp",clock.adjustedSeconds);
	this.reconsiderNow();
}


PriorityAIController.prototype.responseComponent_station_offenceCommittedNearby = function(attacker, victim)
{
	if (this.ship == victim) return; // other handlers can get this one
	if (this.distance(attacker) > this.scannerRange) return; // can't mark what you can't see
	if (this.getParameter("oolite_flag_markOffenders")) 
	{
		if (attacker.bounty == 0 && victim.bounty == 0)
		{
			if ((this.shipInRoleCategory(victim,"oolite-police-dislike") && !this.shipInRoleCategory(attacker,"oolite-police-dislike")) ||
				(this.shipInRoleCategory(attacker,"oolite-police-like") && !this.shipInRoleCategory(victim,"oolite-police-like")))
			{
				if (victim.hasHostileTarget)
				{
					// they're both fighting; it's likely that the
					// attacker is fighting in self-defence; so swap them
					var tmp = victim;
					victim = attacker;
					attacker = tmp;
				}
			}
		}
		if (!attacker.isPlayer && attacker.target != victim)
		{
			// ignore friendly fire if they were aiming at a pirate/assassin
			if (attacker.bounty == 0 && attacker.target && attacker.target.isShip && this.shipInRoleCategory(attacker.target,"oolite-police-dislike"))
			{
				// but we might go after the pirate/assassin ourselves in a bit
				this.ship.addDefenseTarget(attacker.target);
				return;
			}
		}
		else if (attacker.isPlayer && this.ignorePlayerFriendlyFire())
		{
			this.communicate("oolite_friendlyFire",attacker,3);
			return;
		}
		attacker.setBounty(attacker.bounty | 7,"seen by police");
		this.ship.addDefenseTarget(attacker);
		if (this.ship.alertCondition < 3)
		{
			if (this.ship.alertCondition == 1) this.ship.increaseAlertLevel();
			this.ship.increaseAlertLevel();
			this.ship.target = attacker;
		}
		this.reconsiderNow();
	}
}


/* Non-standard response components */

PriorityAIController.prototype.responseComponent_docking_shipAchievedDesiredRange = function()
{
	if (this.ship.dockingInstructions != null)
	{
		var message = this.ship.dockingInstructions.ai_message;
		if (message == "APPROACH" || message == "BACK_OFF" || message == "APPROACH_COORDINATES")
		{
			this.reconsiderNow();
		}
	}
}


PriorityAIController.prototype.responseComponent_docking_shipAIFrustrated = function()
{
	var station = this.getParameter("oolite_dockingStation");
	if (station)
	{
		station.abortDockingForShip(this.ship);
	}
	this.communicate("oolite_abortDocking",{},3);
	this.setParameter("oolite_dockingStation",null);
	this.reconsiderNow();
}


PriorityAIController.prototype.responseComponent_docking_stationWithdrewDockingClearance = function()
{
	this.setParameter("oolite_dockingStation",null);
	this.reconsiderNow();
}


PriorityAIController.prototype.responseComponent_escort_escortDock = function()
{
	this.reconsiderNow();
}


PriorityAIController.prototype.responseComponent_escort_helpRequestReceived = function(ally,enemy)
{
	if (this.allied(this.ship,enemy))
	{
		return;
	}
	this.ship.addDefenseTarget(enemy);
	if (enemy.scanClass == "CLASS_MISSILE" && this.distance(enemy) < this.scannerRange && this.ship.hasEquipmentProviding("EQ_ECM"))
	{
		this.fireECM();
	}
	if (enemy.scanClass == "CLASS_THARGOID" && this.ship.scanClass != "CLASS_THARGOID" && (!this.ship.target || this.ship.target.scanClass != "CLASS_THARGOID"))
	{
		if (this.respondToThargoids(enemy,false))
		{
			this.reconsiderNow();
			return;
		}
	}

	// always help the leader
	if (ally == this.ship.group.leader)
	{
		if (!this.ship.target || !this.ship.hasHostileTarget || this.ship.target.target != ally)
		{
			this.ship.target = enemy;
			this.reconsiderNow();
			return;
		}
	}
	this.ship.addDefenseTarget(enemy);
	if (enemy.scanClass == "CLASS_MISSILE" && this.distance(enemy) < this.scannerRange && this.ship.hasEquipmentProviding("EQ_ECM"))
	{
		this.fireECM();
		return;
	}
	if (!this.ship.hasHostileTarget)
	{
		this.ship.target = enemy;
		this.ship.performAttack();
		this.reconsiderNow();
		return; // not in a combat mode
	}
	if (ally.energy / ally.maxEnergy < this.ship.energy / this.ship.maxEnergy)
	{
		// not in worse shape than ally
		if (this.ship.target.target != ally && this.ship.target != ally.target)
		{
			// not already helping, go for it...
			this.ship.target = enemy;
			this.reconsiderNow();
		}
	}
}


PriorityAIController.prototype.responseComponent_expectWitchspace_shipTargetLost = function(target)
{
	if (!target)
	{
		target = this.getParameter("oolite_rememberedTarget");
	}
	if (target && target.position) {
		var pos = target.position;
		var ws = system.wormholes;
		// most likely to be most recent
		for (var i=ws.length-1; i>=0; i--)
		{
			if (ws[i].position.distanceTo(pos) < 100)
			{
				this.setParameter("oolite_witchspaceWormhole",ws[i]);
				break;
			}
		}
	}
	this.reconsiderNow();
}


PriorityAIController.prototype.responseComponent_scooping_shipAchievedDesiredRange = function()
{
	this.reconsiderNow();
}


PriorityAIController.prototype.responseComponent_scooping_shipScoopedFuel = function()
{
	if (this.ship.fuel == 7)
	{
		this.reconsiderNow();
	}
}


PriorityAIController.prototype.responseComponent_trackPlayer_playerWillEnterWitchspace = function()
{
	var wormhole = this.getParameter("oolite_witchspaceWormhole");
	if (wormhole != null && wormhole.isWormhole)
	{
		this.ship.enterWormhole(wormhole);
	} 
	else if (this.getParameter("oolite_rememberedTarget") == player.ship)
	{
		this.ship.enterWormhole();
	}
}



/* ******************* Templates *************************** */

/* Templates. Common AI priority list fragments which may be useful to
 * multiple AIs. These functions take no parameters and return a
 * list. This can either be used straightforwardly as a truebranch or
 * falsebranch value, or appended to a list using Array.concat() */

PriorityAIController.prototype.templateLeadHuntingMission = function()
{
	return [
		{
			condition: this.conditionInInterstellarSpace,
			truebranch: this.templateWitchspaceJumpAnywhere()
		},
		{
			condition: this.conditionHasWaypoint,
			configuration: this.configurationSetDestinationToWaypoint,
			behaviour: this.behaviourApproachDestination,
			reconsider: 30
		},
		{
			condition: this.conditionHasSelectedStation,
			truebranch: [
				{
					condition: this.conditionSelectedStationNearby,
					configuration: this.configurationSetSelectedStationForDocking,
					behaviour: this.behaviourDockWithStation,
					reconsider: 30
				},
				{
					condition: this.conditionSelectedStationNearMainPlanet,
					truebranch: [
						{
							notcondition: this.conditionMainPlanetNearby,
							configuration: this.configurationSetDestinationToMainPlanet,
							behaviour: this.behaviourApproachDestination,
							reconsider: 30
						}
					]
				},
				// either the station isn't near the planet, or we are
				{
					configuration: this.configurationSetDestinationToSelectedStation,
					behaviour: this.behaviourApproachDestination,
					reconsider: 30
				}
			]
		},
		{
			condition: this.conditionMainPlanetNearby,
			truebranch: [
				{
					condition: this.conditionPatrolIsOver,
					configuration: this.configurationSelectRandomTradeStation,
					behaviour: this.behaviourReconsider
				}
			]
		},
		/* No patrol route set up. Make one */
		{
			configuration: this.configurationSetWaypoint,
			behaviour: this.behaviourApproachDestination,
			reconsider: 30
		}
	];
}

PriorityAIController.prototype.templateLeadPirateMission = function()
{
	return [
		{
			label: "Pirate mission",
			condition: this.conditionInNovaSpace,
			truebranch: this.templateWitchspaceJumpAnywhere()
		},
		{
			preconfiguration: this.configurationForgetCargoDemand,
			condition: this.conditionScannerContainsPirateVictims,
			configuration: this.configurationAcquireScannedTarget,
			truebranch: [
				{
					label: "Check odds",
					condition: this.conditionCombatOddsGood,
					behaviour: this.behaviourRobTarget,
					reconsider: 5
				}
			]
		},
		{
			condition: this.conditionInInterstellarSpace,
			truebranch: this.templateWitchspaceJumpAnywhere()
		},
		{
			/* move to a position on one of the space lanes, preferring lane 1 */
			label: "Lurk",
			configuration: this.configurationSetDestinationToPirateLurk,
			behaviour: this.behaviourApproachDestination,
			reconsider: 30
		},
	];
}


PriorityAIController.prototype.templateReturnToBase = function()
{
	return [
		{
			label: "Return to base",
			condition: this.conditionHasSelectedStation,
			truebranch: [
				{
					condition: this.conditionSelectedStationNearby,
					configuration: this.configurationSetSelectedStationForDocking,
					behaviour: this.behaviourDockWithStation,
					reconsider: 30
				},
				{
					condition: this.conditionSelectedStationNearMainPlanet,
					truebranch: [
						{
							notcondition: this.conditionMainPlanetNearby,
							configuration: this.configurationSetDestinationToMainPlanet,
							behaviour: this.behaviourApproachDestination,
							reconsider: 30
						}
					]
				},
				// either the station isn't near the planet, or we are
				{
					configuration: this.configurationSetDestinationToSelectedStation,
					behaviour: this.behaviourApproachDestination,
					reconsider: 30
				}
			],
			falsebranch: [
				{
					condition: this.conditionFriendlyStationExists,
					configuration: this.configurationSelectRandomTradeStation,
					behaviour: this.behaviourReconsider
				}
			]
		}
	];


}


PriorityAIController.prototype.templateReturnToBaseOrPlanet = function()
{
	return [
		{
			label: "Return to base or planet",
			condition: this.conditionFriendlyStationNearby,
			configuration: this.configurationSetNearbyFriendlyStationForDocking,
			behaviour: this.behaviourDockWithStation,
			reconsider: 30
		},
		{
			condition: this.conditionFriendlyStationExists,
			configuration: this.configurationSetDestinationToNearestFriendlyStation,
			behaviour: this.behaviourApproachDestination,
			reconsider: 30
		},
		{
			condition: this.conditionHasSelectedPlanet,
			truebranch: [
				{
					preconfiguration: this.configurationSetDestinationToSelectedPlanet,
					condition: this.conditionNearDestination,
					behaviour: this.behaviourLandOnPlanet
				},
				{
					behaviour: this.behaviourApproachDestination,
					reconsider: 30
				}
			]
		},
		{
			condition: this.conditionPlanetExists,
			configuration: this.configurationSelectPlanet,
			behaviour: this.behaviourReconsider
		},
		{
			condition: this.conditionCanWitchspaceOut,
			configuration: this.configurationSelectWitchspaceDestination,
			behaviour: this.behaviourEnterWitchspace,
			reconsider: 20
		}
	];
}


PriorityAIController.prototype.templateWitchspaceJumpAnywhere = function()
{
	return [
		{
			label: "Wormhole search",
			condition: this.conditionWormholeNearby,
			configuration: this.configurationSetDestinationToNearestWormhole,
			behaviour: this.behaviourApproachDestination,
			reconsider: 30
		},
		/* Short reconsiders on next two so wormholes aren't missed */
		{
			label: "No wormholes nearby",
			condition: this.conditionCanWitchspaceOut,
			configuration: this.configurationSelectWitchspaceDestination,
			behaviour: this.behaviourEnterWitchspace,
			reconsider: 10
		},
		{
			label: "Lurk around witchpoint",
			configuration: this.configurationSetDestinationToWitchpoint,
			behaviour: this.behaviourApproachDestination,
			reconsider: 10
		}
	];
}


PriorityAIController.prototype.templateWitchspaceJumpInbound = function()
{
	return [
		{
			label: "Jump inbound",
			preconfiguration: this.configurationSelectWitchspaceDestinationInbound,
			condition: this.conditionCanWitchspaceOnRoute,
			behaviour: this.behaviourEnterWitchspace,
			reconsider: 20
		},
		{
			condition: this.conditionReadyToSunskim,
			configuration: this.configurationSetDestinationToSunskimEnd,
			behaviour: this.behaviourSunskim,
			reconsider: 20
		},
		{
			condition: this.conditionSunskimPossible,
			configuration: this.configurationSetDestinationToSunskimStart,
			behaviour: this.behaviourApproachDestination,
			reconsider: 30
		}
	];
}


PriorityAIController.prototype.templateWitchspaceJumpOutbound = function()
{
	return [
		{
			label: "Jump outbound",
			preconfiguration: this.configurationSelectWitchspaceDestinationOutbound,
			condition: this.conditionCanWitchspaceOnRoute,
			behaviour: this.behaviourEnterWitchspace,
			reconsider: 20
		},
		{
			condition: this.conditionReadyToSunskim,
			configuration: this.configurationSetDestinationToSunskimEnd,
			behaviour: this.behaviourSunskim,
			reconsider: 20
		},
		{
			condition: this.conditionSunskimPossible,
			configuration: this.configurationSetDestinationToSunskimStart,
			behaviour: this.behaviourApproachDestination,
			reconsider: 30
		}
	];
}


/* ******************* Waypoint generators *********************** */

/* Waypoint generators. When these are called, they should set up
 * the next waypoint for the ship. Ideally ships should either
 * reach that waypoint or formally give up on it before asking for
 * the next one, but the generator shouldn't assume that unless
 * it's one written specifically for a particular AI . */

PriorityAIController.prototype.waypointsSpacelanePatrol = function()
{
	// nova/interstellar space exception
	if (!system.sun || !system.mainPlanet)
	{
		this.setParameter("oolite_waypoint",new Vector3D(0,0,0));
		this.setParameter("oolite_waypointRange",7500);
		return;
	}

	var p = this.ship.position;
	var choice = "";
	if (p.magnitude() < 10000)
	{
		// near witchpoint
		if (Math.random() < 0.9)
		{ 
			// mostly return to planet
			choice = "PLANET";
		}
		else
		{
			choice = "SUN";
		}
	}
	else if (p.distanceTo(system.mainPlanet) < system.mainPlanet.radius * 2)
	{
		// near planet
		if (Math.random() < 0.75)
		{ 
			// mostly go to witchpoint
			choice = "WITCHPOINT";
		}
		else
		{
			choice = "SUN";
		}
	}
	else if (p.distanceTo(system.sun) < system.sun.radius * 3)
	{
		// near sun
		if (Math.random() < 0.9)
		{ 
			// mostly return to planet
			choice = "PLANET";
		}
		else
		{
			choice = "SUN";
		}
	}
	else if (p.z < system.mainPlanet.position.z && ((p.x * p.x) + (p.y * p.y)) < this.scannerRange * this.scannerRange * 4)
	{
		// on lane 1
		if (Math.random() < 0.5)
		{
			choice = "PLANET";
		}
		else
		{
			choice = "WITCHPOINT";
		}
	}
	else if (p.subtract(system.mainPlanet).dot(p.subtract(system.sun)) < -0.9)
	{
		// on lane 2
		if (Math.random() < 0.5)
		{
			choice = "PLANET";
		}
		else
		{
			choice = "SUN";
		}
	}
	else if (p.dot(system.sun.position) > 0.9)
	{
		// on lane 3
		if (Math.random() < 0.5)
		{
			choice = "WITCHPOINT";
		}
		else
		{
			choice = "SUN";
		}
	}
	else
	{
		// we're not on any lane. Return to the planet
		choice = "PLANET";
	}
	// having chosen, now set up the next stop on the patrol
	switch (choice) {
	case "WITCHPOINT":
		this.setParameter("oolite_waypoint",new Vector3D(0,0,0));
		this.setParameter("oolite_waypointRange",7500);
		break;
	case "PLANET":
		this.setParameter("oolite_waypoint",system.mainPlanet.position);
		this.setParameter("oolite_waypointRange",system.mainPlanet.radius*2);
		break;
	case "SUN":
		this.setParameter("oolite_waypoint",system.sun.position);
		this.setParameter("oolite_waypointRange",system.sun.radius*2.5);
		break;
	}

}


PriorityAIController.prototype.waypointsStationPatrol = function()
{
	var station = null;
	if (this.ship.group && this.ship.group.leader && this.ship.group.leader.isStation)
	{
		station = this.ship.group.leader;
	}
	if (!station)
	{
		station = system.mainStation;
		if (!station)
		{
			this.setParameter("oolite_waypoint",new Vector3D(0,0,0));
			this.setParameter("oolite_waypointRange",7500);
			return;
		}
	}
	var z = station.vectorForward;
	var tmp = new Vector3D(0,1,0);
	if (system.sun)
	{
		tmp = z.cross(system.sun.position.direction());
	}
	var x = z.cross(tmp);
	var y = z.cross(x);
	// x and y now consistent vectors relative to a rotating station

	var waypoints = [
		station.position.add(x.multiply(25000)),
		station.position.add(y.multiply(25000)),
		station.position.add(x.multiply(-25000)),
		station.position.add(y.multiply(-25000))
	];
	
	var waypoint = waypoints[0];
	for (var i=0;i<=3;i++)
	{
		if (this.distance(waypoints[i]) < 500)
		{
			waypoint = waypoints[(i+1)%4];
			break;
		}
	}
	this.setParameter("oolite_waypoint",waypoint);
	this.setParameter("oolite_waypointRange",100);

}


PriorityAIController.prototype.waypointsWitchpointPatrol = function()
{
	if (system.mainPlanet && this.ship.distanceTravelled > system.mainPlanet.position.z + 200000)
	{
		this.setParameter("oolite_waypoint",system.mainStation.position);
		this.setParameter("oolite_waypointRange",10000);
	}
	else
	{
		var waypoints = [
			new Vector3D(15E3,0,5E3),
			new Vector3D(0,15E3,-5E3),
			new Vector3D(-15E3,0,5E3),
			new Vector3D(0,-15E3,-5E3)
		];
		
		var waypoint = waypoints[0];
		for (var i=0;i<=3;i++)
		{
			if (this.distance(waypoints[i]) < 500)
			{
				waypoint = waypoints[(i+1)%4];
				break;
			}
		}
		this.setParameter("oolite_waypoint",waypoint);
		this.setParameter("oolite_waypointRange",100);
	}

}


/* ********** Communications data ****************/

/* Warning: OXPs should only interact with this through the provided
 * API functions. The internals of data storage may be changed at any
 * time. This data is global. */

this.startUp = function()
{
	delete this.startUp;
	// initial definition is just essential communications for now
	this.$commsSettings = {};
	this.$commsAllowed = true;
	this._setCommunications({
		generic: {
			generic: {
				oolite_thanksForHelp: "[oolite-comms-thanksForHelp]",
				oolite_surrender: "[oolite-comms-surrender]"
			}
		},
		trader: { 
			generic: { 
				oolite_agreeingToDumpCargo: "[oolite-comms-acceptPirateDemand]",
				oolite_makeDistressCall: "[oolite-comms-makeDistressCall]"
			} 
		},
		police: {
			generic: {
				oolite_thanksForHelp: "[oolite-comms-police-thanksForHelp]",
				oolite_markForFines: "[oolite-comms-markForFines]",
				oolite_distressResponseAggressor: "[oolite-comms-distressResponseAggressor]",
				oolite_offenceDetected: "[oolite-comms-offenceDetected]",
			}
		},
		pirate: {
			generic: {
				oolite_makePirateDemand: "[oolite-comms-makePirateDemand]",
			}
		},
		assassin: {
			generic: {
				oolite_beginningFight: "[oolite-comms-contractAttack]",
			}
		},
		_thargoid: {
			thargoid: {
				oolite_continuingAttack: "[thargoid_curses]"
			}
		}
	});

}



/* Event handler pair to prevent comms from being received while in
 * witchspace tunnel */
this.shipWillEnterWitchspace = function()
{
	this.$commsAllowed = false;
}

this.shipExitedWitchspace = function()
{
	this.$commsAllowed = true;
}



/* Search through communications from most specific to least specific.
 * role+personality
 * "generic"+personality
 * role+"generic"
 * "generic"+"generic"
 * A return value of "" means no communication is set.
 *
 * Roles or personalities starting with _ do not fall back to generic
 */
this._getCommunication = function(role, personality, key)
{
	if (this.$commsSettings[role] && this.$commsSettings[role][personality] && this.$commsSettings[role][personality][key] && this.$commsSettings[role][personality][key] != "")
	{
		return this.$commsSettings[role][personality][key];
	}
	if (role.charAt(0) != "_")
	{
		if (this.$commsSettings["generic"] && this.$commsSettings["generic"][personality] && this.$commsSettings["generic"][personality][key] && this.$commsSettings["generic"][personality][key] != "")
		{
			return this.$commsSettings["generic"][personality][key];
		}
	}
	if (personality.charAt(0) != "_")
	{
		if (this.$commsSettings[role] && this.$commsSettings[role]["generic"] && this.$commsSettings[role]["generic"][key] && this.$commsSettings[role]["generic"][key] != "")
		{
			return this.$commsSettings[role]["generic"][key];
		}
	}
	if (role.charAt(0) != "_" && personality.charAt(0) != "_")
	{
		if (this.$commsSettings["generic"] && this.$commsSettings["generic"]["generic"] && this.$commsSettings["generic"]["generic"][key] && this.$commsSettings["generic"]["generic"][key] != "")
		{
			return this.$commsSettings["generic"]["generic"][key];
		} 
	}
	return "";
}


/* Returns the available personalities for a particular role */
this._getCommunicationPersonalities = function(role)
{
	if (!this.$commsSettings || !this.$commsSettings[role])
	{
		return [];
	}
	else
	{
		return Object.keys(this.$commsSettings[role]);
	}
}


/* Set a communication for the specified role, personality and comms
 * key. "generic" is used as a fallback role and personality. */
this._setCommunication = function(role, personality, key, value)
{
	if (!this.$commsSettings[role])
	{
		this.$commsSettings[role] = {};
	}
	if (!this.$commsSettings[role][personality])
	{
		this.$commsSettings[role][personality] = {};
	}
	this.$commsSettings[role][personality][key] = value;
}


/* Bulk setting of communications */
this._setCommunications = function(obj)
{
	var roles = Object.keys(obj);
	for (var i = 0; i<roles.length ; i++)
	{
		var personalities = Object.keys(obj[roles[i]]);
		for (var j = 0; j<personalities.length ; j++)
		{
			var keys = Object.keys(obj[roles[i]][personalities[j]]);
			for (var k = 0; k<keys.length ; k++)
			{
				var val = obj[roles[i]][personalities[j]][keys[k]];
				this._setCommunication(roles[i],personalities[j],keys[k],val);
			}
		}
	}

}

/* Intentionally not documented */
this._threatAssessment = function(ship,full)
{
	// experimenting without this one for a while
	//	full = full || ship.hasHostileTarget || (ship.isPlayer && player.alertCondition == 3);
	if (ship.isShip) {
		return ship.threatAssessment(full);
	} else {
		// might have a wormhole targeted?
		return 0;
	}
}