| Scripts/oolite-contracts-cargo.js | /*
oolite-contracts-cargo.js
Script for managing cargo contracts
 
Oolite
Copyright © 2004-2013 Giles C Williams and contributors
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/*jslint white: true, undef: true, eqeqeq: true, bitwise: true, regexp: true, newcap: true, immed: true */
/*global galaxyNumber, missionVariables, system*/
"use strict";
this.name			= "oolite-contracts-cargo";
this.author			= "cim and Switeck";
this.copyright		= "© 2012-2013 the Oolite Team, 2022 Switeck";
this.licence			= "Gnu Public License v2+";
this.description	= "Cargo delivery contracts.";
this.version			= "1.88 MOD 1.64";
/**** Configuration options and API ****/
/* OXPs which wish to add a background to the summary pages should set this value */
this.$cargoSummaryPageBackground = "";
/* OXPs which wish to add an overlay to the cargo mission screens should set this value */
this.$cargoPageOverlay = "";
/* this._addCargoContractToSystem(cargo)
 * This function adds the defined cargo contract to the local main station's interface list. A contract definition is an object with the following parameters, all required:
 * 
 * destination:	system ID of destination system
 * commodity:	the cargo type
 * size:		the number of units of cargo
 * deadline:	the deadline for delivery, in clock seconds
 * payment:	the payment for delivery on time, in credits
 * 
 * and optionally, the following parameters:
 *
 * deposit:	the deposit payment required by the player (default 0)
 * route:		a route object generated with system.info.routeToSystem
 * 	describing the route between the source and destination systems.
 * 
 * If this is not specified, it will be generated automatically.
 * 
 * The function will return true if the contract can be added, false otherwise.
 */
this._addCargoContractToSystem = function(cargo)
{
	if(!system.mainStation) {
		log(this.name,"Contracts require a main station");
		return false;
	}
	if(cargo.destination < 0 || cargo.destination > 255) {
		log(this.name,"Rejected contract: destination missing or invalid");
		return false;
	}
	if(cargo.deadline <= clock.adjustedSeconds) {
		log(this.name,"Rejected contract: deadline invalid");
		return false;
	}
	if(cargo.payment < 0) {
		log(this.name,"Rejected contract: payment invalid");
		return false;
	}
	if(!cargo.size || cargo.size < 1) {
		log(this.name,"Rejected contract: size invalid");
		return false;
	}
	if(!cargo.commodity) {
		log(this.name,"Rejected contract: commodity unspecified");
		return false;
	}
	if(!system.mainStation.market[cargo.commodity]) {
	log(this.name,"Rejected contract: commodity invalid");
	return false;
	}
	if(!cargo.route) {
		var destinationInfo = System.infoForSystem(galaxyNumber,cargo.destination);
		cargo.route = system.info.routeToSystem(destinationInfo);
		if(!cargo.route) {
			log(this.name,"Rejected contract: route invalid");
			return false;
		}
	}
	if(!cargo.deposit) cargo.deposit = 0;
	else if(cargo.deposit > cargo.payment) {
		log(this.name,"Rejected contract: deposit higher than total payment");
		return false;
	}
	this.$contracts.push(cargo);
	this._updateMainStationInterfacesList();
	return true;
}
/**** Internal methods. Do not call these from OXPs as they may change without warning. ****/
/* Event handlers */
this.startUp = function() 
{
	this.$helper = worldScripts["oolite-contracts-helpers"];
	this.$suspendedDestination = null;
	this.$suspendedHUD = false;
// stored contents of local main station's parcel contract list
	if(missionVariables.oolite_contracts_cargo) this.$contracts = JSON.parse(missionVariables.oolite_contracts_cargo);
	else this._initialiseCargoContractsForSystem();
	this._updateMainStationInterfacesList();
}
this.shipWillExitWitchspace = function()
{
	if(!system.isInterstellarSpace && !system.sun.hasGoneNova && system.mainStation) {	// must be a regular system with a main station
		this._initialiseCargoContractsForSystem();
		this._updateMainStationInterfacesList();
	}
}
this.playerWillSaveGame = function()
{
	missionVariables.oolite_contracts_cargo = JSON.stringify(this.$contracts);	// encode the contract list to a string for storage in the savegame
}
// when the player exits the mission screens, reset their destination system and HUD settings, which the mission screens may have affected.
this.shipWillLaunchFromStation = function() 
{
	this._resetViews();
}
this.guiScreenWillChange = function(to, from)
{
	this._resetViews();
}
this.guiScreenChanged = function(to, from)
{
	if(to != "GUI_SCREEN_MISSION") this._resetViews();
}
/* Interface functions */
// resets HUD and jump destination
this._resetViews = function()
{
	if(this.$suspendedHUD !== false) {
		player.ship.hudHidden = false;
		this.$suspendedHUD = false;
	}
	if(this.$suspendedDestination !== null) {
		player.ship.targetSystem = this.$suspendedDestination;
		this.$suspendedDestination = null;
	}
}
// initialise a new cargo contract list for the current system
this._initialiseCargoContractsForSystem = function() 
{
// clear list
	this.$contracts = [];
// this is not the same algorithm as in 1.76, but should give similar results with comparable efficiency.
// no point in generating too many, as route-finding is slow
	var scratchval = system.info.systemsInRange(80-player.contractReputationPrecise*7); // 31 LY range at 7 Rep, 80 LY range at 0 Rep, 129 LY range at -7 Rep...done here to save recalculations time.
	var nearbysystems = [];
	for (var i = 0; i < scratchval.length; i++) {	// this loop removes 'bad' nearby systems from being chosen
		var destination = scratchval[i].systemID;
		if(destination != system.ID) {	// discard if chose the current system
			var destinationInfo = System.infoForSystem(galaxyNumber,destination);	// get the SystemInfo object for the destination
			if(!destinationInfo.sun_gone_nova) {	// Discarded, since novas tend to eliminate markets!
				var routeToDestination = system.info.routeToSystem(destinationInfo);	// check that a route to the destination exists
				if(routeToDestination) nearbysystems = nearbysystems.concat([destination]);	 // if the system cannot be reached, ignore this contract
			}
		}
	}
	if(nearbysystems.length < 1) return;	// failed to find a good destination. Should only happen with Oresrati in Gal. Chart 8!
// system.economy	0=RI, 7=PA		0=RI and 5=RA should increase contracts, 2=PI and 7=PA should decrease contracts
// system.government	0=Anarchy, 7=Corporate State	"higher"-numbered governments should increase contracts, up to 7 more for CS
// system.techLevel	0=TL1, 14=TL15		<TL6 should reduce contracts, >TL10 should increase contracts
//	var numContracts =0;
	var numContracts =system.techLevel*0.2+3;
	if(system.economy==0 || system.economy==5) numContracts++;
	if(system.economy==2 || system.economy==7) numContracts--;
//	var numContracts = Math.max(0,Math.min(18,Math.ceil((numContracts+system.techLevel*0.2+3)*(Math.random()+Math.random())+(system.government*0.5+player.contractReputationPrecise-player.bounty*0.02)*(Math.random()+Math.random()))));
	var numContracts = Math.max(0,Math.min(18,Math.ceil(numContracts*(Math.random()+Math.random())+(system.government*0.5+player.contractReputationPrecise-player.bounty*0.05)*(Math.random()+Math.random()))));
	if(player.contractReputationPrecise > 1 && numContracts < 5 && nearbysystems.length > 5) numContracts += Math.ceil(player.contractReputationPrecise*0.5+Math.random()*(5-numContracts));	// ORG was always +5 at Rep >= 0;
//	if(player.contractReputationPrecise > numContracts) numContracts += Math.ceil(player.contractReputationPrecise*Math.random());	// ORG was always +5 at Rep >= 0;
	var numContracts = Math.min(Math.max(Math.ceil(player.contractReputationPrecise*0.5),nearbysystems.length),numContracts);	// to limit number of contracts in 2-system unreachable locations!
//	if(nearbysystems.length == 0) numContracts = 0;	// Oresrati gets none -- nowhere to deliver them to!
// some of these possible contracts may be discarded later on
	log(this.name," player.contractReputation= "+player.contractReputationPrecise+" player.bounty= "+player.bounty+" numContracts= "+numContracts+" Gov.type= "+system.government+" Eco.type= "+system.economy+" TL= "+system.techLevel+" nearbysystems.length= "+nearbysystems.length);
//	var numContracts = 21;	// FOR TESTING ONLY! Placed here so logging will still record how many contracts there is supposed to be.
	for (var i = 0; i < numContracts; i++) {
		var cargo = new Object;
		if(nearbysystems.length == 1) var destination = nearbysystems[0];
		else var destination = nearbysystems[Math.floor(Math.random() * nearbysystems.length)];
		var destinationInfo = System.infoForSystem(galaxyNumber,destination);	// get the SystemInfo object for the destination
		var routeToDestination = system.info.routeToSystem(destinationInfo);	// check that a route to the destination exists
		var routeJumps = routeToDestination.route.length;
		var daysUntilDeparture = 1+Math.random()*Math.max(-1,routeJumps+player.contractReputationPrecise-destinationInfo.government+system.government*0.5);
//	log(this.name," attempts = "+attempts+" destination = "+destination+" destinationInfo = "+destinationInfo);
// we now have a valid destination, so generate the rest of the cargo contract details
		var attempts = 0;
		do {
			var remotePrice = 0;
			var scratchval = 0;
			var unitPrice = 0;
			attempts++;
// good luck on the fixes needed here!
			if(player.contractReputationPrecise - player.bounty*0.01 + Math.random()*3 < 0) {	// negative reputation and/or high bounty gets awful choices.
				var commodities = ["food","textiles","radioactives","slaves","liquor_wines","luxuries","narcotics","computers","machinery","alloys","firearms","furs","minerals","gold","platinum","gem_stones"];	// 16 entries, missing alien items
				var preccutoff = 13;
				var scratchval = Math.floor(Math.random()*commodities.length);
			} else {
			if(system.economy < 4 || system.economy + 1 < destinationInfo.economy) { // more Industrial eco. only!
				if(system.economy >= destinationInfo.economy && player.contractReputationPrecise + Math.random()*2 > 0) {	// Eliminates marginal profit commodities from "wrong way" Ind to Ind economies
					var commodities = ["narcotics","liquor_wines","alloys","furs","gold","platinum","gem_stones"]; // 7 entries
					var preccutoff = 4;
					var scratchval = Math.floor(Math.random()*(commodities.length-2)+2); // Drops the controlled and low profit items at high rep.
				} else if(system.economy > destinationInfo.economy - Math.floor((Math.random() + player.contractReputationPrecise)*0.3) ) {	// Eliminates low-profit commodities when start economy is only slightly more industrial than destination.
					var commodities = ["firearms","narcotics","furs","machinery","computers","alloys","platinum","gem_stones"]; // 8 entries, removed Gold!
					var preccutoff = 6;
					var scratchval = Math.floor(Math.random()*(commodities.length-2)+2); // Drops the controlled items.
				} else {
					var commodities = ["firearms","narcotics","alloys","machinery","luxuries","computers","platinum","gem_stones"];	// 8 entries, sorted from worst to best, removed Gold!
					var preccutoff = 6;
					var scratchval = Math.floor(Math.random()*(commodities.length-2)+2); // Drops the controlled item.
				}
			} else if(system.economy <= destinationInfo.economy && player.contractReputationPrecise + Math.random()*2 > 0) {	// Eliminates low-profit commodities when start economy >= destination (similar/identical agri economies)
					var commodities = ["slaves","narcotics","liquor_wines","alloys","furs","platinum","gem_stones"]; // 7 entries, removed Gold!
					var preccutoff = 5;
					var scratchval = Math.floor(Math.random()*(commodities.length-3)+3); // Drops the controlled and low profit items at high rep.
				} else {
					var commodities = ["slaves","narcotics","minerals","textiles","food","radioactives","liquor_wines","furs","gold","platinum","gem_stones"]; // 11 entries, sorted from worst to best
					var preccutoff = 8;
					var scratchval = Math.floor(Math.random()*(commodities.length-5)+5); // Drops the controlled and low profit items at high rep.
				}
			}
// At rep>3, there's a chance of getting ONLY good contracts. The odds of this are much better by rep>5.
			if(player.contractReputationPrecise - player.bounty*0.01 + Math.sqrt(routeJumps+Math.random()*5) < 8) var scratchval = Math.max(0,Math.floor(Math.random()*(preccutoff+player.contractReputationPrecise*0.1-player.bounty*0.01)));
			if(scratchval >= preccutoff && player.contractReputationPrecise < 6.5) var scratchval = preccutoff -1;	// sub-TC contracts only available for top rep
			if(scratchval > commodities.length -1) var scratchval = commodities.length -1;	// If high rep pushes off rightside of the chart, give player "best" contract.
			var commodity = commodities[scratchval];
			if(system.mainStation.market[commodity].quantity > 0) {	// Do not use commodities with 0 availability
//				log(this.name," # "+attempts+", "+commodities+", "+commodity+", "+system.economy+", "+destinationInfo.economy+" , "+destinationInfo+" , "+destination);
				var exDiff = Math.abs( system.economy - system.mainStation.market[commodity]["peak_export"])*2;
				var imDiff = Math.abs( system.economy - system.mainStation.market[commodity]["peak_import"])*2;
				var ecoDiff = (exDiff+imDiff)*0.5;
				var efactor;
				if (exDiff == imDiff) efactor = 0	// neutral economy
				else if (exDiff > imDiff) efactor = (imDiff/ecoDiff)-1	// closer to the importer, so return -ve
				else efactor = 1 - (exDiff/ecoDiff);	// closer to the exporter, so return +ve
				var unitPrice = Math.ceil( parseFloat(system.mainStation.market[commodity]["price_average"]) * ( 1 - ( efactor * parseFloat(system.mainStation.market[commodity]["price_economic"]) ) - parseFloat(system.mainStation.market[commodity]["price_random"]) ) ) * 0.1;
				var unitOrgPrice = Number((system.mainStation.market[commodity].price*0.1)).toFixed(1);
				exDiff = Math.abs( destinationInfo.economy - system.mainStation.market[commodity]["peak_export"])*2;
				imDiff = Math.abs( destinationInfo.economy - system.mainStation.market[commodity]["peak_import"])*2;
				ecoDiff = (exDiff+imDiff)*0.5;
				if (exDiff == imDiff) efactor = 0
				else if (exDiff > imDiff) efactor = (imDiff/ecoDiff)-1
				else efactor = 1 - (exDiff/ecoDiff);
				var remotePrice = Math.ceil( parseFloat(system.mainStation.market[commodity]["price_average"]) * ( 1 - ( efactor * parseFloat(system.mainStation.market[commodity]["price_economic"]) ) + parseFloat(system.mainStation.market[commodity]["price_random"]) ) ) * 0.1;
				var remotePrice2 = Number(this._priceForCommodity(commodity,destinationInfo)).toFixed(1); // Random price only!
			}
		} while ( (remotePrice-Math.max(1,(player.contractReputationPrecise + Math.sqrt(routeJumps))*(0.5 -(0.2*parseFloat(system.mainStation.market[commodity]["quantity_unit"])))) < unitPrice || remotePrice == 0) && attempts < Math.max(1,(player.contractReputationPrecise + 3)));	// remotePrice must be at least 1 credit more than unitPrice
		if(remotePrice <= unitPrice) continue; // failed to find a good one. Comment out for testing!
		cargo.commodity = commodity;
		var unitsize = 1;	// larger unit sizes for kg/g commodities
		var amount = Math.max(Math.ceil((1+Math.random()*32)*(1+Math.random()*16)),Math.floor(30+Math.random()*6)); // forced minimum amount to always be at least 30-35. Eliminates while-looping here!
		if(system.mainStation.market[commodity]["quantity_unit"] != 0) {	// this covers Gold, Plat, and Gems
			if(routeJumps > 4) amount = Math.max(amount,Math.ceil((1+Math.random()*32)*(1+Math.random()*16)));	// precious metals contracts get an extra chance to be large amounts, since original used a while loop ADDITION...but only if they're more than 4 jumps.
			unitsize += Math.ceil((Math.random()+Math.random()+Math.random())*Math.min(10,Math.max(1,routeJumps-player.bounty*0.04)));	// Multiplying by routeJumps HAS an upper+lower limit because routeJumps=2 for a 1-jump route. Original used x6 but rounded down on each random part.
			if(system.mainStation.market[commodity]["quantity_unit"] == 2) unitsize *= 2;	// Gems get twice as many as Gold and Platinum!
			amount *= unitsize;
		}
		if(amount > 125 && amount > player.ship.cargoSpaceCapacity && player.contractReputationPrecise - player.bounty*0.02 >= 0 && unitsize == 1) amount = Math.floor(amount/Math.floor(1+Math.random()*4));	// reduce the number of contracts only suitable for Anacondas - poor rep or high bounty leaves more huge+unusable contract visible
		cargo.size = amount;
//		var discount2 = Math.min(10+Math.floor(amount*0.1),35);	// Original discount adjustment to prices based on quantity (larger = more profitable)
		var discount2 = Math.min(10+Math.floor(amount/unitsize*0.1),35);	// Original discount adjustment to prices based on base quantity (larger = more profitable)
		var localValue = Math.ceil(unitOrgPrice * amount * 0.1)*10; // round to nearest 10 credits
		var localValue2 = Math.ceil(unitPrice * amount * 0.1)*10;
		var remoteValue = Math.ceil(remotePrice * amount * 0.1)*10;
		var remoteValue2 = Math.ceil(remotePrice2 * amount * 0.1)*10;
		var profit2 = Math.floor( (remoteValue2 * (200+discount2) *0.0005) - (localValue * (100-discount2) *0.001) )*10;	// nearly original profit given by original/official old version of cargo contracts.
		var profit = remoteValue-localValue2;
		var discount = Math.ceil(30+player.contractReputationPrecise*8 + routeJumps*5 + (amount/unitsize)*0.1 +10000/profit - player.bounty*0.2 - destinationInfo.government - system.government);
		var profit3 = Math.ceil(profit*Math.min(100,Math.max(50,discount))*0.001)*10;
		if(profit2 < 100 && profit < 100) continue;	// skip if unprofitable - comment out for TESTING ONLY!
// we have a valid destination, so generate the rest of the parcel details
		cargo.destination = destination;
		cargo.route = routeToDestination;	// we'll need this again later, and route calculation is slow
// higher share for transporter for longer routes, less safe systems
		var share = Math.min(90,(10*routeJumps) - destinationInfo.government);
		if((profit2*(share*0.01) > profit3 && (system.mainStation.market[commodity]["quantity_unit"] == 0 || profit3 < 100) && player.contractReputationPrecise - player.bounty*0.02 > 3) || (profit2*(share*0.01) < profit3 && profit2*(share*0.01) > 100 && commodity != "narcotics" && commodity != "slaves" && commodity != "firearms" && player.contractReputationPrecise - player.bounty*0.02 < -2) ) {	// Uses most profitable method at high Rep for all regular contracts and Switeck method for most Gold/Plat/Gems contracts.
			cargo.payment = Math.ceil( (localValue + profit2)*0.1)*10; // round to nearest 10 credits
			cargo.deposit = Math.ceil((cargo.payment - Math.floor(profit2 *share *0.01)) *0.1)*10;
		} else {
			cargo.payment = Math.ceil( (localValue + profit)*0.1)*10; // round to nearest 10 credits - profit3 is often < profit due to discount being 100% or less!
			cargo.deposit = Math.ceil((cargo.payment - profit3) *0.1)*10;
		}
		if(cargo.deposit > cargo.payment) continue; // rare but not impossible; last safety check
// time allowed for delivery is time taken by "fewest jumps" route, plus timer above. Higher reputation makes longer times available.
		cargo.deadline = clock.adjustedSeconds + Math.floor(daysUntilDeparture*86400)+(cargo.route.time*3600);
// logs nearly everything
log(this.name,
(i+1)+". "
+cargo.size+" "+commodity
+" multi="+unitsize
+" Eco="+system.economy
+" Dest="+destinationInfo.economy
+" Jumps="+routeJumps
+" unitP="+Number(unitPrice).toFixed(1)
+"-"+unitOrgPrice
+" remP="+Number(remotePrice2).toFixed(1)
+"-"+Number(remotePrice).toFixed(1)
+" Pr="+profit
+" *"+discount
+"%="+profit3
+" PrDif="+(profit3 - Math.floor(profit2 *share *0.01))
+" NetPr="+(Math.floor(profit2 *share *0.01))
+" ="+profit2
+" *1."+discount2
+" *"+share+"%"
+" locVal="+localValue
+"-"+localValue2
+"="+(localValue - localValue2)
+" Dep="+cargo.deposit
+" remVal="+remoteValue
+" Pay="+cargo.payment
);
		this._addCargoContractToSystem(cargo);	// add parcel to contract list
	}
}
// this should be called every time the contents of this.$parcels changes, as it updates the summary of the interface entry.
this._updateMainStationInterfacesList = function()
{
	if(this.$contracts.length === 0) system.mainStation.setInterface("oolite-contracts-cargo",null);	// no contracts, remove interface if it exists
	else {
		var title = expandMissionText("oolite-contracts-cargo-interface-title",{
		"oolite-contracts-cargo-interface-title-count": this.$contracts.length });
		system.mainStation.setInterface("oolite-contracts-cargo",{
		title: title,
		category: expandMissionText("oolite-contracts-cargo-interface-category"),
		summary: expandMissionText("oolite-contracts-cargo-interface-summary"),
		callback: this._cargoContractsScreens.bind(this)
		});
// could alternatively use "cbThis: this" parameter instead of bind()
	}
}
// if the interface is activated, this function is run.
this._cargoContractsScreens = function(interfaceKey)
{
// the interfaceKey parameter is not used here, but would be useful if this callback managed more than one interface entry
	this._validateContracts();
// set up variables used to remember state on the mission screens
	this.$suspendedDestination = null;
	this.$suspendedHUD = false;
	this.$contractIndex = 0;
	this.$routeMode = "LONG_RANGE_CHART_SHORTEST";
	this.$lastOptionChosen = "06_EXIT";
// start on the summary page if more than one contract is available
	var summary = (this.$contracts.length > 1);
	this._cargoContractsDisplay(summary);
}
// this function is called after the player makes a choice which keeps them in the system, and also on initial entry to the system to select the appropriate mission screen and display it
this._cargoContractsDisplay = function(summary) {
	this._validateContracts(); 	// Again. Has to be done on every call to this function, but also has to be done at the start.
// if there are no contracts (usually because the player has taken the last one) display a message and quit.
	if(this.$contracts.length === 0) {
		var missionConfig = {titleKey: "oolite-contracts-cargo-none-available-title",
		messageKey: "oolite-contracts-cargo-none-available-message",
		allowInterrupt: true,
		screenID: "oolite-contracts-cargo-none",
		exitScreen: "GUI_SCREEN_INTERFACES"};
		if(this.$cargoSummaryPageBackground != "") missionConfig.background = this.$cargoSummaryPageBackground;
		if(this.$cargoPageOverlay != "") missionConfig.overlay = this.$cargoPageOverlay;
		mission.runScreen(missionConfig);
		return;		// no callback, just exits contracts system
	}
	if(this.$contractIndex >= this.$contracts.length) this.$contractIndex = 0;	// make sure that the 'currently selected contract' pointer is in bounds
	else if(this.$contractIndex < 0) this.$contractIndex = this.$contracts.length - 1;
	if(summary) this._cargoContractSummaryPage();	// sub functions display either summary or detail screens
	else this._cargoContractSinglePage();
}
// display the mission screen for the summary page
this._cargoContractSummaryPage = function()
{
	var columns = [10,16,21,26];	// column 'tab stops'
	var headline = expandMissionText("oolite-contracts-cargo-column-goods");	// column header line
	headline += this.$helper._paddingText(headline,columns[0]);	// pad to correct length to give a table-like layout
	headline += expandMissionText("oolite-contracts-cargo-column-destination");
	headline += this.$helper._paddingText(headline,columns[1]);
	headline += expandMissionText("oolite-contracts-cargo-column-within");
	headline += this.$helper._paddingText(headline,columns[2]);
	headline += expandMissionText("oolite-contracts-cargo-column-deposit");
	headline += this.$helper._paddingText(headline,columns[3]);
	headline += expandMissionText("oolite-contracts-cargo-column-fee");
	headline = " "+headline;	// required because of way choices are displayed.
	var options = new Object;	// setting options dynamically; one contract per line
	var i;
	var anyWithSpace = false;
	for (i=0; i<this.$contracts.length; i++) {
		var cargo = this.$contracts[i];		// temp variable to simplify following code
		var optionText = this._descriptionForGoods(cargo);		// write the description, padded to line up with the headers
		optionText += this.$helper._paddingText(optionText, columns[0]);
		optionText += System.infoForSystem(galaxyNumber, cargo.destination).name;
		optionText += this.$helper._paddingText(optionText, columns[1]);
		optionText += this.$helper._timeRemaining(cargo);
		optionText += this.$helper._paddingText(optionText, columns[2]);
		var priceText = formatCredits(cargo.deposit,false,true);		// right-align the fee so that the credits signs line up
		priceText = this.$helper._paddingText(priceText, 3.25)+priceText;
		optionText += priceText
		optionText += this.$helper._paddingText(optionText, columns[3]);
		priceText = formatCredits(cargo.payment-cargo.deposit,false,true);		// right-align the fee so that the credits signs line up
		priceText = this.$helper._paddingText(priceText, 3.25)+priceText;
		optionText += priceText
		var istr = i;		// need to pad the number in the key to maintain alphabetical order
		if(i < 10) istr = "0"+i;
		options["01_CONTRACT_"+istr] = { text: optionText, alignment: "LEFT" };		// needs to be aligned left to line up with the heading
		if(!this._hasSpaceFor(cargo)) options["01_CONTRACT_"+istr].color = "darkGrayColor";	// check if there's space for this contract
		else {
			anyWithSpace = true;
			if(this.$helper._timeRemainingSeconds(cargo) < this.$helper._timeEstimateSeconds(cargo)) options["01_CONTRACT_"+istr].color = "orangeColor";			// if there doesn't appear to be sufficient time remaining
		}
	}
	var icstr = this.$contractIndex;	// if we've come from the detail screen, make sure the last contract shown there is selected here
	if(icstr < 10) icstr = "0"+this.$contractIndex;
	var initialChoice = "01_CONTRACT_"+icstr;
	if(!anyWithSpace) initialChoice = "06_EXIT";	// if none of them have any space...
	if(this.$contracts.length<18) options["02_SPACER"] = ""; 	// next, an empty string gives an unselectable row, but only when there's fewer than 18 contracts shown
	options["06_EXIT"] = expandMissionText("oolite-contracts-cargo-command-quit");	// numbered 06 to match the option of the same function in the other branch
	var rowsToFill = 21;	// now need to add further spacing to fill the remaining rows, or the options will end up at the bottom of the screen.
	if(player.ship.hudAllowsBigGui) rowsToFill = 27;
	for (i = 4 + this.$contracts.length; i < rowsToFill ; i++) {
		options["07_SPACER_"+i] = "";		// each key needs to be unique at this stage.
	}
	var missionConfig = {titleKey: "oolite-contracts-cargo-title-summary",
		message: headline,
		allowInterrupt: true,
		screenID: "oolite-contracts-cargo-summary",
		exitScreen: "GUI_SCREEN_INTERFACES",
		choices: options,
		initialChoicesKey: initialChoice}; 
	if(this.$cargoSummaryPageBackground != "") missionConfig.background = this.$cargoSummaryPageBackground;
	if(this.$cargoPageOverlay != "") missionConfig.overlay = this.$cargoPageOverlay;
	mission.runScreen(missionConfig, this._processCargoChoice, this);	// now run the mission screen
}
// display the mission screen for the contract detail page
this._cargoContractSinglePage = function()
{
	var cargo = this.$contracts[this.$contractIndex];	// temp variable to simplify code
// This mission screen uses the long range chart as a backdrop.
// This means that the first 18 lines are taken up by the chart, and we can't put text there without overwriting the chart.
// We therefore need to hide the player's HUD, to get the full 27 lines.
	if(!player.ship.hudAllowsBigGui) {
		this.$suspendedHUD = true; // note that we hid it, for later
		player.ship.hudHidden = true;
	}
	this.$suspendedDestination = player.ship.targetSystem;	// We also set the player's witchspace destination temporarily so we need to store the old one in a variable to reset it later
	player.ship.targetSystem = cargo.destination;	// That done, we can set the player's destination so the map looks right.
	var message = new Array(18).join("\n");	// start with 18 blank lines, since we don't want to overlap the chart
	message += expandMissionText("oolite-contracts-cargo-long-description",{
		"oolite-contracts-cargo-longdesc-goods": this._descriptionForGoods(cargo),
		"oolite-contracts-cargo-longdesc-destination": this.$helper._systemName(cargo.destination),
		"oolite-contracts-cargo-longdesc-deadline": this.$helper._timeRemaining(cargo),
		"oolite-contracts-cargo-longdesc-time": this.$helper._timeEstimate(cargo),
		"oolite-contracts-cargo-longdesc-payment": formatCredits(cargo.payment,false,true),
		"oolite-contracts-cargo-longdesc-deposit": formatCredits(cargo.deposit,false,true)
	});
	var backgroundSpecial = "LONG_RANGE_CHART";	// use a special background
	var options = new Object;	// the available options will vary quite a bit, so this rather than a choicesKey in missiontext.plist
	options["06_EXIT"] = expandMissionText("oolite-contracts-cargo-command-quit");	// this is the only option which is always available
	if(this._hasSpaceFor(cargo)) {	// if the player has sufficient space
		options["05_ACCEPT"] = { text: expandMissionText("oolite-contracts-cargo-command-accept") };
		if(this.$helper._timeRemainingSeconds(cargo) < this.$helper._timeEstimateSeconds(cargo)) options["05_ACCEPT"].color = "orangeColor";		// if there's not much time left, change the option colour as a warning!
	} else {
		options["05_UNAVAILABLE"] = {
			text: expandMissionText("oolite-contracts-cargo-command-unavailable"),
			color: "darkGrayColor",
			unselectable: true
		};
	}
// if the ship has a working advanced nav array, can switch between 'quickest' and 'shortest' routes (and also upgrade the special background)
	if(player.ship.equipmentStatus("EQ_ADVANCED_NAVIGATIONAL_ARRAY") === "EQUIPMENT_OK") {
		backgroundSpecial = this.$routeMode;
		if(this.$routeMode === "LONG_RANGE_CHART_SHORTEST") options["01_MODE"] = expandMissionText("oolite-contracts-cargo-command-ana-quickest");
		else options["01_MODE"] = expandMissionText("oolite-contracts-cargo-command-ana-shortest");
	}
	if(this.$contracts.length > 1) {	// if there's more than one, need options for forward, back, and listing
		options["02_BACK"] = expandMissionText("oolite-contracts-cargo-command-back");
		options["03_NEXT"] = expandMissionText("oolite-contracts-cargo-command-next");
		options["04_LIST"] = expandMissionText("oolite-contracts-cargo-command-list");
	} else if(this.$lastChoice === "02_BACK" || this.$lastChoice === "03_NEXT" || this.$lastChoice === "04_LIST") this.$lastChoice = "06_EXIT";		// if not, we may need to set a different choice we never want 05_ACCEPT to end up selected initially
	var title = expandMissionText("oolite-contracts-cargo-title-detail",{
		"oolite-contracts-cargo-title-detail-number": this.$contractIndex+1,
		"oolite-contracts-cargo-title-detail-total": this.$contracts.length
	});
// finally, after all that setup, actually create the mission screen
	var missionConfig = {
		title: title,
		message: message,
		allowInterrupt: true,
		screenID: "oolite-contracts-cargo-details",
		exitScreen: "GUI_SCREEN_INTERFACES",
		backgroundSpecial: backgroundSpecial,
		choices: options,
		initialChoicesKey: this.$lastChoice
	};
	if(this.$cargoPageOverlay != "") missionConfig.overlay = this.$cargoPageOverlay;
	mission.runScreen(missionConfig,this._processCargoChoice, this);
}
this._processCargoChoice = function(choice)
{
	this._resetViews();
	if(choice === null) return;	// can occur if ship launches mid mission screen
	if(choice.match(/^01_CONTRACT_/)) {	// now process the various choices
		var index = parseInt(choice.slice(12),10);		// contract selected from summary page set the index to that contract, and show details
		this.$contractIndex = index;
		this.$lastChoice = "04_LIST";
		this._cargoContractsDisplay(false);
	} else if(choice === "01_MODE") {
		this.$routeMode = (this.$routeMode === "LONG_RANGE_CHART_SHORTEST")?"LONG_RANGE_CHART_QUICKEST":"LONG_RANGE_CHART_SHORTEST";		// advanced navigation array mode flip
		this.$lastChoice = "01_MODE";
		this._cargoContractsDisplay(false);
	} else if(choice === "02_BACK") {
		this.$contractIndex--;		// reduce contract index (cargoContractsDisplay manages wraparound)
		this.$lastChoice = "02_BACK";
		this._cargoContractsDisplay(false);
	} else if(choice === "03_NEXT") {
		this.$contractIndex++;		// increase contract index (cargoContractsDisplay manages wraparound)
		this.$lastChoice = "03_NEXT";
		this._cargoContractsDisplay(false);
	} else if(choice === "04_LIST") {
		this._cargoContractsDisplay(true);		// display the summary page
	} else if(choice === "05_ACCEPT") {
		this._acceptContract();
		this.$lastChoice = "03_NEXT"; 		// do not leave the setting as accept for the next contract!
		this._cargoContractsDisplay(false);
	}
// if we get this far without having called cargoContractsDisplay that means either 'exit' or an unrecognised option was chosen
}
// move goods from the contracts list to the player's ship (if possible)
this._acceptContract = function()
{
	var cargo = this.$contracts[this.$contractIndex];
	if(cargo.deposit > player.credits) {
		this.$helper._soundFailure();
		return;
	}
// give the cargo to the player
/*
For half-sized cargo contracts...
cargo.size must be divided by 2 into 2 contracts, with 1 rounded up and the other rounded down.
cargo.payment likewise must be divided by 2 into 2 contracts, with 1 rounded up and the other rounded down.
cargo.deposit likewise must be divided by 2 into 2 contracts, with 1 rounded up and the other rounded down.
*/
// Split cargo contract into 2 half-sized contracts if the jumps required is >6-10 and cargo size is >99 TC and you've done at least 1 cargo contract successfully
	if(player.contractReputationPrecise > 1 && cargo.route.length > 10-Math.ceil(player.contractReputationPrecise*0.5) && cargo.size > 99 && system.mainStation.market[cargo.commodity]["quantity_unit"] == "0" ) {
		var scratchval = Math.floor(cargo.size *0.5);
		cargo.size = Math.ceil(cargo.size *0.5);
		cargo.payment = Math.ceil(cargo.payment *0.5);
		cargo.deposit = Math.ceil(cargo.deposit *0.5);
		var result = player.ship.awardContract(cargo.size,cargo.commodity,system.ID,cargo.destination,cargo.deadline,cargo.payment,cargo.deposit);
		if(result) player.credits -= cargo.deposit;
		cargo.size = scratchval;
	}
	var result = player.ship.awardContract(cargo.size,cargo.commodity,system.ID,cargo.destination,cargo.deadline,cargo.payment,cargo.deposit);
	if(result) {
		player.credits -= cargo.deposit;	// pay the deposit
		this.$contracts.splice(this.$contractIndex,1);	// remove the contract from the station list
		this._updateMainStationInterfacesList();	// update the interface description
		this.$helper._soundSuccess();
	} else this.$helper._soundFailure();	// else must have had manifest change recently (unlikely, but another OXP could have done it)
}
// removes any expired contracts
this._validateContracts = function() 
{
	var c = this.$contracts.length-1;
	var removed = false;
	for (var i=c;i>=0;i--) {	// iterate downwards so we can safely remove as we go
		if(this.$helper._timeRemainingSeconds(this.$contracts[i]) < this.$helper._timeEstimateSeconds(this.$contracts[i]) / 3) {		// if the time remaining is less than 1/3 of the estimated delivery time, even in the best case it's probably not going to get there.
			this.$contracts.splice(i,1);		// remove it
			removed = true;
		}
	}
	if(removed) this._updateMainStationInterfacesList(); // update the interface description if we removed any
}
/* Utility functions */
// calculates a sample price for a commodity in a distant system
this._priceForCommodity = function(commodity,systeminfo) 
{
	return systeminfo.samplePrice(commodity)*0.1;	//sample price returns decicredits, need credits
}
// description of the cargo
this._descriptionForGoods = function(cargo)
{
	var unit = "tons";
	if(system.mainStation.market[cargo.commodity]["quantity_unit"] == "1") unit = "kilograms";
	else if(system.mainStation.market[cargo.commodity]["quantity_unit"] == "2") unit = "grams";
	return cargo.size+expandDescription("[cargo-"+unit+"-symbol]")+" "+displayNameForCommodity(cargo.commodity);
}
// check if player's ship has space for the cargo and can afford the deposit
this._hasSpaceFor = function(cargo)
{
	if(cargo.deposit > player.credits) return false;
	var amountInTC = cargo.size;
	if(system.mainStation.market[cargo.commodity]["quantity_unit"] == "1") {
		var spareSafe = 499-(player.ship.manifest[cargo.commodity] % 1000);
		amountInTC -= spareSafe;
		amountInTC = Math.ceil(amountInTC/1000);
		if(amountInTC < 0) amountInTC = 0;
	} else if(system.mainStation.market[cargo.commodity]["quantity_unit"] == "2") {
		var spareSafe = 499999-(player.ship.manifest[cargo.commodity] % 1000000);
		amountInTC -= spareSafe;
		amountInTC = Math.ceil(amountInTC/1000000);
		if(amountInTC < 0) amountInTC = 0;
	}
	return (amountInTC <= player.ship.cargoSpaceAvailable);
}
 |