Back to Index Page generated: May 17, 2025, 5:47:49 AM

Expansion Ship Comparison

Content

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Displays side-by-side comparison of ships, helping players making decisions about new ship purchases. Displays side-by-side comparison of ships, helping players making decisions about new ship purchases.
Identifier oolite.oxp.phkb.ShipComparison oolite.oxp.phkb.ShipComparison
Title Ship Comparison Ship Comparison
Category Ambience Ambience
Author phkb phkb
Version 2.6 2.6
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Information URL https://wiki.alioth.net/index.php/Ship_Comparison n/a
Download URL https://wiki.alioth.net/img_auth.php/5/58/ShipComparison.oxz n/a
License CC-BY-NC-SA 4.0 CC-BY-NC-SA 4.0
File Size n/a
Upload date 1744265305

Documentation

Also read http://wiki.alioth.net/index.php/Ship%20Comparison

readme.txt

Ship Comparison
By Nick Rogers

Overview
========
This OXP aims to help players who are in the market for a new ship by providing a visual, side-by-side comparison of ship specifications.

A new interface screen "Ship Comparisons" allows the details of up to three ships to be displayed. Details included are:
- Price
- Max speed
- Injector speed
- Thrust/pitch/yaw
- Weapon positions
- Missile pylons
- Cargo capacity (plus expansion size, if available)
- Energy banks
- Energy recharge rate
- Hyperspace capable
- Mass
- Equipment space (plus amount of cargo that can be converted to equipment space) - Only if the "Ship Configuration" OXP is installed. Note: values are without an extended cargo bay installed, if application to that ship.

A menu option, "View standard equipment availability", shows a list of all the standard equipment items, and whether each one is (a) fitted as standard, (b) is available for optional installation, or (c) not available.
The standard equipment items shown are:
- Forward weapon type
- ECM
- Fuel scoops
- Passenger berths
- Energy unit
- Naval energy unit
- Docking computer
- Shield booster
- Military shield enhancement
- Galactic hyperdrive
- Scanner targeting enhancement
- Multi-targeting system

Overriding Data
===============
If a particular ship needs to have it's own entry, even if it would normally be listed under another ship type, you can override the list entry for that data key.
Put this into the startUpComplete routine of your OXP:

	var sc = worldScripts.ShipComparison;
	if (sc) {
		// add an override for a particular ship data key, giving it a new name
		sc._override["noshaders_z_groovy_cobra1_baker_player"] = "Cobra Mark I Special";
		
		// optionally, if you need to override the extra cargo capacity or the weapon facings, use the following
		sc._extraData["Cobra Mark I Special"] = [5, 15];
		// data element 1 is the additional cargo available for this ship type (5 in the example)
		// data element 2 is the weapon facings on this ship type (1 = forward only, 3 = forward and aft, 15 = all) (15 = All in the example)
	}
	
License
=======
This work is licensed under the Creative Commons Attribution-Noncommercial-Share Alike 4.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/

Balance image from http://simpleicon.com/balance.html

Version History
===============
2.6
- Bug fix for ship mass calculation without Ship Configuration.

2.5
- Copied "Mass" calculations from Ship Configuration so it can be display without Ship Configuration.

2.4
- Added shortcut key to F3F3 Shipyard screen to jump directly into the comparison tool. Also added text to Contextual Help.
- Added option to set one of the comparison ships to your current ship for faster comparisons.
- Added option to limit the list of ships to only those available in the current shipyard.

2.3
- Fixed issue where the equipment space array was continually growing with duplicate entries.

2.2
- Further improvements to handling of situation where _oo_shipyard property is not defined for a particular player ship.

2.1
- Better handling of situations where the _oo_shipyard property is not defined for a particular player ship.

2.0
- Added ship price to the list of information shown.
- Put pitch, roll and yaw onto 1 line.
- Added new menu option, "View standard equipment availability", which shows which standard equipment items are fitted as standard, available for optional installation, or not available at all.
- Removed requirement to store a value in missionVariables.
- Added option to clear ship list to Library interface.

1.9
- Worked out how to get weapon facings and large cargo bay values without needing to create ship objects to test. Should work with all ships, but I have left the override array in place in case exceptions arise in the future.

1.8
- Added Cobra Mark IV to list of exceptions.

1.7
- Changed "==" comparisons to "===" for performance improvements.
- Added ship mass to display (only if "Ship Configuration" OXP v0.3.1 is installed)
- Moved the F4 interface to "Ship Systems", putting it near "Ship Registrations".

1.6
- Fixed Javascript error when attempting to check for script_info objects on ships that don't have any script_info defined.

1.5
- Added ability to hide ships via script info.

1.4
- Added Equipment space from "Ship Configuration" OXP v0.1.0 or later. Only available if that OXP is installed.
- Additional ship data overrides.

1.3
- Added a "+" symbol to extra cargo value to make it clearer it is in addition to the base cargo capacity.
- Additional ship data overrides.
- Added dictionary and code examples to allow OXP's to override ship data for a particular data key.

1.2
- Added cargo expansion size to list of items.
- Fixed issue where missing "weapon_facings" value was not defaulting to the correct value. If any ships has been missed or show up incorrectly just let me know.
- Added a "change scroll direction" function so if you go past the ship you want to can turn around and go back, rather than having to scroll all the way around the list.

1.1
- Fixed issue where some ship definitions were incorrectly showing with "0" missiles.
- Fixed issue where some ship definitions were showing a blank in the cargo capacity field.
- Added code to hide specifications of some ships (classified data and such).

1.0
- Initial release.

Equipment

This expansion declares no equipment.

Ships

This expansion declares no ships.

Models

This expansion declares no models.

Scripts

Path
Scripts/shipcomparison.js
"use strict";
this.name        = "ShipComparison";
this.author      = "phkb";
this.copyright   = "2017 phkb";
this.description = "Routines for selecting ships and displaying details of them side-by-side.";
this.license     = "CC BY-NC-SA 4.0";

this._lastChoice = "";
this._shipList = [];
this._selectedList = [];
this._direction = 1;
this._itemColor = "yellowColor";
this._menuColor = "orangeColor";
this._exitColor = "yellowColor";
this._disabledColor = "darkGrayColor";
this._hideListBasic = ["GalCop Viper", "GalCop Viper Interceptor"];
this._hideListAll = ["Constrictor"];
this._shipConfig = false;
this._resetData = false;
this._clearShips = false;
this._trueValues = ["yes", "1", 1, "true", true];
this._exitScreen = "GUI_SCREEN_INTERFACES";
this._limitList = false;
this._switch = false;

this._shipMassExceptions = {
	"Cobra Mark III": 214737.6875,
};

// configuration settings for use in Lib_Config
this._compConfig = {Name:this.name, Alias:"Ship Comparison", Display:"Config", Alive:"_compConfig", Notify:"$onChange",
	Bool:{
		B0:{Name:"_resetData", Def:false, Desc:"Reset stored values"},
		B1:{Name:"_clearShips", Def:false, Desc:"Clears ship selections"},
		Info:"0 - Resets all stored equipment space and ship mass values.\n1 - Clears all the currently selected ships"}
};

// set with this._override[data key] = "New ship name";
this._override = {};
// this is a holding array for equipment space data for ships, if ShipConfig is installed
this._equipSpace = [];
this._mass = {};

// element 1: extra cargo
// element 2: weapon facings
this._extraData = {
};

//-------------------------------------------------------------------------------------------------------------
this.$onChange = function() {
	if (this._resetData === true) {
		this._equipSpace = [];
		this._resetData = false;
		player.consoleMessage("Stored ship values cleared");
	}
	if (this._clearShips === true) {
		this._selectedList = [];
		this._clearShips = false;
		player.consoleMessage("Selected ships cleared");
	}
}

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

	if (worldScripts.ShipConfiguration_Core && worldScripts.ShipConfiguration_Core.$getEquipmentSpaceFromShipKey) this._shipConfig = true
	// set up the interface screen, if required
	this.$initInterface(player.ship.dockedStation);
	// load any equipment space calculations that have been done previously
	if (missionVariables.ShipComparison_EquipSpace) {
		this._equipSpace = JSON.parse(missionVariables.ShipComparison_EquipSpace);
		this.$cleanUpEquipSpace();
	}
	if (missionVariables.ShipComparison_Reset) delete missionVariables.ShipComparison_Reset;

	if (oolite.compareVersion("1.91") <= 0) {
		setExtraGuiScreenKeys(this.name, {
			guiScreen:"GUI_SCREEN_SHIPYARD", 
			registerKeys:{"sckey1":[{key:"c",mod1:true}]}, 
			callback:this.$directOpen.bind(this)
		});

		if (worldScripts.ContextualHelp) {
			worldScripts.ContextualHelp.$addHelpTextToGuiScreen(this.name, "GUI_SCREEN_SHIPYARD", "\nPress Ctrl-C on this screen to open the Ship Comparison tool.");
		}

	}	
}

//-------------------------------------------------------------------------------------------------------------
this.playerWillSaveGame = function() {
	if (this._equipSpace.length > 0) {
		// if we've gone to the trouble of calculating equipment space, we'll retrieve the results so that we keep the speed of lookup values
		missionVariables.ShipComparison_EquipSpace = JSON.stringify(this._equipSpace);
	} else {
		delete missionVariables.ShipComparison_EquipSpace;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipDockedWithStation = function(station) {
	// set up interface screen
	this.$initInterface(station);
}

//-------------------------------------------------------------------------------------------------------------
this.missionScreenEnded = function() {
	if (player.ship.hudHidden == true) player.ship.hudHidden = false;
}

//-------------------------------------------------------------------------------------------------------------
// initialise the interface screen in the station
this.$initInterface = function(station) {
	station.setInterface(this.name,{
		title:"Ship comparisons",
		category:"Ship Systems",
		summary:"Displays side-by-side details of selected ships.",
		callback:this.$openComparison.bind(this)
	});
}

//-------------------------------------------------------------------------------------------------------------
this.$openComparison = function() {
	function compare(a,b) {
		return a.name > b.name;
	}

	this._exitScreen = "GUI_SCREEN_INTERFACES";
	this._lastChoice = "";
	this.$buildShipList();
	this._shipList.sort(compare);
	if (this._switch == true) {
		this._switch = false;
		this.$showComparisonEquipment();
		return;
	}
	this.$showComparison();
}

//-------------------------------------------------------------------------------------------------------------
this.$directOpen = function() {
	function compare(a,b) {
		return a.name > b.name;
	}

	this._exitScreen = "GUI_SCREEN_SHIPYARD";
	this._lastChoice = "";
	this.$buildShipList();
	this._shipList.sort(compare);
	if (this._switch == true) {
		this._switch = false;
		this.$showComparisonEquipment();
		return;
	}
	this.$showComparison();
}

//-------------------------------------------------------------------------------------------------------------
this.$showComparison = function() {

	var text = "";
	var colShip = 10;
	var colDesc = 10;
	var curChoices = {};
	var sc = null;
	var keyList = [];
	var shipArray = [];
	
	if (this.$isBigGuiActive() === false) player.ship.hudHidden = true;

	for (var i = 0; i < this._selectedList.length; i++) {
		if (this._selectedList[i] != "") {
			var shipDef = Ship.shipDataForKey(this._selectedList[i]);
			shipArray.push(shipDef);
			keyList.push(this._selectedList[i]);
		}
	}
	colShip = Math.floor(22 / shipArray.length);

	var textList = ["Ship type", "", "Price", "Max speed", "Max thrust", "Injector speed", "Pitch/Roll/Yaw", 
		"Weapon positions", "Missile pylons", "Cargo capacity", "Energy banks", "Energy recharge rate", "Hyperspace capable", "Mass"];
	var outList = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", ""];

		// select ships to be in comparison (up to 4)
	text = "Select up to 3 ships to compare:\n";

	if (this._shipConfig) {
		textList.push("Equipment space");
		outList.push("");
		sc = worldScripts.ShipConfiguration_Core;
		// if this HUD doesn't have big gui enabled, take out the blank line normally under the ship type so there's room for the extra fields
		/*if (player.ship.hudAllowsBigGui === false) {
			textList.splice(1, 1);
			outList.splice(1, 1);
		}*/
	} else {
		text += "\n";
	}

	for (var ol = 0; ol < textList.length; ol++) {
		outList[ol] = this.$padTextRight(textList[ol] + (textList[ol] != "" ? ":" : ""), colDesc);
		for (var i = 0; i < shipArray.length; i++) {
			var shipDef = shipArray[i];
			var shipName = shipDef.name;
			if (this._override[this._selectedList[i]]) shipName = this._override[this._selectedList[i]];

			switch (textList[ol]) {
				case "Ship type":
					outList[ol] += this.$padTextRight(shipName, colShip);
					break;
				case "Price":
					if (shipDef._oo_shipyard) {
						outList[ol] += this.$padTextRight(formatCredits(parseInt(shipDef._oo_shipyard.price), false, true), colShip);
					} else {
						log(this.name, "!!ERROR: _oo_shipyard property not set for ship " + shipName + ", key " + keyList[i]);
					}
					break;
				case "Max speed":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("N/A", colShip);
					} else {
						outList[ol] += this.$padTextRight((parseFloat(shipDef["max_flight_speed"]) / 1000).toFixed(3) + " LS", colShip);
					}
					break;
				case "Max thrust":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("N/A", colShip);
					} else {
						outList[ol] += this.$padTextRight((parseFloat(shipDef["thrust"]) / 1000).toFixed(3) + " LS", colShip);
					}
					break;
				case "Injector speed":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("N/A", colShip);
					} else {
						var inj = 7;
						if (0 >= oolite.compareVersion("1.81") && shipDef["injector_speed_factor"] != undefined) inj = parseFloat(shipDef["injector_speed_factor"]);
						outList[ol] += this.$padTextRight(((parseFloat(shipDef["max_flight_speed"]) * inj) / 1000).toFixed(3) + " LS", colShip);
					}
					break;
				case "Pitch/Roll/Yaw":
					if (this._hideListAll.indexOf(shipName) >= 0 || this._hideListBasic.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("N/A", colShip);
					} else {
						var yaw = parseFloat(shipDef["max_flight_pitch"]);
						if (shipDef["max_flight_yaw"] != undefined) yaw = parseFloat(shipDef["max_flight_yaw"]);
						outList[ol] += this.$padTextRight(parseFloat(shipDef["max_flight_pitch"]).toFixed(1)
							+ "/" + parseFloat(shipDef["max_flight_roll"]).toFixed(1)
							+ "/" + yaw.toFixed(1), colShip);
					}
					break;
				case "Weapon positions":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("N/A", colShip);
					} else {
						var facings = shipDef["weapon_facings"];
						if (shipDef._oo_shipyard && shipDef._oo_shipyard.weapon_facings != undefined) facings = shipDef._oo_shipyard.weapon_facings;
						if (this._extraData[shipName] != null) facings = this._extraData[shipName][1].toString();
						var wpn = "Forward";
						if (facings === "15") {
							wpn = "All";
						} else {
							if (" 3 7 11 ".indexOf(" " + facings + " ") >= 0) wpn += ", Aft";
							if (" 5 7 13 ".indexOf(" " + facings + " ") >= 0) wpn += ", Port";
							if (" 9 11 13 ".indexOf(" " + facings + " ") >= 0) wpn += ", Starboard";
						}
						if (facings === "0") wpn = "None";
						if (facings === "N/A") wpn = "N/A";
						outList[ol] += this.$padTextRight(wpn, colShip);
					}
					break;
				case "Missile pylons":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("N/A", colShip);
					} else {
						var miss = 0;
						if (shipDef["max_missiles"] != undefined) {
							miss = parseInt(shipDef["max_missiles"]);
						} else {
							miss = parseInt(shipDef["missiles"]);
						}
						outList[ol] += this.$padTextRight(miss, colShip);
					}
					break;
				case "Cargo capacity":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("N/A", colShip);
					} else {
						var cargo = 0;
						if (shipDef["max_cargo"] != undefined) cargo = parseInt(shipDef["max_cargo"]);

						var exp = "";
						if (shipDef["extra_cargo"] != undefined) {
							exp = shipDef["extra_cargo"];
						}
						if (exp === "") {
							if (shipDef._oo_shipyard) {
								if ((shipDef._oo_shipyard.standard_equipment.extras && shipDef._oo_shipyard.standard_equipment.extras.indexOf("EQ_CARGO_BAY") >= 0) || 
								(shipDef._oo_shipyard.optional_equipment && shipDef._oo_shipyard.optional_equipment.indexOf("EQ_CARGO_BAY") >=0)) exp = 15;
							}
						}
						if (this._extraData[shipName] != null) exp = this._extraData[shipName][0].toString();

						if (exp === undefined || exp === "0") exp = "";
						outList[ol] += this.$padTextRight(cargo.toString() + (exp != "" ? " (+" + exp + ")" : ""), colShip);
					}
					break;
				case "Energy banks":
					if (this._hideListAll.indexOf(shipName) >= 0 || this._hideListBasic.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("N/A", colShip);
					} else {
						var en = 200;
						if (shipDef["max_energy"] != undefined) en = parseInt(shipDef["max_energy"]);
						if (Math.floor(en / 64) === 0) en = 64;
						outList[ol] += this.$padTextRight(Math.floor(en / 64), colShip);
					}
					break;
				case "Energy recharge rate":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("N/A", colShip);
					} else {
						var rech = 1;
						if (shipDef["energy_recharge_rate"] != undefined) rech = parseFloat(shipDef["energy_recharge_rate"]);
						outList[ol] += this.$padTextRight(rech.toFixed(1), colShip);
					}
					break;
				case "Hyperspace capable":
					var hyper = "Yes";
					if (shipDef["hyperspace_motor"] != undefined && (shipDef["hyperspace_motor"] === "no" || shipDef["hyperspace_motor"] === "false" || shipDef["hyperspace_motor"] === "0")) hyper = "No";
					outList[ol] += this.$padTextRight(hyper, colShip);
					break;
				case "Equipment space":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("N/A", colShip);
					} else {
						var found = false;
						// check to see if we have already analysed this ship type
						for (var j = 0; j < this._equipSpace.length; j++) {
							if (this._equipSpace[j].shipType == shipName) {
								outList[ol] += this.$padTextRight(this._equipSpace[j].equipSpace + (this._equipSpace[j].cargoSpace != "N/A" && this._equipSpace[j].cargoSpace != 0 ? " (+" + this._equipSpace[j].cargoSpace + ")" : ""), colShip);
								found = true;
								break;
							}
						}
						// if we haven't we will need to execute a query against ShipConfig
						if (found === false) {
							var es = sc.$getEquipmentSpaceFromShipKey(this._selectedList[i]);
							var ms = 0;
							outList[ol] += this.$padTextRight(es.equipSpace + (es.cargoSpace != "N/A" && es.cargoSpace != 0 ? " (+" + es.cargoSpace + ")" : ""), colShip);
							// store the result so we don't have to run this process again (but only if there was a definite value, not N/A)
							if (es.equipSpace != "N/A") this._equipSpace.push({shipType:shipName, equipSpace:es.equipSpace, cargoSpace:es.cargoSpace, shipMass:(es.hasOwnProperty("mass") ? es.mass : "N/A")});
						}
					}
					break;
				case "Mass":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("N/A", colShip);
					} else {
						// check to see if we have already analysed this ship type
						if (!sc) {
							// if ship config is not installed
							var mass = this._mass[shipName];
							if (!mass) {
								mass = this.$getMassFromShipKey(this._selectedList[i]);
								// store the result so we don't have to do this again
								this._mass[shipName] = mass;
							}
							if (!mass || mass == 0) {
								outList[ol] += this.$padTextRight("N/A", colShip);
							} else {
								outList[ol] += this.$padTextRight(mass.toFixed(0), colShip);
							}
						} else {
							// if ship config is installed
							for (var j = 0; j < this._equipSpace.length; j++) {
								if (this._equipSpace[j].shipType === shipName) {
									idx = j;
									if (this._equipSpace[j].hasOwnProperty("mass") && this._equipSpace[j].mass != "N/A") {
										outList[ol] += this.$padTextRight(this._equipSpace[j].mass.toFixed(0), colShip);
										found = true;
									}
									break;
								}
							}
							// if we haven't we will need to execute a query against ShipConfig
							if (found === false) {
								var es = sc.$getEquipmentSpaceFromShipKey(this._selectedList[i]);
								if (es.hasOwnProperty("mass") && es.mass != "N/A") {
									outList[ol] += this.$padTextRight(es.mass.toFixed(0), colShip);
								} else {
									outList[ol] += this.$padTextRight("N/A", colShip);
								}
								// store the result so we don't have to run this process again (but only if there was a definite value, not N/A)
								if (es.equipSpace != "N/A" || idx === -1) {
									this._equipSpace.push({shipType:shipName, equipSpace:es.equipSpace, cargoSpace:es.cargoSpace, shipMass:(es.hasOwnProperty("mass") ? es.mass : "N/A")});
								} else {
									this._equipSpace[idx].mass = es.mass;
								}
							}
						}
					}
					break;
				}
		}
	}

	for (var i = 0; i < outList.length; i++) {
		text += outList[i] + "\n";
	}

	if (this._selectedList.length === 0) {
		if (this._direction === 1) {
			curChoices["01_CHANGE"] = {text:"Change first ship to '" + this._shipList[0].name + "'", color:this._menuColor};
			curChoices["02_CHANGE"] = {text:"Change second ship to '" + this._shipList[0].name + "'", unselectable:true, color:this._disabledColor};
			curChoices["03_CHANGE"] = {text:"Change third ship to '" + this._shipList[0].name + "'", unselectable:true, color:this._disabledColor};
		} else {
			curChoices["01_CHANGE"] = {text:"Change first ship to '" + this._shipList[this._shipList.length - 1].name + "'", color:this._menuColor};
			curChoices["02_CHANGE"] = {text:"Change second ship to '" + this._shipList[this._shipList.length - 1].name + "'", unselectable:true, color:this._disabledColor};
			curChoices["03_CHANGE"] = {text:"Change third ship to '" + this._shipList[this._shipList.length - 1].name + "'", unselectable:true, color:this._disabledColor};
		}
	} else {
		for (var i = 0; i < this._selectedList.length; i++) {
			for (var j = 0; j < this._shipList.length; j++) {
				if (this._shipList[j].key === this._selectedList[i]) {
					if (this._direction === 1) {
						if (j < this._shipList.length - 1) {
							curChoices["0" + (i + 1) + "_CHANGE"] = {text:"Change " + this.$numberWord(i + 1) + " ship to '" + this._shipList[j + 1].name + "'", color:this._menuColor};
						} else {
							curChoices["0" + (i + 1) + "_CHANGE"] = {text:"Change " + this.$numberWord(i + 1) + " ship to '" + this._shipList[0].name + "'", color:this._menuColor};
						}
					} else {
						if (j > 0) {
							curChoices["0" + (i + 1) + "_CHANGE"] = {text:"Change " + this.$numberWord(i + 1) + " ship to '" + this._shipList[j - 1].name + "'", color:this._menuColor};
						} else {
							curChoices["0" + (i + 1) + "_CHANGE"] = {text:"Change " + this.$numberWord(i + 1) + " ship to '" + this._shipList[this._shipList.length - 1].name + "'", color:this._menuColor};
						}

					}
				}
			}
		}
		if (this._selectedList.length === 1) {
			if (this._direction === 1) {
				curChoices["02_CHANGE"] = {text:"Change second ship to '" + this._shipList[0].name + "'", color:this._menuColor};
				curChoices["03_CHANGE"] = {text:"Change third ship to '" + this._shipList[0].name + "'", unselectable:true, color:this._disabledColor};
			} else {
				curChoices["02_CHANGE"] = {text:"Change second ship to '" + this._shipList[this._shipList.length - 1].name + "'", color:this._menuColor};
				curChoices["03_CHANGE"] = {text:"Change third ship to '" + this._shipList[this._shipList.length - 1].name + "'", unselectable:true, color:this._disabledColor};
			}
		}
		if (this._selectedList.length === 2) {
			if (this._direction === 1) {
				curChoices["03_CHANGE"] = {text:"Change third ship to '" + this._shipList[0].name + "'", color:this._menuColor};
			} else {
				curChoices["03_CHANGE"] = {text:"Change third ship to '" + this._shipList[this._shipList.length - 1].name + "'", color:this._menuColor};
			}
		}
	}
	curChoices["04_CURRENT"] = {text:"Set first ship to current ship", color:this._menuColor};
	if (this._limitList == false) {
		curChoices["04A_LIMIT"] = {text:"Only include ships in current shipyard", color:this._menuColor};
	} else {
		curChoices["04B_LIMIT"] = {text:"Include all ships", color:this._menuColor};
	}
	curChoices["05_DIRECTION"] = {text:(this._direction === 1 ? "Change scroll direction to ascending" : "Change scroll direction to descending"), color:this._menuColor};
	curChoices["06_EQUIPMENT"] = {text:"View standard equipment availability", color:this._itemColor};
	curChoices["99_EXIT"] = {text:"Return", color:this._itemColor};
	var def = "99_EXIT";

	var opts = {
		screenID: "oolite-ship-compare-map",
		title: "Ship Comparison",
		allowInterrupt: true,
		exitScreen: this._exitScreen,
		overlay: {name:"compare-balance.png", height:546},
		choices: curChoices,
		initialChoicesKey: (this._lastChoice ? this._lastChoice : def),
		message: text
	};

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

//-------------------------------------------------------------------------------------------------------------
this.$showComparisonEquipment = function() {
	
	var text = "";
	var colShip = 10;
	var colDesc = 10;
	var curChoices = {};
	var sc = null;
	var keyList = [];
	var shipArray = [];

	for (var i = 0; i < this._selectedList.length; i++) {
		if (this._selectedList[i] != "") {
			var shipDef = Ship.shipDataForKey(this._selectedList[i]);
			shipArray.push(shipDef);
			keyList.push(this._selectedList[i]);
		}
	}
	colShip = Math.floor(22 / shipArray.length);

	var textList = ["Ship type", "", "Techlevel", "Forward weapon type", "ECM", "Fuel scoops", "Passenger berths", "Energy unit", "Naval energy unit", 
		"Docking computer", "Shield booster", "Military shield enhancement", "Galactic hyperdrive", "Scanner targeting enhancement", "Multi-targeting system"];
	var outList = ["", "", "", "", "", "", "", "", "", "", "", "", "", ""];

		// select ships to be in comparison (up to 4)
	text = "Select up to 3 ships to compare:\n";

	if (this._shipConfig) {
		// if this HUD doesn't have big gui enabled, take out the blank line normally under the ship type so there's room for the extra fields
		if (player.ship.hudAllowsBigGui === false) {
			textList.splice(1, 1);
			outList.splice(1, 1);
		}
	} else {
		text += "\n";
	}

	for (var ol = 0; ol < textList.length; ol++) {
		outList[ol] = this.$padTextRight(textList[ol] + (textList[ol] != "" ? ":" : ""), colDesc);
		for (var i = 0; i < shipArray.length; i++) {
			var shipDef = shipArray[i];
			var shipName = shipDef.name;
			if (this._override[this._selectedList[i]]) shipName = this._override[this._selectedList[i]];

			switch (textList[ol]) {
				case "Ship type":
					outList[ol] += this.$padTextRight(shipName, colShip);
					break;
				case "Techlevel":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("?", colShip);
					} else {
						if (shipDef._oo_shipyard) {
							outList[ol] += this.$padTextRight(shipDef._oo_shipyard.techlevel, colShip);
						} else {
							log(this.name, "!!ERROR: _oo_shipyard property not set for ship " + shipName + ", key " + keyList[i]);
						}
					}
					break;
				case "Forward weapon type":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("?", colShip);
					} else {
						outList[ol] += this.$padTextRight(this.$forwardWeaponType(shipDef), colShip);
					}
					break;
				case "ECM":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("?", colShip);
					} else {
						outList[ol] += this.$padTextRight(this.$shipDefHasOption(shipDef, "EQ_ECM"), colShip);
					}
					break;
				case "Fuel scoops":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("?", colShip);
					} else {
						outList[ol] += this.$padTextRight(this.$shipDefHasOption(shipDef, "EQ_FUEL_SCOOPS"), colShip);
					}
					break;
				case "Passenger berths":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("?", colShip);
					} else {
						outList[ol] += this.$padTextRight(this.$shipDefHasOption(shipDef, "EQ_PASSENGER_BERTH"), colShip);
					}
					break;
				case "Energy unit":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("?", colShip);
					} else {
						outList[ol] += this.$padTextRight(this.$shipDefHasOption(shipDef, "EQ_ENERGY_UNIT"), colShip);
					}
					break;
				case "Naval energy unit":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("?", colShip);
					} else {
						outList[ol] += this.$padTextRight(this.$shipDefHasOption(shipDef, "EQ_NAVAL_ENERGY_UNIT"), colShip);
					}
					break;
				case "Docking computer":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("?", colShip);
					} else {
						outList[ol] += this.$padTextRight(this.$shipDefHasOption(shipDef, "EQ_DOCK_COMP"), colShip);
					}
					break;
				case "Shield booster":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("?", colShip);
					} else {
						outList[ol] += this.$padTextRight(this.$shipDefHasOption(shipDef, "EQ_SHIELD_BOOSTER"), colShip);
					}
					break;
				case "Military shield enhancement":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("?", colShip);
					} else {
						outList[ol] += this.$padTextRight(this.$shipDefHasOption(shipDef, "EQ_NAVAL_SHIELD_BOOSTER"), colShip);
					}
					break;
				case "Galactic hyperdrive":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("?", colShip);
					} else {
						outList[ol] += this.$padTextRight(this.$shipDefHasOption(shipDef, "EQ_GAL_DRIVE"), colShip);
					}
					break;
				case "Scanner targeting enhancement":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("?", colShip);
					} else {
						outList[ol] += this.$padTextRight(this.$shipDefHasOption(shipDef, "EQ_SCANNER_SHOW_MISSILE_TARGET"), colShip);
					}
					break;
				case "Multi-targeting system":
					if (this._hideListAll.indexOf(shipName) >= 0) {
						outList[ol] += this.$padTextRight("?", colShip);
					} else {
						outList[ol] += this.$padTextRight(this.$shipDefHasOption(shipDef, "EQ_MULTI_TARGET"), colShip);
					}
					break;
				}
		}
	}

	for (var i = 0; i < outList.length; i++) {
		text += outList[i] + "\n";
	}

	if (this._selectedList.length === 0) {
		if (this._direction === 1) {
			curChoices["01_CHANGE"] = {text:"Change first ship to '" + this._shipList[0].name + "'", color:this._menuColor};
			curChoices["02_CHANGE"] = {text:"Change second ship to '" + this._shipList[0].name + "'", unselectable:true, color:this._disabledColor};
			curChoices["03_CHANGE"] = {text:"Change third ship to '" + this._shipList[0].name + "'", unselectable:true, color:this._disabledColor};
		} else {
			curChoices["01_CHANGE"] = {text:"Change first ship to '" + this._shipList[this._shipList.length - 1].name + "'", color:this._menuColor};
			curChoices["02_CHANGE"] = {text:"Change second ship to '" + this._shipList[this._shipList.length - 1].name + "'", unselectable:true, color:this._disabledColor};
			curChoices["03_CHANGE"] = {text:"Change third ship to '" + this._shipList[this._shipList.length - 1].name + "'", unselectable:true, color:this._disabledColor};
		}
	} else {
		for (var i = 0; i < this._selectedList.length; i++) {
			for (var j = 0; j < this._shipList.length; j++) {
				if (this._shipList[j].key === this._selectedList[i]) {
					if (this._direction === 1) {
						if (j < this._shipList.length - 1) {
							curChoices["0" + (i + 1) + "_CHANGE"] = {text:"Change " + this.$numberWord(i + 1) + " ship to '" + this._shipList[j + 1].name + "'", color:this._menuColor};
						} else {
							curChoices["0" + (i + 1) + "_CHANGE"] = {text:"Change " + this.$numberWord(i + 1) + " ship to '" + this._shipList[0].name + "'", color:this._menuColor};
						}
					} else {
						if (j > 0) {
							curChoices["0" + (i + 1) + "_CHANGE"] = {text:"Change " + this.$numberWord(i + 1) + " ship to '" + this._shipList[j - 1].name + "'", color:this._menuColor};
						} else {
							curChoices["0" + (i + 1) + "_CHANGE"] = {text:"Change " + this.$numberWord(i + 1) + " ship to '" + this._shipList[this._shipList.length - 1].name + "'", color:this._menuColor};
						}

					}
				}
			}
		}
		if (this._selectedList.length === 1) {
			if (this._direction === 1) {
				curChoices["02_CHANGE"] = {text:"Change second ship to '" + this._shipList[0].name + "'", color:this._menuColor};
				curChoices["03_CHANGE"] = {text:"Change third ship to '" + this._shipList[0].name + "'", unselectable:true, color:this._disabledColor};
			} else {
				curChoices["02_CHANGE"] = {text:"Change second ship to '" + this._shipList[this._shipList.length - 1].name + "'", color:this._menuColor};
				curChoices["03_CHANGE"] = {text:"Change third ship to '" + this._shipList[this._shipList.length - 1].name + "'", unselectable:true, color:this._disabledColor};
			}
		}
		if (this._selectedList.length === 2) {
			if (this._direction === 1) {
				curChoices["03_CHANGE"] = {text:"Change third ship to '" + this._shipList[0].name + "'", color:this._menuColor};
			} else {
				curChoices["03_CHANGE"] = {text:"Change third ship to '" + this._shipList[this._shipList.length - 1].name + "'", color:this._menuColor};
			}
		}
	}
	curChoices["04_CURRENT"] = {text:"Set first ship to current ship", color:this._menuColor};
	if (this._limitList == false) {
		curChoices["04A_LIMIT"] = {text:"Only include ships in current shipyard", color:this._menuColor};
	} else {
		curChoices["04B_LIMIT"] = {text:"Include all ships", color:this._menuColor};
	}
	curChoices["05_DIRECTION"] = {text:(this._direction === 1 ? "Change scroll direction to ascending" : "Change scroll direction to descending"), color:this._menuColor};
	curChoices["06_SHIPSTATS"] = {text:"View general ship statistics", color:this._itemColor};
	curChoices["99_EXIT"] = {text:"Return", color:this._itemColor};
	var def = "99_EXIT";

	var opts = {
		screenID: "oolite-ship-compare-map",
		title: "Ship Comparison",
		allowInterrupt: true,
		exitScreen: this._exitScreen,
		overlay: {name:"compare-balance.png", height:546},
		choices: curChoices,
		initialChoicesKey: (this._lastChoice ? this._lastChoice : def),
		message: text
	};

	mission.runScreen(opts, this.$screenHandlerEq, this);
}
	
//-------------------------------------------------------------------------------------------------------------
this.$screenHandler = function(choice) {
	if (!choice) return;

	this._lastChoice = choice;
	switch (choice) {
		case "01_CHANGE":
			this.$getNewItem(0);
			break;
		case "02_CHANGE":
			this.$getNewItem(1);
			break;
		case "03_CHANGE":
			this.$getNewItem(2);
			break;
		case "04_CURRENT":
			var idx = this.$findCurrentShip();
			if (idx >= 0) {
				this._selectedList[0] = this._shipList[idx].key;
			}
			break;
		case "04A_LIMIT":
			this._limitList = true;
			this._selectedList.length = 0;
			if (this._exitScreen == "GUI_SCREEN_INTERFACES") {
				this.$openComparison();
			} else {
				this.$directOpen();
			}
			return;
		case "04B_LIMIT":
			this._limitList = false;
			this._selectedList.length = 0;
			if (this._exitScreen == "GUI_SCREEN_INTERFACES") {
				this.$openComparison();
			} else {
				this.$directOpen();
			}
			return;
		case "06_EQUIPMENT":
			this._lastChoice = "06_SHIPSTATS";
			this.$showComparisonEquipment();
			return;
		case "05_DIRECTION":
			if (this._direction === 1) {
				this._direction = -1;
			} else {
				this._direction = 1;
			}
			break;
	}
	if (choice != "99_EXIT") this.$showComparison();
}

//-------------------------------------------------------------------------------------------------------------
this.$screenHandlerEq = function(choice) {
	if (!choice) return;

	this._lastChoice = choice;
	switch (choice) {
		case "01_CHANGE":
			this.$getNewItem(0);
			break;
		case "02_CHANGE":
			this.$getNewItem(1);
			break;
		case "03_CHANGE":
			this.$getNewItem(2);
			break;
		case "04_CURRENT":
			var idx = this.$findCurrentShip();
			if (idx >= 0) {
				this._selectedList[0] = this._shipList[idx].key;
			}
			break;
		case "04A_LIMIT":
			this._limitList = true;
			this._selectedList.length = 0;
			this._switch = true;
			if (this._exitScreen == "GUI_SCREEN_INTERFACES") {
				this.$openComparison();
			} else {
				this.$directOpen();
			}
			return;
		case "04B_LIMIT":
			this._limitList = false;
			this._selectedList.length = 0;
			this._switch = true;
			if (this._exitScreen == "GUI_SCREEN_INTERFACES") {
				this.$openComparison();
			} else {
				this.$directOpen();
			}
			return;
		case "06_SHIPSTATS":
			this._lastChoice = "06_EQUIPMENT";
			this.$showComparison();
			return;
		case "05_DIRECTION":
			if (this._direction === 1) {
				this._direction = -1;
			} else {
				this._direction = 1;
			}
			break;
	}
	if (choice != "99_EXIT") this.$showComparisonEquipment();
}

//-------------------------------------------------------------------------------------------------------------
this.$getNewItem = function(idx) {
	var cur = "";
	if (this._selectedList.length >= (idx + 1)) {
		cur = this._selectedList[idx];
	}
	if (cur === "") {
		this._selectedList.push(this._shipList[0].key);
	} else {
		for (var i = 0; i < this._shipList.length; i++) {
			if (this._shipList[i].key === cur) {
				if (this._direction === 1) {
					if (i === this._shipList.length - 1) {
						this._selectedList[idx] = this._shipList[0].key;
					} else {
						this._selectedList[idx] = this._shipList[i + 1].key;
					}
				} else {
					if (i === 0) {
						this._selectedList[idx] = this._shipList[this._shipList.length - 1].key;
					} else {
						this._selectedList[idx] = this._shipList[i - 1].key;
					}
				}
				break;
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$findCurrentShip = function() {
	var p = player.ship;
	for (var i = 0; i < this._shipList.length; i++) {
		if (this._shipList[i].name == p.shipClassName) return i;
	}
	return -1;
}

//-------------------------------------------------------------------------------------------------------------
this.$buildShipList = function() {
	var truevalues = this._trueValues;
	var keys = Ship.keysForRole("player");
	this._shipList.length = 0;

	if (this._limitList) {
		var stn = player.ship.dockedStation;
		// get all ships in the shipyard
		var numShips = stn.shipyard.length;
		for (var i = 0; i < numShips; i++) {
			var shp = stn.shipyard[i];
			this.$addItemToArray(shp.ship.name, shp.shipdata_key);
		}
		// make sure the player ship is added, though
		this.$addItemToArray(player.ship.shipClassName, player.ship.dataKey);
	} else {
		for (var i = 0; i < keys.length; i++) {
			var shp = Ship.shipDataForKey(keys[i]);
			// only add records that have a corresponding shipyard entry
			if (shp._oo_shipyard) {
				if (!shp.script_info || !shp.script_info["sc_ignore"] || truevalues.indexOf(shp.script_info["sc_ignore"]) === -1)
					this.$addItemToArray(shp.name, keys[i]);
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$addItemToArray = function(shipName, shipKey) {
	var ovr = this._override[shipKey];
	if (ovr) shipName = ovr;
	for (var i = 0; i < this._shipList.length; i++) {
		if (this._shipList[i].name === shipName) return;
	}
	this._shipList.push({name:shipName, key:shipKey});
}

//-------------------------------------------------------------------------------------------------------------
this.$numberWord = function(nbr) {
	switch (nbr) {
		case 1: return "First";
		case 2: return "Second";
		case 3: return "Third";
		case 4: return "Fourth"; // in case we ever add a fourth column!
	}
}

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

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

//-------------------------------------------------------------------------------------------------------------
this.$shipDefHasOption = function(def, eqKey) {
	var found = "N/A";
	if (!def._oo_shipyard) return "Data error";
	if (def._oo_shipyard.standard_equipment.extras && def._oo_shipyard.standard_equipment.extras.indexOf(eqKey) >= 0) found = "Standard";
	if (found === "N/A" && def._oo_shipyard.optional_equipment && def._oo_shipyard.optional_equipment.indexOf(eqKey) >= 0) found = "Optional";
	return found;
}

//-------------------------------------------------------------------------------------------------------------
this.$forwardWeaponType = function(def) {
	var found = "None";
	if (!def._oo_shipyard) return "Data error";
	if (def._oo_shipyard.standard_equipment.forward_weapon_type && def._oo_shipyard.standard_equipment.forward_weapon_type != "") {
		var eq = EquipmentInfo.infoForKey(def._oo_shipyard.standard_equipment.forward_weapon_type);
		if (eq) {
			found = eq.name;
		} else {
			found = "Unknown";
		}
	}
	return found;
}

//-------------------------------------------------------------------------------------------------------------
this.$cleanUpEquipSpace = function $cleanUpEquipSpace() {
	var newData = [];
	for (var i = 0; i < this._equipSpace.length; i++) {
		var found = false;
		for (var j = 0; j < newData.length; j++) {
			if (newData[j].shipType == this._equipSpace[i].shipType) {
				found = true;
				break;
			}
		}
		if (found === false) {
			newData.push(JSON.parse(JSON.stringify(this._equipSpace[i])));
		}
	}
	this._equipSpace.length = 0;
	this._equipSpace = newData;
}

//-------------------------------------------------------------------------------------------------------------
// external routine to return the mass for a ship key
// we'll create a temporary ship object and use that to get the details
this.$getMassFromShipKey = function $getMassFromShipKey(shipKey) {
	var result = 0;
	// find a spot well away from any possible objects
	if (system.sun) {
		var temppos = system.sun.position.cross(system.mainPlanet.position).direction().multiply(4E9).subtract(system.mainPlanet.position);
	} else {
		var temppos = Vector3D(0, 0, 0);
	}
	// add a ship with the particular key
	var tempship = system.addShips("[" + shipKey + "]", 1, temppos, 0);
	if (tempship != null) {
		// force this ship to be "dead" in space - we don't want it to do anything disruptive!
		tempship[0].switchAI("oolite-nullAI.js");
		result = this.$getShipMass(tempship[0]);
		// clean up our temp ship
		tempship[0].remove(true);
	} else {
		// this could happen if the ship is not allowed to be spawned in this system due to constraints in the shipdata.plist file for the ship
		result = 0;
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// returns the ships mass, either from the exception list, or from the ship itself
this.$getShipMass = function $getShipMass(ship) {
	var mass = this._shipMassExceptions[ship.shipClassName];
	if (!mass) mass = ship.mass;
	return mass;
}

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