"use strict";
this.name = "GalCopBB_DiseaseOutbreak";
this.author = "phkb";
this.copyright = "2017 phkb";
this.description = "Controls all disease-system mission settings (50-52), including locking down a system by removing all traffic and limiting docking";
this.license = "CC BY-NC-SA 4.0";

/*
Apart from controlling the mission specific code, the goal of this code is to lock down a system from most incoming and insystem traffic
Player can only validly enter the system if they've been issued a security pass as part of a mission (mission type 50)

Ideas For disease systems:
	- add how much outbreak was contained by player input
	- change system 
		- monitor outgoing ships in nearby systems to prevent them heading for disease system
		- change police logic - player needs to send confirmation code to police vessels otherwise they will fine the player and attack
	- outbreak spreading could be caused by player buying stuff at non-galcop stations in an outbreak system then selling those items on another system
		- penalties could be monetary, legal, or in the form of trade restrictions
*/

this._systemID = -1;
this._periodicBroadcastTimer = null;
this._allowDocking = null;
this._sysPopulated = 0;
this._resetStations = false;

this._diseaseSystems = []; // systems where there is major disease
this._diseaseEvents = []; // list of systemID's that are having a major disease outbreak
this._outbreakCheck = false; // flag to indicate the outbreak check routine is currently active
this._galJump = false;
this._debug = true;

//-------------------------------------------------------------------------------------------------------------
this.startUp = function () {
	var gcm = worldScripts.GalCopBB_Missions;
	// add these mission types into the main control
	var list = [50, 51, 52, 53];
	gcm._availableMissionTypes = gcm._availableMissionTypes.concat(list);
	this._debug = gcm._debug;
	// add our reputation entities
	var ent = worldScripts.GalCopBB_Reputation._entities;
	ent[expandDescription("[gcm_reputation_aom]")] = {
		scope: "chart",
		display: true,
		getValueWS: "",
		getValueFN: "",
		maxValue: 7,
		minValue: 0,
		degrade: "0.1|30",
		rewardGrid: [{
			value: 0,
			description: ""
		},
		{
			value: 4,
			description: "",
			achievementWS: "GalCopBB_DiseaseOutbreak",
			achievementFN: "$angelOfMercyReward"
		},
		{
			value: 6,
			description: "",
			achievementWS: "GalCopBB_DiseaseOutbreak",
			achievementFN: "$angelOfMercyReward"
		},
		{
			value: 7,
			description: "",
			achievementWS: "GalCopBB_DiseaseOutbreak",
			achievementFN: "$angelOfMercyReward"
		}
		]
	};
	ent[expandDescription("[gcm_reputation_gcdc]")] = {
		scope: "chart",
		display: true,
		getValueWS: "",
		getValueFN: "",
		maxValue: 7,
		minValue: 0,
		degrade: "0.5|30",
		rewardGrid: []
	};

	// exclude some equipment items from being sold/stored by ShipConfig
	var sc = worldScripts.ShipConfiguration_Core;
	if (sc) {
		sc._noSell.push("EQ_GCM_MEDICAL_SUPPLIES");
		sc._noSell.push("EQ_GCM_VIRUS_SPECIMENS");
		sc._noSell.push("EQ_GCM_PATIENT_TRANSPORT");
	}
}

//-------------------------------------------------------------------------------------------------------------
this.startUpComplete = function () {
	this._diseaseSystems = this.$findDiseaseSystems();
	if (missionVariables.GalCopBBMissions_DiseaseOutbreaks) {
		this._diseaseEvents = JSON.parse(missionVariables.GalCopBBMissions_DiseaseOutbreaks);
		delete missionVariables.GalCopBBMissions_DiseaseOutbreaks;
	}
	if (missionVariables.GalCopBBMissions_SystemLockdown) {
		this._systemID = parseInt(missionVariables.GalCopBBMissions_SystemLockdown);
		delete missionVariables.GalCopBBMissions_SystemLockdown;
	}
	// make sure the populator function runs after the startUpComplete
	if (this._sysPopulated === 1 && this._systemID >= 0) {
		this.systemWillPopulate();
	}
	this._sysPopulated = -1;
	this.shipExitedWitchspace();

	worldScripts.BulletinBoardSystem.$registerBBEvent("GalCopBB_DiseaseOutbreak", "$custom_shipWillDockWithStation", "shipWillDockWithStation_start");
}

//-------------------------------------------------------------------------------------------------------------
this.playerWillSaveGame = function () {
	if (this._systemID >= 0) missionVariables.GalCopBBMissions_SystemLockdown = this._systemID;
	if (this._diseaseEvents.length > 0) missionVariables.GalCopBBMissions_DiseaseOutbreaks = JSON.stringify(this._diseaseEvents);
}

//-------------------------------------------------------------------------------------------------------------
this.playerEnteredNewGalaxy = function (galaxyNumber) {
	this._diseaseEvents.length = 0;
	this._diseaseSystems.length = 0;
	this._galJump = true;
}

//-------------------------------------------------------------------------------------------------------------
this.dayChanged = function (newday) {
	this.$checkDiseaseOutbreaks();
}

//-------------------------------------------------------------------------------------------------------------
this.playerStartedJumpCountdown = function (type, seconds) {
	// no check from interstellar space
	if (system.ID === -1) return;
	// check if the player has a reason to visit a disease system
	var gcm = worldScripts.GalCopBB_Missions;
	var bb = worldScripts.BulletinBoardSystem;
	var dest = gcm.$playerTargetSystem();
	if (type == "standard" && this.$systemHasDiseaseOutbreak(dest) === true) {
		this._systemID = dest;
		// have we been given clearance to go there?
		var id = gcm.$getActiveMissionIDByType(50);
		var found = false;
		if (id >= 0) {
			var item = bb.$getItem(id);
			// is the mission destination the same as our jump destination
			if (item.destination === dest) found = true;
		}
		if (found === false) {
			// check distance from nearest galcop entity (witchpoint beacon, station)
			// if they aren't, the jump can continue
			if (this.$checkGalCopDistance() === false) return;

			// we don't have a reason, so cancel the jump
			player.ship.cancelHyperspaceCountdown();
			player.consoleMessage(expandDescription("[gcm_galcop_jump_override]", { dest: System.systemNameForID(dest) }), 5);
		} else {
			player.consoleMessage(expandDescription("[gcm_galcop_invalid_code]"), 5);
		}
	} else {
		this._systemID = -1;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipExitedWitchspace = function () {
	function getGalCop(entity) {
		return (entity.isShip && (entity.hasRole("buoy-witchpoint") || (entity.isStation && entity.allegiance === "galcop")));
	}
	this._galCopTargets = system.filteredEntities(this, getGalCop);

	if (system.sun && system.mainPlanet) {
		this._sun_planet_dist = system.sun.position.distanceTo(system.mainPlanet);
	} else {
		this._sun_planet_dist = 0;
	}

	// set up the timer to transmit warning messages
	if (this._systemID >= 0 && system.ID === this._systemID) {
		this._periodicBroadcastTimer = new Timer(this, this.$transmitWarning, 10, 120);

		this.$removeShips();

		var gcm = worldScripts.GalCopBB_Missions;
		var bb = worldScripts.BulletinBoardSystem;
		// have we been given clearance to come here?
		var id = gcm.$getActiveMissionIDByType(50);
		var item = bb.$getItem(id);
		if (item && item.data.quantity === 0) {
			// set up broadcast comms interface
			var bcc = worldScripts.BroadcastCommsMFD;
			var stns = system.stations;
			for (var i = 0; i < stns.length; i++) {
				// clearance only required at galcop stations
				if (stns[i].allegiance === "galcop") {
					if (bcc.$checkMessageExists("gcm_transmit_dockcode_" + i) === false) {
						bcc.$createMessage({
							messageName: "gcm_transmit_dockcode_" + i,
							callbackFunction: this.$transmitSecurityCode.bind(this),
							displayText: expandDescription("[gcm_transmit_securitycode]"),
							messageText: expandDescription("[gcm_transmitting_dockcode]"),
							ship: system.mainStation,
							transmissionType: "target",
							deleteOnTransmit: true,
							delayCallback: 5,
							hideOnConditionRed: true
						});
					}
				}
			}
		}
	}
	if (this._galJump === true) {
		this._galJump = false;
		this._diseaseSystems = this.$findDiseaseSystems();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillEnterWitchspace = function (cause, destination) {
	this._resetStations = false;
	// stop the timer
	if (this._periodicBroadcastTimer && this._periodicBroadcastTimer.isRunning) {
		this._periodicBroadcastTimer.stop();
		this._periodicBroadcastTimer = null;
	}
	if (system.ID === this._systemID) {
		// turn on queues again in SDC as we leave
		var sdc = worldScripts.StationDockControl;
		if (sdc) sdc._disableQueues = false;
		this.$enableBigShips();
	}
	if (destination === this._systemID) {
		this.$disableBigShips();
		// tell planetfall not to spawn any planetary locations
		if (worldScripts.PlanetFall2) {
			worldScripts.PlanetFall2.disable = true;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipDied = function (whom, why) {
	// stop the timer
	if (this._periodicBroadcastTimer && this._periodicBroadcastTimer.isRunning) {
		this._periodicBroadcastTimer.stop();
		this._periodicBroadcastTimer = null;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$custom_shipWillDockWithStation = function $custom_shipWillDockWithStation(station) {
	if (this._simulator === true) {
		this._simulator = false;
		return;
	}
	// turn off the docking fees, if it's installed
	if (this._systemID >= 0 && system.ID === this._systemID) {
		if (worldScripts["Docking Fees"]) worldScripts["Docking Fees"]._simulator = true;
	}
	if (this._periodicBroadcastTimer && this._periodicBroadcastTimer.isRunning) {
		this._periodicBroadcastTimer.stop();
		this._periodicBroadcastTimer = null;
	}

	// check for other mission types being completed
	var gcm = worldScripts.GalCopBB_Missions;
	var bb = worldScripts.BulletinBoardSystem;
	var list = gcm.$getListOfMissions(true, [50, 51, 52]);
	for (var i = 0; i < list.length; i++) {
		if (station.isMainStation) {
			if (list[i].data.missionType === 50 && list[i].destination === system.ID) {
				if (player.ship.equipmentStatus("EQ_GCM_MEDICAL_SUPPLIES") === "EQUIPMENT_OK" && list[i].expiry >= clock.adjustedSeconds) {
					list[i].arrivalReportText = expandDescription("[missionType50_arrivalReport_complete]", {
						payment: formatCredits(list[i].payment, false, true)
					});
				} else {
					list[i].arrivalReportText = expandDescription("[missionType50_arrivalReport_late]");
				}

				list[i].data.quantity = 1;
				bb.$updateBBMissionPercentage(list[i].ID, 1);

				gcm.$logMissionData(list[i].ID);
			}
			if (list[i].data.missionType === 51 && list[i].destination === system.ID) {
				if (player.ship.equipmentStatus("EQ_GCM_PATIENT_TRANSPORT") === "EQUIPMENT_OK" && list[i].expiry >= clock.adjustedSeconds) {
					list[i].arrivalReportText = expandDescription("[missionType51_arrivalReport_complete]", {
						payment: formatCredits(list[i].payment, false, true)
					});
				} else {
					list[i].arrivalReportText = expandDescription("[missionType51_arrivalReport_late]");
				}

				list[i].data.quantity = 1;
				bb.$updateBBMissionPercentage(list[i].ID, 1);

				gcm.$logMissionData(list[i].ID);
			}
			if (list[i].data.missionType === 52 && list[i].destination === system.ID) {
				if (player.ship.equipmentStatus("EQ_GCM_VIRUS_SPECIMENS") === "EQUIPMENT_OK" && list[i].expiry >= clock.adjustedSeconds) {
					list[i].arrivalReportText = expandDescription("[missionType52_arrivalReport_complete]", {
						payment: formatCredits(list[i].payment, false, true)
					});
				} else {
					list[i].arrivalReportText = expandDescription("[missionType52_arrivalReport_late]");
				}

				list[i].data.quantity = 1;
				bb.$updateBBMissionPercentage(list[i].ID, 1);

				gcm.$logMissionData(list[i].ID);
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipDockedWithStation = function (station) {
	if (this._simulator === true) this._simulator = false;
}

//-------------------------------------------------------------------------------------------------------------
this.shipLaunchedFromStation = function (station) {
	if (worldScripts.GalCopBB_Missions.$simulatorRunning() === true) this._simulator = true;
	if (this._systemID >= 0 && system.ID === this._systemID) {
		this.$removeShips();
		if (station === system.mainStation) this.$lockStation(station);
		this._periodicBroadcastTimer = new Timer(this, this.$transmitWarning, 10, 120);
	}
	if (this._simulator == false) this._reported = false;
}

//-------------------------------------------------------------------------------------------------------------
this.systemWillPopulate = function () {
	// set the flag to indicate we've been here already
	if (this._sysPopulated !== -1) this._sysPopulated += 1;
	if (this._systemID >= 0 && system.ID === this._systemID) {
		// zero the populator values so no ships are coming in
		var pr = worldScripts["oolite-populator"];
		var k = Object.keys(pr.$repopulatorFrequencyIncoming);
		for (var i = 0; i < k.length; i++) {
			pr.$repopulatorFrequencyIncoming[k[i]] = 0;
		}
		k = Object.keys(pr.$repopulatorFrequencyOutgoing);
		for (i = 0; i < k.length; i++) {
			pr.$repopulatorFrequencyOutgoing[k[i]] = 0;
		}
		// for SDC, make sure all docks are empty
		var sdc = worldScripts.StationDockControl;
		if (sdc) {
			sdc._disableQueues = true;
			sdc.$emptyAllQueues();
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.systemWillRepopulate = function () {
	if (this._systemID > 0 && system.ID === this._systemID) {
		if (this._resetStations === false) {
			this._resetStations = true;
			var stns = system.stations;
			for (var i = 0; i < stns.length; i++) {
				if (stns[i].allegiance === "galcop") {
					// turn off ability to dock at station
					this.$lockStation(stns[i]);
				}
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.guiScreenChanged = function (to, from) {
	if (this._systemID >= 0 && system.ID === this._systemID && player.ship.alertCondition === 0 && player.ship.dockedStation.allegiance === "galcop" && to === "GUI_SCREEN_MARKET") {
		mission.runScreen({
			screenID: "oolite-gcm-marketlockout-summary",
			title: expandDescription("[gcm_market_suspended]"),
			message: expandDescription("[gcm_market_suspended_info]"),
			overlay: { name: "gcm-noentry.png", height: 546 },
			exitScreen: "GUI_SCREEN_STATUS"
		});
	}
}

//-------------------------------------------------------------------------------------------------------------
this.missionScreenOpportunity = function () {
	if (this._simulator == true) return;
	if (this._reported == true) return;
	// tell the player about local systems that are in lockdown
	this._reported = true;
	var sys = System.infoForSystem(galaxyNumber, system.ID);
	var report = [];
	for (var i = 0; i < this._diseaseEvents.length; i++) {
		var dist = sys.distanceToSystem(System.infoForSystem(galaxyNumber, this._diseaseEvents[i].systemID));
		if (dist <= 7) report.push(this._diseaseEvents[i].systemID);
	}
	if (report.length > 0) {
		var systems = "";
		for (var i = 0; i < report.length; i++) {
			systems += (systems == "" ? "" : ", ") + System.systemNameForID(report[i]) + " (" + sys.distanceToSystem(System.infoForSystem(galaxyNumber, report[i])).toFixed(1) + "ly)";
		}
		mission.runScreen({
			title: expandDescription("[gcm_reputation_gcdc]"),
			message: expandDescription("[gcm_gcdc_notification]", { systems: systems }),
			overlay: { name: "gcm-biohazard.png", height: 546 },
			exitScreen: "GUI_SCREEN_STATUS"
		});
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$transmitWarning = function $transmitWarning() {
	function getWPBuoy(entity) {
		return (entity.isShip && (entity.hasRole("buoy-witchpoint") || (entity.isStation && entity.allegiance === "galcop")));
	}
	var targets = system.filteredEntities(this, getWPBuoy);
	if (targets.length > 0) {
		for (var i = 0; i < targets.length; i++) {
			targets[i].commsMessage(expandDescription("[gcm_disease_warning]"), player.ship);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// response after player has transmitted security code to a station (via BroadcastCommsMFD)
this.$transmitSecurityCode = function $transmitSecurityCode() {
	if (player.ship.position.distanceTo(system.mainStation) < player.ship.scannerRange) {
		system.mainStation.commsMessage(expandDescription("[gcm_transmit_dockcode]"), player.ship);
		this._allowDocking = new Timer(this, this.$allowDocking.bind(this), 10, 0);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$populatorPreventer = function $populatorPreventer() {
	// this function intentionally left blank
}

//-------------------------------------------------------------------------------------------------------------
this.$allowDocking = function $allowDocking() {
	system.mainStation.commsMessage(expandDescription("[gcm_dockcode_success]"), player.ship);
	this.$unlockStation(system.mainStation);
}

//-------------------------------------------------------------------------------------------------------------
this.$enableBigShips = function $enableBigShips() {
	// restore bigShips populator on the way out
	var bship = worldScripts["bigShips_populator"];
	if (bship) {
		if (!bship.$gcm_hold_systemWillPopulate) {
			if (bship.$gcm_hold_bship_populator) {
				bship.populator = bship.$gcm_hold_bship_populator;
				delete bship.$gcm_hold_bship_populator;
			}
		} else {
			bship.systemWillPopulate = bship.$gcm_hold_systemWillPopulate;
			delete bship.$gcm_hold_systemWillPopulate;
		}
	} else {
		var liners = worldScripts["liners_populator_script.js"];
		if (liners) {
			if (liners.$gcm_hold_liners_populator) {
				liners.populator = liners.$gcm_hold_liners_populator;
				delete liners.$gcm_hold_liners_populator;
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$disableBigShips = function $disableBigShips() {
	// disable bigShips populator on the way in
	var bship = worldScripts["bigShips_populator"];
	if (bship) {
		if (!bship.systemWillPopulate) {
			if (!bship.$gcm_hold_bship_populator) {
				bship.$gcm_hold_bship_populator = bship.populator;
				bship.populator = this.$populatorPreventer;
			}
		} else {
			bship.$gcm_hold_systemWillPopulate = bship.systemWillPopulate;
			delete bship.systemWillPopulate;
		}
	} else {
		var liners = worldScripts["liners_populator_script.js"];
		if (liners) {
			if (!liners.$gcm_hold_liners_populator) {
				liners.$gcm_hold_liners_populator = liners.populator;
				liners.populator = this.$populatorPreventer;
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$lockStation = function $lockStation(stn) {
	stn.requiresDockingClearance = false;
	stn.hasNPCTraffic = false;
	for (var j = 0; j < stn.subEntities.length; j++) {
		if (stn.subEntities[j].isDock) {
			stn.subEntities[j].allowsDocking = false;
			stn.subEntities[j].disallowedDockingCollides = true;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$unlockStation = function $unlockStation(stn) {
	for (var j = 0; j < stn.subEntities.length; j++) {
		if (stn.subEntities[j].isDock) {
			stn.subEntities[j].allowsDocking = true;
			stn.subEntities[j].disallowedDockingCollides = false;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// forcibly remove any ships that are already in the system (in case the core populator runs before this script)
this.$removeShips = function $removeShips() {
	for (var i = system.allShips.length - 1; i >= 0; i--) {
		var shp = system.allShips[i];
		if (shp.scanClass === "CLASS_NEUTRAL") {
			shp.remove(true);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// checks the players distance from GalCop entities that could stop a witchspace countdown
this.$checkGalCopDistance = function $checkGalCopDistance() {
	if (!player.ship) return false;
	var p = player.ship;
	var inrange = false;
	if (this._galCopTargets.length > 0) {
		for (var i = 0; i < this._galCopTargets.length; i++) {
			if (p.position.distanceTo(this._galCopTargets[i]) < this._sun_planet_dist) {
				inrange = true;
				break;
			}
		}
	}
	return inrange;
}

//-------------------------------------------------------------------------------------------------------------
// perform the disease outbreak checks
this.$checkDiseaseOutbreaks = function $checkDiseaseOutbreaks(override) {
	// make sure we only come here once per day
	if (this._outbreakCheck) return;
	this._outbreakCheck = true;

	if (this._debug) log(this.name, "checking disease outbreaks (override = " + override + ")");

	// are any going to stop today?
	for (var i = this._diseaseEvents.length - 1; i >= 0; i--) {
		var event = this._diseaseEvents[i];
		if (event.endDate < clock.adjustedSeconds) {
			if (this._debug) log(this.name, "disease outbreak stopped on " + event.name + " (" + event.systemID + ")");
			// send a snoopers message (if installed)
			// propagation = how fast it spreads
			// virulence = how fast it kills
			// the total damage (loss of life) = (number of days * ((100 * virulence) * (1 + propagation)));
			var numDeaths = ((
				((clock.adjustedSeconds - event.startDate) / 86400) *
				((100 * event.virulence) * (1 + event.propagation))
			) / 1000).toFixed(1);
			var news = {
				ID: this.name,
				Message: expandDescription("[gcm_disease_contained]", {
					system: this._diseaseSystems[i].name,
					deaths: numDeaths
				}),
				Agency: 1,
				Priority: 1
			};
			// send a GNN/snoopers message (if installed)
			var g = worldScripts.GNN;
			if (g) g._insertNews(news);
			if (!g) {
				var w = worldScripts.snoopers;
				if (w) w.insertNews(news);
			}

			// remove the extra description from the planet description
			var sys = System.infoForSystem(galaxyNumber, event.systemID);
			sys.setProperty(2, "description", sys.description.replace("\n" + expandDescription("[gcm_disease_description]"), ""));
			this._diseaseEvents.splice(i, 1);
		}
	}

	// for our existing outbreaks, are any going to propagate today?
	for (var i = 0; i < this._diseaseEvents.length; i++) {
		var event = this._diseaseEvents[i];
		var chance = event.propagagtion;
		if (Math.random() < chance && Math.random() < chance && Math.random() < chance) {
			if (this._debug) log(this.name, "diease outbreak on " + event.name + " (" + event.systemID + ") spreading...");
			// yep - we're going offworld
			var info = System.infoForSystem(galaxyNumber, event.systemID);
			var sys = info.systemsInRange(4);
			if (sys.length > 0) {
				// pick a random item
				var dest = sys[Math.floor(Math.random() * sys.length)];
				// make sure this dest doesn't already have the disease
				var found = false;
				for (var j = 0; j < this._diseaseEvents.length; j++) {
					if (this._diseaseEvents[j].systemID === dest.systemID) {
						found = true;
						break;
					}
				}
				if (found === false) {
					if (this._debug) log(this.name, "... to " + dest.name + " (" + dest.systemID + ")");
					// we've got a hit - calculate the end point in seconds
					var months = event.endDate + (((Math.random() * 2) + 1) * 30) * 86400;
					this._diseaseEvents.push({
						systemID: dest.systemID,
						name: dest.name,
						startDate: clock.adjustedSeconds,
						endDate: months,
						propagation: (event.propagation / 2),
						virulence: (event.virulence / 2),
						origin: event.origin
					});

					var news = {
						ID: this.name,
						Message: expandDescription("[gcm_disease_spread]", {
							system: System.systemNameForID(dest.systemID),
							origin: System.systemNameForID(this._diseaseEvents[i].origin)
						}),
						Agency: 1,
						Priority: 1
					};
					// send a GNN/snoopers message (if installed)
					var g = worldScripts.GNN;
					if (g) g._insertNews(news);
					if (!g) {
						var w = worldScripts.snoopers;
						if (w) w.insertNews(news)
					}
					break;
				}
			}
		}
	}

	// how many outbreaks do we have?
	if (this._diseaseEvents.length < 3) {
		// how recent was the last one?
		var newdis = true;
		for (var i = 0; i < this._diseaseEvents.length; i++) {
			if (parseInt((clock.adjustedSeconds - this._diseaseEvents[i].startDate) / 86400) < 30) {
				newdis = false;
				break;
			}
		}
		// it's been more than 30 days since the last one - work out if today is the day
		if (newdis === true) {
			if (Math.random() > 0.96 || (override && override === true)) {
				// bingo - create a new outbreak
				// find an unused system
				// resort the array so we don't start with the same system each time
				this._diseaseSystems.sort(function (a, b) {
					return Math.random() - 0.5;
				});
				for (var i = 0; i < this._diseaseSystems.length; i++) {
					//log(this.name, "sys" + i + ", " + this._diseaseSystems[i].systemID + " " + this._diseaseSystems[i].name);
					var found = false;
					for (var j = 0; j < this._diseaseEvents.length; j++) {
						if (this._diseaseSystems[i].systemID === this._diseaseEvents[j].systemID) {
							found = true;
							break;
						}
					}
					if (found === false) {
						// we've got a hit - calculate the end point in seconds
						var months = clock.adjustedSeconds + (((Math.random() * 3) + 1) * 30) * 86400;
						var sys = this._diseaseSystems[i].systemsInRange(4);
						var prop = sys.length / 10;
						if (prop > 0.8) prop = 0.8;
						var vir = Math.random() * 0.5 + 0.3;
						if (this._debug) log(this.name, "new disease outbreak starting on " + this._diseaseSystems[i].name + " (" + this._diseaseSystems[i].systemID + ") - current end date " + months);

						this._diseaseEvents.push({
							systemID: this._diseaseSystems[i].systemID,
							name: this._diseaseSystems[i].name,
							startDate: clock.adjustedSeconds,
							endDate: months,
							propagation: prop,
							virulence: vir,
							origin: this._diseaseSystems[i].systemID
						});

						var news = {
							ID: this.name,
							Message: expandDescription("[gcm_disease_outbreak]", {
								system: this._diseaseSystems[i].name
							}),
							Agency: 1,
							Priority: 1
						};
						// send a GNN/snoopers message (if installed)
						var g = worldScripts.GNN;
						if (g) g._insertNews(news);
						if (!g) {
							var w = worldScripts.snoopers;
							if (w) w.insertNews(news);
						}

						// update the planet description
						this._diseaseSystems[i].setProperty(2, "description", this._diseaseSystems[i].description += "\n" + expandDescription("[gcm_disease_description]"));
						break;
					}
				}
			}
		}
	}

	this._outbreakCheck = false;
}

//-------------------------------------------------------------------------------------------------------------
// returns a list of sysInfos where disease is prevalent
this.$findDiseaseSystems = function $findDiseaseSystems() {
	return SystemInfo.filteredSystems(this, function (other) {
		return (other.description.indexOf("disease") >= 0 && other.techlevel < 11);
	});
}

//-------------------------------------------------------------------------------------------------------------
this.$systemHasDiseaseOutbreak = function $systemHasDiseaseOutbreak(sysID) {
	// if there are no disease events in play, it's a no
	if (this._diseaseEvents.length === 0) {
		return false;
	} else {
		// otherwise, loop through the current disease events list
		var found = false;
		for (var j = 0; j < this._diseaseEvents.length; j++) {
			if (this._diseaseEvents[j].systemID === sysID) found = true;
		}
		// return what we found
		return found;
	}
}

//-------------------------------------------------------------------------------------------------------------
// reduce the end point for the outbreak on a particular system
this.$reduceDiseaseTime = function $reduceDiseaseTime(sysID) {
	for (var i = 0; i < this._diseaseEvents.length; i++) {
		if (this._diseaseEvents[i].systemID === sysID && this._diseaseEvents[i].endDate - 86400 > clock.adjustedSeconds) this._diseaseEvents -= 86400;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$angelOfMercyReward = function $angelOfMercyReward(entity, sysID, rep) {
	var reputation = worldScripts.GalCopBB_Reputation;
	if (rep >= 4 && rep < 6) {
		if (reputation.$checkForAward(entity, expandDescription("[gcm_wings_of_clemency]"), 0) === false) {
			reputation.$addAward({
				title: expandDescription("[gcm_wings_of_clemency]"),
				description: expandDescription("[gcm_wings_clemency_description]", { chart: (galaxyNumber + 1) }),
				entity: entity,
				system: 0,
				imageType: 1
			});
		}
	}
	if (rep >= 6 && rep < 7) {
		if (reputation.$checkForAward(entity, expandDescription("[gcm_wings_of_compassion]"), 0) === false) {
			reputation.$addAward({
				title: expandDescription("[gcm_wings_of_compassion]"),
				description: expandDescription("[gcm_wings_compassion_description]", { chart: (galaxyNumber + 1) }),
				entity: entity,
				system: 0,
				imageType: 2
			});
		}
	}
	if (rep >= 7) {
		if (reputation.$checkForAward(entity, expandDescription("[gcm_wings_of_benevolence]"), 0) === false) {
			reputation.$addAward({
				title: expandDescription("[gcm_wings_of_benevolence]"),
				description: expandDescription("[gcm_wings_benevolence_description]", { chart: (galaxyNumber + 1) }),
				entity: entity,
				system: 0,
				imageType: 3
			});
		}
	}
}


//-------------------------------------------------------------------------------------------------------------
this.$acceptedMission = function $acceptedMission(missID) {
	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(missID);
	var gcm = worldScripts.GalCopBB_Missions;

	if (this._debug) log(this.name, "accepted mission id = " + missID);

	if (!item) {
		log(this.name, "!!ERROR: BB returned null value from $getItem on mission ID " + missID);
		return;
	}
	gcm.$updateLastMissionDate(item.source, item.data.missionType);
	gcm.$updateGeneralSettings(item);

	// give player medical supplies for mission type 50
	if (item.data.missionType === 50) {
		if (player.ship.cargoSpaceAvailable === 0 && player.ship.cargoSpaceCapacity >= 1) this.$freeCargoSpace(1, "");
		player.ship.awardEquipment("EQ_GCM_MEDICAL_SUPPLIES");
	}
	// give player patient transport for mission type 51
	if (item.data.missionType === 51) {
		var eq = EquipmentInfo.infoForKey("EQ_GCM_PATIENT_TRANSPORT");
		if (player.ship.cargoSpaceAvailable < eq.requiredCargoSpace) this.$freeCargoSpace(eq.requiredCargoSpace, "");
		player.ship.awardEquipment("EQ_GCM_PATIENT_TRANSPORT");
	}
	// give player virus specimens for mission type 52
	if (item.data.missionType === 52) {
		player.ship.awardEquipment("EQ_GCM_VIRUS_SPECIMENS");
	}

}

//-------------------------------------------------------------------------------------------------------------
this.$completedMission = function $completedMission(missID) {
	var p = player.ship;
	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(missID);
	var gcm = worldScripts.GalCopBB_Missions;
	var sbm = worldScripts.Smugglers_BlackMarket;

	// if the player has a bounty, doing these missions will reduce it
	if (player.ship.bounty > 0) {
		for (var i = 0; i < 5; i++) {
			if (player.ship.bounty > 0) player.ship.setBounty(player.ship.bounty - 1, "community service");
		}
		player.ship.consoleMessage(expandDescription("[gcm_bounty_reduced]"));
	}

	// update mission history
	gcm.$updateSuccessHistoryReputation(item);

	// *** type 50 - medical delivery (shouldn't need this, but just in case)
	if (item.data.missionType === 50) {
		worldScripts.GalCopBB_DiseaseOutbreak.$reduceDiseaseTime(system.ID);
		p.removeEquipment("EQ_GCM_MEDICAL_SUPPLIES");
		if (sbm) sbm.$removeSaleItem("EQ_GCM_MEDICAL_SUPPLIES:" + missID);
	}
	// *** type 51 - patient transport (shouldn't need this, but just in case)
	if (item.data.missionType === 51) {
		p.removeEquipment("EQ_GCM_PATIENT_TRANSPORT");
	}
	// *** type 52 - virus specimens (shouldn't need this, but just in case)
	if (item.data.missionType === 52) {
		p.removeEquipment("EQ_GCM_VIRUS_SPECIMENS");
		if (sbm) sbm.$removeSaleItem("EQ_GCM_VIRUS_SPECIMENS:" + missID);
	}

}

//-------------------------------------------------------------------------------------------------------------
this.$confirmCompleted = function $confirmCompleted(missID) {
	var p = player.ship;
	var result = "";
	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(missID);
	if (item) {
		// *** type 50 - medical delivery
		if (item.data.missionType === 50) {
			var chk = p.equipmentStatus("EQ_GCM_MEDICAL_SUPPLIES");
			if (chk !== "EQUIPMENT_OK" && chk !== "EQUIPMENT_DAMAGED") {
				result += (result === "" ? "" : "\n") + expandDescription("[gcm_medical_not_found]");
			}
			if (chk === "EQUIPMENT_DAMAGED") {
				result += (result === "" ? "" : "\n") + expandDescription("[gcm_medical_damaged]");
			}
		}
		// *** type 51 - patient transport
		if (item.data.missionType === 51) {
			var chk = p.equipmentStatus("EQ_GCM_PATIENT_TRANSPORT");
			if (chk !== "EQUIPMENT_OK" && chk !== "EQUIPMENT_DAMAGED") {
				result += (result === "" ? "" : "\n") + expandDescription("[gcm_patient_transport_not_found]");
			}
			if (chk === "EQUIPMENT_DAMAGED") {
				result += (result === "" ? "" : "\n") + expandDescription("[gcm_patient_transport_damaged]");
			}
		}
		// *** type 52 - virus specimen transport
		if (item.data.missionType === 52) {
			var chk = p.equipmentStatus("EQ_GCM_VIRUS_SPECIMENS");
			if (chk !== "EQUIPMENT_OK" && chk !== "EQUIPMENT_DAMAGED") {
				result += (result === "" ? "" : "\n") + expandDescription("[gcm_virus_not_found]");
			}
			if (chk === "EQUIPMENT_DAMAGED") {
				result += (result === "" ? "" : "\n") + expandDescription("[gcm_virus_damaged]");
			}
		}
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
this.$terminateMission = function $terminateMission(missID) {
	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(missID);
	var sbm = worldScripts.Smugglers_BlackMarket;
	var gcm = worldScripts.GalCopBB_Missions;

	// adjust reputation only when the terminatePenalty flag is set to true 
	if (item.data.terminatePenalty === true) {
		// update mission history
		gcm.$updateFailedHistoryReputation(item);
	}

	// *** type 50 - medical delivery
	if (item.data.missionType === 50) {
		var eqsts = player.ship.equipmentStatus("EQ_GCM_MEDICAL_SUPPLIES");
		if (eqsts === "EQUIPMENT_OK" || eqsts === "EQUIPMENT_DAMAGED") {
			player.ship.removeEquipment("EQ_GCM_MEDICAL_SUPPLIES");
			if (sbm) sbm.$removeSaleItem("EQ_GCM_MEDICAL_SUPPLIES:" + missID);
		}
	}
	// *** type 51 - patient transport
	if (item.data.missionType === 51) {
		player.ship.removeEquipment("EQ_GCM_PATIENT_TRANSPORT");
	}
	// *** type 52 - virus specimens
	if (item.data.missionType === 52) {
		var eqsts = player.ship.equipmentStatus("EQ_GCM_VIRUS_SPECIMENS");
		if (eqsts === "EQUIPMENT_OK" || eqsts === "EQUIPMENT_DAMAGED") {
			player.ship.removeEquipment("EQ_GCM_VIRUS_SPECIMENS");
			if (sbm) sbm.$removeSaleItem("EQ_GCM_VIRUS_SPECIMENS:" + missID);
		}
	}

}

//-------------------------------------------------------------------------------------------------------------
this.$failedMission = function $failedMission(missID) {
	var bb = worldScripts.BulletinBoardSystem;
	var sbm = worldScripts.Smugglers_BlackMarket;
	var gcm = worldScripts.GalCopBB_Missions;
	var eq = "";

	if (!sbm) {
		// if there's no blackmarket option, just remove the equipment
		this.$terminateMission(missID);
		return;
	}

	var item = bb.$getItem(missID);
	// update mission history
	gcm.$updateFailedHistoryReputation(item);

	if (item.data.missionType === 50) {
		var eqsts = player.ship.equipmentStatus("EQ_GCM_MEDICAL_SUPPLIES");
		if (eqsts === "EQUIPMENT_OK" || eqsts === "EQUIPMENT_DAMAGED") eq = "EQ_GCM_MEDICAL_SUPPLIES";
	}
	if (item.data.missionType === 51) {
		// we'll just remove this one
		player.ship.removeEquipment("EQ_GCM_PATIENT_TRANSPORT");
		eq = "";
	}
	if (item.data.missionType === 52) {
		var eqsts = player.ship.equipmentStatus("EQ_GCM_VIRUS_SPECIMENS");
		if (eqsts === "EQUIPMENT_OK" || eqsts === "EQUIPMENT_DAMAGED") eq = "EQ_GCM_VIRUS_SPECIMENS";
	}

	if (eq != "") gcm._equipmentFromFailedMissions.push({
		missionType: item.data.missionType,
		source: item.source,
		equip: eq,
		date: clock.adjustedSeconds
	});
}

//-------------------------------------------------------------------------------------------------------------
this.$updateManifestEntry = function $updateManifestEntry(missID) {
	var gcm = worldScripts.GalCopBB_Missions;
	gcm.$updateManifestEntry(missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
	return worldScripts.GalCopBB_Missions.$missionAvailability(missID, missType, origSysID);
}

//-------------------------------------------------------------------------------------------------------------
// 50 - deliver medical equipment
// 52 - virus specimen transport
this.$missionType5052_Values = function $missionType5052_Values(workTime, routeTime, routeDistance, destSysInfo) {
	var missValues = worldScripts.GalCopBB_CoreMissionValues;
	var result = {};
	result["quantity"] = 1;
	result["price"] = parseInt((parseInt(Math.random() * 50) + 75) / 10) * 10 + (7 - destSysInfo.government) * 20 + missValues.$calcDistanceBonus(routeDistance, 50);
	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
	result["penalty"] = result.price / 2;
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// 51 - emergency patient transport
this.$missionType51_Values = function $missionType51_Values(workTime, routeTime, routeDistance, destSysInfo) {
	var missValues = worldScripts.GalCopBB_CoreMissionValues;
	var result = {};
	result["quantity"] = 1;
	result["price"] = parseInt((parseInt(Math.random() * 50) + 75) / 10) * 10 + (7 - destSysInfo.government) * 20 + missValues.$calcDistanceBonus(routeDistance, 10);
	result["expiry"] = clock.adjustedSeconds + routeTime + 600; // we want the time to be tight for the patient transport -- transit time + 10 mins
	result["penalty"] = result.price / 2;
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// 53 - collect 3 parts of experimental antidote
this.$missionType53_Values = function $missionType53_Values(workTime, routeTime, routeDistance, destSysInfo) {

	var textlist = expandDescription("[gcm_collect_textlist]").split('|');
	var typelist = expandDescription("[gcm_collect_typelist]").split('|');

	var resultset = [];
	for (var i = 0; i < textlist.length; i++) {
		var list = this.$getSystemsByDescription(textlist[i]);
		if (list.length > 0) resultset.push(typelist[i]);
	}

	if (resultset.length < 3) return null;

	for (var i = 0; i < 5; i++)
		resultset.sort(function (a, b) {
			return Math.random() - 0.5;
		}); // shuffle order so it isn't always the same variant being checked first

	var result = {};
	result["ingredients"] = [resultset[0], resultset[1], resultset[2]];
	result["quantity"] = 3;
	// pay should be good for these (between 1500 and 7500)
	result["price"] = parseInt((parseInt(Math.random() * 3000 + 500) + parseInt(Math.random() * 2000 + 500) + parseInt(Math.random() * 1000 + 500)) / 10) * 10;
	result["expiry"] = -1; // no expiry for these missions
	result["penalty"] = 0;
	return result;
}

//-------------------------------------------------------------------------------------------------------------
this.$getSystemsByDescription = function $getSystemsByDescription(options) {
	var planets = SystemInfo.filteredSystems(this, function (other) {
		return (other.description.indexOf(options) >= 0 && other.systemID != system.ID);
	});
	return planets;
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_confirm_tree_grubs = function $type53_confirm_tree_grubs(missID) {
	return this.$itemValidation(expandDescription("[gcm_desc_tree_grub]"), missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_collect_tree_grubs = function $type53_collect_tree_grubs(missID) {
	this.$update53Mission("tree_grubs", missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_confirm_edible_moths = function $type53_confirm_edible_moths(missID) {
	return this.$itemValidation(expandDescription("[gcm_desc_moth]"), missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_collect_edible_moths = function $type53_collect_edible_moths(missID) {
	this.$update53Mission("edible_moths", missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_confirm_coronal_plasma = function $type53_confirm_coronal_plasma(missID) {
	return this.$itemValidation(expandDescription("[gcm_desc_solar]"), missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_collect_coronal_plasma = function $type53_collect_coronal_plasma(missID) {
	this.$update53Mission("coronal_plasma", missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_confirm_tree_wolf = function $type53_confirm_tree_wolf(missID) {
	return this.$itemValidation(expandDescription("[gcm_desc_tree_wolf]").split("|"), missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_collect_tree_wolf = function $type53_collect_tree_wolf(missID) {
	this.$update53Mission("tree_wolf", missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_confirm_spotted_wolf = function $type53_confirm_spotted_wolf(missID) {
	return this.$itemValidation(expandDescription("[gcm_desc_spotted_wolf]").split("|"), missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_collect_spotted_wolf = function $type53_collect_spotted_wolf(missID) {
	this.$update53Mission("spotted_wolf", missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_confirm_tree_ants = function $type53_confirm_tree_ants(missID) {
	return this.$itemValidation(expandDescription("[gcm_desc_ant]"), missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_collect_tree_ants = function $type53_collect_tree_ants(missID) {
	this.$update53Mission("tree_ants", missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_confirm_deadly_grubs = function $type53_confirm_deadly_grubs(missID) {
	return this.$itemValidation(expandDescription("[gcm_desc_grub]"), missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_collect_deadly_grubs = function $type53_collect_deadly_grubs(missID) {
	this.$update53Mission("deadly_grubs", missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_confirm_yak_milk = function $type53_confirm_yak_milk(missID) {
	return this.$itemValidation(expandDescription("[gcm_desc_yak]"), missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_collect_yak_milk = function $type53_collect_yak_milk(missID) {
	this.$update53Mission("yak_milk", missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_confirm_monkey_glands = function $type53_confirm_monkey_glands(missID) {
	return this.$itemValidation(expandDescription("[gcm_desc_monkey]"), missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_collect_monkey_glands = function $type53_collect_monkey_glands(missID) {
	this.$update53Mission("monkey_glands", missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_confirm_goat_hair = function $type53_confirm_goat_hair(missID) {
	return this.$itemValidation(expandDescription("[gcm_desc_goat]"), missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_collect_goat_hair = function $type53_collect_goat_hair(missID) {
	this.$update53Mission("goat_hair", missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_confirm_tulip_petals = function $type53_confirm_tulip_petals(missID) {
	return this.$itemValidation(expandDescription("[gcm_desc_tulip]"), missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_collect_tulip_petals = function $type53_collect_tulip_petals(missID) {
	this.$update53Mission("tulip_petals", missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_confirm_banana_syrup = function $type53_confirm_banana_syrip(missID) {
	return this.$itemValidation(expandDescription("[gcm_desc_banana]"), missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_collect_banana_syrup = function $type53_collect_banana_syrip(missID) {
	this.$update53Mission("banana_syrip", missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_confirm_lobstoid_eggs = function $type53_confirm_lobstoid_eggs(missID) {
	return this.$itemValidation(expandDescription("[gcm_desc_lobstoid]"), missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_collect_lobstoid_eggs = function $type53_collect_lobstoid_eggs(missID) {
	this.$update53Mission("lobstoid_eggs", missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_confirm_magma = function $type53_confirm_magma(missID) {
	return this.$itemValidation(expandDescription("[gcm_desc_volcano]"), missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$type53_collect_magma = function $type53_collect_magma(missID) {
	this.$update53Mission("magma", missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$itemValidation = function $itemValidation(ingr, missID) {
	var desc = system.info.description;
	if (Array.isArray(ingr) === true) {
		var found = false;
		for (var i = 0; i < ingr.length; i++) {
			if (desc.indexOf(ingr[i]) >= 0) found = true;
		}
		if (found === false) return expandDescription("[gcm_unavailable_in_system]");
	} else {
		if (desc.indexOf(ingr) === -1) return expandDescription("[gcm_unavailable_in_system]");
	}
	if (player.ship.dockedStation.isMainStation === false) return expandDescription("[gcm_unavailable_at_station]");
	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(missID);
	if (system.ID === item.source) return expandDescription("[gcm_invalid_local_supply]");
	return "";
}

//-------------------------------------------------------------------------------------------------------------
this.$update53Mission = function $update53Mission(ingrType, missID) {
	var text = expandDescription("[gcm_ingredient_" + ingrType + "_collected]", {
		destSystem: system.name
	});
	var bb = worldScripts.BulletinBoardSystem;
	var gcm = worldScripts.GalCopBB_Missions;
	var item = bb.$getItem(missID);

	// add text to mission details
	if (item.details.indexOf("*") === -1) item.details += "\n";
	item.details += text;

	// remove this ingredient from the list
	for (var i = 0; i < item.data.ingredients.length; i++) {
		if (item.data.ingredients[i] === ingrType) {
			item.data.ingredients.splice(i, 1);
			break;
		}
	}

	// update the percentage complete
	item.data.quantity += 1;
	bb.$updateBBMissionPercentage(item.ID, (item.data.quantity / item.data.targetQuantity));
	gcm.$logMissionData(item.ID);
	player.consoleMessage(expandDescription("[goal_updated]"));
}