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);
}
|