"use strict";
this.name = "GalCopBB_Reputation";
this.author = "phkb";
this.copyright = "2017 phkb";
this.description = "Handles changes to entity reputation when missions are completed";
this.license = "CC BY-NC-SA 4.0";

/*
	These routines keep track of the players progress with various galactic entities

	The reputation system is designed to be as feature-rich as possible, allow for a wide variety of combinations and adjustments with just a small amount of coding
	It starts with an entity definition. These are templates that are used to control what happens with that entity.
	Entity definitions can be created with different scopes. So an entity with a system-based scope could be created multiple times in the players actual reputations list
	Then there are the actual reputations. These are what the player has achieved for any given entity definition.
	Finally, there are the mission results, which control whether reputations increase or decrease.

	Entities can be defined to degrade over time.
	Entities can have complementary entities, which are links between two entities. 
	So, if entity 1 is complementary to entity 2 in a negative way, then any positive change to the players reputation with entity 1 will result in a negative change to their reputation with entity 2.

	TODO:
	- work out grids for each entity
		- Local Government
		- GalCop
		- GCDC
	For BETA:
	- Create Liberation Front entities
	- Create Religious entities use CCL_NamesGods

	Mission definitions in "descriptions.plist" need the following lines added and populated for any reputation change to kick in.		
		missionType0_reputationEntities = ""; // comma separated list of entities involved with this mission
		missionType0_reputationTarget = "source"; // comma separated list of the target system for each of the entities listed , either "source" or "destination"
		missionType0_reputationSuccess = ""; // comma separated list of amounts of reputation to apply to each entity if the mission is completed successfully
		missionType0_reputationFailure = ""; // comma separated list of amounts of reputation to apply to each entity if the mission is failed/terminated

	Entities have the following properties
		name 				name of the entity
		scope 				either local, region, chart or galactic
		display				boolean to indicate whether the entity will be displayed on the reward screen
		regionSystems		(region scope only) array of system id's relating to the region
		regionGalaxy		(region scope only) galaxy number for the region
		getValueWS			worldScript name of function to get rep value from
		getValueFN			function name to get reputation value from
		maxValue			maximum value of the reputation
		minValue			minimum value of the reputation
		degrade				degrading profile of two values split by "|". first = how much to degrade, second = days between degrading events
		complementary		list of complementary entities that will be updated at the same time
							two values split by "|". 
								first = entity name, 
								second = value from -1 to 1 being percentage of original rep change to be applied to comp entity
								eg "Local Government|0.5" would mean adding half the reputation change to the "Local Government" reputation
								"Pirate|-0.25" would mean deducting a quarter of the reputation change to the "Pirate" reputation
		impact				defines what (if any) impact type will be applied. Choices are:
								markets = adjust prices of legal goods.
								illegals = adjust prices of illegal goods
								equipment = applies a rebate to equipment purchases
								ships = applies a rebate to ship purchases
								bounty = reduces players offender status
							multiple impacts can be defined by using comma delimited entries (eg "markets,equipment")
							For price adjustments, the adjustment will be away from the price_average. 
							For instance, if the current price for computers is 88cr, and the price_average for this station is 80cr,
								the adjustment will increase the price.
							If the current price for computers is 72cr, and the price_average for this station is 80cr, 
								the adjustment will decrease the price.
		impactAllegiance	defines what stations the impact will be applies to. Possible choices are:
								galcop,pirate,neutral,chaotic,hunter,private,restricted,thargoid
							multiple allegiances can be defined by using comma delimited entries (eg "galcop,neutral")
							does not apply to bounty impact
		rewardGrid			dictionary list of value/description pairs defining reward structure
			value			minimum reputation required for this reward
			description		description of the award
			achievementWS	worldscript of achievement function to call when player reaches this level
			achievementFN	function name of the function to call when the player reaches this level
							function will be passed entity name and reputation level
							note: the function will be called any time there is a change in rep even if it leaves the player
							at the same level. In short, destination function needs to handle being called multiple times
			impactValue		If an impact has been defined for this entity, this value sets the amount of impact to be applied at this level
	
	Setting the "getValueWS" and "getValueFN" allows third party reputations to be added to the list. These will not be updated directly.

	Entities can have text substitutions in the name property. So "LocalGov$G" will be translated to "[LocalGov4]" (in a Communist system) and looked up in descriptions.plist
	The substitution will take place for display purposes only - the name of the entity will still be "LocalGov$G" when referenced in other entities (eg complementary items)
*/

this._entities = {
	"`PGN": {
		scope: "system",
		display: true,
		getValueWS: "",
		getValueFN: "",
		maxValue: 140,
		minValue: 0,
		degrade: "1|30",
		impact: "illegals,equipment,ships",
		impactAllegiance: "pirate,chaotic",
		complementary: "Local Government|-0.5",
		rewardGrid: [
			{ value: 0, description: expandDescription("[gcmPGNRep0]"), impactValue: 0 },
			{ value: 1, description: expandDescription("[gcmPGNRep1]"), impactValue: 0 },
			{ value: 10, description: expandDescription("[gcmPGNRep2]"), impactValue: 0.01 },
			{ value: 25, description: expandDescription("[gcmPGNRep3]"), impactValue: 0.04 },
			{ value: 45, description: expandDescription("[gcmPGNRep4]"), impactValue: 0.075 },
			{ value: 70, description: expandDescription("[gcmPGNRep5]"), impactValue: 0.1 },
			{ value: 100, description: expandDescription("[gcmPGNRep6]"), impactValue: 0.15 },
			{ value: 135, description: expandDescription("[gcmPGNRep7]"), impactValue: 0.2 },
		]
	},
};

// dictionary of default trophies and medal overlays
this._medalTypes = {
	1: "gcm_trophy_1.png",
	2: "gcm_trophy_1_star.png",
	3: "gcm_trophy_2.png",
	4: "gcm_trophy_2.star.png",
	5: "gcm_badge_round.png",
	6: "gcm_badge_star.png",
	7: "gcm_medal_round_badge.png",
	8: "gcm_medal_star_badge.png",
	9: "gcm_medal_round_ribbon.png",
	10: "gcm_medal_star_ribbon.png",
};

this._debug = false;
this._disabled = false;
this._convertOtherReputations = false; // flag to indicate whether reputation information from other OXP's should be included
this._maxpage = 0; // total number of pages available for display
this._curpage = 0; // the current page being displayed
this._display = 0;
this._selected = 0;
this._itemColor = "yellowColor";
this._menuColor = "orangeColor";
this._exitColor = "yellowColor";
this._disabledColor = "darkGrayColor";
this._lastDat = 0;
this._trueValues = ["yes", "1", 1, "true", true];
this._pricesAdjusted = false;
this._ignoreEquipment = ["EQ_SHIP_RESPRAY", "EQ_SMUGGLING_COMPARTMENT"];
this._emailGetClientName = null; // reference to function that will return a client nameto be attached to emails
this._reputations = []; // current player reputation with various entities
// entity:			name of entity
// galaxy:			the galaxy in which this reputation applies
// system:			the system in which this reputation applies
// reputation:		the decimal value of the reputation
// reputationText:	text version of the reputation (used as holding spot)
// lastDegrade:		date (in seconds) of the last degrade event for this reputation
// bonusNotified:   boolean indicating if a message has been sent to the player
//                      informing them of the potential bonus available by donating

this._awards = []; // any special awards the player has received

// title: 			name of award
// worldScript:		the name of the worldscript where his award originated
// description: 	any descriptive text that goes with the award
// entity:			the entity responsible for issuing the award
// source:			planet name (not ID) where the award was given
// galaxy:			galaxy number where planet is
// received:		date the award was received
// image:			optional background overlay (dimensions 2048x1024)
// imageType:		number specifying one of the default medal images (1-10)

// array of client name items, used to provide some consistency between missions 
this._clientNames = [];

// configuration settings for use in Lib_Config
this._gcmRepConfig = {
	Name: this.name,
	Alias: expandDescription("[gcm_repconfig_alias]"),
	Display: expandDescription("[gcm_repconfig_display]"),
	Alive: "_gcmRepConfig",
	Bool: {
		B0: {
			Name: "_convertOtherReputations",
			Def: false,
			Desc: expandDescription("[gcm_repconfig_import]")
		},
		Info: expandDescription("[gcm_repconfig_bool_info]")
	},
};

//-------------------------------------------------------------------------------------------------------------
// adds an award into the array
this.$addAward = function $addAward(awd) {
	if (awd.hasOwnProperty("title") === false || awd.title === "") {
		throw "Invalid award settings: 'title' must be specified";
	}
	if (awd.hasOwnProperty("description") === false || awd.description === "") {
		throw "Invalid award settings: 'description' must be specified.";
	}
	if (awd.hasOwnProperty("entity") === false || awd.entity === "") {
		throw "Invalid award settings: 'entity' must be specified";
	}
	if (awd.hasOwnProperty("system") === false) awd.system = system.ID;
	if (awd.hasOwnProperty("galaxy") === false) awd.galaxy = galaxyNumber;
	if (awd.hasOwnProperty("received") === false) awd.received = clock.adjustedSeconds;

	var titl = this.$transformText(awd.title, awd.system);

	// make sure the award isn't already there	
	if (this.$checkForAward(awd.entity, titl, awd.system) === true) return;

	this._awards.push({
		title: titl,
		description: this.$transformText(awd.description, awd.system),
		entity: awd.entity,
		source: System.systemNameForID(awd.system),
		galaxy: awd.galaxy,
		received: awd.received,
		image: (awd.hasOwnProperty("image") ? awd.image : ""),
		imageType: (awd.hasOwnProperty("imageType") ? awd.imageType : 0)
	});
}

this.startUp = function () {
	var ent = this._entities;
	ent[expandDescription("[gcm_reputation_cargo]")] = {
		scope: "galactic",
		display: true,
		getValueWS: "GalCopBB_Reputation",
		getValueFN: "$cargoReputation",
		maxValue: 7,
		minValue: -1000,
		rewardGrid: [
			{ value: -1000, description: expandDescription("[gcmCargoRepBad]") },
			{ value: 0, description: expandDescription("[gcmCargoRep0]") },
			{ value: 1, description: expandDescription("[gcmCargoRep1]") },
			{ value: 2, description: expandDescription("[gcmCargoRep2]") },
			{ value: 3, description: expandDescription("[gcmCargoRep3]") },
			{ value: 4, description: expandDescription("[gcmCargoRep4]") },
			{ value: 5, description: expandDescription("[gcmCargoRep5]") },
			{ value: 6, description: expandDescription("[gcmCargoRep6]") },
			{ value: 7, description: expandDescription("[gcmCargoRep7]") }
		]
	};
	ent[expandDescription("[gcm_reputation_courier]")] = {
		scope: "galactic",
		display: true,
		getValueWS: "GalCopBB_Reputation",
		getValueFN: "$parcelReputation",
		maxValue: 7,
		minValue: -1000,
		rewardGrid: [
			{ value: -1000, description: expandDescription("[gcmCourierRepBad]") },
			{ value: 0, description: expandDescription("[gcmCourierRep0]") },
			{ value: 1, description: expandDescription("[gcmCourierRep1]") },
			{ value: 2, description: expandDescription("[gcmCourierRep2]") },
			{ value: 3, description: expandDescription("[gcmCourierRep3]") },
			{ value: 4, description: expandDescription("[gcmCourierRep4]") },
			{ value: 5, description: expandDescription("[gcmCourierRep5]") },
			{ value: 6, description: expandDescription("[gcmCourierRep6]") },
			{ value: 7, description: expandDescription("[gcmCourierRep7]") }
		]
	};
	ent[expandDescription("[gcm_reputation_passenger]")] = {
		scope: "galactic",
		display: true,
		getValueWS: "GalCopBB_Reputation",
		getValueFN: "$passengerReputation",
		maxValue: 7,
		minValue: -1000,
		rewardGrid: [
			{ value: -1000, description: expandDescription("[gcmPassengerRepBad]") },
			{ value: 0, description: expandDescription("[gcmPassengerRep0]") },
			{ value: 1, description: expandDescription("[gcmPassengerRep1]") },
			{ value: 2, description: expandDescription("[gcmPassengerRep2]") },
			{ value: 3, description: expandDescription("[gcmPassengerRep3]") },
			{ value: 4, description: expandDescription("[gcmPassengerRep4]") },
			{ value: 5, description: expandDescription("[gcmPassengerRep5]") },
			{ value: 6, description: expandDescription("[gcmPassengerRep6]") },
			{ value: 7, description: expandDescription("[gcmPassengerRep7]") }
		]
	};
	ent[expandDescription("[gcm_reputation_galcop]")] = {
		scope: "galactic",
		display: false,
		getValueWS: "",
		getValueFN: "",
		maxValue: 7,
		minValue: 0,
		degrade: "0.5|30",
		rewardGrid: []
	};
	ent[expandDescription("[gcm_reputation_localgov]")] = {
		scope: "system",
		display: true,
		getValueWS: "",
		getValueFN: "",
		maxValue: 110,
		minValue: 0,
		degrade: "0.1|30",
		impact: "markets,equipment,ships",
		impactAllegiance: "galcop",
		complementary: "`PGN|-0.5",
		rewardGrid: [
			{ value: 0, description: expandDescription("[gcmLocalGovRep0]"), impactValue: 0 },
			{ value: 1, description: expandDescription("[gcmLocalGovRep1]"), impactValue: 0 },
			{ value: 5, description: expandDescription("[gcmLocalGovRep2]"), impactValue: 0.01 },
			{ value: 15, description: expandDescription("[gcmLocalGovRep3]"), impactValue: 0.02 },
			{ value: 30, description: expandDescription("[gcmLocalGovRep4]"), impactValue: 0.04 },
			{ value: 50, description: expandDescription("[gcmLocalGovRep5]"), impactValue: 0.07 },
			{ value: 75, description: expandDescription("[gcmLocalGovRep6]"), impactValue: 0.11 },
			{ value: 105, description: expandDescription("[gcmLocalGovRep7]"), impactValue: 0.15 },
		]
	};
	ent[expandDescription("[gcm_reputation_navy]")] = {
		scope: "galactic",
		display: false,
		getValueWS: "",
		getValueFN: "",
		maxValue: 7,
		minValue: 0,
		degrade: "0.1|60",
		rewardGrid: [
			{ value: 0, description: expandDescription("[gcmLocalGovRep0]"), impactValue: 0 },
			{ value: 10, description: expandDescription("[gcmLocalGovRep1]"), impactValue: 0 },
			{ value: 35, description: expandDescription("[gcmLocalGovRep2]"), impactValue: 0.01 },
			{ value: 70, description: expandDescription("[gcmLocalGovRep3]"), impactValue: 0.02 },
			{ value: 200, description: expandDescription("[gcmLocalGovRep4]"), impactValue: 0.04 },
			{ value: 500, description: expandDescription("[gcmLocalGovRep5]"), impactValue: 0.07 },
			{ value: 1200, description: expandDescription("[gcmLocalGovRep6]"), impactValue: 0.11 },
			{ value: 3000, description: expandDescription("[gcmLocalGovRep7]"), impactValue: 0.15 },
		]
	};
}

//-------------------------------------------------------------------------------------------------------------
this.systemWillPopulate = function () {
	this._performedAdjustmentCheck = false;
	if (player.ship.docked === false) {
		this._adjustTimer = new Timer(this, this.$performPriceAdjustment, 2, 0);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.startUpComplete = function () {
	// register our settings, if Lib_Config is present
	if (worldScripts.Lib_Config) worldScripts.Lib_Config._registerSet(this._gcmRepConfig);

	// load stored information
	if (missionVariables.GalCopBBMissions_PricesAdjusted)
		this._pricesAdjusted = (this._trueValues.indexOf(missionVariables.GalCopBBMissions_PricesAdjusted) >= 0 ? true : false);
	if (missionVariables.GalCopBBMissions_Reputations) {
		this._reputations = JSON.parse(missionVariables.GalCopBBMissions_Reputations);
		delete missionVariables.GalCopBBMissions_Reputations;
	}
	if (missionVariables.GalCopBBMissions_Awards) {
		this._awards = JSON.parse(missionVariables.GalCopBBMissions_Awards);
		delete missionVariables.GalCopBBMissions_Awards;
	}
	if (missionVariables.GalCopBBMissions_RepImport) {
		this._convertOtherReputations = (this._trueValues.indexOf(missionVariables.GalCopBBMissions_RepImport) >= 0 ? true : false);
		delete missionVariables.GalCopBBMissions_RepImport;
	}
	if (missionVariables.GalCopBBMissions_ClientNames) {
		this._clientNames = JSON.parse(missionVariables.GalCopBBMissions_ClientNames);
		delete missionVariables.GalCopBBMissions_ClientNames;
	}
	if (this._convertOtherReputations === true) {
		// add in the player's current rep for cargo/parcels/passengers
		if (this.$isReputationInList(expandDescription("[gcm_reputation_cargo]")) === false) {
			this._reputations.push({
				entity: expandDescription("[gcm_reputation_cargo]"),
				galaxy: -1,
				system: -1,
				reputation: this.$cargoReputation()
			});
		}
		if (this.$isReputationInList(expandDescription("[gcm_reputation_courier]")) === false) {
			this._reputations.push({
				entity: expandDescription("[gcm_reputation_courier]"),
				galaxy: -1,
				system: -1,
				reputation: this.$parcelReputation()
			});
		}
		if (this.$isReputationInList(expandDescription("[gcm_reputation_passenger]")) === false) {
			this._reputations.push({
				entity: expandDescription("[gcm_reputation_passenger]"),
				galaxy: -1,
				system: -1,
				reputation: this.$passengerReputation()
			});
		}
		// if the player has either of the display reputation OXP's installed, use their terminology
		if (!worldScripts.display_reputations && !worldScripts["display-reputation-contract"]) {
			this._entities[expandDescription("[gcm_reputation_cargo]")].rewardGrid = [{
				value: -1000,
				description: "display_reputation_contract_minus"
			},
			{
				value: 0,
				description: "display_reputation_contract_0"
			},
			{
				value: 1,
				description: "display_reputation_contract_1"
			},
			{
				value: 2,
				description: "display_reputation_contract_2"
			},
			{
				value: 3,
				description: "display_reputation_contract_3"
			},
			{
				value: 4,
				description: "display_reputation_contract_4"
			},
			{
				value: 5,
				description: "display_reputation_contract_5"
			},
			{
				value: 6,
				description: "display_reputation_contract_6"
			},
			{
				value: 7,
				description: "display_reputation_contract_7"
			}
			];
			this._entities[expandDescription("[gcm_reputation_courier]")].rewardGrid = [{
				value: -1000,
				description: "display_reputation_parcel_minus"
			},
			{
				value: 0,
				description: "display_reputation_parcel_0"
			},
			{
				value: 1,
				description: "display_reputation_parcel_1"
			},
			{
				value: 2,
				description: "display_reputation_parcel_2"
			},
			{
				value: 3,
				description: "display_reputation_parcel_3"
			},
			{
				value: 4,
				description: "display_reputation_parcel_4"
			},
			{
				value: 5,
				description: "display_reputation_parcel_5"
			},
			{
				value: 6,
				description: "display_reputation_parcel_6"
			},
			{
				value: 7,
				description: "display_reputation_parcel_7"
			}
			];
			this._entities[expandDescription("[gcm_reputation_passenger]")].rewardGrid = [{
				value: -1000,
				description: "display_reputation_passenger_minus"
			},
			{
				value: 0,
				description: "display_reputation_passenger_0"
			},
			{
				value: 1,
				description: "display_reputation_passenger_1"
			},
			{
				value: 2,
				description: "display_reputation_passenger_2"
			},
			{
				value: 3,
				description: "display_reputation_passenger_3"
			},
			{
				value: 4,
				description: "display_reputation_passenger_4"
			},
			{
				value: 5,
				description: "display_reputation_passenger_5"
			},
			{
				value: 6,
				description: "display_reputation_passenger_6"
			},
			{
				value: 7,
				description: "display_reputation_passenger_7"
			}
			];
		}

		// add the random hits reputation in
		if (worldScripts.Random_Hits) {
			// RH has a rather complicated ranking system ... I'm just going to get the end result of it, rather than trying to reproduce the calc here
			// thus the rewardGrid is an empty array
			this._entities[expandDescription("[gcm_reputation_bountyhunters]")] = {
				scope: "galactic",
				display: true,
				getValueWS: "GalCopBB_Reputation",
				getValueFN: "$randomHitsReputation",
				maxValue: 200,
				minValue: -200,
				rewardGrid: []
			};
			if (this.$isReputationInList(expandDescription("[gcm_reputation_bountyhunters]")) === false) {
				this._reputations.push({
					entity: expandDescription("[gcm_reputation_bountyhunters]"),
					galaxy: -1,
					system: -1,
					reputation: this.$randomHitsReputation()
				});
			}
		}
		// add the ups reputation in
		if (worldScripts.ups_parcel) {
			this._entities[expandDescription("[gcm_reputation_ups]")] = {
				scope: "galactic",
				display: true,
				getValueWS: "GalCopBB_Reputation",
				getValueFN: "$upsReputation",
				maxValue: 7,
				minValue: -2000,
				rewardGrid: [{
					value: -2000,
					description: expandDescription("[gcmUPSRepBad]")
				}, // for all other negative numbers
				{
					value: -3,
					description: expandDescription("[gcmUPSRepMinus3]")
				},
				{
					value: -2,
					description: expandDescription("[gcmUPSRepMinus2]")
				},
				{
					value: -1,
					description: expandDescription("[gcmUPSRepMinus1]")
				},
				{
					value: 0,
					description: expandDescription("[gcmUPSRep0]")
				},
				{
					value: 1,
					description: expandDescription("[gcmUPSRep1]")
				},
				{
					value: 2,
					description: expandDescription("[gcmUPSRep2]")
				},
				{
					value: 3,
					description: expandDescription("[gcmUPSRep3]")
				},
				{
					value: 4,
					description: expandDescription("[gcmUPSRep4]")
				},
				{
					value: 5,
					description: expandDescription("[gcmUPSRep5]")
				},
				{
					value: 6,
					description: expandDescription("[gcmUPSRep6]")
				},
				{
					value: 7,
					description: expandDescription("[gcmUPSRep7]")
				}
				]
			};
			if (this.$isReputationInList(expandDescription("[gcm_reputation_ups]")) === false) {
				this._reputations.push({
					entity: expandDescription("[gcm_reputation_ups]"),
					galaxy: -1,
					system: -1,
					reputation: this.$upsReputation()
				});
			}
		}
		// add escort contracts reputation in
		if (worldScripts.Escort_Contracts) {
			this._entities[expandDescription("[gcm_reputation_itha]")] = {
				scope: "galactic",
				display: true,
				getValueWS: "GalCopBB_Reputation",
				getValueFN: "$escortContractsReputation",
				maxValue: 20,
				minValue: -1000,
				rewardGrid: [{
					value: -1000,
					description: expandDescription("[gcmITHARepVPoor]")
				},
				{
					value: -5,
					description: expandDescription("[gcmITHARepPoor]")
				},
				{
					value: 0,
					description: expandDescription("[gcmITHARepNeutral]")
				},
				{
					value: 1,
					description: expandDescription("[gcmITHARepGood]")
				},
				{
					value: 6,
					description: expandDescription("[gcmITHARepVGood]")
				},
				{
					value: 11,
					description: expandDescription("[gcmITHARepExcellent]")
				}
				]
			};
			if (this.$isReputationInList(expandDescription("[gcm_reputation_itha]")) === false) {
				this._reputations.push({
					entity: expandDescription("[gcm_reputation_itha]"),
					galaxy: -1,
					system: -1,
					reputation: this.$escortContractsReputation()
				});
			}
		}
		// add rescue stations reputation in
		if (worldScripts["Rescue Stations"]) {
			this._entities[expandDescription("[gcm_reputation_rrs]")] = {
				scope: "galactic",
				display: true,
				getValueWS: "GalCopBB_Reputation",
				getValueFN: "$rescueStationsReputation",
				maxValue: 320,
				minValue: -1000,
				rewardGrid: [{
					value: -1000,
					missiontext: "rescue_rank_0"
				},
				{
					value: -10,
					missiontext: "rescue_rank_1"
				},
				{
					value: 0,
					missiontext: "rescue_rank_2"
				},
				{
					value: 10,
					missiontext: "rescue_rank_3"
				},
				{
					value: 20,
					missiontext: "rescue_rank_4"
				},
				{
					value: 40,
					missiontext: "rescue_rank_5"
				},
				{
					value: 80,
					missiontext: "rescue_rank_6"
				},
				{
					value: 160,
					missiontext: "rescue_rank_7"
				},
				{
					value: 320,
					missiontext: "rescue_rank_8"
				}
				]
			};
			if (this.$isReputationInList(expandDescription("[gcm_reputation_rrs]")) === false) {
				this._reputations.push({
					entity: expandDescription("[gcm_reputation_rrs]"),
					galaxy: -1,
					system: -1,
					reputation: this.$rescueStationsReputation()
				});
			}
		}
	}

	//this._awards.push({title:"Medal of Honour", entity:"Local Government", description:"Award for bravery above and beyond the call of duty, in service to the citizens of Lave.",
	//	source:"Lave", galaxy:0, received:clock.adjustedSeconds - 86400, imageType:2});
	// set up the interface screen, if required
	var p = player.ship;
	if (p.dockedStation) this.$initInterface(p.dockedStation);
}

//-------------------------------------------------------------------------------------------------------------
this.playerWillSaveGame = function () {
	missionVariables.GalCopBBMissions_PricesAdjusted = this._pricesAdjusted;
	if (this._reputations.length > 0) missionVariables.GalCopBBMissions_Reputations = JSON.stringify(this._reputations);
	if (this._awards.length > 0) missionVariables.GalCopBBMissions_Awards = JSON.stringify(this._awards);
	if (this._convertOtherReputations === true) missionVariables.GalCopBBMissions_RepImport = this._convertOtherReputations;
	if (this._clientNames.length > 0) missionVariables.GalCopBBMissions_ClientNames = JSON.stringify(this._clientNames);
}

//-------------------------------------------------------------------------------------------------------------
this.playerBoughtEquipment = function (equipment) {
	if (this._ignoreEquipment.indexOf(equipment) >= 0) return;

	var stn = player.ship.dockedStation;
	if (!stn) return;

	var ent = this.$getImpactEntity("equipment", this.$getStationAllegiance(stn));
	var pct = 0;
	if (ent !== "") pct = this.$getImpactValue(ent);

	if (pct > 0) {
		var equip = EquipmentInfo.infoForKey(equipment);
		if (equip.price <= 0) return;

		var cost = (equip.price / 10);
		var refund = Math.floor((cost * pct) / 10) * 10;
		if (refund > 0) {
			player.credits += refund;
			player.consoleMessage(expandDescription("[gcm_reputation_refund]", { amount: formatCredits(refund, false, true) }));
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.playerBoughtNewShip = function (ship, price) {
	var stn = player.ship.dockedStation;
	if (!stn) return;

	var ent = this.$getImpactEntity("ships", this.$getStationAllegiance(stn));
	var pct = 0;
	if (ent !== "") pct = this.$getImpactValue(ent);

	if (pct > 0) {
		var refund = Math.floor((price * pct) / 10) * 10;
		if (refund > 0) {
			player.credits += refund;
			player.consoleMessage(expandDescription("[gcm_reputation_refund]", { amount: formatCredits(refund, false, true) }));
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillDockWithStation = function (station) {
	if (worldScripts.GalCopBB_Missions._simulator === true) return;
	// have we just completed the nova mission? 
	// we can't just look for "NOVA_HERO" as that text will be applied at the end of the mission no matter what the player did
	if (missionVariables.nova === "NOVA_ESCAPED_SYSTEM") {
		this._awards.push({
			title: expandDescription("[gcm_award_leenra]"),
			entity: expandDescription("[gcm_reputation_galcop]"),
			worldScript: "oolite-nova",
			description: expandDescription("[gcm_award_leenra_info]"),
			source: System.infoForSystem(3, 45).name,
			galaxy: 3,
			received: clock.adjustedSeconds,
			imageType: 6
		});
	}

	if (station.script.hasOwnProperty("_gcm_marketEntity") === true) {
		if (station.script._gcm_pricesAdjusted === true) {
			if (stn._gcm_marketAdjustType === "markets") {
				player.addMessageToArrivalReport(expandDescription("[gcm_price_adjust_market]", { percent: parseInt(station.script._gcm_priceAdjustPercent * 100), entity: this.$transformText(station.script._gcm_marketEntity, system.ID) }));
			} else {
				player.addMessageToArrivalReport(expandDescription("[gcm_price_adjust_illegal]", { percent: parseInt(station.script._gcm_priceAdjustPercent * 100), entity: this.$transformText(station.script._gcm_marketEntity, system.ID) }));
			}
		}
	}

	var alleg = this.$getStationAllegiance(station);
	var ent = this.$getImpactEntity("equipment", alleg);
	var pct = 0;
	if (ent !== "") pct = this.$getImpactValue(ent);
	if (pct > 0) {
		player.addMessageToArrivalReport(expandDescription("[gcm_equipment_refund]", { percent: parseInt(pct * 100), entity: this.$transformText(ent, system.ID) }));
	}
	ent = this.$getImpactEntity("ships", alleg);
	pct = 0;
	if (ent !== "") pct = this.$getImpactValue(ent);
	if (pct > 0) {
		player.addMessageToArrivalReport(expandDescription("[gcm_ship_refund]", { percent: parseInt(pct * 100), entity: this.$transformText(ent, system.ID) }));
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipDockedWithStation = function (station) {
	this.$initInterface(station);
}

//-------------------------------------------------------------------------------------------------------------
this.dayChanged = function (newDay) {
	if (newDay != this._lastDay) {
		this._lastDay = newDay;
		// look for any reputations that have a degrading policy
		for (var i = 0; i < this._reputations.length; i++) {
			var rep = this._reputations[i];
			var ent = this._entities[rep.entity];
			if (ent && ent.hasOwnProperty("degrade") && ent.degrade !== "" && ent.degrade !== "0|0") {
				var amt = parseFloat(ent.degrade.split("|")[0]);
				var days = parseFloat(ent.degrade.split("|")[1]);
				if (rep.hasOwnProperty("lastDegrade") === false) rep.lastDegrade = clock.adjustedSeconds;
				if ((rep.lastDegrade + (days * 86400)) < clock.adjustedSeconds) {
					rep.reputation -= amt;
					if (rep.reputation < ent.minValue) rep.reputation = ent.minValue;
					if (this._debug) log(this.name, "degrading " + rep.entity + ", new value = " + rep.reputation);
				}
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.guiScreenWillChange = function (to, from) {
	if (to === "GUI_SCREEN_MANIFEST" && this._convertOtherReputations === true) {
		// remove these items so they can be displayed with other reputation entries
		if (worldScripts.display_reputations) mission.setInstructions(null, "display_reputations");
		if (worldScripts["display-reputation-contract"]) mission.setInstructions(null, "display-reputation-contract");
		if (worldScripts["display-reputation-parcel"]) mission.setInstructions(null, "display-reputation-parcel");
		if (worldScripts["display-reputation-passenger"]) mission.setInstructions(null, "display-reputation-passenger");
		// RH, EC, RRS UPS Parcel alternate between a mission description and a reputation, depending on what's active.
		// so only remove the item if there's no active mission
		if (worldScripts.Random_Hits)
			if (!missionVariables.random_hits_status || missionVariables.random_hits_status !== "RUNNING")
				mission.setInstructions(null, "Random_Hits");
		if (worldScripts.Escort_Contracts)
			if (worldScripts.Escort_Contracts.ec_currentcontract === false)
				mission.setInstructions(null, "Escort_Contracts_Rep");
		if (worldScripts["Rescue Stations"])
			if ((!missionVariables.rescuestation_scenario || missionVariables.rescuestation_scenario === "") && missionVariables.rescuestation_stage === 0)
				mission.setInstructions(null, "Rescue Stations");
		var opts = ["NO", "NOT_NOW"];
		if (worldScripts["ups_parcel"])
			if (opts.indexOf(worldScripts["ups_parcel"].ups_parcel) >= 0)
				mission.setInstructions(null, "ups_parcel");
	}
}

//-------------------------------------------------------------------------------------------------------------
// initialise the F4 screen entries
this.$initInterface = function $initInterface(station) {
	station.setInterface(this.name, {
		title: expandDescription("[gcm_reputation_interface]"),
		category: expandDescription("[interfaces-category-logs]"),
		summary: expandDescription("[gcm_reputation_summary]"),
		callback: this.$showScreen.bind(this)
	});
}

//-------------------------------------------------------------------------------------------------------------
this.$showScreen = function $showScreen() {
	this._curpage = 0;
	this._display = 0;
	// update/populate rep list
	this.$updateReputationValues();
	this.$showPage();
}

//-------------------------------------------------------------------------------------------------------------
this.$showPage = function $showPage() {
	function compareEntityName(a, b) {
		return ((a.entity > b.entity) ? 1 : -1);
	}

	function compareSystem(a, b) {
		return ((a.system > b.system) ? 1 : -1);
	}

	// todo add sort function for entities
	// sort galactic/chart entities by name
	// sort system entities by system name or ID
	// sort regional entites by 
	var lines = [];
	var pagetitle = "";
	var curChoices = {};
	var def = "99_EXIT";
	var ovrly = {
		name: "gcm_trophy_1.png",
		height: 546
	};
	var text = "";
	var maxlines = 16;
	if (this.$isBigGuiActive() === true) maxlines = 22;

	// lists all reputations
	if (this._display === 0) {
		var headers = expandDescription("[gcm_reputation_headers]").split("|");
		pagetitle = expandDescription("[gcm_reputation_title_rep]");

		// go through the list 4 times: 1 for galactic reps, 1 for chart reps, 1 for regional reps, 1 for system reps
		// we won't show reputations for chart-wide entities outside the current chart, or for system entities not in the current chart
		for (var j = 0; j <= 3; j++) {
			var scope = "";
			var header = false;
			var lastHdr = "";
			switch (j) {
				case 0:
					scope = "galactic";
					this._reputations.sort(compareEntityName);
					break;
				case 1:
					scope = "chart";
					this._reputations.sort(compareEntityName);
					break;
				case 2:
					scope = "region";
					this._reputations.sort(compareSystem);
					break;
				case 3:
					scope = "system";
					this._reputations.sort(compareSystem);
					break;
			}

			for (var i = 0; i < this._reputations.length; i++) {
				if (this._entities[this._reputations[i].entity]) {
					if (this._debug) log(this.name, "checking " + this._reputations[i].entity);
					if (this._entities[this._reputations[i].entity].display === true && this._entities[this._reputations[i].entity].scope === scope) {
						var include = false;
						switch (scope) {
							case "chart":
							case "region":
							case "system":
								if (parseInt(this._reputations[i].galaxy) === galaxyNumber) include = true;
								break;
							case "galactic":
								include = true;
								break;
						}
						if (this._debug) log(this.name, "include " + include);
						if (include === true) {
							var ins = "";
							// for system-level entities, because the same entity could exist in many systems, add the system name to the front of the title
							if (scope === "system") ins = "(" + System.systemNameForID(this._reputations[i].system) + ") ";

							var rep = this.$getReputation(this._reputations[i].entity, parseInt(this._reputations[i].galaxy), this._reputations[i].system);
							if (this._debug) log(this.name, "rep = " + rep);
							var txt = (this.$isNumeric(rep) === true ? this.$getReputationReward(this._reputations[i].entity, rep) : rep);
							if (txt !== "") {
								if (header === false) {
									lines.push(headers[j]);
									header = true;
								}
								lines.push("• " + ins + this.$transformText(this._reputations[i].entity, this._reputations[i].system) + ": " + txt);
							}
						}
					}
				}
			}
		}

		// we should now have our complete list of reps
		// display the current page to the player
		//log(this.name, "min value = " + (this._curpage * maxlines - maxlines) + ", max value = " + (this._curpage * maxlines - 1));
		for (var i = 0; i < lines.length; i++) {
			if (i >= (this._curpage * maxlines) && i <= (this._curpage * maxlines + maxlines - 1)) text += lines[i] + "\n";
		}

		this._maxpage = Math.ceil(lines.length / maxlines);
		if (this._maxpage > 1) {
			pagetitle += expandDescription("[gcm_reputation_paging]", { page: (this._curpage + 1), max: this._maxpage });
		}

		if (this._maxpage > 1) {
			if (this._curpage < this._maxpage - 1) {
				curChoices["10_GOTONEXT"] = {
					text: "[gcm_option_nextpage]",
					color: this._menuColor
				};
			} else {
				curChoices["10_GOTONEXT"] = {
					text: "[gcm_option_nextpage]",
					color: this._disabledColor,
					unselectable: true
				};
			}
			if (this._curpage > 0) {
				curChoices["11_GOTOPREV"] = {
					text: "[gcm_option_prevpage]",
					color: this._menuColor
				};
			} else {
				curChoices["11_GOTOPREV"] = {
					text: "[gcm_option_prevpage]",
					color: this._disabledColor,
					unselectable: true
				};
			}
		}
		//if (this._awards.length > 0) {
		curChoices["20_AWARDS"] = {
			text: "[gcm_option_awards]",
			color: this._menuColor
		};
		// } else {
		//	curChoices["20_AWARDS"] = {text:"[gcm_option_awards]", color:this._disabledColor, unselectable:true};
		// }
		curChoices["99_EXIT"] = {
			text: "[gcm_option_exit]",
			color: this._exitColor
		};

	}

	// list of awards received
	if (this._display === 1) {
		pagetitle = expandDescription("[gcm_reputation_title_awards]");
		var added = 0;
		if (this._awards.length === 0) {
			text = expandDescription("[gcm_no_awards]");
		} else {
			for (var i = 0; i < this._awards.length; i++) {
				// we'll be adding these as menu items
				var item = this.$padTextRight(this._awards[i].title, 22) +
					this.$padTextRight(this._awards[i].source + (parseInt(this._awards[i].galaxy) !== galaxyNumber ? " (G" + (parseInt(this._awards[i].galaxy) + 1) + ")" : ""), 9);

				lines.push("01_AWARD~" + (i < 10 ? "0" : "") + i + "|" + item);
			}
			// display the current page to the player
			for (var i = 0; i < lines.length; i++) {
				if (i >= (this._curpage * maxlines) && i <= (this._curpage * maxlines + maxlines - 1)) {
					curChoices[lines[i].split("|")[0]] = {
						text: lines[i].split("|")[1],
						color: this._itemColor
					};
					added += 1;
				}
			}

			this._maxpage = Math.ceil(lines.length / maxlines);
			if (this._maxpage > 1) {
				pagetitle += expandDescription("[gcm_reputation_paging]", { page: (this._curpage + 1), max: this._maxpage });
			}

			if (this._maxpage > 1) {
				if (this._curpage < this._maxpage - 1) {
					curChoices["10_GOTONEXT"] = {
						text: "[gcm_option_nextpage]",
						color: this._menuColor
					};
					added += 1;
				} else {
					curChoices["10_GOTONEXT"] = {
						text: "[gcm_option_nextpage]",
						color: this._disabledColor,
						unselectable: true
					};
					added += 1;
				}
				if (this._curpage > 0) {
					curChoices["11_GOTOPREV"] = {
						text: "[gcm_option_prevpage]",
						color: this._menuColor
					};
					added += 1;
				} else {
					curChoices["11_GOTOPREV"] = {
						text: "[gcm_option_prevpage]",
						color: this._disabledColor,
						unselectable: true
					};
					added += 1;
				}
			}
			/*if (this._awards.length > 0) {
				curChoices["20_AWARDS"] = {text:"[gcm_option_awards]", color:this._menuColor};
				added += 1;
			} else {
				curChoices["20_AWARDS"] = {text:"[gcm_option_awards]", color:this._disabledColor, unselectable:true};
				added += 1;
			}*/

			added += 2; // for the exit menu item
			for (var i = 0; i < ((maxlines + 5) - added); i++) {
				curChoices["02_SPACER_" + i] = "";
			}
		}
		curChoices["21_REPUTATION"] = {
			text: "[gcm_option_reputation]",
			color: this._menuColor
		};
		curChoices["99_EXIT"] = {
			text: "[gcm_option_exit]",
			color: this._exitColor
		};
	}

	// details of particular award
	if (this, _display === 2) {
		pagetitle = expandDescription("[gcm_reputation_title_award]");
		def = "98_EXIT";
		var awd = this._awards[this._selected];
		text = this.$padTextRight(expandDescription("[gcm_award_title]"), 10) + this.$padTextRight(awd.title, 22) + "\n";
		var cols = this.$columnText(awd.description, 22);
		for (var i = 0; i < cols.length; i++) {
			if (i === 0) {
				text += this.$padTextRight(expandDescription("[gcm_award_description]"), 10) + cols[i] + "\n";
			} else {
				text += this.$padTextRight(" ", 10) + cols[i] + "\n";
			}
		}
		text += this.$padTextRight(expandDescription("[gcm_award_system]"), 10) + this.$padTextRight(awd.source + (parseInt(awd.galaxy) !== galaxyNumber ? " (G" + parseInt(awd.galaxy) + 1 + ")" : ""), 22) + "\n";
		text += this.$padTextRight(expandDescription("[gcm_award_time]"), 10) + this.$padTextRight(clock.clockStringForTime(awd.received), 22) + "\n";

		if (awd.image !== "") {
			ovrly = awd.image;
		}
		if (awd.imageType > 0) {
			ovrly = {
				name: this._medalTypes[awd.imageType],
				height: 546
			};
		}
		curChoices["98_EXIT"] = {
			text: "[gcm_option_exit]",
			color: this._exitColor
		};
	}

	var opts = {
		screenID: "oolite-gcm-reputation-map",
		title: pagetitle,
		allowInterrupt: true,
		exitScreen: "GUI_SCREEN_INTERFACES",
		choices: curChoices,
		overlay: ovrly,
		initialChoicesKey: (this._lastchoice ? this._lastchoice : def),
		message: text
	};

	mission.runScreen(opts, this.$screenHandler, this);

}

//-------------------------------------------------------------------------------------------------------------
this.$screenHandler = function $screenHandler(choice) {
	delete this._lastchoice;
	var newChoice = "";

	switch (choice) {
		case "11_GOTOPREV":
			this._curpage -= 1;
			if (this._curpage === 0) newChoice = "10_GOTONEXT";
			break;
		case "10_GOTONEXT":
			this._curpage += 1;
			if (this._curpage === this._maxpage - 1) newChoice = "11_GOTOPREV";
			break;
		case "20_AWARDS":
			this._curpage = 0;
			this._display = 1;
			break;
		case "21_REPUTATION":
			this._curpage = 0;
			this._display = 0;
			break;
		case "98_EXIT":
			this._display = 1;
			break;
	}
	if (choice.indexOf("01_AWARD") >= 0) {
		this._display = 2;
		this.selected = parseInt(choice.split("~")[1], 10);
	}

	this._lastchoice = choice;
	if (newChoice !== "") {
		this._lastchoice = newChoice;
	}

	if (choice !== "99_EXIT") {
		this.$showPage();
	}
}

//-------------------------------------------------------------------------------------------------------------
// returns the player's current reputation with the specified entity in the specified system
// system is ignored for chart-wide entities
// galaxy and system are ignored for galactic-type entities
this.$getReputation = function $getReputation(entity, galaxyID, systemID) {
	var gal = -1;
	var sys = -1;
	switch (this._entities[entity].scope) {
		case "chart":
			gal = galaxyID;
			break;
		case "system":
			gal = galaxyID;
			sys = systemID;
			break;
	}
	for (var i = 0; i < this._reputations.length; i++) {
		if (this._reputations[i].entity === entity && !isNaN(this._reputations[i].reputation) && parseInt(this._reputations[i].galaxy) === gal && this._reputations[i].system === sys) return this._reputations[i].reputation;
	}
	return 0;
}

//-------------------------------------------------------------------------------------------------------------
// returns the human readable version of the players reputation with an entity.
this.$getReputationReward = function $getReputationReward(entity, reputation) {
	var grid = this._entities[entity].rewardGrid;
	if (grid.length === 0) return "";
	var result = "";
	for (var i = 0; i < grid.length; i++) {
		if (parseFloat(reputation) >= parseFloat(grid[i].value)) {
			if (grid[i].hasOwnProperty("description") === true) {
				result = grid[i].description;
				// check to see if there is text that needs expanding.
				if (result != null && result !== "") {
					var check = expandDescription(result);
					result = check;
				} else {
					result = "";
				}
			}
			if (grid[i].hasOwnProperty("missiontext") === true) {
				result = grid[i].missiontext;
				// check to see if there is text that needs expanding.
				if (result != null && result !== "") {
					var check = expandMissionText(result);
					result = check;
				} else {
					result = "";
				}
			}
		}
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// returns an array of integers being the number of systems where the player has earned a reputation with a particular entity
this.$getReputationSystems = function $getReputationSystems(entity, galaxy) {
	var result = [];
	for (var i = 0; i < this._reputations.length; i++) {
		if (this._reputations[i].entity === entity && this._reputations[i].galaxy === galaxy && result.indexOf(this._reputations[i].system) === -1) {
			result.push(this._reputations[i].system)
		}
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// apply the success reputation to all entities
this.$adjustReputationSuccess = function $adjustReputationSuccess(missType, sourceSysID, destSysID, percentComplete) {
	// if we've been allowed to complete a mission at 0%, don't count it as a success - switch to failure
	if (percentComplete === 0) {
		this.$adjustReputation(missType, sourceSysID, destSysID, "Failure", percentComplete);
		return;
	}
	this.$adjustReputation(missType, sourceSysID, destSysID, "Success", percentComplete);
}

//-------------------------------------------------------------------------------------------------------------
// apply the failure reputation to all entities
this.$adjustReputationFailure = function $adjustReputationFailure(missType, sourceSysID, destSysID, percentComplete) {
	this.$adjustReputation(missType, sourceSysID, destSysID, "Failure", percentComplete);
}

//-------------------------------------------------------------------------------------------------------------
// apply the adjustment to reputation
this.$adjustReputation = function $adjustReputation(missType, sourceSysID, destSysID, adjType, percentComplete) {
	// get list of entities for this mission type
	var itemlist = expandDescription("[missionType" + missType + "_reputationEntities]");
	if (itemlist === "") return;
	var items = itemlist.split(",");
	// get list of reputation targets
	var targetlist = expandDescription("[missionType" + missType + "_reputationTarget]");
	var targets = targetlist.split(",");
	// get rep value of successful mission for each entity
	var valuelist = expandDescription("[missionType" + missType + "_reputation" + adjType + "]");
	var values = valuelist.split(",");
	var compCheck = [];
	var sysID = -1;
	var hs_dest_check = false;
	var hs_rep_decrease = 0;

	// items = entities in scope
	// apply value to existing entity
	for (var i = 0; i < this._reputations.length; i++) {
		var rep = this._reputations[i];
		for (var j = 0; j < items.length; j++) {
			if (rep.entity === items[j]) {
				sysID = -1;
				hs_dest_check = false;
				switch (targets[j]) {
					default:
					case "source":
						sysID = sourceSysID;
						break;
					case "destination":
						sysID = destSysID;
						hs_dest_check = true;
						break;
				}

				var ent = this._entities[rep.entity];
				if (ent) {
					var adding = false;
					if (ent.scope === "galactic") adding = true;
					if (ent.scope === "region" && parseInt(rep.galaxy) === galaxyNumber && ent.regionSystems.indexOf(sysID) >= 0) adding = true;
					if (ent.scope === "chart" && parseInt(rep.galaxy) === galaxyNumber) adding = true;
					if (ent.scope === "system" && parseInt(rep.galaxy) === galaxyNumber && rep.system === sysID) adding = true;
					if (adding === true) {
						var amt = parseFloat(values[j]) * (adjType === "Success" ? 1 * percentComplete : -1 * (1 - percentComplete))
						if (hs_dest_check === true && amt < 0) hs_rep_decrease = values[j];
						rep.reputation += amt;
						// make sure we don't go outside any limits
						if (rep.reputation > ent.maxValue) rep.reputation = ent.maxValue;
						if (rep.reputation < ent.minValue) rep.reputation = ent.minValue;
						values[j] = 0;
						compCheck.push({
							ent: rep.entity,
							amount: amt,
							sys: sysID
						});
						//this.$checkForComplimentary(rep.entity, amt, sysID);
						this.$checkForAchievement(rep.entity, rep.reputation, sysID);

						// check for home system
					}
				}
			}
		}
	}
	// look for any new entity records and add them
	for (var i = 0; i < items.length; i++) {
		var ent = this._entities[items[i]];
		sysID = -1;
		hs_dest_check = false;
		switch (targets[i]) {
			default:
			case "source":
				sysID = sourceSysID;
				break;
			case "destination":
				sysID = destSysID;
				hs_dest_check = true;
				break;
		}
		if (ent.scope != "region" || (ent.regionGalaxy === galaxyNumber && ent.regionSystems.indexOf(sysID) >= 0)) {
			if (values[i] != 0) {
				var amt = parseFloat(values[i]) * (adjType === "Success" ? 1 * percentComplete : -1 * (1 - percentComplete));
				if (hs_dest_check === true && amt < 0) hs_rep_decrease = values[i];
				// make sure we don't go outside any limits
				if (amt > ent.maxValue) amt = ent.maxValue;
				if (amt < ent.minValue) amt = ent.minValue;
				this._reputations.push({
					entity: items[i],
					galaxy: (ent.scope != "galactic" ? galaxyNumber : -1),
					system: (ent.scope === "system" ? sysID : -1),
					reputation: amt
				});

				compCheck.push({
					ent: items[i],
					amount: amt,
					sys: sysID
				});
				//this.$checkForComplimentary(items[i], amt, sysID);
				this.$checkForAchievement(items[i], amt, sysID);
			}
		}
	}
	var hs = worldScripts.HomeSystem;
	if (hs) {
		// is this a home system
		if (hs.$isHomeSystem(destSysID) === true) {
			// we aren't ever going to increase our hs rep, but doing a mission against a home sys will definitely decrease your rep
			var hsrep = parseInt(hs._dockCounts[galaxyNumber][destSysID]);
			hsrep -= parseInt(hs_rep_decrease);
			hs._dockCounts[galaxyNumber][destSysID] = hsrep;
		}
	}
	// apply any complementary values
	if (compCheck.length > 0) {
		for (var i = 0; i < compCheck.length; i++) {
			this.$checkForComplimentary(compCheck[i].ent, compCheck[i].amount, compCheck[i].sys);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$adjustReputationForEntity = function(entity, sysID, adjType, amount, percentComplete) {
	var compCheck = [];
	for (var i = 0; i < this._reputations.length; i++) {
		var rep = this._reputations[i];
		if (rep.entity === entity) {
			var ent = this._entities[rep.entity];
			if (ent) {
				var adding = false;
				if (ent.scope === "galactic") adding = true;
				if (ent.scope === "region" && parseInt(rep.galaxy) === galaxyNumber && ent.regionSystems.indexOf(sysID) >= 0) adding = true;
				if (ent.scope === "chart" && parseInt(rep.galaxy) === galaxyNumber) adding = true;
				if (ent.scope === "system" && parseInt(rep.galaxy) === galaxyNumber && rep.system === sysID) adding = true;
				if (adding === true) {
					var amt = parseFloat(amount) * (adjType === "Success" ? 1 * percentComplete : -1 * (1 - percentComplete))
					rep.reputation += amt;
					// make sure we don't go outside any limits
					if (rep.reputation > ent.maxValue) rep.reputation = ent.maxValue;
					if (rep.reputation < ent.minValue) rep.reputation = ent.minValue;
					amount = 0;
					compCheck.push({
						ent: rep.entity,
						amount: amt,
						sys: sysID
					});
					//this.$checkForComplimentary(rep.entity, amt, sysID);
					this.$checkForAchievement(rep.entity, rep.reputation, sysID);
				}
			}
		}
	}
	// look for a new entity record and add it
	var ent = this._entities[entity];
	if (ent.scope != "region" || (ent.regionGalaxy === galaxyNumber && ent.regionSystems.indexOf(sysID) >= 0)) {
		if (amount != 0) {
			var amt = parseFloat(amount) * (adjType === "Success" ? 1 * percentComplete : -1 * (1 - percentComplete));
			// make sure we don't go outside any limits
			if (amt > ent.maxValue) amt = ent.maxValue;
			if (amt < ent.minValue) amt = ent.minValue;
			this._reputations.push({
				entity: entity,
				galaxy: (ent.scope != "galactic" ? galaxyNumber : -1),
				system: (ent.scope === "system" ? sysID : -1),
				reputation: amt
			});

			compCheck.push({
				ent: entity,
				amount: amt,
				sys: sysID
			});
			//this.$checkForComplimentary(items[i], amt, sysID);
			this.$checkForAchievement(entity, amt, sysID);
		}
	}
	// apply any complementary values
	if (compCheck.length > 0) {
		for (var i = 0; i < compCheck.length; i++) {
			this.$checkForComplimentary(compCheck[i].ent, compCheck[i].amount, compCheck[i].sys);
		}
	}

}

//-------------------------------------------------------------------------------------------------------------
// adjust the reputation in any complementary entities that current exist in the players reputation array
this.$checkForComplimentary = function $checkForComplimentary(entity, amount, sysID) {
	var ent = this._entities[entity];
	if (ent.hasOwnProperty("complementary") === false || ent.complementary === "") return;
	var entlist = ent.complementary.split("~");
	var hs_rep_decrease = 0;

	for (var j = 0; j < entlist.length; j++) {
		var comp = entlist[j].split("|");
		var c_ent = comp[0];
		var c_pct = parseFloat(comp[1]);
		var compent = this._entities[c_ent];
		var found = false;
		for (var i = 0; i < this._reputations.length; i++) {
			if (this._reputations[i].entity === c_ent &&
				(compent.scope === "galactic" ||
					(compent.scope === "chart" && this._reputations[i].galaxy === galaxyNumber) ||
					(compent.scope === "region" && this._reputations[i].galaxy === galaxyNumber && compent.regionSystems.indexOf(sysID) >= 0) ||
					(compent.scope === "system" && this._reputations[i].galaxy === galaxyNumber && this._reputations[i].system === sysID))) {
				this._reputations[i].reputation += (amount * c_pct);
				if (this._reputations[i].reputation > compent.maxValue) this._reputations[i].reputation = compent.maxValue;
				if (this._reputations[i].reputation < compent.minValue) this._reputations[i].reputation = compent.minValue;
				found = true;
			}
		}
		if (found === false) {
			var newamt = (amount * c_pct)
			if (newamt < compent.minValue) newamt = compent.minValue;
			if (newamt > compent.maxValue) newamt = compent.maxValue
			this._reputations.push({
				entity: c_ent,
				galaxy: (compent.scope != "galactic" ? galaxyNumber : -1),
				system: (compent.scope === "system" ? sysID : -1),
				reputation: newamt
			});
		}
		// special case for a home system
		if (c_ent === expandDescription("[gcm_reputation_localgov]") && (amount * c_pct) < 0) hs_rep_decrease = 1;
	}
	var hs = worldScripts.HomeSystem;
	if (hs && hs_rep_decrease !== 0) {
		// is this a home system
		if (hs.$isHomeSystem(sysID) === true) {
			// we aren't ever going to increase our hs rep, but doing a mission against a home sys will definitely decrease your rep
			var rep = parseInt(hs._dockCounts[galaxyNumber][sysID]);
			rep -= hs_rep_decrease;
			hs._dockCounts[galaxyNumber][sysID] = rep;
		}
	}

}

//-------------------------------------------------------------------------------------------------------------
this.$checkForAchievement = function $checkForAchievement(entity, reputation, sysID) {
	var grid = this._entities[entity].rewardGrid;
	if (grid.length === 0) return;
	var item;
	// find the item in the grid the player has reached
	for (var i = 0; i < grid.length; i++) {
		if (reputation >= parseFloat(grid[i].value)) item = grid[i];
	}
	// if this item has an achievement WS and FN, call it, passing the entity and rep values
	if (item && item.hasOwnProperty("achievementWS") && item.hasOwnProperty("achievementFN")) {
		worldScripts[item.achievementWS][item.achievementFN](entity, sysID, reputation);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$checkForAward = function $checkForAward(entity, title, sysID) {
	if (this._awards.length > 0) {
		for (var i = 0; i < this._awards.length; i++) {
			if (this._awards[i].entity === entity &&
				this._awards[i].title === title &&
				this._awards[i].galaxy === galaxyNumber &&
				this._awards[i].source === System.systemNameForID(sysID))
				return true;
		}
	}
	return false;
}

//-------------------------------------------------------------------------------------------------------------
// update any reputations that have an update link (ie. reps that are actually external to this system but we report here anyway)
this.$updateReputationValues = function $updateReputationValues() {
	for (var i = 0; i < this._reputations.length; i++) {
		var ent = this._entities[this._reputations[i].entity];
		if (ent && ent.getValueWS !== "") {
			this._reputations[i].reputation = worldScripts[ent.getValueWS][ent.getValueFN]();
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// returns true if the entity has been added to the reputations list. otherwise false
this.$isReputationInList = function $isReputationInList(entity) {
	var result = false;
	for (var i = 0; i < this._reputations.length; i++) {
		if (this._reputations[i].entity === entity) {
			var ent = this._entities[entity];
			if (ent && (ent.scope === "galactic" || parseInt(this._reputations[i].galaxy) === galaxyNumber)) result = true;
		}
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// returns the entity that has the required impact type, and for which the player has the highest reputation
this.$getImpactEntity = function $getImpactEntity(impactType, allegiance) {
	var sel = 0;
	var ent = "";
	for (var i in this._entities) {
		if (this._entities[i].hasOwnProperty("impact") === true &&
			this._entities[i].impact.indexOf(impactType) >= 0 &&
			this._entities[i].impactAllegiance.indexOf(allegiance) >= 0) {

			var result = this.$getReputation(i, galaxyNumber, system.ID);
			if (result > sel) {
				ent = i;
				sel = result;
			}
		}
	}
	return ent;
}

//-------------------------------------------------------------------------------------------------------------
// returns the impact value for a given entity, based on the player's current reputation
this.$getImpactValue = function $getImpactValue(entity) {
	var details = this._entities[entity];
	var grid = details.rewardGrid;
	var rep = this.$getReputation(entity, galaxyNumber, system.ID);
	var result = 0;
	for (var i = 0; i < grid.length; i++) {
		if (grid[i].value < rep) result = grid[i].impactValue;
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
this.$performPriceAdjustment = function $performPriceAdjustment() {
	// are there any price adjustments to apply? check the reputation
	var found = false;
	for (var j = 0; j < system.stations.length; j++) {
		var stn = system.stations[j];
		var alleg = this.$getStationAllegiance(stn);
		var legal_pct = 0;
		var legal_ent = this.$getImpactEntity("markets", alleg);
		if (legal_ent !== "") legal_pct = this.$getImpactValue(legal_ent);
		var illegal_pct = 0;
		var illegal_ent = this.$getImpactEntity("illegals", alleg);
		if (illegal_ent !== "") illegal_pct = this.$getImpactValue(illegal_ent);
		if (legal_pct > 0 && legal_pct > illegal_pct) {
			// only update the prices if they haven't been done already
			// a save/load sequence should not redo the price change
			if (stn.script.hasOwnProperty("_gcm_pricesAdjusted") === false || stn.script._gcm_pricesAdjusted === false) {
				var mkt = stn.market;
				var adjust = [];
				for (var i in mkt) {
					// for a std "markets" adj, only update legal goods - leave illegals alone
					if (mkt[i].legality_import == 0 && mkt[i].legality_export == 0) {
						var price = parseFloat(mkt[i].price) / 10;
						if ((parseFloat(mkt[i].price_average) / 10) > price)
							adjust.push({
								commodity: i,
								old: mkt[i].price,
								new: parseInt((price * (1 - legal_pct)) * 10)
							});
						if ((parseFloat(mkt[i].price_average) / 10) < price)
							adjust.push({
								commodity: i,
								old: mkt[i].price,
								new: parseInt((price * (1 + legal_pct)) * 10)
							});
					}
				}
				for (var i = 0; i < adjust.length; i++) {
					stn.setMarketPrice(adjust[i].commodity, adjust[i].new);
					if (this._debug) log(this.name, "adjusting " + stn.displayName + " price for " + adjust[i].commodity + ": avg=" + mkt[adjust[i].commodity].price_average + ", old=" + adjust[i].old + ", new=" + adjust[i].new);
				}
			}
			stn.script._gcm_marketEntity = legal_ent;
			stn.script._gcm_pricesAdjusted = true;
			stn.script._gcm_priceAdjustPercent = legal_pct;
			stn.script._gcm_marketAdjustType = "markets";
			found = true;
		}

		// only update illegals prices if we haven't also updated legals.
		if (illegal_pct > 0 && illegal_pct > legal_pct) {
			// only update the prices if they haven't been done already
			// a save/load sequence should not redo the price change
			if (stn.script.hasOwnProperty("_gcm_pricesAdjusted") === false || stn.script._gcm_pricesAdjusted === false) {
				var mkt = stn.market;
				var adjust = [];
				for (var i in mkt) {
					// for a std "markets" adj, only update legal goods - leave illegals alone
					if (mkt[i].legality_import !== 0 || mkt[i].legality_export !== 0) {
						var price = parseFloat(mkt[i].price) / 10;
						if ((parseFloat(mkt[i].price_average) / 10) > price)
							adjust.push({
								commodity: i,
								old: mkt[i].price,
								new: parseInt((price * (1 - illegal_pct)) * 10)
							});
						if ((parseFloat(mkt[i].price_average) / 10) < price)
							adjust.push({
								commodity: i,
								old: mkt[i].price,
								new: parseInt((price * (1 + illegal_pct)) * 10)
							});
					}
				}
				for (var i = 0; i < adjust.length; i++) {
					stn.setMarketPrice(adjust[i].commodity, adjust[i].new);
					if (this._debug) log(this.name, "adjusting " + stn.displayName + " price for " + adjust[i].commodity + ": avg=" + mkt[adjust[i].commodity].price_average + ", old=" + adjust[i].old + ", new=" + adjust[i].new);
				}
			}
			stn.script._gcm_marketEntity = illegal_ent;
			stn.script._gcm_pricesAdjusted = true;
			stn.script._gcm_priceAdjustPercent = illegal_pct;
			stn.script._gcm_marketAdjustType = "illegals";
			found = true;
		}

	}
	this._pricesAdjusted = found;
}

//-------------------------------------------------------------------------------------------------------------
this.$performBountyAdjustment = function $performBountyAdjustment(entity) {
	// ? Should the reduction happen each time the player has a change to rep? Or once per level?

	// if the Bounty System is not installed, it's just a straight reduction.
	// if the Bounty System is installed, perform a reduction in bounty across all player's outstanding bounties.
	// for repLevel's 4, 5, 6, 7 only: 25% for 4, 50% for 5, 75% for 6, and 100% for 7.
	// do this each time the player changes their rep in a +ive way

	var recover = this.$getImpactValue(entity);
	if (recover === 0) return;

	var update = false;

	if (worldScripts.BountySystem) {
		var b = worldScripts.BountySystem;
		if (b._bountyDelta > 0) b._bountyDelta = parseInt(b._bountyDelta * (1 - recover));
		if (recover === 1) {
			b._offences.length = 0;
		} else {
			if (b._offences.length > 0) {
				// don't worry about whether the offences are transferred or not - just reduce them all
				for (var i = 0; i < b._offences.length; i++) {
					b._offences[i].bounty = parseInt(b._offences[i].bounty * (1 - recover));
					update = true;
				}
			}
		}
	}
	if (player.bounty > 0) {
		player.ship.setBounty(parseInt(player.bounty * (1 - recover)));
		update = true;
	}

	if (update === true) {
		//send email
		var email = worldScripts.EmailSystem;
		if (email) {
			var client = this.$getClientName(system.ID, entity);
			email.$createEmail({
				sender: client,
				subject: expandDescription("[gcm_bounty_reduction_email_subject]"),
				date: global.clock.adjustedSeconds,
				message: this.$transformText(expandDescription("[gcm_bounty_reduction_email]", {
					reduction: parseInt(recover * 100),
					orgname: entity,
					name: client
				}), system.ID)
			});
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// internal function to return the cargo reputation
this.$cargoReputation = function $cargoReputation() {
	return player.contractReputation;
}

//-------------------------------------------------------------------------------------------------------------
// internal function to return the parcel reputation
this.$parcelReputation = function $parcelReputation() {
	return player.parcelReputation;
}

//-------------------------------------------------------------------------------------------------------------
// internal function to return the passenger reputation
this.$passengerReputation = function $passengerReputation() {
	return player.passengerReputation;
}

//-------------------------------------------------------------------------------------------------------------
this.$randomHitsReputation = function $randomHitsReputation() {
	return expandDescription("[gcm_reputation_randomhits_current]", { rank: missionVariables.random_hits_currentrank, title: missionVariables.random_hits_playertitle });
}

//-------------------------------------------------------------------------------------------------------------
this.$upsReputation = function $upsReputation() {
	return expandDescription("[gcm_reputation_ups_current]", { rep: this.$getReputationReward("UPS", worldScripts.ups_parcel._reputation()) });
}

//-------------------------------------------------------------------------------------------------------------
this.$escortContractsReputation = function $escortContractsReputation() {
	return worldScripts.Escort_Contracts.ec_escortrep;
}

//-------------------------------------------------------------------------------------------------------------
this.$rescueStationsReputation = function $rescueStationsReputation() {
	return missionVariables.rescuestation_reputation;
}

//-------------------------------------------------------------------------------------------------------------
// appends space to currentText to the specified length in 'em'
this.$padTextRight = function $padTextRight(currentText, desiredLength, leftSwitch) {
	if (currentText == null) currentText = "";
	var hairSpace = String.fromCharCode(31);
	var ellip = "…";
	var currentLength = defaultFont.measureString(currentText.replace(/%%/g, "%"));
	var hairSpaceLength = defaultFont.measureString(hairSpace);
	// calculate number needed to fill remaining length
	var padsNeeded = Math.floor((desiredLength - currentLength) / hairSpaceLength);
	if (padsNeeded < 1) {
		// text is too long for column, so start pulling characters off
		var tmp = currentText;
		do {
			tmp = tmp.substring(0, tmp.length - 2) + ellip;
			if (tmp === ellip) break;
		} while (defaultFont.measureString(tmp.replace(/%%/g, "%")) > desiredLength);
		currentLength = defaultFont.measureString(tmp.replace(/%%/g, "%"));
		padsNeeded = Math.floor((desiredLength - currentLength) / hairSpaceLength);
		currentText = tmp;
	}
	// quick way of generating a repeated string of that number
	if (!leftSwitch || leftSwitch === false) {
		return currentText + new Array(padsNeeded).join(hairSpace);
	} else {
		return new Array(padsNeeded).join(hairSpace) + currentText;
	}
}

//-------------------------------------------------------------------------------------------------------------
// appends space to currentText to the specified length in 'em'
this.$padTextLeft = function $padTextLeft(currentText, desiredLength) {
	return this.$padTextRight(currentText, desiredLength, true);
}

//-------------------------------------------------------------------------------------------------------------
// arranges text into a array of strings with a particular column width
this.$columnText = function $columnText(originalText, columnWidth) {
	var returnText = [];
	if (defaultFont.measureString(originalText) > columnWidth) {
		var hold = originalText;
		do {
			var newline = "";
			var remain = "";
			var point = hold.length;
			do {
				point = hold.lastIndexOf(" ", point - 1);
				newline = hold.substring(0, point).trim();
				remain = hold.substring(point + 1).trim();
			} while (defaultFont.measureString(newline) > columnWidth);
			returnText.push(newline);
			if (remain != "") {
				if (defaultFont.measureString(remain) <= columnWidth) {
					returnText.push(remain);
					hold = "";
				} else {
					hold = remain;
				}
			} else {
				hold = "";
			}
		} while (hold != "");
	} else {
		returnText.push(originalText);
	}
	return returnText;
}

//-------------------------------------------------------------------------------------------------------------
// returns true if a HUD with allowBigGUI is enabled, otherwise false
this.$isBigGuiActive = function $isBigGuiActive() {
	if (oolite.compareVersion("1.83") <= 0) {
		return player.ship.hudAllowsBigGui;
	} else {
		var bigGuiHUD = ["XenonHUD.plist", "coluber_hud_ch01-dock.plist"]; // until there is a property we can check, I'll be listing HUD's that have the allow_big_gui property set here
		if (bigGuiHUD.indexOf(player.ship.hud) >= 0) {
			return true;
		} else {
			return false;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// calculates the player's bonus factor for a particular mission type by looking at their reputation
this.$playerMissionReputation = function $playerMissionReputation(sysID, missType) {
	// get the list of entities related to this mission type
	var entities = expandDescription("[missionType" + missType + "_reputationEntities]");
	if (entities != "") {
		var entlist = entities.split(",");
		var rep = 0;
		var max = 0;
		var min = 0;
		var total = 0;
		// for each entity, add the player's current reputation, calculated as a percentage based on the maxValue/minValue settings for the entity
		for (var i = 0; i < entlist.length; i++) {
			var ent = this._entities[entlist[i]];
			// just get the single value if we've either asked for a single system, or the entity is chart/galactic based.
			if (sysID !== -1 || ent.scope !== "system") {
				rep = this.$getReputation(entlist[i], galaxyNumber, sysID);
				max = ent.maxValue;
				min = ent.minValue;
				if (max - min !== 0) total += (rep - min) / (max - min);
			} else {
				// if we've been passed a "-1", this means get the total for this entity in all systems in the current chart
				// we should only do this if the scope of the entity is system based.
				var syslist = this.$getReputationSystems(entlist[i], galaxyNumber);
				var subtotal = 0;
				if (syslist.length > 0) {
					for (var j = 0; j < syslist.length; j++) {
						rep = this.$getReputation(entlist[i], galaxyNumber, syslist[j]);
						max = ent.maxValue;
						min = ent.minValue;
						if (max - min !== 0) subtotal += (rep - min) / (max - min);
					}
					total += (subtotal / syslist.length);
				}
			}
		}
		// final percentage = sum of rep percentages / count of entities involved
		var result = (total / entlist.length);
		// convert this percentage into a number betwen 0.5 and 2.0
		return (1.5 * result) + 0.5;
	} else {
		return 0.5; // if no rep - return min value
	}
}

//-------------------------------------------------------------------------------------------------------------
// adds or updates a client name in our stored list.
this.$addNameToClientList = function $addNameToClientList(cname, sysID, entity, idx) {
	var found = false;
	for (var i = 0; i < this._clientNames.length; i++) {
		var item = this._clientNames[i];
		if (item.galaxy == galaxyNumber && item.systemID == sysID && item.entity == entity && item.index == idx) {
			// update name
			item.clientName = cname;
			found = true;
			break;
		}
	}
	if (found === false) {
		// new name
		this._clientNames.push({
			clientName: cname,
			galaxy: galaxyNumber,
			systemID: sysID,
			entity: entity,
			index: idx,
			created: clock.adjustedSeconds,
		});
	}
}

//-------------------------------------------------------------------------------------------------------------
// gets a stored name, or generates a new one if no stored name found
this.$getClientName = function $getClientName(sysID, entity) {
	var cname = "";
	// possible 4 names for local governments
	var idx = Math.floor(Math.random() * 4);
	// only 2 from other station types
	if (entity.indexOf("Local Government") === -1) idx = Math.floor(Math.random() * 2);

	for (var i = 0; i < this._clientNames.length; i++) {
		var item = this._clientNames[i];
		if (item.galaxy == galaxyNumber && item.systemID == sysID && item.entity == entity && item.index == idx) {
			// start to change names after a random number of days (between 14 and 28)
			if (clock.adjustedSeconds - item.created < (86400 * (Math.floor(Math.random() * 14) + 14)))
				cname = item.clientName;
		}
	}
	if (cname === "") {
		cname = this.$generateName();
		if (entity !== "") this.$addNameToClientList(cname, sysID, entity, idx);
	}
	return cname;
}

//-------------------------------------------------------------------------------------------------------------
this.$generateName = function $generateName() {
	var text = worldScripts.GNN_PhraseGen._makePhrase(worldScripts.GNN_PhraseGen.$pool["GNN_Names"])
	return text;
}

//-------------------------------------------------------------------------------------------------------------
// process any text lookup or replacements in a text item
// this is to allow an reputations or awards to be defined in a generic way, but appear unique for a particular system
// eg, the same award could be called different things based on the government type
// thus the definition in descriptions.plist would be "AngelOfMercy^G"
// this would result in a expansion lookup of "[AngelOfMercy7]" for a Corp State award
this.$transformText = function $transformText(text, sysID, destID) {
	var sys = null;
	var result = text;
	var controlChar = ["\[", "~", "$"];
	//look for any lookup expansions based on gov, eco, or TL,
	var found = false;
	for (var i = 0; i < controlChar.length; i++) {
		if (result.indexOf(controlChar[i]) >= 0) {
			found = true;
			break;
		}
	}
	if (found === true) {
		sys = System.infoForSystem(galaxyNumber, sysID);
		var lookup = result;
		lookup = lookup.replace(/\$G/g, sys.government); // insert government number (0-7)
		lookup = lookup.replace(/\$E/g, sys.economy); // insert economy number (0-7)
		lookup = lookup.replace(/\$T/g, sys.techLevel); // insert techlevel (0-14)
		lookup = lookup.replace(/\$H/g, sys.name); // insert system name (eg Lave)
		lookup = lookup.replace(/\$S/g, galaxyNumber + "-" + sysID); // insert system ID (eg 0-7, where first digit is galaxy num, second is system id)
		if (lookup.indexOf("~") >= 0 || lookup.indexOf("] ") >= 0) {
			// replace our custom brackets with real ones
			// a space and ~ char is an opening bracket.
			lookup = lookup.replace(/ ~/g, " [");
			// a ~ char and a space is a closing bracket
			lookup = lookup.replace(/~ /g, "] ");
			// because of the need for a space after a closing bracket symbol, look for any "] ." or "] ," and remove the space
			lookup = lookup.replace(/\] \./g, "].");
			lookup = lookup.replace(/\] \,/g, "],");
			lookup = lookup.replace(/\] \!/g, "]!");
			lookup = lookup.replace(/\] \?/g, "]?");
		} else {
			if (lookup.indexOf("[") != 0 && lookup.indexOf("]") != lookup.length - 1) {
				lookup = "[" + lookup + "]";
			}
		}
		// perform the expansion with the new text
		//log(this.name, "checking transform " + lookup);
		result = expandDescription(lookup);
	} else {
		result = expandDescription(result);
	}
	// look for any inplace expansions
	if (result.indexOf("`") >= 0) {
		if (sys == null) sys = System.infoForSystem(galaxyNumber, sysID);
		var pgn = this.$getPirateGroupName(galaxyNumber, sysID);
		var d_pgn = "";
		if (destID && destID >= 0 && destID <= 255) d_pgn = this.$getPirateGroupName(galaxyNumber, destID);
		var repl = result;
		do {
			repl = repl.replace(/`H/g, sys.name);
			repl = repl.replace(/`I/g, sys.name + "ian");
			repl = repl.replace(/`PGN/g, pgn);
			repl = repl.replace(/`DPGN/g, d_pgn);
		} while (repl.indexOf("`") >= 0);
		result = repl;
	}
	return result;
}

//--------------------------------------------------------------------------------------------------------------
// returns true if the passed value is numeric, otherwise false
this.$isNumeric = function $isNumeric(n) {
	return !isNaN(parseFloat(n)) && isFinite(n);
}

//-------------------------------------------------------------------------------------------------------------
this.$getStationAllegiance = function $getStationAllegiance(station) {
	var alleg = station.allegiance;
	if (!alleg || alleg === "") alleg = "neutral";
	if (station.isMainStation) alleg = "galcop";
	return alleg;
}

//-------------------------------------------------------------------------------------------------------------
// returns a random but system-consistent pirate band name for a particular system
this.$getPirateGroupName = function $getPirateGroupName(galID, sysID) {
	var seed = (galID + 1) * 1000 + sysID;
	var sysName = System.infoForSystem(galID, sysID).name;
	var def = {
		fieldA: ["Bloody", "Silver", "Wild", "Crazy", "Cyclone", "Black", "Blue", "Dark", "Gray", "Iron", "Red", "Steel", "White"],
		fieldB: ["dread", "lost", "alligator", "angel", "asura", "banshee", "basilisk", "beast", "bull", "bunyip", "butcher", "chimera", "cockatrice", "crocodile", "cuttlefish", "cyclops", "daimon", "demon", "deva", "devil", "dragon", "executioner", "fiend", "gargoyle", "&genie", "ghost", "ghoul", "goblin", "god", "goddess", "golem", "gorgon", "guardian", "harpy", "hellcat", "hippogriff", "hydra", "hyena", "jinn", "judge", "kelpie", "kraken", "lion", "lioness", "loup-garou", "minotaur", "monster", "octopus", "ogre", "&oni", "orca", "owl", "phantom", "pharaoh", "prophet", "rakshasa", "reaper", "revenant", "sasquatch", "scorpion", "&seraph", "serpent", "shadow", "shoggoth", "shrike", "sorceror", "spectre", "sphinx", "spider", "spirit", "&squid", "tarantula", "tengu", "tiger", "tigress", "troll", "vampire", "vulture", "walrus", "warlock", "warthog", "wendigo", "werewolf", "witch", "wizard", "wolf", "wolverine", "wyrm", "wyvern"],
		fieldC: ["bandit$", "pillager$", "pirate$", "plunderer$", "raider$", "renegade$", "marauder$", "sinner$", "ravager$", "buccaneer$", "drifter$", "corsair$", "rover$"],
		fieldD: ["bloodied", "giant", "stray", "abandoned", "abominable", "ageless", "ancient", "archaic", "boundless", "bright", "burning", "dead", "dark", "deserted", "dread", "eternal", "everlasting", "fiery", "forgotten", "forsaken", "golden", "grey", "hallowed", "hidden", "hoary", "holy", "horned", "icy", "infinite", "lightless", "living", "lost", "millenial", "misty", "never-ending", "primal", "primeval", "primordial", "pristine", "sacred", "shining", "silver", "sunless", "sunken", "timeless", "unbroken", "undying", "unholy"],
		fieldE: ["adder", "bear", "cobra", "dog", "dragon", "eagle", "hawk", "jackal", "lion", "octopus", "panther", "python", "rat", "scorpion", "shark", "spider", "squid", "stag", "tiger", "wolf"],
		fieldF: ["claw", "cove", "dagger", "eye", "fist", "flag", "flame", "hammer", "hand", "heart", "knife", "mountain", "skull", "sword"],
		fieldG: [],
		fieldH: [],
		fieldI: [],
		fieldJ: [],
		fieldK: [],
		fieldL: [],
		fieldM: [],
		fieldN: [],
		fieldO: [],
		fieldP: [],
		fieldQ: [],
		fieldR: [],
		sentences: [
			"The +4 >+3 of " + sysName,
			"The +4 >+3",
			"The >+5",
			"The >+5 of " + sysName,
			"The 1 +5 >+3 of " + sysName,
			"The 1 +5 >+3",
			"The +2 >+5 of " + sysName,
			"The +2 >+5",
			">3 of the +4 +6",
			"The 1 >+3 of " + sysName,
			"The 1 >+3",
			"The " + sysName + "ian >+3",
			"The 1 " + sysName + "ian >+3",
			"The +2 >+3 of " + sysName,
			"The +2 >+3"
		],
	};
	var text = worldScripts.GNN_PhraseGen._makePhrase(def, null, 0, seed, null);
	return text;
}