"use strict";
this.name = "PlanetFall2";
this.author = "Thargoid, phkb, with bits from Eric Walch, Frame and Svengali";
this.copyright = "Creative Commons Attribution - Non-Commercial - Share Alike 3.0 license with clauses - see readme.txt.";
this.description = "Enables planetary landing";

this.fx_mult = 1.0;
this.fx_step = 2; // controls how fast the cloud frame moves
this.fx_land_dist = 200; // controls how far in front of player to position landing visual effects
this.fx_start = -120;
this.fx_launch_dist = 250; // controls how far in front of player to position launching visual effects
this.boostAmount = 3500; // how far past the dock to jump the player to when launching
this.planetFallLaunch = false; // as in 1.77 the alert condition is still set on launch - need a mask
this.lastPlanetType = "Prime"; // what type of planet/moon we landed on
this.planetCount = 1; // default setting
this.moonCount = 0; // default setting
this.messageBlock = false; // flag to stop request/cancel messages at launch from planet
this.planetFallOverride = false; // flag to allow other OXPs to override PlanetFall locations
this.overrideRole = "default";
this.disable = false; // turns off the spawning of planetary locations
this.landingTimer = null;
this.landingMonitor = null; // timer to monitor proximity to planetary locations and thus when to trigger docking requests
this.permitMilitaryDock = false; // flag to indicate player is permitted to dock at military bases
this.additionalPlanets = false;
this.globalCount = 0;
this.indexPlanets = 0;
this.indexMoons = 0;
this.subServiceLevel = 0.0;
this.destination = null;
this.debug = false;

this._landingImages = {
	"city": ["planetFall2_city_1.png", "planetFall2_city_2.png", "planetFall2_city_3.png", "planetFall2_city_4.png", "planetFall2_city_5.png",
		"planetFall2_city_6.png", "planetFall2_city_7.png", "planetFall2_city_8.png", "planetFall2_city_9.png", "planetFall2_city_10.png",
		"planetFall2_city_11.png", "planetFall2_city_12.png"],
	"military": ["planetFall2_military_1.png", "planetFall2_military_2.png", "planetFall2_military_3.png", "planetFall2_military_4.png"],
	"leisure": ["planetFall2_leisure_1.png", "planetFall2_leisure_2.png", "planetFall2_leisure_3.png", "planetFall2_leisure_4.png",
		"planetFall2_leisure_5.png", "planetFall2_leisure_6.png", "planetFall2_leisure_7.png", "planetFall2_leisure_8.png"],
	"factory": ["planetFall2_factory_1.png", "planetFall2_factory_2.png", "planetFall2_factory_3.png", "planetFall2_factory_4.png", "planetFall2_factory_5.png"],
	"dump": ["planetFall2_dump_1.png", "planetFall2_dump_2.png", "planetFall2_dump_3.png", "planetFall2_dump_4.png"],
	"colony": ["planetFall2_colony_1.png", "planetFall2_colony_2.png", "planetFall2_colony_3.png", "planetFall2_colony_4.png"],
	"moon_dome": ["planetFall2_moon_1.png", "planetFall2_moon_2.png", "planetFall2_moon_3.png", "planetFall2_moon_4.png", "planetFall2_moon_5.png",
		"planetFall2_moon_6.png", "planetFall2_moon_7.png", "planetFall2_moon_8.png"],
	"moon_leisure": ["planetFall2_moonleisure_1.png", "planetFall2_moonleisure_2.png", "planetFall2_moonleisure_3.png"],
	"moon_military": ["planetFall2_moonmilitary_1.png", "planetFall2_moonmilitary_2.png", "planetFall2_moonmilitary_3.png"],
	"moon_research": ["planetFall2_research_1.png", "planetFall2_research_2.png", "planetFall2_research_3.png", "planetFall2_research_4.png"],
	"moon_mine": ["planetFall2_moonmine_1.png", "planetFall2_moonmine_2.png", "planetFall2_moonmine_3.png", "planetFall2_moonmine_4.png", "planetFall2_moonmine_5.png",
		"planetFall2_moonmine_6.png"],
	"moon_factory": ["planetFall2_moonfactory_1.png", "planetFall2_moonfactory_2.png", "planetFall2_moonfactory_3.png", "planetFall2_moonfactory_4.png"],
	"moon_wasteland": ["planetFall2_wasteland_1.png", "planetFall2_wasteland_2.png", "planetFall2_wasteland_3.png", "planetFall2_wasteland_4.png",
		"planetFall2_wasteland_5.png"],
};

// short versions of the roles because (a) they're easier to type in, and (b) some of them have multiple copies (ie 01, 02, 03 etc)
this._builtInLocations = {
	main: {
		"capitalCity": "planetFall2_mainSurface_capitalCity",
		"militaryBase": "planetFall2_mainSurface_militaryBase",
		"leisureComplex": "planetFall2_mainSurface_leisureComplex",
		"factory": "planetFall2_mainSurface_factory",
		"dump": "planetFall2_mainSurface_dump"
	},
	planet: {
		"colony": "planetFall2_subSurface_colony",
		"militaryBase": "planetFall2_subSurface_militaryBase",
		"leisureComplex": "planetFall2_subSurface_leisureComplex",
		"factory": "planetFall2_subSurface_factory",
		"dump": "planetFall2_subSurface_dump"
	},
	moon: {
		"colonyDome": "planetFall2_moonSurface_dome",
		"leisureDome": "planetFall2_moonSurface_leisureDome",
		"mine": "planetFall2_moonSurface_mine",
		"prison": "planetFall2_moonSurface_prison",
		"researchComplex": "planetFall2_moonSurface_researchComplex",
		"factory": "planetFall2_moonSurface_factory",
		"wasteLand": "planetFall2_moonSurface_wasteLand"
	},
};

this._locationOverrides = {
};

// all the config you can eat (which will be saved with the players save game)
this._config = {
	equipShip: {
		planetFall2_mainSurface_capitalCity: true,
		planetFall2_mainSurface_militaryBase: true,
		planetFall2_mainSurface_leisureComplex: false,
		planetFall2_mainSurface_factory: true,
		planetFall2_mainSurface_dump: false,
		planetFall2_subSurface_colony: true,
		planetFall2_subSurface_militaryBase: true,
		planetFall2_subSurface_leisureComplex: false,
		planetFall2_subSurface_factory: true,
		planetFall2_subSurface_dump: false,
		planetFall2_moonSurface_dome: true,
		planetFall2_moonSurface_leisureDome: false,
		planetFall2_moonSurface_mine: false,
		planetFall2_moonSurface_prison: false,
		planetFall2_moonSurface_researchComplex: false,
		planetFall2_moonSurface_factory: true,
		planetFall2_moonSurface_wasteLand: false
	},
	marketMethod: { // 0 = none, 1 = copy main planet, 2 = sfep, 3 = original
		planetFall2_mainSurface_capitalCity: 0,
		planetFall2_mainSurface_militaryBase: 0,
		planetFall2_mainSurface_leisureComplex: 0,
		planetFall2_mainSurface_factory: 0,
		planetFall2_mainSurface_dump: 0,
		planetFall2_subSurface_colony: 0,
		planetFall2_subSurface_militaryBase: 0,
		planetFall2_subSurface_leisureComplex: 0,
		planetFall2_subSurface_factory: 0,
		planetFall2_subSurface_dump: 0,
		planetFall2_moonSurface_dome: 0,
		planetFall2_moonSurface_leisureDome: 0,
		planetFall2_moonSurface_mine: 0,
		planetFall2_moonSurface_prison: 0,
		planetFall2_moonSurface_researchComplex: 0,
		planetFall2_moonSurface_factory: 0,
		planetFall2_moonSurface_wasteLand: 0
	},
	maxLocations: { main: 5, planet: 3, moon: 2 },
	chances: {
		main: { city: 0.7, militaryBase: 0.6, leisureComplex: 0.5, factory: 0.5, dump: 0.4 },
		planet: { city: 0.7, militaryBase: 0.25, leisureComplex: 0.2, factory: 0.3, dump: 0.3 },
		moon: { dome: 0.3, leisureComplex: 0.1, mine: 0.3, prison: 0.1, researchComplex: 0.2, factory: 0.2, wasteLand: 0.1 },
		tlFactors: { main: { min: 0.75, max: 1.2 }, planet: { min: 0.5, max: 1.1 }, moon: { min: 0.3, max: 0.8 } },
	},
	transit: {
		allowed: true,
		fee: 20, // cr
		trainSpeed: 90, //kph
		governmentFactor: { "0": 1.3, "1": 0.0, "2": 1.3, "3": 1.5, "4": 0.5, "5": 0.8, "6": 0.0, "7": 0.75 },
	},
	shipTransfer: {
		fee: 1000,
		time: 2.0,
		governmentFactor: { "0": 1.8, "1": 1.7, "2": 1.3, "3": 1.6, "4": 0.5, "5": 0.8, "6": 0.75, "7": 0.5 },
	},
	moveShipyards: true,
	moveRenovation: true,
	useTurbulence: true,
	shallowApproach: true,
	useLandingImages: true,
	useGANames: true,
	atmosphereMax: 15, // km
	beaconRange: 200, // km
	serviceLevelDecrease: 1.0,
	renovationMessage: false,
	turbulenceImpactScale: 1.0,
};

// configuration settings for use in Lib_Config
// first, location limits
this._pfConfig = {
	Name: this.name,
	Alias: expandMissionText("planetFall2_config_alias"),
	Display: expandMissionText("planetFall2_config_display1"),
	Alive: "_pfConfig",
	Notify: "$changeFlags",
	Bool: {
		B0: { Name: "_config.moveShipyards", Def: true, Desc: expandMissionText("planetFall2_move_shipyards") },
		B1: { Name: "_config.moveRenovation", Def: true, Desc: expandMissionText("planetFall2_move_maintenance") },
		B2: { Name: "_config.useTurbulence", Def: true, Desc: expandMissionText("planetFall2_turbulence") },
		B3: { Name: "_config.shallowApproach", Def: true, Desc: expandMissionText("planetFall2_shallow_approach") },
		B4: { Name: "_config.useLandingImages", Def: true, Desc: expandMissionText("planetFall2_landing_images") },
		B5: { Name: "_config.useGANames", Def: true, Desc: expandMissionText("planetFall2_alamanc_names") },
		B6: { Name: "_config.transit.allowed", Def: true, Desc: expandMissionText("planetFall2_transit_allowed") },
		Info: expandMissionText("planetFall2_config_bool1_info")
	},
	SInt: {
		S0: { Name: "_config.maxLocations.main", Def: 5, Min: 2, Max: 10, Desc: expandMissionText("planetFall2_max_main") },
		S1: { Name: "_config.maxLocations.planet", Def: 3, Min: 2, Max: 7, Desc: expandMissionText("planetFall2_max_extra") },
		S2: { Name: "_config.maxLocations.moon", Def: 2, Min: 0, Max: 5, Desc: expandMissionText("planetFall2_max_moon") },
		S3: { Name: "_config.atmosphereMax", Def: 15, Min: 5, Max: 25, Desc: expandMissionText("planetFall2_atmosphere") },
		S4: { Name: "_config.beaconRange", Def: 200, Min: 0, Max: 10000, Desc: expandMissionText("planetFall2_beacon_range") },
		S5: { Name: "_config.serviceLevelDecrease", Def: 1.0, Min: 0.0, Max: 1.0, Desc: expandMissionText("planetFall2_servicelevel_dec"), Float: true },
		S6: { Name: "_config.shipTransfer.time", Def: 2.0, Min: 0.5, Max: 10.0, Desc: expandMissionText("planetFall2_ship_transfer_time"), Float: true },
		S7: { Name: "_config.transit.trainSpeed", Def: 90, Min: 50, Max: 300, Desc: expandMissionText("planetFall2_train_speed") },
		S8: { Name: "_config.turbulenceImpactScale", Def: 1.0, Min: 0.0, Max: 1.0, Desc: expandMissionText("planetFall2_turbulence_scale"), Float: true },
		Info: expandMissionText("planetFall2_config_sint1_info")
	}
};

// then spawning chances for the main planet ...
this._pfConfigMainSpawn = {
	Name: this.name,
	Alias: expandMissionText("planetFall2_config_alias"),
	Display: expandMissionText("planetFall2_config_display2"),
	Alive: "_pfConfigMainSpawn",
	SInt: {
		S0: { Name: "_config.chances.main.city", Def: 0.6, Min: 0.0, Max: 1, Desc: expandMissionText("planetFall2_spawn_city"), Float: true },
		S1: { Name: "_config.chances.main.militaryBase", Def: 0.6, Min: 0.0, Max: 1, Desc: expandMissionText("planetFall2_spawn_military"), Float: true },
		S2: { Name: "_config.chances.main.leisureComplex", Def: 0.5, Min: 0.0, Max: 1, Desc: expandMissionText("planetFall2_spawn_leisure"), Float: true },
		S3: { Name: "_config.chances.main.factory", Def: 0.5, Min: 0.0, Max: 1, Desc: expandMissionText("planetFall2_spawn_factory"), Float: true },
		S4: { Name: "_config.chances.main.dump", Def: 0.4, Min: 0.0, Max: 1, Desc: expandMissionText("planetFall2_spawn_dump"), Float: true },
		Info: expandMissionText("planetFall2_config_sint2_info")
	}
};
// market methods for main planet locations
this._pfConfigMainServices = {
	Name: this.name,
	Alias: expandMissionText("planetFall2_config_alias"),
	Display: expandMissionText("planetFall2_config_display3"),
	Alive: "_pfConfigMainServices",
	Notify: "$changeSettings",
	Bool: {
		B0: { Name: "_config.equipShip.planetFall2_mainSurface_capitalCity", Def: true, Desc: expandMissionText("planetFall2_capital_cities") },
		B1: { Name: "_config.equipShip.planetFall2_mainSurface_militaryBase", Def: true, Desc: expandMissionText("planetFall2_military_bases") },
		B2: { Name: "_config.equipShip.planetFall2_mainSurface_leisureComplex", Def: false, Desc: expandMissionText("planetFall2_leisure_complexes") },
		B3: { Name: "_config.equipShip.planetFall2_mainSurface_factory", Def: true, Desc: expandMissionText("planetFall2_factories") },
		B4: { Name: "_config.equipShip.planetFall2_mainSurface_dump", Def: false, Desc: expandMissionText("planetFall2_dumps") },
		Info: expandMissionText("planetFall2_config_bool3_info")
	},
	SInt: {
		S0: { Name: "_config.marketMethod.planetFall2_mainSurface_capitalCity", Def: 0, Min: 0, Max: 3, Desc: expandMissionText("planetFall2_capital_cities") },
		S1: { Name: "_config.marketMethod.planetFall2_mainSurface_militaryBase", Def: 0, Min: 0, Max: 3, Desc: expandMissionText("planetFall2_military_bases") },
		S2: { Name: "_config.marketMethod.planetFall2_mainSurface_leisureComplex", Def: 0, Min: 0, Max: 3, Desc: expandMissionText("planetFall2_leisure_complexes") },
		S3: { Name: "_config.marketMethod.planetFall2_mainSurface_factory", Def: 0, Min: 0, Max: 3, Desc: expandMissionText("planetFall2_factories") },
		S4: { Name: "_config.marketMethod.planetFall2_mainSurface_dump", Def: 0, Min: 0, Max: 3, Desc: expandMissionText("planetFall2_dumps") },
		Info: expandMissionText("planetFall2_config_sint3_info")
	}
};

// .. extra planets ...
this._pfConfigPlanetSpawn = {
	Name: this.name,
	Alias: expandMissionText("planetFall2_config_alias"),
	Display: expandMissionText("planetFall2_config_display4"),
	Alive: "_pfConfigPlanetSpawn",
	SInt: {
		S0: { Name: "_config.chances.planet.city", Def: 0.6, Min: 0.0, Max: 1, Desc: expandMissionText("planetFall2_spawn_colony"), Float: true },
		S1: { Name: "_config.chances.planet.militaryBase", Def: 0.25, Min: 0.0, Max: 1, Desc: expandMissionText("planetFall2_spawn_military"), Float: true },
		S2: { Name: "_config.chances.planet.leisureComplex", Def: 0.2, Min: 0.0, Max: 1, Desc: expandMissionText("planetFall2_spawn_leisure"), Float: true },
		S3: { Name: "_config.chances.planet.factory", Def: 0.3, Min: 0.0, Max: 1, Desc: expandMissionText("planetFall2_spawn_factory"), Float: true },
		S4: { Name: "_config.chances.planet.dump", Def: 0.3, Min: 0.0, Max: 1, Desc: expandMissionText("planetFall2_spawn_dump"), Float: true },
		Info: expandMissionText("planetFall2_config_sint4_info")
	}
};
// market methods for extra planet locations
this._pfConfigPlanetServices = {
	Name: this.name,
	Alias: expandMissionText("planetFall2_config_alias"),
	Display: expandMissionText("planetFall2_config_display5"),
	Alive: "_pfConfigPlanetServices",
	Notify: "$changeSettings",
	Bool: {
		B0: { Name: "_config.equipShip.planetFall2_subSurface_colony", Def: true, Desc: expandMissionText("planetFall2_colonies") },
		B1: { Name: "_config.equipShip.planetFall2_subSurface_militaryBase", Def: true, Desc: expandMissionText("planetFall2_military_bases") },
		B2: { Name: "_config.equipShip.planetFall2_subSurface_leisureComplex", Def: false, Desc: expandMissionText("planetFall2_leisure_complexes") },
		B3: { Name: "_config.equipShip.planetFall2_subSurface_factory", Def: true, Desc: expandMissionText("planetFall2_factories") },
		B4: { Name: "_config.equipShip.planetFall2_subSurface_dump", Def: false, Desc: expandMissionText("planetFall2_dumps") },
		Info: expandMissionText("planetFall2_config_bool5_info")
	},
	SInt: {
		S0: { Name: "_config.marketMethod.planetFall2_subSurface_colony", Def: 0, Min: 0, Max: 3, Desc: expandMissionText("planetFall2_colonies") },
		S1: { Name: "_config.marketMethod.planetFall2_subSurface_militaryBase", Def: 0, Min: 0, Max: 3, Desc: expandMissionText("planetFall2_military_bases") },
		S2: { Name: "_config.marketMethod.planetFall2_subSurface_leisureComplex", Def: 0, Min: 0, Max: 3, Desc: expandMissionText("planetFall2_leisure_complexes") },
		S3: { Name: "_config.marketMethod.planetFall2_subSurface_factory", Def: 0, Min: 0, Max: 3, Desc: expandMissionText("planetFall2_factories") },
		S4: { Name: "_config.marketMethod.planetFall2_subSurface_dump", Def: 0, Min: 0, Max: 3, Desc: expandMissionText("planetFall2_dumps") },
		Info: expandMissionText("planetFall2_config_sint5_info")
	}
};

// .. and moons.
this._pfConfigMoonSpawn = {
	Name: this.name,
	Alias: expandMissionText("planetFall2_config_alias"),
	Display: expandMissionText("planetFall2_config_display6"),
	Alive: "_pfConfigMoonSpawn",
	SInt: {
		S0: { Name: "_config.chances.moon.dome", Def: 0.3, Min: 0.0, Max: 1, Desc: expandMissionText("planetFall2_spawn_colonydome"), Float: true },
		S1: { Name: "_config.chances.moon.mine", Def: 0.3, Min: 0.0, Max: 1, Desc: expandMissionText("planetFall2_spawn_mine"), Float: true },
		S2: { Name: "_config.chances.moon.leisureComplex", Def: 0.1, Min: 0.0, Max: 1, Desc: expandMissionText("planetFall2_spawn_leisuredome"), Float: true },
		S3: { Name: "_config.chances.moon.factory", Def: 0.2, Min: 0.0, Max: 1, Desc: expandMissionText("planetFall2_spawn_robotfactory"), Float: true },
		S4: { Name: "_config.chances.moon.wasteLand", Def: 0.1, Min: 0.0, Max: 1, Desc: expandMissionText("planetFall2_spawn_wasteland"), Float: true },
		S5: { Name: "_config.chances.moon.prison", Def: 0.1, Min: 0.0, Max: 1, Desc: expandMissionText("planetFall2_spawn_prison"), Float: true },
		S6: { Name: "_config.chances.moon.researchComplex", Def: 0.2, Min: 0.0, Max: 1, Desc: expandMissionText("planetFall2_spawn_research"), Float: true },
		Info: expandMissionText("planetFall2_config_sint6_info")
	}
};
// market methods for moon locations
this._pfConfigMoonServices = {
	Name: this.name,
	Alias: expandMissionText("planetFall2_config_alias"),
	Display: expandMissionText("planetFall2_config_display7"),
	Alive: "_pfConfigMoonServices",
	Notify: "$changeSettings",
	Bool: {
		B0: { Name: "_config.equipShip.planetFall2_moonSurface_dome", Def: true, Desc: expandMissionText("planetFall2_colonydomes") },
		B1: { Name: "_config.equipShip.planetFall2_moonSurface_mine", Def: false, Desc: expandMissionText("planetFall2_mines") },
		B2: { Name: "_config.equipShip.planetFall2_moonSurface_prison", Def: false, Desc: expandMissionText("planetFall2_prisons") },
		B3: { Name: "_config.equipShip.planetFall2_moonSurface_leisureDome", Def: false, Desc: expandMissionText("planetFall2_leisuredomes") },
		B4: { Name: "_config.equipShip.planetFall2_moonSurface_factory", Def: true, Desc: expandMissionText("planetFall2_robotfactories") },
		B5: { Name: "_config.equipShip.planetFall2_moonSurface_researchComplex", Def: false, Desc: expandMissionText("planetFall2_research") },
		B6: { Name: "_config.equipShip.planetFall2_moonSurface_wasteLand", Def: false, Desc: expandMissionText("planetFall2_wastelands") },
		Info: expandMissionText("planetFall2_config_bool7_info")
	},
	SInt: {
		S0: { Name: "_config.marketMethod.planetFall2_moonSurface_dome", Def: 0, Min: 0, Max: 3, Desc: expandMissionText("planetFall2_colonydomes") },
		S1: { Name: "_config.marketMethod.planetFall2_moonSurface_mine", Def: 0, Min: 0, Max: 3, Desc: expandMissionText("planetFall2_mines") },
		S2: { Name: "_config.marketMethod.planetFall2_moonSurface_prison", Def: 0, Min: 0, Max: 3, Desc: expandMissionText("planetFall2_prisons") },
		S3: { Name: "_config.marketMethod.planetFall2_moonSurface_leisureDome", Def: 0, Min: 0, Max: 3, Desc: expandMissionText("planetFall2_leisuredomes") },
		S4: { Name: "_config.marketMethod.planetFall2_moonSurface_factory", Def: 0, Min: 0, Max: 3, Desc: expandMissionText("planetFall2_robotfactories") },
		S5: { Name: "_config.marketMethod.planetFall2_moonSurface_researchComplex", Def: 0, Min: 0, Max: 3, Desc: expandMissionText("planetFall2_research") },
		S6: { Name: "_config.marketMethod.planetFall2_moonSurface_wasteLand", Def: 0, Min: 0, Max: 3, Desc: expandMissionText("planetFall2_wastelands") },
		Info: expandMissionText("planetFall2_config_sint7_info")
	}
};

// additional factors applied to spawn chances, based on TL
this._pfConfigTLFactors = {
	Name: this.name,
	Alias: expandMissionText("planetFall2_config_alias"),
	Display: expandMissionText("planetFall2_config_display8"),
	Alive: "_pfConfigTLFactors",
	SInt: {
		S0: { Name: "_config.chances.tlFactors.main.min", Def: 0.75, Min: 0.0, Max: 2, Desc: expandMissionText("planetFall2_tl_main_min"), Float: true },
		S1: { Name: "_config.chances.tlFactors.main.max", Def: 1.2, Min: 0.0, Max: 2, Desc: expandMissionText("planetFall2_tl_main_max"), Float: true },
		S2: { Name: "_config.chances.tlFactors.planet.min", Def: 0.5, Min: 0.0, Max: 2, Desc: expandMissionText("planetFall2_tl_planet_min"), Float: true },
		S3: { Name: "_config.chances.tlFactors.planet.max", Def: 1.1, Min: 0.0, Max: 2, Desc: expandMissionText("planetFall2_tl_planet_max"), Float: true },
		S4: { Name: "_config.chances.tlFactors.moon.min", Def: 0.3, Min: 0.0, Max: 2, Desc: expandMissionText("planetFall2_tl_moon_min"), Float: true },
		S5: { Name: "_config.chances.tlFactors.moon.max", Def: 0.8, Min: 0.0, Max: 2, Desc: expandMissionText("planetFall2_tl_moon_max"), Float: true },
		Info: expandMissionText("planetFall2_config_sint8_info")
	}
};

// transit costs
this._pfConfigTransit = {
	Name: this.name,
	Alias: expandMissionText("planetFall2_config_alias"),
	Display: expandMissionText("planetFall2_config_display9"),
	Alive: "_pfConfigTransit",
	SInt: {
		S0: { Name: "_config.transit.fee", Def: 20, Min: 0, Max: 2000, Desc: expandMissionText("planetFall2_transit_cost_base") },
		S1: { Name: "_config.transit.governmentFactor.0", Def: 1.3, Min: 0.0, Max: 100.0, Desc: expandMissionText("planetFall2_anarchy"), Float: true },
		S2: { Name: "_config.transit.governmentFactor.1", Def: 0.0, Min: 0.0, Max: 100.0, Desc: expandMissionText("planetFall2_feudal"), Float: true },
		S3: { Name: "_config.transit.governmentFactor.2", Def: 1.3, Min: 0.0, Max: 100.0, Desc: expandMissionText("planetFall2_multigov"), Float: true },
		S4: { Name: "_config.transit.governmentFactor.3", Def: 1.5, Min: 0.0, Max: 100.0, Desc: expandMissionText("planetFall2_dictatorship"), Float: true },
		S5: { Name: "_config.transit.governmentFactor.4", Def: 0.5, Min: 0.0, Max: 100.0, Desc: expandMissionText("planetFall2_communist"), Float: true },
		S6: { Name: "_config.transit.governmentFactor.5", Def: 0.8, Min: 0.0, Max: 100.0, Desc: expandMissionText("planetFall2_confederacy"), Float: true },
		S7: { Name: "_config.transit.governmentFactor.6", Def: 0.0, Min: 0.0, Max: 100.0, Desc: expandMissionText("planetFall2_democracy"), Float: true },
		S8: { Name: "_config.transit.governmentFactor.7", Def: 0.75, Min: 0.0, Max: 100.0, Desc: expandMissionText("planetFall2_corpstate"), Float: true },
		Info: expandMissionText("planetFall2_config_sint9_info")
	}
};

// ship transfer costs
this._pfConfigTransfer = {
	Name: this.name,
	Alias: expandMissionText("planetFall2_config_alias"),
	Display: expandMissionText("planetFall2_config_display10"),
	Alive: "_pfConfigTransfer",
	SInt: {
		S0: { Name: "_config.shipTransfer.fee", Def: 1000, Min: 0, Max: 10000, Desc: expandMissionText("planetFall2_transfer_cost_base") },
		S1: { Name: "_config.shipTransfer.governmentFactor.0", Def: 1.8, Min: 0.0, Max: 10.0, Desc: expandMissionText("planetFall2_anarchy"), Float: true },
		S2: { Name: "_config.shipTransfer.governmentFactor.1", Def: 1.7, Min: 0.0, Max: 10.0, Desc: expandMissionText("planetFall2_feudal"), Float: true },
		S3: { Name: "_config.shipTransfer.governmentFactor.2", Def: 1.3, Min: 0.0, Max: 10.0, Desc: expandMissionText("planetFall2_multigov"), Float: true },
		S4: { Name: "_config.shipTransfer.governmentFactor.3", Def: 0.5, Min: 0.0, Max: 10.0, Desc: expandMissionText("planetFall2_dictatorship"), Float: true },
		S5: { Name: "_config.shipTransfer.governmentFactor.4", Def: 0.5, Min: 0.0, Max: 10.0, Desc: expandMissionText("planetFall2_communist"), Float: true },
		S6: { Name: "_config.shipTransfer.governmentFactor.5", Def: 0.8, Min: 0.0, Max: 10.0, Desc: expandMissionText("planetFall2_confederacy"), Float: true },
		S7: { Name: "_config.shipTransfer.governmentFactor.6", Def: 0.75, Min: 0.0, Max: 10.0, Desc: expandMissionText("planetFall2_democracy"), Float: true },
		S8: { Name: "_config.shipTransfer.governmentFactor.7", Def: 0.5, Min: 0.0, Max: 10.0, Desc: expandMissionText("planetFall2_corpstate"), Float: true },
		Info: expandMissionText("planetFall2_config_sint10_info")
	}
};

this._prePopulationFunctions = [];

//-------------------------------------------------------------------------------------------------------------
this.$changeFlags = function () {
	if (worldScripts.RandomStationNames) {
		worldScripts.PlanetFall2_Names.rsnInUse = this._config.useGANames;
		if (worldScripts.Feudal_PlanetFall2) {
			worldScripts.Feudal_PlanetFall2.rsnInUse = this._config.useGANames;
		}
	}
	worldScripts.PlanetFall2_Transit.shipDockedWithStation(player.ship.dockedStation);
}

//-------------------------------------------------------------------------------------------------------------
this.$changeSettings = function () {
	var mi = worldScripts["market_inquirer"];
	if (!mi) return;
	var ar = mi.$inquirerStations;
	var keys = Object.keys(this._config.marketMethod);
	var i = keys.length;
	while (i--) {
		// check for a disabled market
		if (this._config.marketMethod[keys[i]] == 0 && ar.indexOf(keys[i]) >= 0) {
			ar.splice(ar.indexOf(keys[i]), 1);
		}
		// check for a newly established market
		if (this._config.marketMethod[keys[i]] > 0 && ar.indexOf(keys[i]) == -1) {
			ar.push(keys[i]);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$addPrepopulationFunction = function (wsName, fnName) {
	this._prePopulationFunctions.push({ ws: wsName, fn: fnName });
}

//-------------------------------------------------------------------------------------------------------------
this.$prepopulationControls = function () {
	var i = this._prePopulationFunctions.length;
	while (i--) {
		var item = this._prePopulationFunctions[i];
		worldScripts[item.ws][item.fn]();
	}
	// run the main scripts extra population function after all the others
	this.$extraPopulation();
}

//-------------------------------------------------------------------------------------------------------------
// public function to allow OXP's to determine whether player ship is currently local to them or not.
this.playerShipIsLocal = function () {
	if (!missionVariables.PlanetFall2_DockedStation) return true;
	if (player.ship.dockedStation.hasRole("planetFall2_surface")) {
		if (missionVariables.PlanetFall2_DockedStation == player.ship.dockedStation.dataKey) {
			return true;
		} else {
			return false;
		}
	} else {
		return true;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.startUp = function () {
	if (missionVariables.PlanetFall2_Config) {
		this._config = JSON.parse(missionVariables.PlanetFall2_Config);
		// make sure any newly added properties are added into the config.
		if (!this._config.hasOwnProperty("beaconRange")) {
			this._config.beaconRange = 200;
		}
		if (this._config.hasOwnProperty("planetRotation")) {
			this._config.planetRotation = null;
		}
		if (!this._config.hasOwnProperty("serviceLevelDecrease")) {
			this._config.serviceLevelDecrease = 1.0;
		}
		if (!this._config.hasOwnProperty("transit")) {
			this._config.transit = {
				allowed: true,
				fee: 20,
				trainSpeed: 90,
				governmentFactor: { "0": 1.3, "1": 0.0, "2": 1.3, "3": 1.5, "4": 0.5, "5": 0.8, "6": 0.0, "7": 0.75 },
			};
		}
		if (!this._config.hasOwnProperty("shipTransfer")) {
			this._config.shipTransfer = {
				fee: 1000,
				time: 2.0,
				governmentFactor: { "0": 1.8, "1": 1.7, "2": 1.3, "3": 1.6, "4": 0.5, "5": 0.8, "6": 0.75, "7": 0.5 },
			};
		}
		if (!this._config.hasOwnProperty("turbulenceImpactScale")) {
			this._config.turbulenceImpactScale = 1.0;
		}
		if (!this._config.hasOwnProperty("shallowApproach")) {
			this._config.shallowApproach = true;
		}
		// a bit of cleanup, if required.
		if (this._config.chances.main.oohaul) {
			delete this._config.chances.main.oohaul;
			delete this._config.chances.main.blackmonks;
			delete this._config.chances.main.hoopyCasino;
			delete this._config.chances.planet.oohaul;
			delete this._config.chances.planet.blackmonks;
			delete this._config.chances.planet.hoopyCasino;
			delete this._config.chances.moon.oohaul;
			delete this._config.chances.moon.blackmonks;
			delete this._config.chances.moon.hoopyCasino;
		}
	}
	this.lastPlanet = system.mainPlanet; // details of the last planet to be landed on, so we know what to move the player away from on launch

	if (missionVariables.hasOwnProperty("PlanetFall2_SubServiceLevel")) {
		this.subServiceLevel = parseFloat(missionVariables.PlanetFall2_SubServiceLevel);
	}

	if (missionVariables.hasOwnProperty("PlanetFall2_Override")) {
		this.planetFallOverride = (missionVariables.PlanetFall2_Override == "yes" ? true : false);
		this.overrideRole = missionVariables.PlanetFall2_OverrideRole;
	}

	if (worldScripts["System Redux"]) this.additionalPlanets = true;

	/* For convenience of mission writers, access for other OXPs is via "worldScripts.PlanetFall2.lastPlanet" and "worldScripts.PlanetFall2.lastPlanetType". 
	Values for lastPlanetType are "Prime" for the main planet, "Sub" for every other planet, "Moon" for any moon and "Sun" / "GasGiant" for a sun or a Solar System OXP 
	gas giant (for completeness - cannot land on either of the last two). */

	// sdc integration
	// because planetfall docks only exist for the player, we need to tell SDC to ignore all of them
	if (worldScripts.StationDockControl) {
		var sdc = worldScripts.StationDockControl;
		sdc._trafficNone.push("planetFall2_surface");
	}
	this.shipWillEnterWitchspace();

	// update docking fees (if installed)
	if (worldScripts["Docking Fees"]) {
		worldScripts["Docking Fees"].$addFee("planetFall2_surface", { multiplier: 1.2, messageKey: "dockfee_planet" }); // planets are slightly more to discourage spacers
	}
}

//-------------------------------------------------------------------------------------------------------------
this.startUpComplete = function () {
	// register our settings, if Lib_Config is present
	if (worldScripts.Lib_Config) {
		var lib = worldScripts.Lib_Config;
		lib._registerSet(this._pfConfig);
		lib._registerSet(this._pfConfigMainServices);
		lib._registerSet(this._pfConfigPlanetServices);
		lib._registerSet(this._pfConfigMoonServices);
		lib._registerSet(this._pfConfigMainSpawn);
		lib._registerSet(this._pfConfigPlanetSpawn);
		lib._registerSet(this._pfConfigMoonSpawn);
		lib._registerSet(this._pfConfigTLFactors);
		lib._registerSet(this._pfConfigTransit);
		lib._registerSet(this._pfConfigTransfer);
	}
	// make sure the lastPlanet and lastPlanetType are populated after a reload
	if (player.ship.dockedStation.hasRole("planetFall2_surface")) {
		this.shipApproachingPlanetSurface(this.$getPlanetFromCoords(player.ship.dockedStation.linkedPlanetPos));
	}
	this.monitorPoints = system.shipsWithRole("planetFall2_surface")
	this.$turnOnAllBeacons();
}

//-------------------------------------------------------------------------------------------------------------
this.playerWillSaveGame = function () {
	missionVariables.PlanetFall2_Config = JSON.stringify(this._config);
	missionVariables.PlanetFall2_SubServiceLevel = this.subServiceLevel;
	if (this.planetFallOverride) {
		missionVariables.PlanetFall2_Override = "yes";
		missionVariables.PlanetFall2_OverrideRole = this.overrideRole;
	} else {
		delete missionVariables.PlanetFall2_Override;
		delete missionVariables.PlanetFall2_OverrideRole;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillEnterWitchspace = function () {
	// reset the list of available keys for the next system
	this.usedKeys = [];
	this.overrideRole = "default";
	if (this.landingMonitor && this.landingMonitor.isRunning) this.landingMonitor.stop();
}

//-------------------------------------------------------------------------------------------------------------
this.shipExitedWitchspace = function () {
	this.disable = false; // reset the disabled flag
	this.monitorPoints = system.shipsWithRole("planetFall2_surface");
	this.landingMonitor = new Timer(this, this.$monitorDockPoints.bind(this), 1, 1);
}


//-------------------------------------------------------------------------------------------------------------
this.systemWillPopulate = function () {
	if (this.disable) return;
	this.globalCount = 0;
	this.doRepopulate = true
	if (!additionalPlanets) this.$prepopulationControls();
}

//-------------------------------------------------------------------------------------------------------------
// called either by the PlanetFall2_MonkeyPatch.$prepopulationControls
this.$extraPopulation = function () {
	this.$addExtraPlanetDocks(system.mainPlanet.position, system.mainPlanet.radius, system.mainPlanet.hasAtmosphere, true);
}

//-------------------------------------------------------------------------------------------------------------
this.systemWillRepopulate = function () {
	// we only do this once per system
	if (this.doRepopulate) {
		this.doRepopulate = false;
		this.indexPlanets = 0;
		this.indexMoons = 0;
		this.$systemScan();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$removeUsedKeys = function (keylist) {
	if (!keylist) return [];
	var i = keylist.length;
	var used = this.usedKeys;
	while (i--) {
		if (used.indexOf(keylist[i]) >= 0) keylist.splice(i, 1);
	}
	return keylist;
}

//-------------------------------------------------------------------------------------------------------------
this.$removeOXPKeys = function (keylist, ovr) {
	if (!keylist) return [];
	if (!ovr || ovr.length == 0) return keylist;
	var i = keylist.length;
	while (i--) {
		if (ovr.indexOf(keylist[i]) >= 0) keylist.splice(i, 1);
	}
	return keylist;
}

//-------------------------------------------------------------------------------------------------------------
// if we have the planet object to work with
this.$addExtraPlanetDocksForPlanet = function (planet) {
	this.$addExtraPlanetDocks(planet.position, planet.radius, planet.hasAtmosphere, planet.isMainPlanet);
}

//-------------------------------------------------------------------------------------------------------------
// if we only have the specs of the planet
this.$addExtraPlanetDocks = function $addExtraPlanetDocks(coords, radius, hasAtmosphere, isMain) {
	function compare(a, b) {
		return (b > a ? -1 : 1);
	}
	if (this.disable) return;

	var pfKeys = new Array();
	var role = "";
	var distOffset = 16;
	var maxLocations = 0;
	var ovr = "";
	var tlFactor = 1;
	var typ = "";
	var names = [];
	var isCustom = false;
	var idx = 0;

	if (isMain) {
		typ = "main";
		role = "planetFall2_mainSurface";
		maxLocations = this._config.maxLocations.main;
		ovr = "planetFall2_mainSurface_externalOXP";
		tlFactor = this.$getTLFactor("main");
	} else if (hasAtmosphere) {
		typ = "planet";
		role = "planetFall2_subSurface";
		maxLocations = this._config.maxLocations.planet;
		distOffset = 17;
		ovr = "planetFall2_subSurface_externalOXP";
		tlFactor = this.$getTLFactor("planet");
		idx = this.indexPlanets;
		this.indexPlanets += 1;
	} else {
		typ = "moon";
		role = "planetFall2_moonSurface";
		maxLocations = this._config.maxLocations.moon;
		distOffset = 18;
		ovr = "planetFall2_moonSurface_externalOXP";
		tlFactor = this.$getTLFactor("moon");
		idx = this.indexMoons;
		this.indexMoons += 1;
	}

	if (this.planetFallOverride == true) {
		if (this.overrideRole != "default") ovr = this.overrideRole;
		// get the OXP keys for the override role
		pfKeys = this.$removeUsedKeys(Ship.keysForRole(ovr));
	} else {
		// check to see if a custom setup has been defined for this system
		var custom = this._locationOverrides[galaxyNumber + " " + system.ID];
		if (custom && custom[typ]) {
			// check that we have a definition for this planet/moon
			if (idx <= custom[typ].length - 1) {
				isCustom = true;
				var list = custom[typ][idx].roles;
				names = custom[typ][idx].names;
				for (var i = 0; i < list.length; i++) {
					role = list[i];
					var comp = Object.keys(this._builtInLocations[typ]);
					var rolelookup = "";
					// a blank role here means "pick a random one"
					if (role == "") {
						if (isMain) {
							role = "planetFall2_mainSurface";
						} else if (hasAtmosphere) {
							role = "planetFall2_subSurface";
						} else {
							role = "planetFall2_moonSurface";
						}
					}
					if (comp.indexOf(role) >= 0) {
						rolelookup = this._builtInLocations[typ][role];
					} else {
						rolelookup = role;
					}
					var keys = this.$removeUsedKeys(Ship.keysForRole(rolelookup));
					if (!keys || keys.length == 0) {
						log(this.name, "no shipkeys found for role " + rolelookup);
					} else {
						// only add one key per custom entry
						pfKeys.push(keys[0]);
						// add this role into 
						this.usedKeys.push(keys[0]);
					}
				}
				// look for and add any OXP locations.
				var add = Ship.keysForRole(ovr);
				if (add.length > 0) {
					pfKeys = pfKeys.concat(add);
					this.usedKeys = this.usedKeys.concat(add);
				}
			}
		}
		// otherwise, it's a default system setup
		if (!isCustom) {
			pfKeys = this.$removeUsedKeys(Ship.keysForRole(role));
		}
	}

	if (pfKeys.length == 0) { // no more keys to allocate!
		log(this.name, "No more keys available to allocate!");
		return;
	}
	// only sort these if we aren't using a custom set
	// when using a custom set, we need the order to stay the same as what's in the data
	if (!isCustom) pfKeys.sort(compare);

	var pfn = worldScripts.PlanetFall2_Names;
	var chances = this._config.chances;

	// array for tracking what we've added to this planet, so we don't end up adding everything to the one place
	var types = [];
	var locationCount = 0;

	for (var i = 0; i < pfKeys.length; i++) {
		var name = ""; // holds the displayName we generate
		var addDock = false; // flag to determine if a particular dock is to be added
		// get a copy of the ship key, but with the last character removed (which is normally something like 1-10)
		var seedOffset = parseInt(pfKeys[i].substring(pfKeys[i].length - 2, pfKeys[i].length)) - 1;
		var generalKey = pfKeys[i].substring(0, pfKeys[i].length - 2);
		// some OXP keys won't have a numeric last character, in which case the seedOffset would be NaN
		// so switch the generalKey back to being the actual key
		if (isNaN(seedOffset)) { generalKey = pfKeys[i]; seedOffset = 0; }

		switch (pfKeys[i]) {
			case "planetFall2_mainSurface_capitalCity01":
				// always add one of these
				addDock = true;
				name = pfn.$PF_CapitalCity(pfKeys[i]);
				break;
			case "planetFall2_mainSurface_capitalCity02":
			case "planetFall2_mainSurface_capitalCity03":
			case "planetFall2_mainSurface_capitalCity04":
			case "planetFall2_mainSurface_capitalCity05":
			case "planetFall2_mainSurface_capitalCity06":
			case "planetFall2_mainSurface_capitalCity07":
			case "planetFall2_mainSurface_capitalCity08":
			case "planetFall2_mainSurface_capitalCity09":
			case "planetFall2_mainSurface_capitalCity10":
				// additional cities are random chances (that reduces each loop)
				if (isCustom || (locationCount < (maxLocations * 0.6) && system.scrambledPseudoRandomNumber(2023 + seedOffset) < ((chances.main.city * tlFactor) - (0.08 * i)))) {
					addDock = true;
					name = pfn.$PF_CapitalCity(pfKeys[i]);
				}
				break;
			case "planetFall2_mainSurface_militaryBase01":
			case "planetFall2_mainSurface_militaryBase02":
			case "planetFall2_mainSurface_militaryBase03":
			case "planetFall2_mainSurface_militaryBase04":
			case "planetFall2_mainSurface_militaryBase05":
			case "planetFall2_mainSurface_militaryBase06":
			case "planetFall2_mainSurface_militaryBase07":
			case "planetFall2_mainSurface_militaryBase08":
			case "planetFall2_mainSurface_militaryBase09":
			case "planetFall2_mainSurface_militaryBase10":
				if (types.indexOf(generalKey) == -1) {
					if (isCustom || (locationCount < maxLocations && system.government > 2 && system.government < 6 && system.techLevel > 7 && system.scrambledPseudoRandomNumber(333) < (chances.main.militaryBase * tlFactor))) {
						addDock = true;
						name = pfn.$PF_MilitaryBase(pfKeys[i]);
					}
				}
				break;
			case "planetFall2_mainSurface_leisureComplex01":
			case "planetFall2_mainSurface_leisureComplex02":
			case "planetFall2_mainSurface_leisureComplex03":
			case "planetFall2_mainSurface_leisureComplex04":
			case "planetFall2_mainSurface_leisureComplex05":
			case "planetFall2_mainSurface_leisureComplex06":
			case "planetFall2_mainSurface_leisureComplex07":
			case "planetFall2_mainSurface_leisureComplex08":
			case "planetFall2_mainSurface_leisureComplex09":
			case "planetFall2_mainSurface_leisureComplex10":
				if (types.indexOf(generalKey) == -1) {
					if (isCustom || (locationCount < maxLocations && system.government > 2 && system.techLevel > 4 && system.scrambledPseudoRandomNumber(220) < (chances.main.leisureComplex * tlFactor))) {
						addDock = true;
						name = pfn.$PF_LeisureComplex(pfKeys[i]);
					}
				}
				break;
			case "planetFall2_mainSurface_factory01":
			case "planetFall2_mainSurface_factory02":
			case "planetFall2_mainSurface_factory03":
			case "planetFall2_mainSurface_factory04":
			case "planetFall2_mainSurface_factory05":
			case "planetFall2_mainSurface_factory06":
			case "planetFall2_mainSurface_factory07":
			case "planetFall2_mainSurface_factory08":
			case "planetFall2_mainSurface_factory09":
			case "planetFall2_mainSurface_factory10":
				if (types.indexOf(generalKey) == -1) {
					if (isCustom || (locationCount < maxLocations && system.economy < 4 && system.scrambledPseudoRandomNumber(823) < (chances.main.factory * tlFactor))) {
						addDock = true;
						name = pfn.$PF_Factory(pfKeys[i]);
					}
				}
				break;
			case "planetFall2_mainSurface_dump01":
			case "planetFall2_mainSurface_dump02":
			case "planetFall2_mainSurface_dump03":
			case "planetFall2_mainSurface_dump04":
			case "planetFall2_mainSurface_dump05":
			case "planetFall2_mainSurface_dump06":
			case "planetFall2_mainSurface_dump07":
				if (types.indexOf(generalKey) == -1) {
					if (isCustom || (locationCount < maxLocations && system.economy < 4 && system.scrambledPseudoRandomNumber(63) < (chances.main.dump * tlFactor))) {
						addDock = true;
						name = pfn.$PF_Dump(pfKeys[i]);
					}
				}
				break;

			case "planetFall2_subSurface_colony01":
			case "planetFall2_subSurface_colony02":
			case "planetFall2_subSurface_colony03":
			case "planetFall2_subSurface_colony04":
			case "planetFall2_subSurface_colony05":
			case "planetFall2_subSurface_colony06":
			case "planetFall2_subSurface_colony07":
			case "planetFall2_subSurface_colony08":
			case "planetFall2_subSurface_colony09":
			case "planetFall2_subSurface_colony10":
			case "planetFall2_subSurface_colony11":
			case "planetFall2_subSurface_colony12":
			case "planetFall2_subSurface_colony13":
			case "planetFall2_subSurface_colony14":
			case "planetFall2_subSurface_colony15":
			case "planetFall2_subSurface_colony16":
			case "planetFall2_subSurface_colony17":
			case "planetFall2_subSurface_colony18":
			case "planetFall2_subSurface_colony19":
			case "planetFall2_subSurface_colony20":
				// we will add at least 1 colony city, after which it's a random chance (which reduces each loop)
				if (isCustom || (locationCount < (maxLocations * 0.6) && (types.indexOf(generalKey) == -1 || system.scrambledPseudoRandomNumber(4113 + seedOffset) < ((chances.planet.city * tlFactor) - (0.04 * i))))) {
					addDock = true;
					name = pfn.$PF_ColonyCity(pfKeys[i]);
				}
				break;
			case "planetFall2_subSurface_militaryBase01":
			case "planetFall2_subSurface_militaryBase02":
			case "planetFall2_subSurface_militaryBase03":
			case "planetFall2_subSurface_militaryBase04":
			case "planetFall2_subSurface_militaryBase05":
			case "planetFall2_subSurface_militaryBase06":
			case "planetFall2_subSurface_militaryBase07":
			case "planetFall2_subSurface_militaryBase08":
			case "planetFall2_subSurface_militaryBase09":
			case "planetFall2_subSurface_militaryBase10":
				if (types.indexOf(generalKey) == -1) {
					if (isCustom || (locationCount < maxLocations && system.government > 2 && system.government < 6 && system.techLevel > 7 && system.scrambledPseudoRandomNumber(24 + seedOffset) < ((chances.planet.militaryBase * tlFactor) - (0.01 * seedOffset)))) {
						addDock = true;
						name = pfn.$PF_ColonyMilitaryBase(pfKeys[i]);
					}
				}
				break;
			case "planetFall2_subSurface_leisureComplex01":
			case "planetFall2_subSurface_leisureComplex02":
			case "planetFall2_subSurface_leisureComplex03":
			case "planetFall2_subSurface_leisureComplex04":
			case "planetFall2_subSurface_leisureComplex05":
			case "planetFall2_subSurface_leisureComplex06":
			case "planetFall2_subSurface_leisureComplex07":
			case "planetFall2_subSurface_leisureComplex08":
			case "planetFall2_subSurface_leisureComplex09":
			case "planetFall2_subSurface_leisureComplex10":
				if (types.indexOf(generalKey) == -1) {
					if (isCustom || (locationCount < maxLocations && system.government > 2 && system.techLevel > 4 && system.scrambledPseudoRandomNumber(220 + seedOffset) < ((chances.planet.leisureComplex * tlFactor) - (0.01 * seedOffset)))) {
						addDock = true;
						name = pfn.$PF_LeisureComplex(pfKeys[i]);
					}
				}
				break;
			case "planetFall2_subSurface_factory01":
			case "planetFall2_subSurface_factory02":
			case "planetFall2_subSurface_factory03":
			case "planetFall2_subSurface_factory04":
			case "planetFall2_subSurface_factory05":
			case "planetFall2_subSurface_factory06":
			case "planetFall2_subSurface_factory07":
			case "planetFall2_subSurface_factory08":
			case "planetFall2_subSurface_factory09":
			case "planetFall2_subSurface_factory10":
				if (types.indexOf(generalKey) == -1) {
					if (isCustom || (locationCount < maxLocations && system.economy < 4 && system.scrambledPseudoRandomNumber(1190 + seedOffset) < ((chances.planet.factory * tlFactor) - (0.01 * seedOffset)))) {
						addDock = true;
						name = pfn.$PF_Factory(pfKeys[i]);
					}
				}
				break;
			case "planetFall2_subSurface_dump01":
			case "planetFall2_subSurface_dump02":
			case "planetFall2_subSurface_dump03":
			case "planetFall2_subSurface_dump04":
			case "planetFall2_subSurface_dump05":
			case "planetFall2_subSurface_dump06":
			case "planetFall2_subSurface_dump07":
				if (types.indexOf(generalKey) == -1) {
					if (isCustom || (locationCount < maxLocations && system.economy < 4 && system.scrambledPseudoRandomNumber(87 + seedOffset) < ((chances.planet.dump * tlFactor) - (0.01 * seedOffset)))) {
						addDock = true;
						name = pfn.$PF_Dump(pfKeys[i]);
					}
				}
				break;

			case "planetFall2_moonSurface_dome01":
			case "planetFall2_moonSurface_dome02":
			case "planetFall2_moonSurface_dome03":
			case "planetFall2_moonSurface_dome04":
			case "planetFall2_moonSurface_dome05":
			case "planetFall2_moonSurface_dome06":
			case "planetFall2_moonSurface_dome07":
			case "planetFall2_moonSurface_dome08":
			case "planetFall2_moonSurface_dome09":
			case "planetFall2_moonSurface_dome10":
			case "planetFall2_moonSurface_dome11":
			case "planetFall2_moonSurface_dome12":
			case "planetFall2_moonSurface_dome13":
			case "planetFall2_moonSurface_dome14":
			case "planetFall2_moonSurface_dome15":
			case "planetFall2_moonSurface_dome16":
			case "planetFall2_moonSurface_dome17":
			case "planetFall2_moonSurface_dome18":
			case "planetFall2_moonSurface_dome19":
			case "planetFall2_moonSurface_dome20":
				if (types.indexOf(generalKey) == -1) {
					if (isCustom || (locationCount < maxLocations && system.scrambledPseudoRandomNumber(342 + seedOffset) < ((chances.moon.dome * tlFactor) - (0.01 * seedOffset)))) {
						addDock = true;
						name = pfn.$PF_MoonDome(pfKeys[i]);
					}
				}
				break;
			case "planetFall2_moonSurface_leisureDome01":
			case "planetFall2_moonSurface_leisureDome02":
			case "planetFall2_moonSurface_leisureDome03":
			case "planetFall2_moonSurface_leisureDome04":
			case "planetFall2_moonSurface_leisureDome05":
			case "planetFall2_moonSurface_leisureDome06":
			case "planetFall2_moonSurface_leisureDome07":
			case "planetFall2_moonSurface_leisureDome08":
			case "planetFall2_moonSurface_leisureDome09":
			case "planetFall2_moonSurface_leisureDome10":
				if (types.indexOf(generalKey) == -1) {
					if (isCustom || (locationCount < maxLocations && !worldScripts["Random_Hits"] && system.government > 2 && system.techLevel > 4 && system.scrambledPseudoRandomNumber(477 + seedOffset) < ((chances.moon.leisureComplex * tlFactor) - (0.01 * seedOffset)))) {
						addDock = true;
						name = pfn.$PF_LeisureComplex(pfKeys[i]);
					}
				}
				break;
			case "planetFall2_moonSurface_mine01":
			case "planetFall2_moonSurface_mine02":
			case "planetFall2_moonSurface_mine03":
			case "planetFall2_moonSurface_mine04":
			case "planetFall2_moonSurface_mine05":
			case "planetFall2_moonSurface_mine06":
			case "planetFall2_moonSurface_mine07":
			case "planetFall2_moonSurface_mine08":
			case "planetFall2_moonSurface_mine09":
			case "planetFall2_moonSurface_mine10":
				if (types.indexOf(generalKey) == -1) {
					if (isCustom || (locationCount < maxLocations && system.scrambledPseudoRandomNumber(20 + seedOffset) < ((chances.moon.mine * tlFactor) - (0.01 * seedOffset)))) {
						addDock = true;
						name = pfn.$PF_MoonMine(pfKeys[i]);
					}
				}
				break;
			case "planetFall2_moonSurface_prison01":
			case "planetFall2_moonSurface_prison02":
			case "planetFall2_moonSurface_prison03":
			case "planetFall2_moonSurface_prison04":
			case "planetFall2_moonSurface_prison05":
			case "planetFall2_moonSurface_prison06":
			case "planetFall2_moonSurface_prison07":
			case "planetFall2_moonSurface_prison08":
			case "planetFall2_moonSurface_prison09":
			case "planetFall2_moonSurface_prison10":
				if (types.indexOf(generalKey) == -1) {
					if (isCustom || (locationCount < maxLocations && system.scrambledPseudoRandomNumber(95 + seedOffset) < ((chances.moon.prison * tlFactor) - (0.01 * seedOffset)))) {
						addDock = true;
						name = pfn.$PF_MoonPrison(pfKeys[i]);
					}
				}
				break;
			case "planetFall2_moonSurface_researchComplex01":
			case "planetFall2_moonSurface_researchComplex02":
			case "planetFall2_moonSurface_researchComplex03":
			case "planetFall2_moonSurface_researchComplex04":
			case "planetFall2_moonSurface_researchComplex05":
			case "planetFall2_moonSurface_researchComplex06":
			case "planetFall2_moonSurface_researchComplex07":
			case "planetFall2_moonSurface_researchComplex08":
			case "planetFall2_moonSurface_researchComplex09":
			case "planetFall2_moonSurface_researchComplex10":
				if (types.indexOf(generalKey) == -1) {
					if (isCustom || (locationCount < maxLocations && system.techLevel > 11 && system.scrambledPseudoRandomNumber(1178 + seedOffset) < ((chances.moon.researchComplex * tlFactor) - (0.01 * seedOffset)))) {
						addDock = true;
						name = pfn.$PF_MoonResearch(pfKeys[i]);
					}
				}
				break;
			case "planetFall2_moonSurface_factory01":
			case "planetFall2_moonSurface_factory02":
			case "planetFall2_moonSurface_factory03":
			case "planetFall2_moonSurface_factory04":
			case "planetFall2_moonSurface_factory05":
			case "planetFall2_moonSurface_factory06":
			case "planetFall2_moonSurface_factory07":
			case "planetFall2_moonSurface_factory08":
			case "planetFall2_moonSurface_factory09":
			case "planetFall2_moonSurface_factory10":
				if (types.indexOf(generalKey) == -1) {
					if (isCustom || (locationCount < maxLocations && system.techLevel <= 11 && system.scrambledPseudoRandomNumber(4783 + seedOffset) < ((chances.moon.factory * tlFactor) - (0.01 * seedOffset)))) {
						addDock = true;
						name = pfn.$PF_MoonFactory(pfKeys[i]);
					}
				}
				break;
			case "planetFall2_moonSurface_wasteLand01":
			case "planetFall2_moonSurface_wasteLand02":
			case "planetFall2_moonSurface_wasteLand03":
			case "planetFall2_moonSurface_wasteLand04":
			case "planetFall2_moonSurface_wasteLand05":
			case "planetFall2_moonSurface_wasteLand06":
			case "planetFall2_moonSurface_wasteLand07":
				if (types.indexOf(generalKey) == -1) {
					if (isCustom || (locationCount < maxLocations && system.scrambledPseudoRandomNumber(7861 + seedOffset) < ((chances.moon.wasteLand * tlFactor) - (0.01 * seedOffset)))) {
						addDock = true;
						name = pfn.$PF_MoonWasteland(pfKeys[i]);
					}
				}
				break;

			default:
				var sh = Ship.shipDataForKey(pfKeys[i]);
				var allowed = true;
				if (sh.condition_script) {
					// create a dummy object to attach the script to so it can be executed
					// we should have a sun and main planet by this stage so this should work.
					var temppos = system.sun.position.cross(system.mainPlanet.position).direction().multiply(4E9).subtract(system.mainPlanet.position);
					var tempalloy = system.addShips("alloy", 1, temppos, 0);
					tempalloy[0].setScript(sh.condition_script);
					allowed = tempalloy[0].script.allowSpawnShip(pfKeys[i]);
					tempalloy[0].remove(true);
				}
				if (allowed) {
					var chance = 1;
					var seed = 8888;
					var alwaysSpawn = false;
					if (sh && sh.hasOwnProperty("script_info")) {
						if (sh.script_info.hasOwnProperty("spawn_chance") && !isNaN(sh.script_info.spawn_chance)) {
							chance = parseFloat(sh.script_info.spawn_chance) * tlFactor;
						}
						if (sh.script_info.hasOwnProperty("always_spawn")) {
							alwaysSpawn = (sh.script_info.always_spawn == "yes" ? true : false);
							if (alwaysSpawn) chance = 1;
						}
						if (sh.script_info.hasOwnProperty("seed") && !isNaN(sh.script_info.seed)) {
							seed = parseInt(sh.script_info.seed);
						}
					}
					if (isCustom || ((alwaysSpawn || locationCount < maxLocations) && system.scrambledPseudoRandomNumber(seed) < chance)) {
						addDock = true;
					}
					// check if we've for a naming function to use.
					if (sh && sh.hasOwnProperty("script_info")) {
						if (sh.script_info.hasOwnProperty("naming_function") && sh.script_info.hasOwnProperty("naming_ws")) {
							if (worldScripts[sh.script_info.naming_ws][sh.script_info.naming_function]) {
								name = worldScripts[sh.script_info.naming_ws][sh.script_info.naming_function](pfKeys[i]);
							}
						}
					}
				}
				break;
		}
		if (!addDock) continue; // nothing to add? continue the loop

		// get the custom name to override whatever was generated
		//log(this.name, "isCustom " + isCustom + ", names " + names.length + ", i = " + i.toString() + ", names[0] = " + names[0]);
		if (isCustom && names && (names.length - 1) >= i && names[i] != "") name = names[i];

		var popRef = "pf_sub_" + pfKeys[i]; // unique key to use for the populator name
		locationCount += 1;
		this.globalCount += 1;
		// put each station in a different position so they don't "bump" each other
		var posPF = Vector3D(distOffset * this.globalCount, distOffset * this.globalCount, distOffset * this.globalCount).fromCoordinateSystem("psu");
		if (!isCustom) {
			types.push(generalKey);
			// add key to the used array (for custom setups this is done much earlier)
			this.usedKeys.push(pfKeys[i]);
		}
		this.$addPopulatorFunction(popRef, posPF, pfKeys[i], name, coords, radius);
	}
	if (isMain) {
		if (this.debug) log(this.name, "final count for main planet: " + locationCount);
	} else {
		if (this.debug) log(this.name, "final count for " + (hasAtmosphere ? "planet" : "moon") + " with radius " + radius + ": " + locationCount);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$addPopulatorFunction = function (reference, stnPosition, dataKey, name, planetPosition, planetRadius) {
	system.setPopulator(reference, {
		callback: function (pos, key, displayName, entPos, radius) {
			function calcLetters(text) {
				var ret = 0;
				var letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_";
				for (var i = 0; i < text.length; i++) {
					ret += letters.indexOf(text[i]);
				}
				return ret;
			}
			var militaryBaseRestriction = function (ship) {
				if (!(ship instanceof PlayerShip)) { // Only for the player ship
					return true;
				}
				if (worldScripts.PlanetFall2.permitMilitaryDock) {
					return true;
				}
				this.commsMessage(expandMissionText("planetFall2_warning_military"), player.ship);
				return false;
			};

			var ws = worldScripts.PlanetFall2;
			var pfAdd = system.addShips("[" + key + "]", 1, pos, 0);
			if (pfAdd && pfAdd.length > 0) {
				var pf = pfAdd[0];
				// add a docking restriction for military bases
				if (pf.allegiance == "restricted") {
					var ses = pf.subEntities, y = ses.length;
					while (y--) {
						var se = ses[y];
						if (se.isDock) {
							se.script.acceptDockingRequestFrom = militaryBaseRestriction.bind(pf);
							break;
						}
					}
				}
				pf.linkedPlanetPos = entPos;
				if (displayName != "") pf.displayName = displayName;
				// use the characters in the key to generate a seed value, offset by the last two characters (which should be 01-20)
				var idx = parseInt(key.substring(key.length - 1, key.length)) - 1;
				if (isNaN(idx)) idx = 0;
				var safe = false;
				var loop = 0;
				var dockPos;
				var letterVal = calcLetters(key);
				var seed = 0;
				while (safe == false) {
					safe = true;
					seed = letterVal + idx + (loop * letterVal);

					/*
						we need a way to *quickly* get random locations that aren't too close to each other
						the method I am using now does this in the following way:
							an angle of 0.2095 radians creates around 2 scanners worth of distance between two points at the planets surface.
							given a full circle is covered by 6.28 radians, this means there are around 30 separated angles in one full circle
							because this is 3D, we need two angles to describe a vector
							that gives us around 900 (30 x 30) unique positions on a planet
							we offset one of the angles by half of the 0.2095 value so they aren't complete duplicates of each other.
							we then convert the two angles into the components of a vector by using trig.
							we still need to check the distances, but now the retry process is more likely to find a free spot faster.
						there still might be cases where the loop hits the retry limit, but hopefully it won't be as often.
					*/
					var alpha = Math.floor(system.scrambledPseudoRandomNumber(system.ID + seed + 1) * 30) * 0.2095;
					var beta = Math.floor(system.scrambledPseudoRandomNumber(system.ID + seed + 2) * 30) * 0.2095 - 0.10475;
					var x2 = (Math.cos(alpha) * Math.cos(beta)) * (radius + 750);
					var z2 = (Math.sin(alpha) * Math.cos(beta)) * (radius + 750);
					var y2 = (Math.sin(beta)) * (radius + 750);
					
					/*
					var x1 = system.scrambledPseudoRandomNumber(system.ID + seed + 1) * 2 - 1;
					var y1 = system.scrambledPseudoRandomNumber(system.ID + seed + 2) * 2 - 1;
					var z1 = system.scrambledPseudoRandomNumber(system.ID + seed + 3) * 2 - 1;
					var x2 = (x1 * (1 / Math.sqrt(x1 * x1 + y1 * y1 + z1 * z1))) * (radius + 750);
					var y2 = (y1 * (1 / Math.sqrt(x1 * x1 + y1 * y1 + z1 * z1))) * (radius + 750);
					var z2 = (z1 * (1 / Math.sqrt(x1 * x1 + y1 * y1 + z1 * z1))) * (radius + 750);
					*/
					dockPos = entPos.add(Vector3D(x2, y2, z2));
					if (ws.$checkDockPosition(dockPos) == false) {
						//log("PlanetFall2_populatorCB", key + ": whoops! too close to another site");
						safe = false;
						loop += 1;
					};
					if (loop > 10) {
						if (ws.debug) log("PlanetFall2_populatorCB", key + ": Oh no! Didn't find a place after 10 rounds!");
						safe = true; // in case we can't find a spot
					}
				}
				pf.dockPos = dockPos;
				ws.$addExternalDock(pf, entPos, seed);
			}
		}.bind(this, stnPosition, dataKey, name, planetPosition, planetRadius),
		location: "COORDINATES",
		coordinates: stnPosition,
		deterministic: true,
		priority: 500
	});
}

//-------------------------------------------------------------------------------------------------------------
// checks a position against all other surface points, making sure they are not too close together
this.$checkDockPosition = function (pos) {
	var minDist = player.ship.scannerRange * 1;
	var s = system.shipsWithRole("planetFall2_stnVE");
	//log(this.name, "count = " + s.length);
	var i = s.length;
	while (i--) {
		//log(this.name, "checking against " + i + ", " + s[i].dataKey + ", " + s[i].position.distanceTo(pos) + " (" + s[i].position + ")");
		if (s[i].position.distanceTo(pos) < minDist) return false;
	}
	return true;
}

//-------------------------------------------------------------------------------------------------------------
this.$preDockSetup = function (station) {
	if (!station.hasOwnProperty("_dockOK")) {
		this.$checkingForDockingClearance();
	}
};

//-------------------------------------------------------------------------------------------------------------
this.$postDockSetup = function (station) {
	delete station.dockOK;
	delete station.performedDockRequest;
	if (this.prefx) this.prefx.remove();
	this.prefx = null;
};

//-------------------------------------------------------------------------------------------------------------
this.$addExternalDock = function (pf, entPos, seed) {
	var eds = worldScripts.ExternalDockSystem;
	var ve = eds.$addExternalDock({
		station: pf,
		realPosition: pf.dockPos,
		allowLaunch: true,
		launchSubEntityIndex: 0,
		launchDistanceBoost: this.boostAmount,
		dockRange: 2000,
		preDockCallback: this.$preDockSetup.bind(this),
		postDockCallback: this.$postDockSetup.bind(this)
	});
	this.$turnStation(pf, ve, entPos);

	var dve = system.addShips("[planetFall2_stnVE]", 1, ve.position, 0)[0];
	dve._seed = seed;
	dve.orientation = ve.orientation;
	if (pf.scriptInfo && pf.scriptInfo.hasOwnProperty("beacon_code")) {
		dve.hold_beaconCode = pf.scriptInfo.beacon_code;
	} else {
		dve.hold_beaconCode = "P";
	}
	dve.hold_beaconLabel = (pf.dataKey.indexOf("moon") >= 0 ? expandMissionText("planetFall2_beacon_moon") + ": " : expandMissionText("planetFall2_beacon_planet") + ": ") + pf.displayName;
	dve.displayName = pf.displayName;
	pf.linkedVE = dve;
}

//-------------------------------------------------------------------------------------------------------------
this.$turnStation = function (tempStation, altEntity, planetPos) {
	if (!tempStation || !altEntity) {
		return;
	} // just in case the station doesn't survive spawning.
	let stationVector = altEntity.position.subtract(planetPos).direction(); // unit vector pointing away from the planet
	let angle = tempStation.heading.angleTo(stationVector); // angle between current heading and target heading
	let cross = tempStation.heading.cross(stationVector).direction(); // set the plane where we should rotate in
	tempStation.orientation = tempStation.orientation.rotate(cross, -angle); // re-orient the station away from the planet
	altEntity.orientation = tempStation.orientation; // re-orient our alt entity the same way
	tempStation.breakPattern = false;
}

//-------------------------------------------------------------------------------------------------------------
this.shipApproachingPlanetSurface = function (planet) {
	if (!planet || planet.hasOwnProperty("PFNoLandQuiet")) {
		return;
	}

	if (planet.hasOwnProperty("PFNoLand")) {
		this.$sendPlanetaryComms(expandMissionText("planetFall2_urgent_no_land"));
		return;
	}

	this.lastPlanet = planet;
	if (!planet.isSun && !planet.hasOwnProperty("solarGasGiant") && !planet.hasOwnProperty("isGasGiant")) {
		if (planet === system.mainPlanet) {
			this.lastPlanetType = "Prime";
		} else {
			if (planet.hasAtmosphere) {
				this.lastPlanetType = "Sub";
			} else {
				this.lastPlanetType = "Moon";
			}
		}
	} else {
		if (planet.hasOwnProperty("solarGasGiant") || planet.hasOwnProperty("isGasGiant")) {
			this.lastPlanetType = "GasGiant";
		} else {
			this.lastPlanetType = "Sun";
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipEnteredPlanetaryVicinity = function (planet) {
	if (!planet) return;

	this.lastPlanet = planet;

	if (planet.isSun) {
		player.consoleMessage(expandMissionText("planetFall2_alert_sun"), 10);
	}
	if (planet.hasOwnProperty("solarGasGiant") || planet.hasOwnProperty("isGasGiant")) {
		player.consoleMessage(expandMissionText("planetFall2_alert_gas_giant"), 10);
	}
	if (planet.hasOwnProperty("PFNoLand")) {
		this.$sendPlanetaryComms(expandMissionText("planetFall2_alert_no_land_pull_up"));
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipLeavingPlanetSurface = function (planet) {
	this.lastPlanet = planet;
	this.messageBlock = false;
}

//-------------------------------------------------------------------------------------------------------------
this.shipExitedPlanetaryVicinity = function () {
	this.messageBlock = false;
	this.shipDied();
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillLaunchFromStation = function (station) {
	if (station && station.hasRole("planetFall2_surface")) {
		if (station.dataKey != missionVariables.PlanetFall2_DockedStation) {
			var tm = this._config.shipTransfer.time * 3600;
			clock.addSeconds(tm);
			var fee = parseInt(this._config.shipTransfer.fee * this._config.shipTransfer.governmentFactor[system.government]);
			if (player.credits < fee) {
				fee = player.credits;
			}
			player.credits -= fee;
			player.consoleMessage(expandMissionText("planetFall2_ship_transferred", { cost: formatCredits(fee, false, true) }));
		}

		this.holdStation = station;
		// we're adding a dummy station at the ve point for the real station.
		// this is because the "shipLaunchedFromStation" function searches for the nearest station and assumes you launched from that.
		// but because our real station is nowhere near the player by that point, it will probably lock onto the main station
		this.dummy = system.addShips("[" + station.dataKey + "]", 1, station.dockPos, 0)[0];
		this.dummy.name = station.name;
		this.dummy.displayName = station.displayName;
		// make sure the eds script has run first
		var eds = worldScripts.ExternalDockSystem;
		if (!isValidFrameCallback(eds._fcb)) {
			eds.shipWillLaunchFromStation(station);
		}
		this.planetFallLaunch = true;
		this.launchTexture = Math.floor(system.scrambledPseudoRandomNumber(772) * 6) + 1;
		// create the image behind the player initially, to give it time to load
		var fx = system.addVisualEffect("planetFall2_launchFX", player.ship.position.add(player.ship.vectorForward.multiply(this.fx_launch_dist * -1)));
		fx.setMaterials({
			"planetFall2_launchFX0.png": {
				fragment_shader: "planetFall2_breakPattern.fragment",
				vertex_shader: "planetFall2_breakPattern.vertex",
				textures: ["planetFall2_launchFX" + this.launchTexture + "_0.png"],
			}
		});
		// then put it in front of the player.
		fx.position = player.ship.position.add(player.ship.vectorForward.multiply(this.fx_launch_dist));
		station.state = 3;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipLaunchedFromStation = function (station) {
	this._beaconRange = this._config.beaconRange * 1000;

	if (EquipmentInfo.infoForKey("EQ_FRAME_SAVE") != null) { // if launch is for Save Anywhere, don't want to move the player or nuke the station
		return;
	}

	if (station && station.hasRole("planetFall2_surface")) {
		if (this.landingTimer) {
			delete this.landingTimer;
		}
		this.$sendPlanetaryComms(expandMissionText("planetFall2_orbital_boost"));
		this.$sendPlanetaryComms(expandMissionText("planetFall2_farewell"));
		this.messageBlock = false;
		this.$serviceLevelDecrease();
		// in case the player turns around immediately to redock
		// because the "station" object passed to this function won't be the true station, 
		// we're pointing to the holding variable to get the real station object
		this.holdStation.dockOK = true;
		this.holdStation.performedDockRequest = true;
		this.holdStation = null;
		this.dummy.remove(true);
	}
	this.monitorPoints = system.shipsWithRole("planetFall2_surface")
	if (this._config.shallowApproach) {
		this.landingMonitor = new Timer(this, this.$monitorDockPoints.bind(this), 1, 1);
	} else {
		this.landingMonitor = new Timer(this, this.$monitorDockPointsWide.bind(this), 1, 1);
	}

	delete missionVariables.PlanetFall2_DockedStation;
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillDockWithStation = function (station) {
	if (this.landingMonitor && this.landingMonitor.isRunning) this.landingMonitor.stop();

	this.$clearStationState();

	if (!station.hasRole("planetFall2_surface")) return;

	var p = player.ship;
	var break_chance = this.$damageChance();

	if (this._config.shallowApproach) {
		if (Math.abs(station.linkedVE.heading.dot(p.heading)) > 0.7) {
			if (Math.random() < 0.9) {
				var result = p.takeInternalDamage();
				if (result) {
					this.$playSound("explosion");
					player.addMessageToArrivalReport(expandMissionText("planetFall2_ship_damaged"));
				}
			}
		}
	}

	var pl = this.$getPlanetFromCoords(station.linkedPlanetPos);
	var plType = "";
	if (pl == system.mainPlanet) {
		plType = "main";
	} else {
		if (pl.hasAtmosphere) {
			plType = "sub";
		} else {
			plType = "moon";
		}
	}
	var color = this.$returnMainColor(pl);
	// load landing fx for planets with atmosphere
	if (plType == "main" || plType == "sub") {
		var texture = Math.floor(system.scrambledPseudoRandomNumber(772) * 4) + 1;
		var file = "planetFall2_clouds" + texture + "_" + color + ".png";
		if (pl._pf2_landingImageOverrides && pl._pf2_landingImageOverrides.hasOwnProperty("clouds")) {
			if (color == "night") {
				if (pl._pf2_landingImageOverrides.clouds.night != "")
					file = pl._pf2_landingImageOverrides.clouds.night;
			} else {
				if (pl._pf2_landingImageOverrides.clouds.normal != "")
					file = pl._pf2_landingImageOverrides.clouds.normal;
			}
		}
		// initially place the image behind the player to give it time to load in.
		var fx = system.addVisualEffect("planetFall2_landingFX", Vector3D(25, 25, 25).fromCoordinateSystem("psu"));
		fx.orientation = p.orientation;
		fx.setMaterials({
			"planetFall2_clouds.png": {
				fragment_shader: "planetFall2_breakPattern.fragment",
				vertex_shader: "planetFall2_breakPattern.vertex",
				textures: [file],
			}
		});
	}
	// load landing fx for planets with no atmosphere
	if (plType == "moon") {
		// initially place the image behind the player to give it time to load in.
		var fx = system.addVisualEffect("planetFall2_landingFX", Vector3D(25, 25, 25).fromCoordinateSystem("psu"));
		fx.orientation = p.orientation;
		fx.setMaterials({
			"planetFall2_clouds.png": {
				fragment_shader: "planetFall2_breakPattern.fragment",
				vertex_shader: "planetFall2_breakPattern.vertex",
				textures: ["planetFall2_stars.png"],
			}
		});
	}
	// then put it in front of the player
	fx.position = p.position.add(p.vectorForward.multiply(this.fx_land_dist)).add(p.vectorUp.multiply(this.fx_start));
	if (pl._pf2_landingImageOverrides && pl._pf2_landingImageOverrides.hasOwnProperty("dock") && pl._pf2_landingImageOverrides.dock.normal != "") {
		this.fx_landing_pad_filename = pl._pf2_landingImageOverrides.dock.normal;
	} else {
		this.fx_landing_pad_filename = "planetFall2_landingPad" + (Math.floor(system.scrambledPseudoRandomNumber(446) * 6) + 1).toString() + ".png";
	}

	// make sure the eds link is added to our station (in case some other function forced the player to dock)
	if (!station.hasOwnProperty("_dockedViaED")) {
		var eds = worldScripts.ExternalDockSystem;
		var ed = eds._externalDocks;
		for (var i = 0; i < ed.length; i++) {
			if (ed[i].station == station) {
				station._dockedViaED = ed[i];
				break;
			}
		}
	}

	if (!station.dockOK) {
		var fine = parseInt(player.credits * 0.05);
		if (fine > 5000) fine = 5000;
		player.credits -= fine;
		var txt = expandDescription("[station-docking-clearance-fined-@-cr]").replace("%@", formatCredits(fine, false, true));
		player.addMessageToArrivalReport(txt);
	}

	if (Math.random() < break_chance) {
		if (Math.random() < break_chance) this.$serviceLevelDecrease();
	}
	player.addMessageToArrivalReport(expandMissionText("planetFall2_successful_landing", { name: station.displayName }));

	missionVariables.PlanetFall2_DockedStation = station.dataKey;

	// pick the background image to go on the arrival report
	var seedOffset = parseInt(station.dataKey.substring(station.dataKey.length - 2, station.dataKey.length)) - 1;
	var generalKey = station.dataKey.substring(0, station.dataKey.length - 2);
	// some OXP keys won't have a numeric last character, in which case the seedOffset would be NaN
	// so switch the generalKey back to being the actual key
	if (isNaN(seedOffset)) { generalKey = station.dataKey; seedOffset = 0; }
	if (generalKey.indexOf("subSurface")) seedOffset += 10;

	this.background = "";
	this.overlay = "";
	var imagekey = station.scriptInfo.landing_image_set;
	if (!imagekey) return; // no image set specified - assume it's an OXP and allow it to do what it wants

	var keys = Object.keys(this._landingImages);
	var arr = this._landingImages[imagekey];
	if (!arr || arr.length == 0) return; // no images in array or invalid key
	var idx = Math.floor(system.scrambledPseudoRandomNumber(100 + (keys.indexOf(imagekey) * 30) + seedOffset) * arr.length);
	this.background = arr[idx];

	if (this.background != "") {
		if (color != "") this.overlay = "planetFall2_filter_" + color + ".png";
		if (pl._pf2_landingImageOverrides && pl._pf2_landingImageOverrides.hasOwnProperty("overlay")) {
			if (color == "night") {
				if (pl._pf2_landingImageOverrides.overlay.night != "")
					this.overlay = pl._pf2_landingImageOverrides.overlay.night;
			} else {
				if (pl._pf2_landingImageOverrides.overlay.normal != "")
					this.overlay = pl._pf2_landingImageOverrides.overlay.normal;
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipDockedWithStation = function(station) {
	// so things like galactic almanac have visibility of everything
	this.$turnOnAllBeacons();
}

//-------------------------------------------------------------------------------------------------------------
this.alertConditionChanged = function () {
	if (this.planetFallLaunch) {
		this.planetFallLaunch = false;
		return;
	}

	if (player.alertAltitude && (this.lastPlanet.hasOwnProperty("solarGasGiant") || this.lastPlanet.hasOwnProperty("isGasGiant"))) {
		player.consoleMessage(expandMissionText("planetFall2_alert_gas_giant"), 10);
	}

	if (player.alertAltitude && this.lastPlanet.hasOwnProperty("PFNoLand")) {
		player.consoleMessage(expandMissionText("planetFall2_alert_no_land"), 10);
		return;
	}

	// player left without docking
	if (!player.alertAltitude && this.landingTimer && this.landingTimer.isRunning && !player.ship.docked && EquipmentInfo.infoForKey("EQ_FRAME_SAVE") == null) {
		this.landingTimer.stop();
		delete this.landingTimer;
		this.$serviceLevelDecrease();
		if (!this.messageBlock) {
			this.$sendPlanetaryComms(expandMissionText("planetFall2_request_cancelled"));
			this.$sendPlanetaryComms(expandMissionText("planetFall2_goodbye"));
			this.messageBlock = false;
			this.$clearStationState();
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipDied = function () {
	if (this.landingTimer && this.landingTimer.isRunning) {
		this.landingTimer.stop();
		delete this.landingTimer;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$landingClearance = function $landingClearance() {
	this.landingTimer.stop();
	delete this.landingTimer;
	if (player.ship.docked) return;
	this.$checkingForDockingClearance();
	if (!this.checkingStation.dockOK) {
		if (this.checkingStation.hasOwnProperty("storeCommsMessage") && this.checkingStation.storeCommsMessage != "") {
			this.$sendPlanetaryComms(this.checkingStation.storeCommsMessage);
		} else {
			this.$sendPlanetaryComms(expandMissionText("planetFall2_landing_denied"));
		}
		this.$sendPlanetaryComms(expandMissionText("planetFall2_clear_area"));
		return;
	}
	this.$sendPlanetaryComms(expandMissionText("planetFall2_landing_authorised"));
	this.$sendPlanetaryComms(expandMissionText("planetFall2_landing_instruction"));
}

//-------------------------------------------------------------------------------------------------------------
this.$checkingForDockingClearance = function () {
	var stn = this.checkingStation;
	stn.storeCommsMessage = "";
	// check for the following condition:
	// the dock has an "acceptDockingRequestFrom" function, and we haven't run it yet.
	if (!stn.performedDockRequest) {
		if (this._config.shallowApproach) {
			var align = Math.abs(stn.linkedVE.heading.dot(player.ship.heading));
			if (align > 0.7) {
				stn.dockOK = false;
				stn.storeCommsMessage = expandMissionText("planetFall2_too_steep");
				return;
			}
		}
		// find dock entity of station
		var dock = null;
		for (var j = 0; j < stn.subEntities.length; j++) {
			if (stn.subEntities[j].isDock) {
				dock = stn.subEntities[j];
				break;
			}
		}
		if (dock && dock.script && dock.script.acceptDockingRequestFrom && !stn.performedDockRequest) {
			// run the function and store the result
			// intercept the commsMessage function for the station, so anything sent to the player when requesting docking can be stored
			stn.adjustCommsMessage = stn.commsMessage;
			stn.commsMessage = function (msg, tgt) {
				stn.adjustCommsMessage(msg, tgt);
				stn.storeCommsMessage = msg;
			}
			stn.dockOK = dock.script.acceptDockingRequestFrom(player.ship);
			stn.performedDockRequest = true;
			stn.commsMessage = stn.adjustCommsMessage;
			delete stn.adjustCommsMessage;
		} else {
			stn.dockOK = true;
			stn.performedDockRequest = true;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$turnOnAllBeacons = function () {
	var stns = this.monitorPoints;
	var i = stns.length;
	var stn = null;
	while (i--) {
		stn = stns[i];
		stn.linkedVE.beaconCode = stn.linkedVE.hold_beaconCode;
		stn.linkedVE.beaconLabel = stn.linkedVE.hold_beaconLabel;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$monitorDockPoints = function $monitorDockPoints() {
	var p = player.ship;
	if (!p || !p.isValid) return;

	var stns = this.monitorPoints;
	var i = stns.length;
	var stn = null;
	var state = 0;
	while (i--) {
		stn = stns[i];
		if (stn.state > 2) continue;
		var check = stn.dockPos.distanceTo(p);
		if (this._beaconRange == 0 || check < this._beaconRange) {
			stn.linkedVE.beaconCode = stn.linkedVE.hold_beaconCode;
			stn.linkedVE.beaconLabel = stn.linkedVE.hold_beaconLabel;
		} else {
			stn.linkedVE.beaconCode = "";
			stn.linkedVE.beaconLabel = "";
		}
		// only start doing checks if we're inside 20km
		if (check < 20000) {
			var align = stn.linkedVE.heading.dot(p.heading);
			if (align < 0) align = -align;
			// we're multiplying the deviation by the distance to eliminate the impact of the increasing angle as you get closer.
			var deviation = p.vectorForward.angleTo(stn.linkedVE.position.subtract(p.position)) * check;
			if (check < 3000 && !stn.dockOK && !stn.dockWarning && deviation < 245) {
				stn.dockWarning = true;
				this.$sendPlanetaryComms(expandMissionText("planetFall2_landing_not_clear"));
				break;
			}
			if (check < 3000 && stn.dockOK && align > 0.9 && deviation < 245 && !stn.alignWarning3) {
				stn.alignWarning3 = true;
				this.$sendPlanetaryComms(expandMissionText("planetFall2_warning_too_steep"));
				stn.state = 1; // reset state back so that a correct alignment will trigger a docking request
				stn.dockOK = false;
				break;
			}
			if (check < 7000 && stn.state == 2 && align > 0.7 && deviation < 245 && !stn.alignWarning2) {
				stn.alignWarning2 = true;
				this.$sendPlanetaryComms(expandMissionText("planetFall2_warning_too_steep2"));
				break;
			}
			if (check < 12000 && stn.state < 2 && align > 0.7 && deviation < 245 && !stn.alignWarning1) {
				stn.alignWarning1 = true;
				this.$sendPlanetaryComms(expandMissionText("planetFall2_warning_too_steep3"));
				break;
			}
			if (check < 12000 && stn.state < 2 && align <= 0.7 && deviation < 245) {
				stn.state = 2;
				state = 2;
				stn.alignWarning1 = false;
				stn.alignWarning2 = false;
				stn.alignWarning3 = false;
				stn.dockWarning = false;
				break;
			}
			if (check < 18000 && (!stn.state || stn.state == 0)) {
				stn.state = 1;
				state = 1;
				break;
			}
			if (check > 18000) {
				stn.state = 0;
				stn.alignWarning1 = false;
				stn.alignWarning2 = false;
				stn.alignWarning3 = false;
				stn.dockWarning = false;
			}
		}
	}
	if (state == 1) {
		this.$sendPlanetaryComms(expandMissionText("planetFall2_approach_landing"));
	}
	if (state == 2) {
		if (!this.messageBlock) {
			this.$sendPlanetaryComms(expandMissionText("planetFall2_landing_requested"));
			this.$sendPlanetaryComms(expandMissionText("planetFall2_landing_wait"));
		}
		this.clearanceDelay = 5 + (Math.ceil(Math.random() * 15)); // delay between 6 and 20 seconds
		this.landingTimer = new Timer(this, this.$landingClearance, this.clearanceDelay);
		this.checkingStation = stn;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$monitorDockPointsWide = function $monitorDockPointsWide() {
	var p = player.ship;
	if (!p || !p.isValid) return;

	var stns = this.monitorPoints;
	var i = stns.length;
	var stn = null;
	var state = 0;
	while (i--) {
		stn = stns[i];
		if (stn.state > 2) continue;
		var check = stn.dockPos.distanceTo(p);
		if (this._beaconRange == 0 || check < this._beaconRange) {
			stn.linkedVE.beaconCode = stn.linkedVE.hold_beaconCode;
			stn.linkedVE.beaconLabel = stn.linkedVE.hold_beaconLabel;
		} else {
			stn.linkedVE.beaconCode = "";
			stn.linkedVE.beaconLabel = "";
		}
		// only start doing checks if we're inside 20km
		if (check < 20000) {
			//var align = stn.linkedVE.heading.dot(p.heading);
			//if (align < 0) align = -align;
			// we're multiplying the deviation by the distance to eliminate the impact of the increasing angle as you get closer.
			var deviation = p.vectorForward.angleTo(stn.linkedVE.position.subtract(p.position)) * check;
			if (check < 3000 && !stn.dockOK && !stn.dockWarning && deviation < 245) {
				stn.dockWarning = true;
				this.$sendPlanetaryComms(expandMissionText("planetFall2_landing_not_clear"));
				break;
			}
			if (check < 12000 && stn.state < 2 && deviation < 245) {
				stn.state = 2;
				state = 2;
				stn.alignWarning1 = false;
				stn.alignWarning2 = false;
				stn.alignWarning3 = false;
				stn.dockWarning = false;
				break;
			}
			if (check < 18000 && (!stn.state || stn.state == 0)) {
				stn.state = 1;
				state = 1;
				break;
			}
			if (check > 18000) {
				stn.state = 0;
				stn.alignWarning1 = false;
				stn.alignWarning2 = false;
				stn.alignWarning3 = false;
				stn.dockWarning = false;
			}
		}
	}
	if (state == 1) {
		this.$sendPlanetaryComms(expandMissionText("planetFall2_approach_landing"));
	}
	if (state == 2) {
		if (!this.messageBlock) {
			this.$sendPlanetaryComms(expandMissionText("planetFall2_landing_requested"));
			this.$sendPlanetaryComms(expandMissionText("planetFall2_landing_wait"));
		}
		this.clearanceDelay = 5 + (Math.ceil(Math.random() * 15)); // delay between 6 and 20 seconds
		this.landingTimer = new Timer(this, this.$landingClearance, this.clearanceDelay);
		this.checkingStation = stn;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$clearStationState = function () {
	var stns = system.shipsWithRole("planetFall2_surface");
	var i = stns.length;
	while (i--) {
		stns[i].state = 0;
		stns[i].dockWarning = false;
		stns[i].alignWarning1 = false;
		stns[i].alignWarning2 = false;
		stns[i].alignWarning3 = false;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.missionScreenOpportunity = function () {
	// Player is docked at a military base or penal colony - not somewhere a criminal should make himself known ;)
	if (player.ship.bounty > 25) { // player is mid-level offender or fugitive
		if (player.ship.dockedStation.hasRole("planetFall2_mainSurface_militaryBase") || player.ship.dockedStation.hasRole("planetFall2_subSurface_militaryBase") || player.ship.dockedStation.hasRole("planetFall2_moonSurface_prison")) {
			missionVariables.planetFallFine = 20 + (Math.ceil(Math.random() * 480)); // fine of 21-500 credits
			if (player.ship.bounty > 50) { // if the player is a fugitive
				// raise fine by another 1-500 credits
				missionVariables.planetFallFine += (Math.ceil(Math.random() * 500));
			}

			if (worldScripts.Cabal_Common_Functions) {
				var t1 = expandMissionText("planetFall2_prison");
				var t2 = t1.split(".");
				var obj = {
					role: "planetFall2_scene_prison",
					absolutePos: [0, -5, 150],
					briefing: [
						[1, "walk", [0, -1, 6, 10, 0.15, 2, 0, 0], t2[0].trim() + "."],
						[24, "rot", [0, -1, 4.5, 0.0015, 0.0015, 0, 0, 1, 1], t2[1].trim() + "."],
						[25, "stopVelo", [0, -1]],
						[100, "mes", t2[2].trim() + "."],
						[101, "kill"],
						[102, 0]
					],
					title: expandMissionText("planetFall2_do_not_pass_go"),
					callback: this.name,
					callbackf: "$jailRelease"
				};
				worldScripts.Cabal_Common_Briefing.startBriefing(obj);
			} else {
				mission.runScreen({
					titleKey: "planetFall2_do_not_pass_go",
					messageKey: "planetFall2_prison",
					model: "[planetFall2_scene_prison]",
					spinModel: false,
				}, this.$jailRelease);
				var m = mission.displayModel;
				m.position = Vector3D(0, 0, m.collisionRadius * 1.82);
			}
			return;
		}
	}
	if (player.ship.dockedStation.allegiance == "restricted" && player.ship.dockedStation.hasRole("planetFall2_surface") && !this.permitMilitaryDock) {
		mission.runScreen({
			titleKey: "planetFall2_military_restricted",
			messageKey: "planetFall2_restricted",
			overlay: { name: "planetFall2_noEntry.png", height: 546 }
		}, this.$kickedOut);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.guiScreenChanged = function (to, from) {
	var p = player.ship;

	if (!p.docked) { // for GUI screen changes whilst in flight, which we can ignore
		return;
	}

	if (guiScreen == "GUI_SCREEN_SHIPYARD" && this._config.moveShipyards && p.dockedStation.hasShipyard && !p.dockedStation.scriptInfo["planetFall_leaveShipyard"] && !p.dockedStation.hasRole("planetFall2_surface")) {
		mission.runScreen({
			titleKey: "planetFall2_shipyardNotice_title",
			messageKey: "planetFall2_shipyardNotice_message",
			overlay: { name: "planetFall2_noEntry.png", height: 546 },
			exitScreen: "GUI_SCREEN_EQUIP_SHIP"
		});
		return;
	}
	// if we going to the equip ship screen, and we have a service level less than 85
	// and this isn't a planetary location, and maintenance overhauls have been move to the planet
	// and we haven't told the player about this yet...better let them know.
	if (guiScreen == "GUI_SCREEN_EQUIP_SHIP" && p.serviceLevel < 85 && !p.dockedStation.hasRole("planetFall2_surface") && this._config.moveRenovation && !this._config.renovationMessage) {
		this._config.renovationMessage = true;
		mission.runScreen({
			titleKey: "planetFall2_maintenance_warning",
			messageKey: "planetFall2_renovation",
			overlay: { name: "planetFall2_equipship.png", height: 546 },
			exitScreen: "GUI_SCREEN_EQUIP_SHIP"
		});
		return;
	}

	if (!p.dockedStation.hasRole("planetFall2_surface")) { // if we're at a non-PlanetFall location
		return;
	}

	if (guiScreen == "GUI_SCREEN_REPORT" && this.background != "" && this._config.useLandingImages) {
		this.showImage = new Timer(this, function showBackgroundImageAndOverlay() {
			if (typeof this.background == "string") {
				setScreenBackground({ name: this.background, height: 492 });
			} else {
				setScreenBackground(this.background);
			}
			if (this.overlay != "") {
				setScreenOverlay({ name: this.overlay, height: 492 });
			} else {
				setScreenOverlay(null);
			}
		}, 0.25, 0);
		return;
	}

	var key = p.dockedStation.dataKey.substring(0, p.dockedStation.dataKey.length - 2);
	var noTrade = false;
	var noEquip = false;
	var noShip = false;

	if (guiScreen == "GUI_SCREEN_MARKET") {
		if (p.dockedStation.hasRole("planetFall2_noTrade") || this._config.marketMethod[key] == 0) noTrade = true;
		if (missionVariables.PlanetFall2_DockedStation != p.dockedStation.dataKey) noShip = true;
	}
	if (guiScreen == "GUI_SCREEN_EQUIP_SHIP") {
		if (p.dockedStation.hasRole("planetFall2_noEquip") || this._config.equipShip[key] == false) noEquip = true;
		if (p.dockedStation.scriptInfo && p.dockedStation.scriptInfo.hasOwnProperty("allow_f3")) noEquip = !(p.dockedStation.scriptInfo.allow_f3 == "true");
		if (missionVariables.PlanetFall2_DockedStation != p.dockedStation.dataKey) noShip = true;
	}

	if (noTrade || noEquip) {
		mission.runScreen({
			titleKey: "planetFall2_service_unavailable",
			messageKey: "planetFall2_noTrade",
			overlay: { name: "planetFall2_noEntry.png", height: 546 },
			exitScreen: "GUI_SCREEN_STATUS"
		});
		return;
	}

	if (noShip) {
		mission.runScreen({
			titleKey: "planetFall2_service_unavailable",
			messageKey: "planetFall2_noShip",
			overlay: { name: "planetFall2_noEntry.png", height: 546 },
			exitScreen: "GUI_SCREEN_STATUS"
		});
		return;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$jailRelease = function () {
	if (player.credits > missionVariables.planetFallFine) {
		player.credits -= missionVariables.planetFallFine;
		player.ship.bounty -= (Math.ceil(missionVariables.planetFallFine * 0.05)); // lower bounty corresponding to fine.
	} else {
		player.credits = 10;
	}
	player.ship.launch();
	this.jailTime = 21600 + (Math.random() * 43200); // between 6 and 18 hours in the cells
	clock.addSeconds(this.jailTime);
}

//-------------------------------------------------------------------------------------------------------------
this.$kickedOut = function () {
	player.ship.launch();
}

//-------------------------------------------------------------------------------------------------------------
// this is to avoid the issue where player.ship.commsMessages appear to come out of nowhere.
this.$sendPlanetaryComms = function (msg) {
	if (!player.ship || !player.ship.isValid) return;
	var temp = system.addShips("[planetFall2_commObject]", 1, player.ship.position.add(player.ship.vectorUp.multiply(300)))[0];
	temp.displayName = expandMissionText("planetFall2_comms");
	temp.commsMessage(msg, player.ship);
	temp.remove(true);
}

//-------------------------------------------------------------------------------------------------------------
// play sound effects
this.$playSound = function $playSound(soundtype) {
	var mySound = new SoundSource;
	switch (soundtype) {
		case "explosion":
			mySound.sound = "[player-scrape-damage]";
			break;
		case "depart":
			mySound.sound = "planetFall2_depart.ogg";
			break;
		case "arrive":
			mySound.sound = "planetFall2_arrive.ogg";
			break;
		default:
			return;
	}
	mySound.loop = false;
	mySound.play();
}

//-------------------------------------------------------------------------------------------------------------
this.$getPlanetFromCoords = function (coords) {
	var pl = system.planets;
	var i = pl.length;
	while (i--) {
		if (parseInt(pl[i].position.distanceTo(coords)) == 0) return pl[i];
	}
	return null;
}

//-------------------------------------------------------------------------------------------------------------
this.$systemScan = function () {
	function isMoon(entity) { return (entity.isPlanet && !entity.hasAtmosphere) };
	this.systemArray = system.planets;
	this.systemCount = this.systemArray.length;
	this.moonCount = system.filteredEntities(this.systemArray, isMoon).length;
	this.planetCount = this.systemCount - this.moonCount;
}

//-------------------------------------------------------------------------------------------------------------
// reduces airColor of planet to 1 of 8 color descriptors
this.$returnMainColor = function (planet) {
	if (!player.ship.isSunlit) return "night";
	var ac = planet.airColor;
	if (!ac) ac = [0, 0, 1];

	var hsv = this.rgb2hsv(ac[0], ac[1], ac[2]);
	if (hsv.s > 30) {
		// note: in the core game, only cyan, blue and purple will be returned, based on the aircolor property
		// OXPs may change things to broaden the range (for instance, TionislaIsMars)
		if (hsv.h >= 338 || hsv.h < 22) return "red";
		if (hsv.h >= 22 && hsv.h < 45) return "orange";
		if (hsv.h >= 45 && hsv.h < 70) return "yellow";
		if (hsv.h >= 70 && hsv.h < 140) return "green";
		if (hsv.h >= 140 && hsv.h < 202) return "clear"; //"cyan";
		if (hsv.h >= 202 && hsv.h < 251) return "clear"; //"blue"; // I'm calling this as equivalent to clear
		if (hsv.h >= 251 && hsv.h < 292) return "clear"; //"purple";
		if (hsv.h >= 292 && hsv.h < 338) return "violet"; // this doesn't turn up very often.
	} else {
		return "clear"; // ie no filter
	}
}

//-------------------------------------------------------------------------------------------------------------
// assumes r g b are all between 0 and 1
this.rgb2hsv = function rgb2hsv(r, g, b) {
	let v = Math.max(r, g, b), c = v - Math.min(r, g, b);
	let h = c && ((v == r) ? (g - b) / c : ((v == g) ? 2 + (b - r) / c : 4 + (r - g) / c));
	return { h: 60 * (h < 0 ? h + 6 : h), s: (v && c / v) * 100, v: v * 100 };
}

//-------------------------------------------------------------------------------------------------------------
// gets the TL factor for a particular landing zone type (main, planet or moon)
this.$getTLFactor = function (type) {
	var tl = system.techLevel + 1;
	var r_min = this._config.chances.tlFactors[type].min;
	var r_max = this._config.chances.tlFactors[type].max;
	var inc = (r_max - r_min) / 15;
	return (tl * inc) + r_min;
}

//-------------------------------------------------------------------------------------------------------------
this.$damageChance = function () {
	var chance = 0;
	var p = player.ship;
	if (!p.hasEquipmentProviding("EQ_HEAT_SHIELD")) chance += 0.05;
	return chance;
}

//-------------------------------------------------------------------------------------------------------------
// we're utilising a secondary value so if the player choose a decrease value of between 0 and 1, we can still track it.
this.$serviceLevelDecrease = function () {
	this.subServiceLevel += this._config.serviceLevelDecrease;
	if (this.subServiceLevel >= 1) {
		player.ship.serviceLevel -= 1;
		this.subServiceLevel -= 1;
	}
}

//-------------------------------------------------------------------------------------------------------------
// returns an object containing the ship keys and names of all planetary locations for a system
this.$getSystemLocations = function(galNum, sysID) {
	var generated = worldScripts.PlanetFall2_Names.nameStore[galNum][sysID];
	// if there are no generated values, that means the player has not yet visited the planet
	// even if there is an override in place, the location might need to have some randomly generated one
	// on additional planets, for instance. As the full list will be a combination of predefined and generated
	// names, we can't know for sure what will get generated. So, until that data is present, respond as if there
	// was nothing to report.
	if (Object.keys(generated).length == 0) return {};

	if (this._locationOverrides[galNum.toString() + " " + sysID.toString()]) {
		var ovr = this._locationOverrides[galNum.toString() + " " + sysID.toString()];
		var result = {};
		var counters = {}
		var role = "";
		var name = "";
		for (var i = 0; i < ovr.main[0].roles.length; i++) {
			role = ovr.main[0].roles[i];
			name = ovr.main[0].names[i];
			var idx = 0;
			if (role == "") {
				// find the missing role in the nameStore
				var data = this.$findMissingRole(result, generated, "mainSurface");
				if (data) {
					role = data.role;
					idx = parseInt(role.substring(role.length - 2, role.length));
					role = this.$lookupShortRoleName(role.substring(0, role.length - 2), "main");
					name = data.name;
				} else {
					// shouldn't get here, but just in case
					continue;
				}
			} else {
				if (counters[role]) idx = counters[role];
				idx += 1;
			}
			counters[role] = idx;
			result[this._builtInLocations.main[role] + (idx < 10 ? "0" : "") + idx] = name;
		}
		if (ovr.moon) {
			counters = {};
			for (var i = 0; i < ovr.moon.length; i++) {
				for (var j = 0; j < ovr.moon[i].roles.length; j++) {
					role = ovr.moon[i].roles[j];
					name = ovr.moon[i].names[j];
					var idx = 0;
					if (role == "") {
						// find the missing role in the nameStore
						var data = this.$findMissingRole(result, generated, "moonSurface");
						if (data) {
							role = data.role;
							idx = parseInt(role.substring(role.length - 2, role.length));
							role = this.$lookupShortRoleName(role.substring(0, role.length - 2), "moon");
							name = data.name;
						} else {
							// shouldn't get here, but just in case
							continue;
						}
					} else {
						if (counters[role]) idx = counters[role];
						idx += 1;
					}
					counters[role] = idx;
					result[this._builtInLocations.moon[role] + (idx < 10 ? "0" : "") + idx] = name;
				}
			}
		}
		if (ovr.planet) {
			counters = {};
			for (var i = 0; i < ovr.planet.length; i++) {
				for (var j = 0; i < ovr.planet[i].roles.length; j++) {
					role = ovr.planet[i].roles[j];
					name = ovr.planet[i].names[j];
					var idx = 0;
					if (role == "") {
						// find the missing role in the nameStore
						var data = this.$findMissingRole(result, generated, "subSurface");
						if (data) {
							role = data.role;
							idx = parseInt(role.substring(role.length - 2, role.length));
							role = this.$lookupShortRoleName(role.substring(0, role.length - 2), "planet");
							name = data.name;
						} else {
							// shouldn't get here, but just in case
							continue;
						}
					} else {
						if (counters[role]) idx = counters[role];
						idx += 1;
					}
					counters[role] = idx;
					result[this._builtInLocations.planet[role] + (idx < 10 ? "0" : "") + idx] = name;
				}
			}
		}
		return result;
	} else {
		return generated;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$findMissingRole = function(predef, generated, type) {
	var keys = Object.keys(generated);
	for (var i = 0; i < keys.length; i++) {
		if (!predef[keys[i]] && keys[i].indexOf("_" + type + "_") >= 0) return {role:keys[i], name:generated[keys[i]]};
	}
	return null;
}

//-------------------------------------------------------------------------------------------------------------
this.$lookupShortRoleName = function(role, section) {
	var builtin = this._builtInLocations[section];
	var keys = Object.keys(builtin);
	for (var i = 0; i < keys.length; i++) {
		if (builtin[keys[i]] == role) return keys[i];
	}
	return "";
}

//-------------------------------------------------------------------------------------------------------------
// these functions are not in use - they are used to help work out the smallest angle that gives the best separation for landing sites
this.$checkDistCalc = function(value1, value2) {
	var radius = system.mainPlanet.radius;
	var pos1 = this.$subFunction(value1, radius);
	var pos2 = this.$subFunction(value2, radius);
	log(this.name, "dist = " + pos1.distanceTo(pos2));
}

//-------------------------------------------------------------------------------------------------------------
this.$subFunction = function(value, radius) {
	var alpha = value * 6.28;
	var beta = 0; //value * 6.28;
	var x2 = (Math.cos(alpha) * Math.cos(beta)) * (radius + 750);
	var z2 = (Math.sin(alpha) * Math.cos(beta)) * (radius + 750);
	var y2 = (Math.sin(beta)) * (radius + 750);
	return system.mainPlanet.position.add(Vector3D(x2, y2, z2));	
}