/*

oolite-populator.js

Built-in system populator settings


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.

*/


/*jslint white: true, undef: true, eqeqeq: true, bitwise: true, regexp: true, newcap: true, immed: true */
/*global missionVariables, player*/


"use strict";


this.name			= "oolite-populator";
this.author			= "cim";
this.copyright		= "© 2008-2013 the Oolite team.";


this.startUp = function()
{
	/* For translations: the Moray Medical Boat gets different cargo */
	this.$medicalReg = new RegExp(expandDescription("[medical-word]"),"i");

	/* The global level of bias in weapon selection. A few useful
	 * threshold values if modifying this:
	 *
	 * 0: default weapon selection
	 *
	 * 0.2: above this military lasers start being granted to rare ships
	 *
	 * 1.2: above this aft military lasers start appearing; forward
	 * military/aft beam is common
	 *
	 * -0.8: no ships have aft beam laser; forward beam is rare
	 *
	 * -1.8: no ships have beam lasers
	 *
	 * A few ships (e.g. police ships) do not have variable lasers and
	 * are not affected by this setting.
	 */
	this.$weaponLevelBias = 0;

	/* The global level of bias in AI skill selection. Per-ship bias
	 * varies between +3 and -3. Ships with accuracy already >= 5 are
	 * not affected by this biasing.
	 *
	 * At -3, no ships will get an accuracy higher than the random
	 * -5..5 they spawned with; at +3 no ships will get an accuracy
	 * lower than that.
	 *
	 * Regardless of this parameter, 
	 */
	this.$skillLevelBias = 0;

	/* The maximum accuracy a ship can be granted by skill
	 * biasing. Values >10 can be set and will make it more likely
	 * that any ship with positive skill bias will get accuracy
	 * 10. Setting a negative value here will have odd results.  
	 */
	this.$skillLevelMaximum = 4.99;


	// internal variables; do not modify outside this script
	this.$populatorRun = 0;
	delete this.startUp;
}


/* This allows this.systemWillPopulate to be called by scripts running
 * before it, so that its entries are in place before they try to
 * modify them. Generally useful if you want to modify the basic
 * system population. */
this.shipWillEnterWitchspace = function()
{
	this.$populatorRun = 0; // reset variable after all population complete
}

/* Basic system population */
this.systemWillPopulate = function() 
{
	if (this.$populatorRun)
	{
		return;
	}

	/* Priority range 0-99 used by Oolite default populator */
	// anything added here with priority > 20 will be cancelled by the
	// nova mission populator if necessary

	/* Add navigation buoys */
	// for the compass to work properly, the buoys need to be added first,
	// in this order.
	system.setPopulator("oolite-nav-buoy",
						{
							priority: 5,
							location: "COORDINATES",
							coordinates: system.mainStation.position.add(system.mainStation.vectorForward.multiply(10E3)),
							callback: function(pos) {
								var nb = system.addShips("buoy",1,pos,0)[0];
								nb.scanClass = "CLASS_BUOY";
								nb.reactToAIMessage("START_TUMBLING");
								nb.beaconLabel = "[oolite-beacon-label-station]";
							},
							deterministic: true
						});

	system.setPopulator("oolite-witch-buoy",
						{
							priority: 10,
							location: "COORDINATES",
							coordinates: [0,0,0],
							callback: function(pos) {
								var wb = system.addShips("buoy-witchpoint",1,pos,0)[0];
								wb.scanClass = "CLASS_BUOY";
								wb.reactToAIMessage("START_TUMBLING");
								wb.beaconLabel = "[oolite-beacon-label-witchpoint]";
							},
							deterministic: true
						});

	/* Add asteroids */
	var clusters = 2*(1+Math.floor(system.scrambledPseudoRandomNumber(51728)*4));
	var psclusters = 1+(clusters/2);
	clusters = clusters-psclusters;
	
	var addRockCluster = function(pos) 
	{
		var size = 1+Math.floor(system.scrambledPseudoRandomNumber(Math.floor(pos.x))*11);
		var hermit = (system.scrambledPseudoRandomNumber(Math.floor(pos.y))*31) <= size;
		var rocks = system.addShips("asteroid",size,pos,25E3);
		// don't add rock hermits if the sun is about to explode
		if (hermit && !system.sun.isGoingNova) 
		{
			var allegiance = this._hermitAllegiance(pos,system.info.government);
			var role = "rockhermit";
			if (allegiance == "chaotic")
			{
				role = "rockhermit-chaotic";
			}
			else if (allegiance == "pirate")
			{
				role = "rockhermit-pirate";
			}

			var rh = system.addShips(role,1,pos,0)[0];
			rh.scanClass = "CLASS_ROCK";

		}
	}.bind(this);

	system.setPopulator("oolite-route1-asteroids",
						{
							priority: 20,
							location: "LANE_WP",
							locationSeed: 51728,
							groupCount: clusters,
							callback: addRockCluster,
							deterministic: true
						});
	system.setPopulator("oolite-route2-asteroids",
						{
							priority: 20,
							location: "LANE_PS",
							locationSeed: 82715,
							groupCount: psclusters,
							callback: addRockCluster,
							deterministic: true
						});

	/* To ensure there's at least one hermit, for smugglers to dock at */
	system.setPopulator("oolite-offlane-hermit",
						{
							priority: 99, // make sure all other core population is done
							location: "PLANET_ORBIT_HIGH",
							locationSeed: 71258,
							groupCount: 1,
							callback: function(pos) {
								if (system.countShipsWithPrimaryRole("rockhermit")+system.countShipsWithPrimaryRole("rockhermit-chaotic")+system.countShipsWithPrimaryRole("rockhermit-pirate")==0) {
									var allegiance = this._hermitAllegiance(pos,system.info.government);
									var role = "rockhermit";
									if (allegiance == "chaotic")
									{
										role = "rockhermit-chaotic";
									}
									else if (allegiance == "pirate")
									{
										role = "rockhermit-pirate";
									}
									var rh = system.addShips(role,1,pos,0)[0];
									// just the hermit, no other rocks
									rh.scanClass = "CLASS_ROCK";
								}
							}.bind(this),
							deterministic: true
						});


	/* Mainly for nova mission. If the nova script runs first, then
	 * this is set and we stop here. If this script runs first, the
	 * nova mission populator removes the entries this script adds. */
	if (system.sun.isGoingNova)
	{
		return;
	}


	/* Calculate numbers of major groups */
	var i; var j; var k; var local; var second;

	/* Prepare repopulator information. The populator will calculate
	 * average hourly rates, and then populate the initial system
	 * based on those. The repopulator will add at those rates to try
	 * to maintain a steady state. */
	this.$repopulatorFrequencyIncoming = {};
	this.$repopulatorFrequencyOutgoing = {};

	/* Many traffic densities depend on systems in range. Get once for
	 * use in the rest of the set up */
	var locals = system.info.systemsInRange();
	this.$populatorLocals = locals;
	var seconds = [];
	for (i = 0; i < locals.length ; i++)
	{
		seconds[i] = locals[i].systemsInRange();
	}

	var verylocals = system.info.systemsInRange(3.5);
	this.$populatorVeryLocals = verylocals;
	var veryseconds = [];
	for (i = 0; i < verylocals.length ; i++)
	{
		veryseconds[i] = verylocals[i].systemsInRange(3.5);
	}

	this._debug("G"+(galaxyNumber+1)+": "+system.info.name);
	this._debug("Hub count: "+locals.length+" ("+verylocals.length+")");

	var bottleneck = this._systemIsBottleneck(locals,seconds);

	/* Calculate trader hourly rates first, as most other rates depend
	 * on them */
	var freighters = 0; // standard trade ships
	var couriers = 0; // fast parcel couriers or big passenger liners
	var smugglers = 0; // small fast illegal goods traders

	/* // for now just generate sources and destinations dynamically
	   this.$repopulatorFrequencyIncoming.traderFreighters = {};
	   this.$repopulatorFrequencyIncoming.traderCouriers = {};
	   this.$repopulatorFrequencyIncoming.traderSmugglers = {};
	   this.$repopulatorFrequencyOutgoing.traderFreighters = {};
	*/
	this.$repopulatorFrequencyIncoming.traderFreighters = 0;
	this.$repopulatorFrequencyIncoming.traderCouriers = 0;
	this.$repopulatorFrequencyIncoming.traderSmugglers = 0;
	this.$repopulatorFrequencyOutgoing.traderFreighters = 0;

	for (i = 0; i < locals.length ; i++)
	{
		// standard freighter traffic is mirrored
		local = locals[i];
		// traffic is higher between systems on opposite side of economy
		var ecomatch = -(system.info.economy-3.5)*(local.economy-3.5);
		var trdanger = 0;
		var rate = 0;
		// if either local or remote end is more dangerous than
		// Communist, reduce trader frequency
		if (local.government < 4)
		{
			trdanger = (4-local.government)*2.5;
		}
		if (system.info.government < 4)
		{
			trdanger += (4-system.info.government)*2.5;
		}
		// good economic match: one every 30 minutes if safe
		if (ecomatch > 0)
		{
			rate = 60/(30+trdanger);
		}
		// bad economic match: one every 2 hours if safe
		else
		{
			rate = 60/(120+(trdanger*2));
		}
		
		this.$repopulatorFrequencyIncoming.traderFreighters += rate;
		this.$repopulatorFrequencyOutgoing.traderFreighters += rate;
		freighters += rate;

		second = seconds[i];
		// couriers are non-mirrored
		rate = (20/(10+((14-local.techlevel)*5)))/second.length;
		if (bottleneck)
		{
			couriers *= 1.5; // simulate long-range routes
		}
		this.$repopulatorFrequencyIncoming.traderCouriers += rate;
		couriers += rate;
		// smugglers are non-mirrored
		rate = (20/(10+(local.techlevel*5)))/second.length;
		this.$repopulatorFrequencyIncoming.traderSmugglers += rate;
		smugglers += rate;
	}
	// and outgoing rates for smugglers/couriers. Don't need to
	// specify destination since all rates are equal
	rate = 20/(10+((14-system.info.techlevel)*5));
	this.$repopulatorFrequencyOutgoing.traderCouriers = rate;
	rate = (20/(10+(system.info.techlevel*5)))/locals.length;
	this.$repopulatorFrequencyOutgoing.traderSmugglers = rate;

	var traders = freighters+couriers+smugglers;
	
	/* Pirate rates next, based partly on trader rates */

	if (system.info.government == 0 && system.info.techlevel < 8)
	{
		this.$repopulatorFrequencyOutgoing.pirateAegisRaiders = 1/(8-system.info.techlevel);
	}
	else
	{
		this.$repopulatorFrequencyOutgoing.pirateAegisRaiders = 0;
	}

	// local independent pirate packs
	var pindependents = (1-system.info.government/12)*traders; 
	// organised pirate packs led by increasingly bigger freighters
	var lrate = 3/5; var mrate = 3/8; var hrate = 1/8;
	var pflight = 0;
	var pfmedium = 0;
	var pfheavy = 0;
	var pflightremote = 0;
	var pfmediumremote = 0;
	var pfheavyremote = 0;
	// rates of organised packs go up as government reduces
	if (system.info.government < 6)
	{
		pflight += lrate;
		if (system.info.government < 4)
		{
			pflight += lrate;
			pfmedium += mrate;
			if (system.info.government < 2)
			{
				// additional boost for independent packs in Feudal
				pindependents *= 1.2;
				pflight += lrate*2;
				pfmedium += mrate;
				pfheavy += hrate;
				if (system.info.government < 1)
				{
					pindependents *= 1.2;
					pflight *= 1.5;
					pfmedium *= 1.5;
					pfheavy *= 2;
				}
			}
		}
	}
	while (pindependents > 10)
	{
		pindependents *= 0.5;
	}
	this.$repopulatorFrequencyOutgoing.pirateIndependents = pindependents;
	this.$repopulatorFrequencyOutgoing.pirateLightPacks = pflight;
	this.$repopulatorFrequencyOutgoing.pirateMediumPacks = pfmedium;
	this.$repopulatorFrequencyOutgoing.pirateHeavyPacks = pfheavy;
	this.$repopulatorFrequencyIncoming.pirateLightPacks = 0;
	this.$repopulatorFrequencyIncoming.pirateMediumPacks = 0;
	this.$repopulatorFrequencyIncoming.pirateHeavyPacks = 0;
	if (verylocals.length > 0)
	{
		var found = false;
		// if there's at least one suitable system in range, half the
		// packs will jump out and then come back later.
		for (i = 0; i < verylocals.length ; i++)
		{
			local = verylocals[i];
			if (local.government < system.info.government)
			{
				found = true;
				break;
			}
		}
		if (found)
		{
			this.$repopulatorFrequencyIncoming.pirateLightPacksReturn = pflight/6; // (light packs 1/3 chance of jumping out)
			this.$repopulatorFrequencyIncoming.pirateMediumPacksReturn = pfmedium/2;
			this.$repopulatorFrequencyIncoming.pirateHeavyPacksReturn = pfheavy/2;
		}

		// we might also get packs coming from other nearby systems

		for (i = 0; i < verylocals.length ; i++)
		{
			local = verylocals[i];
			second = veryseconds[i];
			// pirate raids only against more ordered systems
			if (local.government < system.info.government)
			{
				var factor = 0;
				// split raids evenly between all systems in range of remote system
				for (j=0;j<second.length;j++)
				{
					if (local.government < second[j].government)
					{
						factor++;
					}
				}
				// only half generated there go out to raid 
				factor = 1/(factor*2); 
				var rlight = 0;
				var rmedium = 0;
				var rheavy = 0;
				if (local.government < 6)
				{
					rlight += lrate*factor;
					if (local.government < 4)
					{
						rlight += lrate*factor;
						rmedium += mrate*factor;
						if (local.government < 2)
						{
							rlight += lrate*factor;
							rmedium += mrate*factor;
							rheavy += hrate*factor;
							if (local.government < 1)
							{
								rlight *= 1.5;
								rmedium *= 1.5;
								rheavy *= 2.0;
							}
						}
					}
				}
				// light groups much less likely to jump systems
				pflight += rlight/3; pflightremote += rlight/3;
				pfmedium += rmedium; pfmediumremote += rmedium;
				pfheavy += rheavy; pfheavyremote += rheavy;
				this.$repopulatorFrequencyIncoming.pirateLightPacks += rlight/3;
				this.$repopulatorFrequencyIncoming.pirateMediumPacks += rmedium;
				this.$repopulatorFrequencyIncoming.pirateHeavyPacks += rheavy;
			}
		}
	}
	else
	{
		this.$repopulatorFrequencyIncoming.pirateLightPacksReturn = 0;
		this.$repopulatorFrequencyIncoming.pirateMediumPacksReturn = 0;
		this.$repopulatorFrequencyIncoming.pirateHeavyPacksReturn = 0;
	}
	
	var pirates = pindependents + pflight + pfmedium + pfheavy;
	
	/* Now hunters. Total hunter rate dependent on pirate numbers */
	// light fighters, mainly non-witchspace-capable
	// most common in confed/communist systems.
	var hlight = 60/(10+(5*(Math.abs(system.info.government-4.5)-0.5)));
	var hmedium = 0;
	var hheavy = 0;
	if (hlight > pirates * 2)
	{
		hlight = pirates * 2;
	}
	hlight = hlight / 3;
	this.$repopulatorFrequencyOutgoing.hunterLightPacks = hlight;

	this.$repopulatorFrequencyOutgoing.hunterMediumPacks = 0;
	this.$repopulatorFrequencyOutgoing.hunterHeavyPacks = 0;
	this.$repopulatorFrequencyIncoming.hunterMediumPacksReturn = 0;
	this.$repopulatorFrequencyIncoming.hunterHeavyPacksReturn = 0;
	this.$repopulatorFrequencyIncoming.hunterMediumPacks = 0;
	this.$repopulatorFrequencyIncoming.hunterHeavyPacks = 0;

	if (verylocals.length > 0)
	{
		var hmediumremote = 0;
		var hheavyremote = 0;
		if (system.info.government > 5)
		{
			// medium launch from Corp+Demo to Conf-Anar
			for (i=0;i<verylocals.length;i++)
			{
				local = verylocals[i];
				if (local.government <= 5)
				{
					// launch every 30, 2/3 return
					hmedium += 60/45;
					this.$repopulatorFrequencyOutgoing.hunterMediumPacks += 60/30;
					this.$repopulatorFrequencyIncoming.hunterMediumPacksReturn += 60/45;
				}
			}
		}
		else
		{
			// we're receiving them from nearby systems
			for (i=0;i<verylocals.length;i++)
			{
				local = verylocals[i];
				if (local.government > 5)
				{
					hmedium += 2;
					this.$repopulatorFrequencyIncoming.hunterMediumPacks += 60/30;
				}
			}
			
		}
		if (system.info.government > 1)
		{
			// heavy launch from Corp-Mult to Feud+Anar
			for (i=0;i<verylocals.length;i++)
			{
				local = verylocals[i];
				if (local.government <= 1)
				{
					// launch every 60, 2/3 return
					hheavy += 60/90;
					this.$repopulatorFrequencyOutgoing.hunterHeavyPacks += 1;
					this.$repopulatorFrequencyIncoming.hunterHeavyPacksReturn += 60/90;

				}
			}
		}
		else
		{
			// we're receiving them from nearby systems
			for (i=0;i<verylocals.length;i++)
			{
				local = verylocals[i];
				if (local.government > 1)
				{
					hheavy += 1;
					this.$repopulatorFrequencyIncoming.hunterHeavyPacks += 1;
				}
			}
		}
	}
	else
	{
		// no nearby systems. A very small number of medium and heavy
		// packs on local patrol
		hmedium = hlight / 3;
		hheavy = hlight / 10;
		this.$repopulatorFrequencyOutgoing.hunterMediumPacks = hmedium;
		this.$repopulatorFrequencyOutgoing.hunterHeavyPacks = hheavy;
		this.$repopulatorFrequencyIncoming.hunterMediumPacks = 0;
		this.$repopulatorFrequencyIncoming.hunterHeavyPacks = 0;
	}
	
	var hunters = hlight+hmedium+hheavy;
	
	/* Police patrols, also depend on trader+pirate numbers */

	var police = 60/(5*(8-system.info.government));
	if (police > traders + pirates)
	{
		police = traders + pirates;
	}
	police = police / 3;
	if (system.info.government <= 1)
	{
		// no police patrols away from the station - add more bounty
		// hunters instead
		hlight += police;
		hunters += police;
		this.$repopulatorFrequencyOutgoing.hunterLightPacks = hlight;
		police = 0;
	}
	/* high-tech systems will send interceptor wings out specifically
	 * to deal with incoming heavy pirate packs */
	var interceptors = 0;
	if (system.info.techlevel >= 9)
	{
		interceptors += pflightremote/2 + pfmediumremote + pfheavyremote*2;
	}

	this.$repopulatorFrequencyOutgoing.policePacks = police;
	this.$repopulatorFrequencyOutgoing.policeInterceptors = interceptors;

	/* Assassin numbers */
	var assassins = couriers*(Math.random()+Math.random());
	if (bottleneck)
	{
		assassins += couriers;
	}
	assassins = assassins * 2/(system.info.government+1);
	this.$repopulatorFrequencyOutgoing.assassins = assassins;

	/* Thargoid numbers */

	// more common in isolated systems with low hubcount
	var thargoids = this.$repopulatorFrequencyIncoming.thargoidScouts = 1/(locals.length+5);
	// larger strike forces try to disrupt bottleneck systems
	var thargoidstrike = this.$repopulatorFrequencyIncoming.thargoidStrikes = bottleneck ? 0.02 : 0;

	/* Current repopulator frequencies are in groups/hour. Need to
	 * convert to groups/20 seconds */

	k = Object.keys(this.$repopulatorFrequencyIncoming);
	for (i = 0 ; i < k.length ; i++)
	{
		this.$repopulatorFrequencyIncoming[k[i]] = this.$repopulatorFrequencyIncoming[k[i]] / 180;
		this._debugR("Incoming chance: "+k[i]+" = "+this.$repopulatorFrequencyIncoming[k[i]]);
	}
	k = Object.keys(this.$repopulatorFrequencyOutgoing);
	for (i = 0 ; i < k.length ; i++)
	{
		this.$repopulatorFrequencyOutgoing[k[i]] = this.$repopulatorFrequencyOutgoing[k[i]] / 180;
		this._debugR("Outgoing chance: "+k[i]+" = "+this.$repopulatorFrequencyOutgoing[k[i]]);
	}
	

	/* The repopulator frequencies are now set up */
	
	// route 1: witchpoint-planet
	var l1length = system.mainPlanet.position.magnitude();
	// route 2: sun-planet
	var l2length = system.mainPlanet.position.subtract(system.sun.position).magnitude();
	// route 3: witchpoint-sun
	var l3length = system.sun.position.magnitude();
	// triangle: circular patrol between three main points. 1/6 of
	// hunters, police go this way
	var trilength = l1length+l2length+l3length;
	
	this._debug("Routes: "+l1length+" , "+l2length+" , "+l3length);

	/* Calculate initial populations based on approximate attrition
	 * rates and lane lengths */

	function randomise(count)
	{
		count = count*(0.25+0.75*(Math.random()+Math.random()));
		var r = Math.floor(count);
		if (Math.random() < count-r)
		{
			r++;
		}
		return r;
	}

	// traders
	var initial = freighters * (l1length/600000);
	if (system.info.government < 4)
	{
		initial *= (1-0.15*(4-system.info.government));
	}
	system.setPopulator("oolite-freighters",
						{
							priority: 40,
							location: "LANE_WP",
							groupCount: randomise(initial),
							callback: this._addFreighter.bind(this)
						});
	system.setPopulator("oolite-freighters-docking",
						{
							priority: 40,
							location: "STATION_AEGIS",
							groupCount: randomise(initial/10),
							callback: this._addFreighter.bind(this)
						});
	initial = couriers/2 * (l1length/600000);
	system.setPopulator("oolite-couriers-route1",
						{
							priority: 40,
							location: "LANE_WP",
							groupCount: randomise(initial),
							callback: this._addCourierShort.bind(this)
						});
	initial = couriers/2 * (l3length/600000);
	system.setPopulator("oolite-couriers-route3",
						{
							priority: 40,
							location: "LANE_WS",
							groupCount: randomise(initial),
							callback: this._addCourierLong.bind(this)
						});
	initial = smugglers * (l1length/600000);
	system.setPopulator("oolite-smugglers",
						{
							priority: 40,
							location: "LANE_WP",
							groupCount: randomise(initial),
							callback: this._addSmuggler.bind(this)
						});

	// hunters
	// 5/6 go route 1, and back. 50% faster ships than traders, on average
	initial = hlight * 1/6 * (trilength / 900000) * (1.0-0.1*(7-system.info.government));
	system.setPopulator("oolite-hunters-triangle",
						{
							priority: 40,
							location: "LANE_WPS",
							groupCount: randomise(initial),
							callback: this._addLightHunter.bind(this)
						});
	initial = hlight * 5/6 * (l1length*2 / 900000) * (1.0-0.1*(7-system.info.government));
	system.setPopulator("oolite-hunters-route1",
						{
							priority: 40,
							location: "LANE_WP",
							groupCount: randomise(initial),
							callback: this._addLightHunter.bind(this)
						});
	initial = hmedium * l1length/900000 * (2/3) * 2/3;
	system.setPopulator("oolite-hunters-medium-route1",
						{
							priority: 40,
							location: "LANE_WP",
							groupCount: randomise(initial),
							callback: this._addMediumHunterRemote.bind(this)
						});
	initial = hmedium * l3length/900000 * (2/3) * 1/3;
	system.setPopulator("oolite-hunters-medium-route3",
						{
							priority: 40,
							location: "LANE_WS",
							groupCount: randomise(initial),
							callback: this._addMediumHunterRemote.bind(this)
						});

	initial = hheavy * l1length/900000 * (2/3) * 2/3;
	system.setPopulator("oolite-hunters-heavy-route1",
						{
							priority: 40,
							location: "LANE_WP",
							groupCount: randomise(initial),
							callback: this._addHeavyHunterRemote.bind(this)
						});

	initial = hheavy * l3length/900000 * (2/3) * 1/3;
	system.setPopulator("oolite-hunters-heavy-route3",
						{
							priority: 40,
							location: "LANE_WS",
							groupCount: randomise(initial),
							callback: this._addHeavyHunterRemote.bind(this)
						});

	// pirates
	// 2/3 to lane 1 (with higher governmental attrition), 1/6 to each of other lanes
	initial = pindependents * ((l1length*2/3)/600000) * (1.0-0.05*system.info.government) * 5/6;
	system.setPopulator("oolite-pirate-independent-route1",
						{
							priority: 40,
							location: "LANE_WP",
							groupCount: randomise(initial),
							callback: this._addIndependentPirate.bind(this)
						});
	initial = pindependents * ((l2length*2/3)/600000) * (1.0-0.05*system.info.government) / 12;
	system.setPopulator("oolite-pirate-independent-route2",
						{
							priority: 40,
							location: "LANE_PS",
							groupCount: randomise(initial),
							callback: this._addIndependentPirate.bind(this)
						});
	initial = pindependents * ((l3length*2/3)/600000) * (1.0-0.05*system.info.government) / 12;
	system.setPopulator("oolite-pirate-independent-route3",
						{
							priority: 40,
							location: "LANE_WS",
							groupCount: randomise(initial),
							callback: this._addIndependentPirate.bind(this)
						});

	// pirate packs
	initial = pflight - pflightremote; // domestic source
	system.setPopulator("oolite-pirate-light-route1",
						{
							priority: 40,
							location: "LANE_WP",
							groupCount: randomise(initial*3/4),
							callback: this._addLightPirateLocal.bind(this)
						});
	system.setPopulator("oolite-pirate-light-triangle",
						{
							priority: 40,
							location: "LANE_WPS",
							groupCount: randomise(initial*1/4),
							callback: this._addLightPirateLocal.bind(this)
						});
	initial = pflightremote; // other system
	system.setPopulator("oolite-pirate-light-remote",
						{
							priority: 40,
							location: "LANE_WP",
							groupCount: randomise(initial),
							callback: this._addLightPirateRemote.bind(this)
						});

	initial = pfmedium - pfmediumremote; // domestic source
	system.setPopulator("oolite-pirate-medium-route1",
						{
							priority: 40,
							location: "LANE_WP",
							groupCount: randomise(initial*3/4),
							callback: this._addMediumPirateLocal.bind(this)
						});
	system.setPopulator("oolite-pirate-medium-triangle",
						{
							priority: 40,
							location: "LANE_WPS",
							groupCount: randomise(initial*1/4),
							callback: this._addMediumPirateLocal.bind(this)
						});
	initial = pfmediumremote; // other system
	system.setPopulator("oolite-pirate-medium-remote",
						{
							priority: 40,
							location: "LANE_WP",
							groupCount: randomise(initial),
							callback: this._addMediumPirateRemote.bind(this)
						});

	initial = pfheavy - pfheavyremote; // domestic source
	system.setPopulator("oolite-pirate-heavy-route1",
						{
							priority: 40,
							location: "LANE_WP",
							groupCount: randomise(initial*3/4),
							callback: this._addHeavyPirateLocal.bind(this)
						});

	system.setPopulator("oolite-pirate-heavy-triangle",
						{
							priority: 40,
							location: "LANE_WPS",
							groupCount: randomise(initial*1/4),
							callback: this._addHeavyPirateLocal.bind(this)
						});

	initial = pfheavyremote; // other system
	system.setPopulator("oolite-pirate-heavy-remote",
						{
							priority: 40,
							location: "LANE_WP",
							groupCount: randomise(initial),
							callback: this._addHeavyPirateRemote.bind(this)
						});
	// assassins
	initial = assassins;
	var maxas = 2;
	if (system.info.government < 3)
	{
		// if carrying high-risk contracts through dangerous systems,
		// especially bottlenecks, add some more assassins
		// specifically waiting for the player
		var cs = player.ship.parcels;
		for (var i = cs.length-1; i >= 0 ; i--)
		{
			if (bottleneck)
			{
				if (cs[i].risk == 1 && Math.random() < 0.1)
				{
					initial++;
				} 
				if (cs[i].risk == 2 && Math.random() < 0.5)
				{
					maxas += 2;
					initial++;
				}
			}
			else if (cs[i].destination == system.ID)
			{
				// if you're going to intercept, waiting in the
				// destination system for the package isn't a bad idea
				initial += Math.random()*cs[i].risk;
			}
		}
		cs = player.ship.passengers;
		for (i = cs.length-1; i >= 0 ; i--)
		{
			if (bottleneck)
			{
				if (cs[i].risk == 1 && Math.random() < 0.1)
				{
					initial++;
				} 
				if (cs[i].risk == 2 && Math.random() < 0.5)
				{
					maxas += 2;
					initial++;
				}
			}
			else if (cs[i].destination == system.ID)
			{
				initial += Math.random()*cs[i].risk;
			}
		}
	}
	var agc = randomise(initial);
	/* Because the assassin groups all appear at the witchpoint it can
	 * end up ridiculously populated in certain systems. Cap the
	 * number of initial assassin groups at 2, unless the player is
	 * carrying high-risk items, in which case they deserve whatever
	 * shows up for jumping into an Anarchy bottleneck. */
	if (agc > maxas)
	{
		agc = maxas;
	}
	system.setPopulator("oolite-assassins",
						{
							priority: 40,
							location: "WITCHPOINT",
							groupCount: agc,
							callback: this._addAssassin.bind(this)
						});
	

	// police
	// 5/6 go route 1, and back. 
	initial = police * 5/6 * (l1length*2 / 900000) * (1.0-0.1*(7-system.info.government));
	system.setPopulator("oolite-police-route1",
						{
							priority: 40,
							location: "LANE_WP",
							groupCount: randomise(initial),
							callback: this._addPolicePatrol.bind(this)
						});

	initial = police * 1/6 * (trilength / 900000) * (1.0-0.1*(7-system.info.government));
	system.setPopulator("oolite-police-triangle",
						{
							priority: 40,
							location: "LANE_WPS",
							groupCount: randomise(initial),
							callback: this._addPolicePatrol.bind(this)
						});
	system.setPopulator("oolite-police-stationpatrol",
						{
							priority: 40,
							location: "STATION_AEGIS",
							callback: this._addPoliceStationPatrol.bind(this)
						});
	

	// interceptors
	initial = interceptors / 2;
	// half on way or returning
	system.setPopulator("oolite-interceptors-route1",
						{
							priority: 40,
							location: "LANE_WP",
							groupCount: randomise(initial),
							callback: this._addInterceptors.bind(this)
						});

	// half on station
	system.setPopulator("oolite-interceptors-witchpoint",
						{
							priority: 40,
							location: "WITCHPOINT",
							groupCount: randomise(initial),
							callback: this._addInterceptors.bind(this)
						});

	// thargoids
	system.setPopulator("oolite-thargoid-scouts",
						{
							priority: 40,
							location: "LANE_WPS",
							groupCount: randomise(thargoids),
							callback: this._addThargoidScout.bind(this)
						});

	system.setPopulator("oolite-thargoid-strike",
						{
							priority: 40,
							location: "LANE_WPS",
							groupCount: randomise(thargoidstrike),
							callback: this._addThargoidStrike.bind(this)
						});


	var pset = system.populatorSettings;
	this._debugP("Freighters",pset["oolite-freighters"].groupCount);
	this._debugP("Freighters (D)",pset["oolite-freighters-docking"].groupCount);
	this._debugP("Couriers (1)",pset["oolite-couriers-route1"].groupCount);
	this._debugP("Couriers (3)",pset["oolite-couriers-route3"].groupCount);
	this._debugP("Smugglers",pset["oolite-smugglers"].groupCount);
	this._debugP("Hunters (1)",pset["oolite-hunters-route1"].groupCount);
	this._debugP("Hunters (T)",pset["oolite-hunters-triangle"].groupCount);
	this._debugP("HuntersM (1)",pset["oolite-hunters-medium-route1"].groupCount);
	this._debugP("HuntersM (3)",pset["oolite-hunters-medium-route3"].groupCount);
	this._debugP("HuntersH (1)",pset["oolite-hunters-heavy-route1"].groupCount);
	this._debugP("HuntersH (3)",pset["oolite-hunters-heavy-route3"].groupCount);	
	this._debugP("Pirates (1)",pset["oolite-pirate-independent-route1"].groupCount);
	this._debugP("Pirates (2)",pset["oolite-pirate-independent-route2"].groupCount);
	this._debugP("Pirates (3)",pset["oolite-pirate-independent-route3"].groupCount);	
	this._debugP("Pirates (L1)",pset["oolite-pirate-light-route1"].groupCount);
	this._debugP("Pirates (LT)",pset["oolite-pirate-light-triangle"].groupCount);
	this._debugP("Pirates (LR)",pset["oolite-pirate-light-remote"].groupCount);
	this._debugP("Pirates (M1)",pset["oolite-pirate-medium-route1"].groupCount);
	this._debugP("Pirates (MT)",pset["oolite-pirate-medium-triangle"].groupCount);
	this._debugP("Pirates (MR)",pset["oolite-pirate-medium-remote"].groupCount);
	this._debugP("Pirates (H1)",pset["oolite-pirate-heavy-route1"].groupCount);
	this._debugP("Pirates (HT)",pset["oolite-pirate-heavy-triangle"].groupCount);
	this._debugP("Pirates (HR)",pset["oolite-pirate-heavy-remote"].groupCount);
	this._debugP("Assassins (WP)",pset["oolite-assassins"].groupCount);
	this._debugP("Police (1)",pset["oolite-police-route1"].groupCount);
	this._debugP("Police (T)",pset["oolite-police-triangle"].groupCount);
	this._debugP("Police (I1)",pset["oolite-interceptors-route1"].groupCount);
	this._debugP("Police (IW)",pset["oolite-interceptors-witchpoint"].groupCount);
	this._debugP("Thargoid (SC)",pset["oolite-thargoid-scouts"].groupCount);
	this._debugP("Thargoid (ST)",pset["oolite-thargoid-strike"].groupCount);

	// and the initial ships are done...
	this.$populatorRun = 1;
}


// function responsible for replenishing system contents
this.systemWillRepopulate = function()
{
	// if main station or planet is missing, something odd has
	// happened, so stop repopulation
	if (system.sun.isGoingNova || !system.mainStation || !system.mainPlanet)
	{
		return;
	}
	/* repopulate incoming traffic */

	// traders
	if (Math.random() < this.$repopulatorFrequencyIncoming.traderFreighters)
	{
		this._debugR("Incoming freighter");
		this._addFreighter(this._wormholePos());
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.traderCouriers)
	{
		this._debugR("Incoming courier");
		if (Math.random() < 0.5)
		{
			this._addCourierShort(this._wormholePos());
		}
		else
		{
			this._addCourierLong(this._wormholePos());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.traderSmugglers)
	{
		this._debugR("Incoming smuggler");
		this._addSmuggler(this._wormholePos());
	}

	// pirates
	if (Math.random() < this.$repopulatorFrequencyIncoming.pirateLightPacks)
	{
		if (system.countShipsWithPrimaryRole("pirate-light-freighter") < 6)
		{
			this._debugR("Incoming light pirate");
			this._addLightPirateRemote(this._wormholePos());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.pirateLightPacksReturn)
	{
		if (system.countShipsWithPrimaryRole("pirate-light-freighter") < 6)
		{
			this._debugR("Returning light pirate");
			this._addLightPirateReturn(this._wormholePos());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.pirateMediumPacks)
	{
		if (system.countShipsWithPrimaryRole("pirate-medium-freighter") < 4)
		{
			this._debugR("Incoming medium pirate");
			this._addMediumPirateRemote(this._wormholePos());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.pirateMediumPacksReturn)
	{
		if (system.countShipsWithPrimaryRole("pirate-medium-freighter") < 4)
		{
			this._debugR("Returning medium pirate");
			this._addMediumPirateReturn(this._wormholePos());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.pirateHeavyPacks)
	{
		if (system.countShipsWithPrimaryRole("pirate-heavy-freighter") < 2)
		{
			this._debugR("Incoming heavy pirate");
			this._addHeavyPirateRemote(this._wormholePos());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.pirateHeavyPacksReturn)
	{
		if (system.countShipsWithPrimaryRole("pirate-heavy-freighter") < 2)
		{
			this._debugR("Returning heavy pirate");
			this._addHeavyPirateReturn(this._wormholePos());
		}
	}

	// hunters
	if (Math.random() < this.$repopulatorFrequencyIncoming.hunterMediumPacks)
	{
		if (system.countShipsWithPrimaryRole("hunter-medium") < 5)
		{
			this._debugR("Incoming medium hunter");
			this._addMediumHunterRemote(this._wormholePos());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.hunterMediumPacksReturn)
	{
		if (system.countShipsWithPrimaryRole("hunter-medium") < 5)
		{
			this._debugR("Returning medium hunter");
			this._addMediumHunterReturn(this._wormholePos());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.hunterHeavyPacks)
	{
		if (system.countShipsWithPrimaryRole("hunter-heavy") < 3)
		{
			this._debugR("Incoming heavy hunter");
			this._addHeavyHunterRemote(this._wormholePos());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.hunterHeavyPacksReturn)
	{
		if (system.countShipsWithPrimaryRole("hunter-heavy") < 3)
		{
			this._debugR("Returning heavy hunter");
			this._addHeavyHunterReturn(this._wormholePos());
		}
	}

	// thargoids (do not appear at normal witchpoint)
	if (Math.random() < this.$repopulatorFrequencyIncoming.thargoidScouts)
	{
		this._debugR("Incoming thargoid scout");
		this._addThargoidScout(system.locationFromCode("TRIANGLE"));
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.thargoidStrike)
	{
		this._debugR("Incoming thargoid strike force");
		this._addThargoidStrike(system.locationFromCode("TRIANGLE"));
	}

	/* repopulate outgoing traffic */

	// traders
	if (Math.random() < this.$repopulatorFrequencyOutgoing.traderFreighters)
	{
		this._debugR("Launching freighter");
		this._addFreighter(this._tradeStation(true));
	}
	if (Math.random() < this.$repopulatorFrequencyOutgoing.traderCouriers)
	{
		this._debugR("Launching courier");
		if (Math.random() < 0.5)
		{
			this._addCourierShort(this._tradeStation(true));
		}
		else
		{
			this._addCourierLong(this._tradeStation(true));
		}
	}
	if (Math.random() < this.$repopulatorFrequencyOutgoing.traderSmugglers)
	{
		this._debugR("Launching smuggler");
		this._addSmuggler(this._tradeStation(false));
	}

	// pirates
	if (Math.random() < this.$repopulatorFrequencyOutgoing.pirateIndependents)
	{
		if (system.countShipsWithPrimaryRole("pirate") < 40)
		{
			this._debugR("Launching pirates");
			this._addIndependentPirate(this._pirateLaunch());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyOutgoing.pirateLightPacks)
	{
		if (system.countShipsWithPrimaryRole("pirate-light-freighter") < 6)
		{
			this._debugR("Launching light pirate");
			if (Math.random() < 0.83) // light pirates rarely jump
			{
				this._addLightPirateLocal(this._pirateLaunch());
			}
			else
			{
				this._addLightPirateOutbound(this._pirateLaunch());
			}
		}
	}
	if (Math.random() < this.$repopulatorFrequencyOutgoing.pirateMediumPacks)
	{
		if (system.countShipsWithPrimaryRole("pirate-medium-freighter") < 4)
		{
			this._debugR("Launching medium pirate");
			if (Math.random() < 0.5)
			{
				this._addMediumPirateLocal(this._pirateLaunch());
			}
			else
			{
				this._addMediumPirateOutbound(this._pirateLaunch());
			}
		}
	}
	if (Math.random() < this.$repopulatorFrequencyOutgoing.pirateHeavyPacks)
	{
		if (system.countShipsWithPrimaryRole("pirate-heavy-freighter") < 2)
		{
			this._debugR("Launching heavy pirate");
			if (Math.random() < 0.5)
			{
				this._addHeavyPirateLocal(this._pirateLaunch());
			}
			else
			{
				this._addHeavyPirateOutbound(this._pirateLaunch());
			}
		}
	}
	if (Math.random() < this.$repopulatorFrequencyOutgoing.pirateAegisRaiders)
	{
		this._addAegisRaiders();
	}

	// hunters
	if (Math.random() < this.$repopulatorFrequencyOutgoing.hunterLightPacks)
	{
		if (system.countShipsWithPrimaryRole("hunter") < 30)
		{
			this._debugR("Launching light hunter");
			this._addLightHunter(this._hunterLaunch());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyOutgoing.hunterMediumPacks)
	{
		if (system.countShipsWithPrimaryRole("hunter-medium") < 6)
		{
			this._debugR("Launching medium hunter");
			// outbound falls back to local if no systems in range
			this._addMediumHunterOutbound(this._hunterLaunch());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyOutgoing.hunterHeavyPacks)
	{
		if (system.countShipsWithPrimaryRole("hunter-heavy") < 3)
		{
			this._debugR("Launching heavy hunter");
			// outbound falls back to local if no systems in range
			this._addHeavyHunterOutbound(this._hunterLaunch());
		}
	}
	
	// assassins
	if (Math.random() < this.$repopulatorFrequencyOutgoing.assassins)
	{
		if (system.countShipsWithPrimaryRole("assassin-medium")+system.countShipsWithPrimaryRole("assassin-heavy") < 30)
		{
			this._debugR("Launching assassin");
			this._addAssassin(this._tradeStation(false));
		}
	}

	// police
	if (Math.random() < this.$repopulatorFrequencyOutgoing.policePacks)
	{
		if (system.countShipsWithPrimaryRole("police") < 30)
		{
			this._debugR("Launching police patrol");
			this._addPolicePatrol(this._policeLaunch());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyOutgoing.policeInterceptors)
	{
		if (system.countShipsWithPrimaryRole("police-witchpoint-patrol") < 30)
		{
			this._debugR("Launching police interception patrol");
			this._addInterceptors(this._policeLaunch());
		}
	}

	/* Generic traffic */
	if (Math.random() < 0.005 * system.info.techlevel)
	{
		// TODO: planet launches
		this._debugR("Launching shuttle");
		this._tradeStation(true).launchShuttle();
	}
}


/* And the equivalent functions for interstellar space */

this.interstellarSpaceWillPopulate = function() 
{
	if (this.$populatorRun)
	{
		return;
	}
	system.setPopulator("oolite-interstellar-thargoids",
						{
							priority: 10,
							location: "WITCHPOINT",
							groupCount: 2+Math.floor(Math.random()*4),
							callback: function(pos) {
								system.addShips("thargoid",1,pos,0);
							}
						});
	this.$populatorRun = 1;
}

this.interstellarSpaceWillRepopulate = function()
{
	if (Math.random() < 0.25)
	{
		if (system.countShipsWithPrimaryRole("thargoid") < 3)
		{
			if (Math.random() > 0.05)
			{
				system.addShips("thargoid",1+Math.floor(Math.random()*3),[0,0,0],25600);
			}
			else
			{
				// everyone's getting ambushed today
				system.addShips("trader",1,[0,0,0],6400);
			}
		}
	}
}

/* And finally the default nova system populators */

this.novaSystemWillPopulate = function()
{
	if (this.$populatorRun)
	{
		return;
	}
	// just burnt-out rubble
	system.setPopulator("oolite-nova-cinders",
						{
							priority: 10,
							location: "WITCHPOINT",
							groupCount: 1,
							callback: function(pos) {
								system.addShips("cinder",10,pos,25600);
								pos.z += 300000;
								system.addShips("cinder",10,pos,25600);
							}
						});
	this.$populatorRun = 1;
}


/* // no repopulation is needed, but other OXPs could use this function
   this.novaSystemWillRepopulate = function()
   {

   }
*/

/* Ship addition functions */

/* These functions use this._addShips/_addGroup so "pos" can either be
 * a coordinate or a station, to simplify ship addition */

this._addFreighter = function(pos)
{
	var t = this._addShips("trader",1,pos,0);
	if (t[0])
	{
		var goods;
		if (pos.isStation)
		{
			t[0].homeSystem = system.ID;
			t[0].destinationSystem = this._weightedNearbyTradeSystem();
			goods = "PLENTIFUL_GOODS";
			t[0].fuel = 7;
		}
		else
		{
			t[0].homeSystem = this._weightedNearbyTradeSystem();
			this._setFuel(t[0]);
			t[0].destinationSystem = system.ID;
			goods = "SCARCE_GOODS";
			if (Math.random()*8 > system.info.government)
			{
				// may have used some missiles already
				this._setMissiles(t[0],-1);
			}
		}
		// crude, but compatible with the approach in previous versions
		// and now translatable
		if (t[0].name.match(this.$medicalReg)) 
		{
			goods = "MEDICAL_GOODS";
			t[0].bounty = 0;
		}
		else
		{
			// medical ships always clean
			if (Math.random() < 0.05)
			{
				t[0].bounty = Math.ceil(Math.random()*20);
				// half of the offender traders are a bit more sinister
				// can only happen with ships which allow autoAI
				if (Math.random() < 0.5 && t[0].autoAI)
				{
					t[0].switchAI("oolite-traderOpportunistAI.js");
					if (t[0].autoWeapons)
					{
						this._setSkill(t[0],2); // need to be decent pilots to make this work
						this._setWeapons(t[0],2.5); // boost weapons
					}
					goods = "PIRATE_GOODS";
				} 
				var eg = t[0].escortGroup.ships;
				for (var i = 0; i < eg.length; i++)
				{
					if (eg[i] != t[0])
					{
						// ensure offender escorts have a bounty
						eg[i].bounty |= 3+Math.floor(Math.random()*12);
					}
				}
			}
			else
			{
				t[0].bounty = 0;
			}
		}
		t[0].setCargoType(goods);

		this._setEscortWeapons(t[0]);
	}
}


this._addCourier = function(pos)
{
	if (this._roleExists("trader-courier"))
	{
		var t = this._addShips("trader-courier",1,pos,0);
	}
	else
	{
		log(this.name,"Tried to add trader-courier but no ships of that role found - using default 'trader'");
		var t = this._addShips("trader",1,pos,0);
	}
	t[0].bounty = 0;
	t[0].heatInsulation = 6;
	if (t.escortGroup)
	{
		var gs = t.escortGroup.ships;
		for (var i=gs.length=1; i>=0; i++)
		{
			gs[i].heatInsulation = 6;
			gs[i].bounty = 0;
		}
	}
	this._setEscortWeapons(t[0]);
	return t;
}


this._addCourierShort = function(pos)
{
	var t = this._addCourier(pos);
	if (t[0])
	{
		// don't need to worry at this stage where it came from before that
		if (pos.isStation)
		{
			t[0].destinationSystem = this._nearbySystem(7);
			t[0].homeSystem = system.ID;
			t[0].setCargoType("PLENTIFUL_GOODS");
		}
		else
		{
			t[0].homeSystem = this._nearbySystem(7);
			this._setFuel(t[0]);
			t[0].destinationSystem = system.ID;
			t[0].setCargoType("SCARCE_GOODS");
		}
	}
}


this._addCourierLong = function(pos)
{
	var t = this._addCourier(pos);
	if (t[0])
	{
		if (pos.isStation)
		{
			t[0].destinationSystem = this._nearbySystem(25);
			t[0].homeSystem = system.ID;
			t[0].setCargoType("PLENTIFUL_GOODS");
		}
		else
		{
			// don't need to worry at this stage where it came from before that
			t[0].homeSystem = this._nearbySystem(7);
			this._setFuel(t[0]);
			t[0].destinationSystem = this._nearbySystem(25);
			t[0].setCargoType("SCARCE_GOODS");
		}
	}
}


this._addSmuggler = function(pos)
{
	if (this._roleExists("trader-smuggler"))
	{
		var t = this._addShips("trader-smuggler",1,pos,0);
	}
	else
	{
		log(this.name,"Tried to add trader-smuggler but no ships of that role found - using default 'trader'");
		var t = this._addShips("trader",1,pos,0);
	}
	if (t[0])
	{
		t[0].bounty = Math.ceil(Math.random()*20);
		if (t[0].bounty > t[0].cargoSpaceCapacity * 2)
		{
			t[0].bounty = t[0].cargoSpaceCapacity * 2;
		}
		if (pos.isStation)
		{
			t[0].destinationSystem = this._nearbySystem(7);
			t[0].homeSystem = system.ID;
		}
		else
		{
			t[0].homeSystem = this._nearbySystem(7);
			this._setFuel(t[0]);
			t[0].destinationSystem = system.ID;
		}
		t[0].setCargoType("ILLEGAL_GOODS");
		if (t[0].autoWeapons)
		{
			t[0].awardEquipment("EQ_FUEL_INJECTION"); // smugglers always have injectors
		}
		this._setWeapons(t[0],1.2); // rarely good weapons
		this._setEscortWeapons(t[0]);
		var eg = t[0].escortGroup.ships;
		for (var i = 0; i < eg.length; i++)
		{
			if (eg[i] != t[0])
			{
				// ensure smuggler escorts have a bounty
				eg[i].bounty |= 3+Math.floor(Math.random()*12);
			}
		}

	}
}


this._addLightHunter = function(pos)
{
	var h = this._addGroup("hunter",Math.floor(Math.random()*2)+Math.floor(Math.random()*2)+2,pos,2E3);
	for (var i = 0 ; i < h.ships.length ; i++)
	{
		h.ships[i].bounty = 0;
		h.ships[i].homeSystem = system.ID;
		h.ships[i].destinationSystem = system.ID;
		this._setWeapons(h.ships[i],1.5); // mixed weapons
		this._setSkill(h.ships[i],-1); // if they were any good, they'd have signed on with a proper hunting pack
	}
}


this._addMediumHunterRemote = function(pos)
{
	this._addHunterPack(pos,this._nearbySafeSystem(2),system.ID,"hunter-medium",false);
}


this._addMediumHunterReturn = function(pos)
{
	this._addHunterPack(pos,system.ID,this._nearbyDangerousSystem(4),"hunter-medium",true);
}


this._addMediumHunterOutbound = function(pos)
{
	this._addHunterPack(pos,system.ID,this._nearbyDangerousSystem(4),"hunter-medium",false);
}


this._addHeavyHunterRemote = function(pos)
{
	this._addHunterPack(pos,this._nearbySafeSystem(2),system.ID,"hunter-heavy",false);
}


this._addHeavyHunterReturn = function(pos)
{
	this._addHunterPack(pos,system.ID,this._nearbyDangerousSystem(1),"hunter-heavy",true);
}

this._addHeavyHunterOutbound = function(pos)
{
	this._addHunterPack(pos,system.ID,this._nearbyDangerousSystem(1),"hunter-heavy",false);
}


this._addHunterPack = function(pos,home,dest,role,returning)
{
	if (this._roleExists(role))
	{
		var t = this._addShips(role,1,pos,0);
	}
	else
	{
		log(this.name,"Tried to add "+role+" but no ships of that role found - using default 'hunter'");
		var t = this._addShips("hunter",1,pos,0);
	}
	if (t[0])
	{
		t[0].bounty = 0;
		t[0].homeSystem = home;
		t[0].destinationSystem = dest;
		
		if (returning)
		{
			this._setMissiles(t[0],-1);
			this._setReturnFuel(t[0]);
		}
		else
		{
			this._setFuel(t[0]);
		}

		var group = new ShipGroup("hunter group",t[0]);
		t[0].group = group;

		var hs = this._addShips("hunter",1+Math.floor(Math.random()*4)+Math.floor(Math.random()*4),pos,3E3);
		for (var i = 0; i<hs.length; i++)
		{
			hs[i].group = group;
			group.addShip(hs[i]);
			hs[i].bounty = 0;
			hs[i].fuel = 7;
			hs[i].homeSystem = t[0].homeSystem;
			hs[i].destinationSystem = t[0].destinationSystem;
			this._setWeapons(hs[i],1.5); // mixed weapons
			if (returning)
			{
				this._setMissiles(hs[i],-1);
			}
		}
		if (role == "hunter-heavy")
		{
			// occasionally give heavy hunters aft lasers
			this._setWeapons(t[0],2.2);
		}
		else
		{
			// usually ensure medium hunters have beam lasers
			this._setWeapons(t[0],1.9);
		}
		this._setSkill(t[0],3); // likely to be good pilot
		if (t[0].autoAI)
		{
			t[0].switchAI("oolite-bountyHunterLeaderAI.js"); 
// auto AI will normally get this already but not if the hunter
// addition used fallback roles
		}
	}
}


this._addIndependentPirate = function(pos)
{
	// a group < 3 in size is probably too small to actually attack anyone
	// mostly 2-6 groups
	var size = Math.floor(Math.random()*3)+Math.floor(Math.random()*3)+2;
	if (size > 8-system.info.government)
	{
		// in the safer systems may have lost some ships already, though
		size = 1+Math.floor(Math.random()*size);
	}
	var pg = this._addGroup("pirate",size,pos,2.5E3);
	for (var i=0;i<size;i++)
	{
		pg.ships[i].setBounty(20+system.info.government+size+Math.floor(Math.random()*8),"setup actions");
		if (!pos.isStation && !pos.isPlanet)
		{
			pg.ships[i].setCargoType("PIRATE_GOODS");
			if (pg.ships[i].hasHyperspaceMotor)
			{
				this._setWeapons(pg.ships[i],1.75); // bigger ones sometimes well-armed
			}
			else
			{
				this._setWeapons(pg.ships[i],1.3); // rarely well-armed
			}
			// in the safer systems, rarely highly skilled (the
			// skilled ones go elsewhere)
			this._setSkill(pg.ships[i],4-system.info.government);
			if (pos.z) // not if freshly launching
			{
				if (Math.random()*16 < system.info.government)
				{
					this._setMissiles(pg.ships[i],-1);
				}
			}
		}
	}
}


this._addPirateAssistant = function(role,lead,pos)
{
	if (this._roleExists(role))
	{
		var asst = this._addShips(role,1,pos,4E3);
	}
	else
	{
		log(this.name,"Tried to add "+role+" but no ships of that role found - using default 'pirate'");
		var asst = this._addShips("pirate",1,pos,4E3);
	}
	asst[0].homeSystem = lead.homeSystem;
	asst[0].destinationSystem = lead.destinationSystem;
	if (role == "pirate-interceptor")
	{
		// autoAI gets this (except if we've fallen back to generic
		// pirates) once OXPs have caught up and we can be confident
		// that the new roles will be available, this can be
		// simplified.
		if (asst[0].autoAI)
		{
			asst[0].switchAI("oolite-pirateInterceptorAI.js");
		}
		asst[0].setBounty(50+system.government+Math.floor(Math.random()*36),"setup actions");
		// interceptors not actually part of group: they just get the
		// same destinations
		this._setWeapons(asst[0],2.3); // heavily armed
		if (asst[0].autoWeapons)
		{
			asst[0].awardEquipment("EQ_FUEL_INJECTION"); // interceptors always have injectors
		}
	}
	else
	{ 
		asst[0].group = lead.group;
		lead.group.addShip(asst[0]);
		// autoAI gets this (except if we've fallen back to generic
		// pirates - see above)
		if (asst[0].autoAI)
		{
			asst[0].switchAI("oolite-pirateFighterAI.js");
		}
		asst[0].setBounty(20+system.government+Math.floor(Math.random()*12),"setup actions");
		if (role == "pirate-light-fighter")
		{
			this._setWeapons(asst[0],1.2); // basic fighters
		}
		else if (role == "pirate-medium-fighter")
		{
			this._setWeapons(asst[0],1.8); // often beam weapons
		}
		else if (role == "pirate-heavy-fighter")
		{
			this._setWeapons(asst[0],2.05); // very rarely aft lasers
		}
	}
}


this._addPiratePack = function(pos,leader,lf,mf,hf,thug,home,destination,returning)
{
	if (this._roleExists(leader))
	{
		var lead = this._addShips(leader,1,pos,0);		
	}
	else
	{
		log(this.name,"Tried to add "+leader+" but no ships of that role found");
		var lead = this._addShips("pirate",1,pos,0);		
	}
	lead[0].setBounty(60+system.government+Math.floor(Math.random()*8),"setup actions");
	lead[0].homeSystem = home;
	lead[0].destinationSystem = destination;

	var group = new ShipGroup("pirate pack",lead[0]);
	lead[0].group = group;
	for (var i = Math.floor(lf+(0.5+Math.random()-Math.random())); i > 0; i--)
	{
		this._addPirateAssistant("pirate-light-fighter",lead[0],pos);
	}
	for (var i = Math.floor(mf+(0.5+Math.random()-Math.random())); i > 0; i--)
	{
		this._addPirateAssistant("pirate-medium-fighter",lead[0],pos);
	}
	for (var i = Math.floor(hf+(0.5+Math.random()-Math.random())); i > 0; i--)
	{
		this._addPirateAssistant("pirate-heavy-fighter",lead[0],pos);
	}
	for (var i = Math.floor(thug+(0.5+Math.random()-Math.random())); i > 0; i--)
	{
		this._addPirateAssistant("pirate-interceptor",lead[0],pos);
	}
	if (lead[0].autoWeapons)
	{
		lead[0].awardEquipment("EQ_SHIELD_BOOSTER");
		lead[0].awardEquipment("EQ_ECM");
	}
	this._setWeapons(lead[0],2.8); // usually give aft laser
	this._setSkill(lead[0],3); // likely to be good pilot
	if (returning)
	{
		this._setMissiles(lead[0],-1);
		this._setReturnFuel(lead[0]);
	}
	else
	{
		if (thug > 0)
		{
			// medium and especially heavy may have better missiles
			this._setMissiles(lead[0],thug-0.5);
		}
		this._setFuel(lead[0]);
	}
	if (lead[0].escortGroup)
	{
		var eg = lead[0].escortGroup.ships;
		for (var i = 0; i < eg.length; i++)
		{
			if (eg[i] != lead[0])
			{
				// ensure freighter escorts have a bounty
				eg[i].bounty |= 3+Math.floor(Math.random()*12);
			}
		}
	}
	if (!pos.isStation && !pos.isPlanet)
	{
		lead[0].setCargoType("PIRATE_GOODS");
	}
	this._setEscortWeapons(lead[0]);
	if (lead[0].autoAI)
	{
		// may have fallen back to generic 'pirate' role, so make sure
		lead[0].switchAI("oolite-pirateFreighterAI.js");
	}
	return lead[0];
}

this._addLightPirateLocal = function(pos)
{
	return this._addPiratePack(pos,"pirate-light-freighter",2,1,-1,0,system.ID,system.ID,false);
}


this._addLightPirateRemote = function(pos)
{
	pos.z = pos.z % 100000;
	return this._addPiratePack(pos,"pirate-light-freighter",2,1,-1,0,this._nearbyDangerousSystem(system.info.government-1),system.ID,false);
}



this._addLightPirateOutbound = function(pos)
{
	return this._addPiratePack(pos,"pirate-light-freighter",2,1,-1,0,system.ID,this._nearbySafeSystem(system.info.government+1),false);
}


this._addLightPirateReturn = function(pos)
{
	return this._addPiratePack(pos,"pirate-light-freighter",2,1,-1,0,system.ID,this._nearbySafeSystem(system.info.government+1),true);
}


this._addMediumPirateLocal = function(pos)
{
	return this._addPiratePack(pos,"pirate-medium-freighter",3,2,0,1,system.ID,system.ID,false);
}


this._addMediumPirateRemote = function(pos)
{
	pos.z = pos.z % 100000;
	return this._addPiratePack(pos,"pirate-medium-freighter",3,2,0,1,this._nearbyDangerousSystem(Math.min(system.info.government-1,3)),system.ID,false);
}

// tmp for testing (needs adjusting to simulate planetary launch *or*
// use a suitable friendly station)
this._addMediumPirateOutbound = function(pos)
{
	return this._addPiratePack(pos,"pirate-medium-freighter",3,2,0,1,system.ID,this._nearbySafeSystem(system.info.government+1),false);
}


this._addMediumPirateReturn = function(pos)
{
	return this._addPiratePack(pos,"pirate-medium-freighter",3,2,0,1,system.ID,this._nearbySafeSystem(system.info.government+1),true);
}


this._addHeavyPirateLocal = function(pos)
{
	return this._addPiratePack(pos,"pirate-heavy-freighter",4,4,2,2,system.ID,system.ID,false);
}


this._addHeavyPirateRemote = function(pos)
{
	pos.z = pos.z % 100000;
	return this._addPiratePack(pos,"pirate-heavy-freighter",4,4,2,2,this._nearbyDangerousSystem(Math.min(system.info.government-1,1)),system.ID,false);
}


// tmp for testing (needs adjusting to simulate planetary launch *or*
// use a suitable friendly station)
this._addHeavyPirateOutbound = function(pos)
{
	return this._addPiratePack(pos,"pirate-heavy-freighter",4,4,2,2,system.ID,this._nearbySafeSystem(system.info.government+1),false);
}


this._addHeavyPirateReturn = function(pos)
{
	return this._addPiratePack(pos,"pirate-heavy-freighter",4,4,2,2,system.ID,this._nearbySafeSystem(system.info.government+1),true);
}


this._addAegisRaiders = function()
{
	if (!this._roleExists("pirate-aegis-raider")) 
	{
		log(this.name,"No ships with role pirate-aegis-raider defined - skipping addition");
		return;
	}
	var g = this._addGroup("pirate-aegis-raider",3+Math.floor(Math.random()*5),system.mainPlanet,3E3);
	var gs = g.ships;
	for (var i=0; i < gs.length ; i++)
	{
		gs[i].setBounty(50+system.government+Math.floor(Math.random()*36),"setup actions")
		this._setWeapons(gs[i],2.7); // very heavily armed
		this._setSkill(gs[i],3); // boost combat skill
		if (gs[i].autoWeapons)
		{
			// raiders need the best equipment
			gs[i].awardEquipment("EQ_FUEL_INJECTION"); 
			gs[i].awardEquipment("EQ_SHIELD_BOOSTER"); 
			gs[i].awardEquipment("EQ_ECM"); 
			gs[i].fuel = 7;
		}
	}
}


this._addAssassin = function(pos)
{
	var role = "assassin-light";
	var extra = 0;
	var ws = 2;
	var g = system.info.government+2;
	if (Math.random() > g / 10)
	{
		role = "assassin-medium";
		extra = 1;
		ws = 2.5;
		if (Math.random() > g / 5)
		{
			role = "assassin-heavy";
			ws = 2.8;
		}
	}
	if (!this._roleExists(role)) 
	{
		log(this.name,"No ships with role "+role+" defined - skipping addition");
		return;
	}
	var main = this._addShips(role,1,pos,0)[0];
	if (main.autoWeapons)
	{
		main.awardEquipment("EQ_FUEL_INJECTION");
		main.awardEquipment("EQ_ECM");
		if (2+Math.random() < ws)
		{
			main.awardEquipment("EQ_SHIELD_BOOSTER"); 
		}
		// assassins don't respect escape pods and won't expect anyone
		// else to either.
		main.removeEquipment("EQ_ESCAPE_POD");
		main.fuel = 7;
		this._setWeapons(main,ws);
		this._setSkill(main,extra);
	}
	//	main.bounty = 1+Math.floor(Math.random()*10);
//	main.switchAI("oolite-assassinAI.js"); // autoAI can get this
	if (extra > 0)
	{
		var g = new ShipGroup("assassin group",main);
		main.group = g;
		var numext = Math.floor(Math.random()*3)+1;
		if (role == "assassin-heavy")
		{
			var extras = this._addShips("assassin-medium",numext,pos,3E3);
		}
		else
		{
			var extras = this._addShips("assassin-light",numext,pos,3E3);
		}
		for (var i=0;i<numext;i++)
		{
			extras[i].group = g;
			g.addShip(extras[i]);
			if (extras[i].autoWeapons)
			{
				extras[i].awardEquipment("EQ_FUEL_INJECTION");
				extras[i].removeEquipment("EQ_ESCAPE_POD");
				extras[i].fuel = 7;
				this._setWeapons(extras[i],1.8);
			}
			//			extras[i].bounty = 1+Math.floor(Math.random()*5);
//			extras[i].switchAI("oolite-assassinAI.js");
		}
	}
}


this._addPolicePatrol = function(pos)
{
	var role = "police";
	if (9+Math.random()*6 < system.info.techlevel)
	{
		role = "interceptor";
	}
	var h = this._addGroup(role,Math.floor(Math.random()*2)+Math.floor(Math.random()*2)+2,pos,2E3);
	for (var i = 0 ; i < h.ships.length ; i++)
	{
		h.ships[i].bounty = 0;
		h.ships[i].homeSystem = system.ID;
		h.ships[i].destinationSystem = system.ID;
		if (h.ships[i].AIScript.oolite_intership)
		{
			h.ships[i].AIScript.oolite_intership.initial_group = h.ships.length;
		}
		if (system.info.techlevel >= 14)
		{
			this._setMissiles(h.ships[i],1);
		}

	}
}


this._addPoliceStationPatrol = function(pos)
{
	var role = "police";
	if (9+Math.random()*6 < system.info.techlevel)
	{
		role = "interceptor";
	}
	var p = system.addShips(role,1,pos,0)[0];
	p.primaryRole = "police-station-patrol";
	p.group = system.mainStation.group;
	p.group.addShip(p);
	// p.switchAI("oolite-policeAI.js");// autoAI gets this
	p.bounty = 0;
	p.maxEscorts = 16;
	if (system.info.techlevel >= 14)
	{
		this._setMissiles(p,1);
	}

}


this._addInterceptors = function(pos)
{
	var h = this._addGroup("interceptor",Math.floor(Math.random()*2)+Math.floor(Math.random()*2)+1+Math.ceil(system.info.techlevel/6),pos,2E3);
	for (var i = 0 ; i < h.ships.length ; i++)
	{
		h.ships[i].bounty = 0;
		h.ships[i].primaryRole = "police-witchpoint-patrol";
		h.ships[i].maxEscorts = 16;
		h.ships[i].homeSystem = system.ID;
		h.ships[i].destinationSystem = system.ID;
//		h.ships[i].switchAI("oolite-policeAI.js"); // auto AI
		// only +1 as core already gives police ships better AI
		this._setSkill(h.ships[i],1);

		if (system.info.techlevel >= 14)
		{
			this._setMissiles(h.ships[i],1);
		}
	}
}


this._addThargoidScout = function(pos)
{
	this._addShips("thargoid",1,pos,0);
}


this._addThargoidStrike = function(pos)
{
	var thargs = this._addShips("thargoid",Math.floor(5+Math.random()*4),pos,10E3);
	for (var i = 0; i < thargs.length ; i++)
	{
		// raiding parties have better pilots
		this._setSkill(thargs[i],1);
	}
}

/* Utility functions */

this._debug = function(msg)
{
	log("universe.populate.information",msg);
}


this._debugP = function(gtype,ct)
{
	log("universe.populate.information",gtype+": "+ct);
}


this._debugR = function(msg)
{
	log("universe.populate.repopulate",msg);
}


this._roleExists = function(role)
{
	if (Ship.keysForRole(role) && Ship.keysForRole(role).length > 0)
	{
		return true;
	}
	return false;
}


/* Run _setWeapons on the escort group */
this._setEscortWeapons = function(mothership)
{
	if (!mothership.escortGroup)
	{
		return;
	}
	var eg = mothership.escortGroup.ships;
	for (var i = eg.length-1 ; i >= 0 ; i--)
	{
		var ship = eg[i];
		if (ship == mothership)
		{
			continue;
		}
		if (!ship.autoWeapons)
		{
			continue;
		}
		var pr = ship.primaryRole;
		if (pr == "escort" || pr == "pirate-light-fighter")
		{
			this._setWeapons(ship,1.3); // usually lightly armed as escorts
		}
		else if (pr == "escort-medium" || pr == "pirate-medium-fighter")
		{
			this._setWeapons(ship,1.8); // usually heavily armed as escorts
		}
		else if (pr == "escort-heavy" || pr == "pirate-heavy-fighter")
		{
			this._setWeapons(ship,2.05); // rarely have an aft laser
		}
	}
}


/* Levels:
 * <= 1: FP
 *    2: FB
 *    3: FB, AB (rare in core)
 *    4: FM, AB (not used in core)
 * >= 5: FM, AM (not used in core)
 * Fractional levels may be one or other (e.g. 2.2 = 80% 2, 20% 3)
 * Side weapons unchanged
 */
this._setWeapons = function(ship,level)
{
	level += this.$weaponLevelBias;
	if (!ship.autoWeapons)
	{
		// default is not to change anything
		return false;
	}
	var fwent = ship;
	if (ship.forwardWeapon == null)
	{
		var se = ship.subEntities;
		for (var i=0;i<se.length;i++)
		{
			if (se[i].forwardWeapon != null)
			{
				if (fwent != ship)
				{
					return false; // auto_weapons doesn't work on ships with MFLs
				}
				fwent = se[i];
			}
		}
	}
	var choice = Math.floor(level);
	if (level-Math.floor(level) > Math.random())
	{
		choice++;
	}
	if (choice <= 1)
	{
		fwent.forwardWeapon = "EQ_WEAPON_PULSE_LASER";
		ship.aftWeapon = null;
	}
	else if (choice == 2)
	{
		fwent.forwardWeapon = "EQ_WEAPON_BEAM_LASER";
		ship.aftWeapon = null;
	}
	else if (choice == 3)
	{
		fwent.forwardWeapon = "EQ_WEAPON_BEAM_LASER";
		ship.aftWeapon = "EQ_WEAPON_BEAM_LASER";
	}
	else if (choice == 4)
	{
		fwent.forwardWeapon = "EQ_WEAPON_MILITARY_LASER";
		ship.aftWeapon = "EQ_WEAPON_BEAM_LASER";
	}
	else if (choice >= 5)
	{
		fwent.forwardWeapon = "EQ_WEAPON_MILITARY_LASER";
		ship.aftWeapon = "EQ_WEAPON_MILITARY_LASER";
	}
	//	log(this.name,"Set "+fwent.forwardWeapon+"/"+ship.aftWeapon+" for "+ship.name+" ("+ship.primaryRole+")");
	return true;
}


this._setSkill = function(ship,bias)
{
	bias += this.$skillLevelBias;
	if (ship.autoWeapons && ship.accuracy < 5 && bias != 0)
	{
		// shift skill towards end of accuracy range
		var target = this.$skillLevelMaximum;
		if (bias < 0)
		{
			target = -5;
		}
		var acc = ship.accuracy;
		for (var i=Math.abs(bias) ; i > 0 ; i--)
		{
			acc += (target-acc)*Math.random();
		}
		ship.accuracy = acc;
	}
}


/* Bias:
 * +N = N 50% chances that each normal missile converted to hardened
 * -N = N 50% chances that each normal missile removed
 */
this._setMissiles = function(ship,bias)
{
	if (ship.autoWeapons)
	{
		var chance = Math.pow(0.5,Math.abs(bias));
		for (var i = ship.missiles.length -1 ; i >= 0 ; i--)
		{
			if (ship.missiles[i].primaryRole == "EQ_MISSILE" && chance < Math.random())
			{
				ship.removeEquipment("EQ_MISSILE");
				if (bias > 0)
				{
					ship.awardEquipment("EQ_HARDENED_MISSILE");
				}
			}
		}

	}
}


this._setFuel = function(ship)
{
	if (ship.homeSystem != system.ID)
	{
		ship.fuel = 7-system.info.distanceToSystem(System.infoForSystem(galaxyNumber,ship.homeSystem));
	}
	else 
	{
		ship.fuel = 7;
	}
}


this._setReturnFuel = function(ship)
{
	if (ship.destinationSystem != system.ID)
	{
		ship.fuel = 7-system.info.distanceToSystem(System.infoForSystem(galaxyNumber,ship.destinationSystem));
	}
	else 
	{
		ship.fuel = 7;
	}
}


this._wormholePos = function()
{
	var v = Vector3D.randomDirection().multiply(2000+Math.random()*3000);
	if (v.z < 0 && Math.abs(v.x)+Math.abs(v.y) < 1000)
	{
		v.z = -v.z; // avoid collision risk with witchbuoy
	}
	return v;
}


this._addShips = function(role,num,pos,spread)
{
	if (pos.isStation)
	{
		var result = [];
		for (var i = 0 ; i < num ; i++)
		{
			result.push(pos.launchShipWithRole(role));
		}
		return result;
	}
	else if (pos.isPlanet)
	{
		var result = system.addShips(role,num,pos,spread);
		this._repositionForLaunch(pos,result);
		return result;
	}
	else
	{
		if (pos.z === undefined)
		{
			log(this.name,"Unexpected populator position "+pos+" for "+role+". Please report this error.");
		}
		return system.addShips(role,num,pos,spread);
	}
}

this._addGroup = function(role,num,pos,spread)
{
	if (pos.isStation)
	{
		var group = new ShipGroup;
		for (var i = 0 ; i < num ; i++)
		{
			var ship = pos.launchShipWithRole(role);
			ship.group = group;
			group.addShip(ship);
		}
		return group;
	}
	else if (pos.isPlanet)
	{
		var result = system.addGroup(role,num,pos,spread);
		this._repositionForLaunch(pos,result.ships);
		return result;
	}
	else
	{
		return system.addGroup(role,num,pos,spread);
	}
}


this._repositionForLaunch = function(planet,ships)
{
	var launchvector;
	var launchpos;
	if (planet != system.mainPlanet)
	{
		launchvector = Vector3D.randomDirection();
	}
	else
	{
		if (system.sun.position.subtract(planet.position).dot(system.mainStation.position.subtract(planet.position)) < 0)
		{
			// sun and station on opposite sides of planet; best sneak
			// vector probably the cross product
			launchvector = system.sun.position.subtract(planet.position).cross(system.mainStation.position.subtract(planet.position)).direction();
		}
		else
		{
			// sun and station on same side of planet; best sneak
			// vector probably the negative normalisation of the
			// average
			launchvector = system.sun.position.subtract(planet.position).direction().add(system.mainStation.position.subtract(planet.position).direction()).direction().multiply(-1);
		}
	}
	launchpos = planet.position.add(launchvector.multiply(planet.radius+125));
	for (var i=ships.length -1 ; i >= 0 ; i--)
	{
		var cross = launchvector.cross(Vector3D.randomDirection()).multiply(15000); // perpendicular to surface
		launchvector = launchpos.add(cross).direction();
		launchpos = planet.position.add(launchvector.multiply(planet.radius+250));
		ships[i].position = launchpos;
		ships[i].orientation = launchvector.rotationTo([0, 0, 1]);
		ships[i].velocity = ships[i].vectorForward.multiply(ships[i].maxSpeed);
	}
}


this._hermitAllegiance = function(position,government)
{
	// default hermit status, allows all dockings but pirates will tend to
	// go elsewhere. We set this in shipdata but a shipset might not
	var allegiance = "neutral"; 
	if ((Math.floor(Math.abs(position.z)) % 4) * (Math.floor(Math.abs(position.y)) % 4) > government)
	{
		// pirates will use this hermit for docking and launching, but
		// other ships might too
		allegiance = "chaotic"; 
		if (Math.abs(Math.floor(position.x) % 4) > government+1)
		{
			// in Feudal or Anarchy systems, some of the hermits are
			// so pirate-friendly that legitimate traffic avoids them
			allegiance = "pirate";
		}
	}
	return allegiance;
}

/* System selectors */

this._nearbySystem = function(range)
{
	var poss;
	if (!range)
	{
		poss = this.$populatorLocals;
	}
	else
	{
		poss = system.info.systemsInRange(range);
	}
	if (poss.length == 0)
	{
		return system.ID;
	}
	return poss[Math.floor(Math.random()*poss.length)].systemID;
}


this._nearbyDangerousSystem = function(gov)
{
	var poss = this.$populatorVeryLocals;
	var id = system.ID;
	if (poss.length > 0)
	{
		var found = 0;
		for (var i = 0 ; i < poss.length ; i++)
		{
			if (poss[i].government <= gov)
			{
				found++;
				if (Math.random() < 1/found)
				{
					id = poss[i].systemID;
				}
			}
		}
	}
	return id;
}


this._nearbySafeSystem = function(gov)
{
	var poss = this.$populatorVeryLocals;
	var id = system.ID;
	if (poss.length > 0)
	{
		var found = 0;
		for (var i = 0 ; i < poss.length ; i++)
		{
			if (poss[i].government >= gov)
			{
				found++;
				if (Math.random() < 1/found)
				{
					id = poss[i].systemID;
				}
			}
		}
	}
	return id;
}


this._systemIsBottleneck = function(locals,seconds)
{
	if (locals.length > 0)
	{
		// Oresrati's bottleneck status is undefined. The Thargoids
		// don't bother striking at it routinely.
		var connectionset = [locals[0].systemID];
		var sofar = 0;
		do 
		{
			sofar = connectionset.length;
			for (var j = 0; j < seconds.length ; j++)
			{
				// if the connected set doesn't already contain this system
				if (connectionset.indexOf(locals[j].systemID) == -1)
				{
					var second = seconds[j];
					for (var k = 0; k < second.length ; k++)
					{
						// if the connected set contains a system
						// connected to the system being tested
						if (connectionset.indexOf(second[k].systemID) > -1)
						{
							// add this system to the connection set
							connectionset.push(locals[j].systemID);
							break;
						}
					}
				}
			}
		} 
		while (connectionset.length > sofar);
		// if we didn't add any more, we've connected all we can.
		if (connectionset.length < locals.length)
		{
			// there are still some disconnected, so this is a
			// bottleneck system
			return true;
		}
	}
	return false;
}


this._weightedNearbyTradeSystem = function()
{
	var locals = this.$populatorLocals;
	var weights = [];
	var total = 0;
	for (var i = 0; i < locals.length ; i++)
	{
		var local = locals[i];
		var ecomatch = -(system.info.economy-3.5)*(local.economy-3.5);
		var trdanger = 0;
		var rate = 0;
		// if either local or remote end is more dangerous than
		// Communist, reduce trader frequency
		if (local.government < 4)
		{
			trdanger = (4-local.government)*2.5;
		}
		if (system.info.government < 4)
		{
			trdanger += (4-system.info.government)*2.5;
		}
		if (ecomatch > 0)
		{
			rate = 60/(30+trdanger);
		}
		// bad economic match: one every 2 hours if safe
		else
		{
			rate = 60/(120+(trdanger*2));
		}
		total += rate;
		weights[i] = rate;
	}
	var pick = 0;
	total *= Math.random();
	for (i = 0; i < locals.length ; i++)
	{
		pick += weights[i];
		if (pick >= total)
		{
			return locals[i].systemID;
		}
	}
	// fallback
	if (locals.length > 0)
	{
		return locals[0].systemID;
	}
	return system.ID;
}

/* Station selectors */

this._launchReady = function(station)
{
	var docks = station.subEntities;
	var space = 0;
	for (var i=0; i<docks.length;i++)
	{
		if (docks[i].isDock)
		{
			space += 16 - docks[i].launchingQueueLength;
		}
	}
//	log("station.debug",station.displayName+" has "+space +" in docking queue");
	return (space >= 8);
}


// station for launching traders
this._tradeStation = function(usemain)
{
	// usemain biases, but does not guarantee or forbid
	if (usemain && Math.random() < 0.67 && this._launchReady(system.mainStation))
	{
		return system.mainStation;
	}
	var stats = system.stations;
	var tries = 0;
	do
	{
		var stat = system.stations[Math.floor(Math.random()*stats.length)];
		if (stat.hasNPCTraffic)
		{
			if (stat.allegiance == "neutral" || stat.allegiance == "galcop" || stat.allegiance == "chaotic")
			{
				if (this._launchReady(stat))
				{
					return stat;
				}
			}
		}
	} while (++tries < 5 && tries < stats.length);
	return system.mainStation;
}

// station for launching pirates (or planet)
this._pirateLaunch = function()
{
	var stats = system.stations;
	var stat = system.stations[Math.floor(Math.random()*stats.length)];
	if (stat.hasNPCTraffic)
	{
		if (stat.allegiance == "pirate" || stat.allegiance == "chaotic")
		{
			if (this._launchReady(stat))
			{
				return stat;
			}
		}
	}
	return system.mainPlanet;
}


// station for launching hunters
this._hunterLaunch = function()
{
	var stats = system.stations;
	var stat = system.stations[Math.floor(Math.random()*stats.length)];
	if (stat.hasNPCTraffic)
	{
		if (stat.allegiance == "hunter" || stat.allegiance == "galcop")
		{
			if (this._launchReady(stat))
			{
				return stat;
			}
		}
	}
	return system.mainStation;
}


// station for launching police
this._policeLaunch = function()
{
	var stats = system.stations;
	var stat = system.stations[Math.floor(Math.random()*stats.length)];
	if (stat.hasNPCTraffic)
	{
		if (stat.allegiance == "galcop")
		{
			if (this._launchReady(stat))
			{
				return stat;
			}
		}
	}
	return system.mainStation;
}
