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, 2014 Switeck";
this.licence = "Gnu Public License v2+";
this.description = "Cargo delivery contracts.";
this.version = "1.79 MOD 1.5";
// Mod changes by Switeck with help+ideas from PhantorGorth and others on Oolite IRC chat...
// All changes covered under creative commons open source:
// "Creative Commons Attribution - Non-Commercial - Share Alike 3.0";
/**** 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()
{
// encode the contract list to a string for storage in the savegame
missionVariables.oolite_contracts_cargo = JSON.stringify(this.$contracts);
}
// 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 numContracts = Math.floor(5*(Math.random()+Math.random())+(system.government+player.contractReputationPrecise-player.bounty*0.02)*Math.random());
// I added that bounty can reduce the number of contracts. Corporate state systems can add extra contracts.
if (numContracts < 0) numContracts = 0;
if (player.contractReputationPrecise >= 0 && numContracts < 5) numContracts += 5;
if (numContracts > 18) numContracts = 18;
// some of these possible contracts may be discarded later on
var nearbysystems = system.info.systemsInRange(80-player.contractReputationPrecise*7); // 31-80-129 LY range...done here to save recalculations time.
log(this.name," player.contractReputation= "+player.contractReputationPrecise+" numContracts= "+numContracts+" nearbysystems.length= "+nearbysystems.length);
// log(this.name," player.contractReputation= "+player.contractReputation+" numContracts= "+numContracts+" nearbysystems.length= "+nearbysystems.length+" nearbysystems= "+nearbysystems);
for (var i = 0; i < numContracts; i++) {
var scratchval = 1;
var attempts = 0;
do {
var scratchval = 1;
attempts++;
// pick a random system to take the goods to
// var destination = Math.floor(Math.random()*256); // ORIGINAL -- FOR TESTING ONLY!
var destination = nearbysystems[Math.floor(Math.random() * nearbysystems.length)].systemID;
// log(this.name," destination = "+destination);
// discard if chose the current system
if (destination === system.ID) {
var scratchval = 0;
} else {
// get the SystemInfo object for the destination
var destinationInfo = System.infoForSystem(galaxyNumber,destination);
// Decides firsthand to drop a bad system and choose another one?
if (Math.abs(system.economy - destinationInfo.economy)<(1+player.contractReputationPrecise*0.28) && player.contractReputationPrecise < 6.5 && system.economy <7) {
var scratchval = 0; // Discarded, since identical or "worse" economies leave little opportunity for profitable trading.
} else {
// check that a route to the destination exists
// route calculation is expensive so leave this check to last
var routeToDestination = system.info.routeToSystem(destinationInfo);
//log(this.name," routeToDestination = "+routeToDestination);
// if the system cannot be reached, ignore this contract
if (!routeToDestination) {
var scratchval = 0; // No route, no point trying!
} else {
var daysUntilDeparture = 1+(Math.random()*(7+player.contractReputationPrecise-destinationInfo.government)); // NEEDS a better estimate based on time needed to reach target!
if (daysUntilDeparture < 0) var scratchval = 0; // negative reputation can make a contract out-of-time failed before starting.
}
}
}
} while (scratchval == 0 && attempts < 10);
if (attempts > 9) continue; // failed to find a good destination.
// we now have a valid destination, so generate the rest of the cargo contract details
//log(this.name," daysUntilDeparture = "+daysUntilDeparture);
// var commodities = Object.keys(system.mainStation.market);
var attempts = 0;
do {
var unitPrice = 0;
var unitMinPrice = 0;
var unitMaxPrice = 0;
var remotePrice = 0;
var remoteMinPrice = 0;
attempts++;
// if destinationInfo.economy < system.economy means destination is more industrial than start locaction.
// if system.economy <4 then it's "industrial" so choose comp,lux,mach,alloys (+firearms?)
// else it's "agricultural" so choose food,textiles,radioactives,liquor_wines,furs,minerals (+slaves, narcotics?)
// Odds get lowered if offender/fugitive, raised if high reputation...forcing more controlled items if "bad" and more gold/plat/gems if "good".
// var commodities = ["food","textiles","radioactives","slaves","liquor_wines","luxuries","narcotics","computers","machinery","alloys","firearms","furs","minerals","gold","platinum","gem_stones"];
// var commodity = commodities[Math.floor(Math.random()*commodities.length)]; // Chooses random commondity
if (system.economy > destinationInfo.economy || system.economy >5) {
if (system.economy < destinationInfo.economy + 1 + player.contractReputationPrecise*0.4) { // Eliminates marginal profit commodities from similar 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 items and low profit stuff.
} else {
var commodities = ["slaves","narcotics","food","minerals","textiles","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 items and low profit stuff.
}
} else if (system.economy > destinationInfo.economy - Math.max(0, player.contractReputationPrecise*0.3)) { // Eliminates low-profit commodities when start economy == destination (identical ind economies)
var commodities = ["alloys","furs","gold","platinum","gem_stones"]; // 5 entries
var preccutoff = 2;
var scratchval = Math.floor(Math.random()*commodities.length);
} else if (system.economy > destinationInfo.economy - 1 - player.contractReputationPrecise*0.2 ) { // Eliminates low-profit commodities when start economy is only slightly more industrial than destination.
var commodities = ["narcotics","firearms","computers","alloys","furs","platinum","gem_stones"]; // 7 entries, removed Gold!
var preccutoff = 5;
var scratchval = Math.floor(Math.random()*(commodities.length-2)+2); // Drops the controlled items and low profit stuff.
} else {
var commodities = ["narcotics","firearms","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 items and low profit stuff.
}
if (player.contractReputationPrecise < 6.5 - Math.random()*5) { // At rep>2, there's a chance of getting ONLY good contracts. The odds of this are much better by rep>5.
var scratchval = Math.floor(Math.random()*(preccutoff+player.contractReputationPrecise*0.1-player.bounty*0.01));
}
if (scratchval < 0) var scratchval = 0; // If bad rep plus bad bounty pushes off leftside of the chart, give player "worst" contract.
if (scratchval >= preccutoff && player.contractReputationPrecise < 6.5) var scratchval = preccutoff -1; // Prevents Gold contracts before player.contractReputation == 7
if (scratchval > commodities.length -1) var scratchval = commodities.length -1; // If high rep pushes off rightside of the chart, give player "best" contract.
// var commodities = ["gold","platinum","gem_stones"]; // 3 entries, tests only Gold/Plat/Gems!
// var scratchval = Math.floor(Math.random()*commodities.length);
var commodity = commodities[scratchval];
if (system.mainStation.market[commodity].quantity > 0) { // ignore commodities with 0 availability here
var unitPrice = Number((system.mainStation.market[commodity].price*0.1).toFixed(1));
var unitMinPrice = Number((this._priceForCommodity(system.mainStation.market[commodity],System.infoForSystem(galaxyNumber,system.ID).economy,0)).toFixed(1));
var unitMaxPrice = Number((this._priceForCommodity(system.mainStation.market[commodity],System.infoForSystem(galaxyNumber,system.ID).economy,255)).toFixed(1));
var remoteMinPrice = Number((this._priceForCommodity(system.mainStation.market[commodity],destinationInfo.economy,0)).toFixed(1)); // ,0 means return smallest value
var remotePrice = Number((this._priceForCommodity(system.mainStation.market[commodity],destinationInfo.economy,255)).toFixed(1)); // ,255 means return largest value
// var remotePrice = Number((this._priceForCommodity(system.mainStation.market[commodity],destinationInfo.economy,Math.floor(Math.random()*256))).toFixed(1)); // random prices!
// log(this.name," # "+attempts+", "+commodities+", "+preccutoff+", "+commodity+" val1="+scratchval+" Price="+unitPrice+" MinPrice="+unitMinPrice+" MaxPrice="+unitMaxPrice+" remMin="+remoteMinPrice+" remP= "+remotePrice+", "+system.economy+", "+destinationInfo.economy);
// This can happen with narcotics!
if(remotePrice<remoteMinPrice) {
var scratchval = remoteMinPrice;
var remoteMinPrice = remotePrice;
var remotePrice = scratchval;
}
if(unitMaxPrice<unitMinPrice) {
var scratchval = unitMaxPrice;
var unitMaxPrice = unitMinPrice;
var unitMinPrice = scratchval;
}
if(unitPrice<unitMinPrice) unitMinPrice = unitPrice;
if(unitPrice>unitMaxPrice) unitMaxPrice = unitPrice;
}
} while ((remotePrice - unitMinPrice) < Math.max(1,player.contractReputationPrecise*0.5 - system.mainStation.market[commodity].quantityUnit*3) && attempts < 10); // Allows Gold more often!
if (attempts > 9) continue; // failed to find a good commodity.
var cargo = new Object;
cargo.commodity = commodity;
var amount = 0;
var unitsize = 1;
// larger unit sizes for kg/g commodities
if (system.mainStation.market[commodity].quantityUnit == 1) {
unitsize += Math.ceil((Math.random()+Math.random()+Math.random())*routeToDestination.route.length); // Adds more if routeToDestination.route.length > 5
} else if (system.mainStation.market[commodity].quantityUnit == 2) {
unitsize += Math.ceil((Math.random()+Math.random()+Math.random())*routeToDestination.route.length*2);
}
amount += Math.max(Math.ceil((1+Math.random()*32)*(1+Math.random()*16)),Math.floor(30+Math.random()*6))*unitsize; // forced minimum amount to always be at least 30-35. Eliminates while-looping here!
if (amount > 125 && amount > player.ship.cargoSpaceCapacity && player.contractReputationPrecise >= 0 && system.mainStation.market[commodity].quantityUnit == 0) { // reduce the number of contracts only suitable for Anacondas
amount = Math.floor(amount/Math.floor(1+(Math.random()*4)));
}
cargo.size = amount;
var localValue = Math.floor(unitPrice * amount); // Total cost at Current/Original Price
var remoteValue = Math.floor(remotePrice * amount); // Remote max value
var profitMax = remoteValue-Math.floor(unitMinPrice * amount);
// discount adjustment to prices. Higher reputation and bulk quantities should make this more profitable
var discount = Number((Math.ceil(Math.max(50,Math.min(100,20+ player.contractReputationPrecise*10 + routeToDestination.route.length*10 + amount*0.1 +50000/profitMax - player.bounty*0.1 - destinationInfo.government - system.government)))*0.01).toFixed(2));
var profit = Math.ceil(profitMax * discount); // Uses discount directly on max profit to determine profit.
// if (profit < 100 + player.contractReputationPrecise*100 || profit > profitMax) profit = profitMax; // Ensures higher profits if payout would otherwise be too low.
// Formulas to generate ORIGINAL PROFIT AMOUNTS!
/*
var discount = Math.min(10+Math.floor(amount/10),35);
var unitPrice = system.mainStation.market[commodity].price * (100-discount) / 1000;
var localValue = Math.floor(unitPrice * amount);
remotePrice = remotePrice * (200+discount) / 200;
var remoteValue = Math.floor(remotePrice * amount);
var profit = remoteValue-localValue;
*/
var scratchval = this._priceForCommodity(system.mainStation.market[commodity],destinationInfo.economy,Math.floor(Math.random()*256)); // random prices!
// var scratchval = remotePrice; // max selling price!
var discount2 = Number(Math.min(0.1+Math.floor(amount*0.1)*0.01,0.35).toFixed(2));
var profitORG = Math.floor(scratchval * (1 + 0.5*discount2 ) * amount ) - Math.floor(unitPrice * (1 - discount2 ) * amount ); // Original results for profit!
var profitORGmax = Math.floor(remotePrice * (1 + 0.5*discount2 ) * amount ) - Math.floor(unitMinPrice * (1 - discount2 ) * amount ); // Theoretical MAX Original results for profit!
// higher share for transporter for longer routes, less safe systems, and Higher reputation. lower share for high bounty! 100 bounty = 10% share loss!
var share = 100 + destinationInfo.government - (10*routeToDestination.route.length);
if (share < 10) share = 10;
if (share > 100) share = 100;
share = 100-share;
var profitORGmax = Math.floor(profitORGmax*share*0.01);
// safety: now multiply the fee by 2 compared with 1.76 contracts
// prevents exploit discovered by Mad Hollander at
// http://aegidian.org/bb/viewtopic.php?p=188127
// localValue *= 2; // This amount is so high in the event of a failed contract that it's practically PUNATIVE PUNISHMENT for those doing contracts.
// this may need to be raised further
// fee is total selling price.
// This IF-ELSE makes the cargo deposit equal or greater than the current selling value for the commodity.
if(localValue > remoteValue-profit) {
var fee = Math.ceil((localValue + profit) *0.1)*10; // round to nearest 10 credits
} else {
var fee = Math.ceil(remoteValue*0.1)*10; // round to nearest 10 credits
}
cargo.payment = fee;
cargo.deposit = Math.ceil((fee - profit)*0.1)*10; // round to nearest 10 credits
// logs nearly everything
log(this.name,
"Rep="+player.contractReputationPrecise
+", "+commodity
+" size="+cargo.size
+" econ="+system.economy
+" DestEcon="+destinationInfo.economy
+" jumps="+routeToDestination.route.length
+" unitPrice="+unitPrice
+" MinP="+unitMinPrice
+" MaxP="+unitMaxPrice
+" remMinP="+remoteMinPrice
+" remP="+remotePrice
+" locVal="+localValue
+" deposit="+cargo.deposit
+" profit="+profit
+" pMax="+profitMax
+" *"+discount
+" pORG="+profitORG
+" pORGmax="+profitORGmax
+" share="+share
+" discount="+discount2
+" DestVal="+remoteValue
+" Total="+fee
);
cargo.destination = destination;
// we'll need this again later, and route calculation is slow
cargo.route = routeToDestination;
// 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);
// add parcel to contract list
if (cargo.deposit < cargo.payment) this._addCargoContractToSystem(cargo);
}
}
// 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) {
// no contracts, remove interface if it exists
system.mainStation.setInterface("oolite-contracts-cargo",null);
} 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) {
// Again. Has to be done on every call to this function, but also
// has to be done at the start.
this._validateContracts();
// 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);
// no callback, just exits contracts system
return;
}
// make sure that the 'currently selected contract' pointer
// is in bounds
if (this.$contractIndex >= this.$contracts.length) {
this.$contractIndex = 0;
} else if (this.$contractIndex < 0) {
this.$contractIndex = this.$contracts.length - 1;
}
// sub functions display either summary or detail screens
if (summary) {
this._cargoContractSummaryPage();
} else {
this._cargoContractSinglePage();
}
}
// display the mission screen for the summary page
this._cargoContractSummaryPage = function()
{
// column 'tab stops'
var columns = [10,16,21,26];
// column header line
var headline = expandMissionText("oolite-contracts-cargo-column-goods");
// pad to correct length to give a table-like layout
headline += this.$helper._paddingText(headline,columns[0]);
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");
// required because of way choices are displayed.
headline = " "+headline;
// setting options dynamically; one contract per line
var options = new Object;
var i;
var anyWithSpace = false;
for (i=0; i<this.$contracts.length; i++) {
// temp variable to simplify following code
var cargo = this.$contracts[i];
// write the description, padded to line up with the headers
var optionText = this._descriptionForGoods(cargo);
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]);
// right-align the fee so that the credits signs line up
var priceText = formatCredits(cargo.deposit,false,true);
priceText = this.$helper._paddingText(priceText, 3.25)+priceText;
optionText += priceText
optionText += this.$helper._paddingText(optionText, columns[3]);
// right-align the fee so that the credits signs line up
priceText = formatCredits(cargo.payment-cargo.deposit,false,true);
priceText = this.$helper._paddingText(priceText, 3.25)+priceText;
optionText += priceText
// need to pad the number in the key to maintain alphabetical order
var istr = i;
if (i < 10) istr = "0"+i;
// needs to be aligned left to line up with the heading
options["01_CONTRACT_"+istr] = { text: optionText, alignment: "LEFT" };
// check if there's space for this contract
if (!this._hasSpaceFor(cargo)) {
options["01_CONTRACT_"+istr].color = "darkGrayColor";
} else {
anyWithSpace = true;
// if there doesn't appear to be sufficient time remaining
if (this.$helper._timeRemainingSeconds(cargo) < this.$helper._timeEstimateSeconds(cargo)) options["01_CONTRACT_"+istr].color = "orangeColor";
}
}
// if we've come from the detail screen, make sure the last
// contract shown there is selected here
var icstr = this.$contractIndex;
if (icstr < 10) icstr = "0"+this.$contractIndex;
var initialChoice = "01_CONTRACT_"+icstr;
// if none of them have any space...
if (!anyWithSpace) initialChoice = "06_EXIT";
// next, an empty string gives an unselectable row
options["02_SPACER"] = "";
// numbered 06 to match the option of the same function in the other branch
options["06_EXIT"] = expandMissionText("oolite-contracts-cargo-command-quit");
// now need to add further spacing to fill the remaining rows, or
// the options will end up at the bottom of the screen.
var rowsToFill = 21;
if (player.ship.hudHidden) rowsToFill = 27;
for (i = 4 + this.$contracts.length; i < rowsToFill ; i++) {
// each key needs to be unique at this stage.
options["07_SPACER_"+i] = "";
}
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;
}
// now run the mission screen
mission.runScreen(missionConfig, this._processCargoChoice, this);
}
// display the mission screen for the contract detail page
this._cargoContractSinglePage = function()
{
// temp variable to simplify code
var cargo = this.$contracts[this.$contractIndex];
// 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.hudHidden)
{
this.$suspendedHUD = true; // note that we hid it, for later
player.ship.hudHidden = true;
}
// We also set the player's witchspace destination temporarily
// so we need to store the old one in a variable to reset it later
this.$suspendedDestination = player.ship.targetSystem;
// That done, we can set the player's destination so the map looks
// right.
player.ship.targetSystem = cargo.destination;
// start with 18 blank lines, since we don't want to overlap the chart
var message = new Array(18).join("\n");
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)
});
// use a special background
var backgroundSpecial = "LONG_RANGE_CHART";
// the available options will vary quite a bit, so this rather
// than a choicesKey in missiontext.plist
var options = new Object;
// this is the only option which is always available
options["06_EXIT"] = expandMissionText("oolite-contracts-cargo-command-quit");
// if the player has sufficient space
if (this._hasSpaceFor(cargo)) {
options["05_ACCEPT"] = {text: expandMissionText("oolite-contracts-cargo-command-accept")};
// if there's not much time left, change the option colour as a warning!
if (this.$helper._timeRemainingSeconds(cargo) < this.$helper._timeEstimateSeconds(cargo)) options["05_ACCEPT"].color = "orangeColor";
} 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 there's more than one, need options for forward, back, and listing
if (this.$contracts.length > 1) {
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 not, we may need to set a different choice
// we never want 05_ACCEPT to end up selected initially
if (this.$lastChoice === "02_BACK" || this.$lastChoice === "03_NEXT" || this.$lastChoice === "04_LIST") this.$lastChoice = "06_EXIT";
}
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
// now process the various choices
if (choice.match(/^01_CONTRACT_/)) {
// contract selected from summary page
// set the index to that contract, and show details
var index = parseInt(choice.slice(12),10);
this.$contractIndex = index;
this.$lastChoice = "04_LIST";
this._cargoContractsDisplay(false);
} else if (choice === "01_MODE") {
// advanced navigation array mode flip
this.$routeMode = (this.$routeMode === "LONG_RANGE_CHART_SHORTEST")?"LONG_RANGE_CHART_QUICKEST":"LONG_RANGE_CHART_SHORTEST";
this.$lastChoice = "01_MODE";
this._cargoContractsDisplay(false);
} else if (choice === "02_BACK") {
// reduce contract index (cargoContractsDisplay manages wraparound)
this.$contractIndex--;
this.$lastChoice = "02_BACK";
this._cargoContractsDisplay(false);
} else if (choice === "03_NEXT") {
// increase contract index (cargoContractsDisplay manages wraparound)
this.$contractIndex++;
this.$lastChoice = "03_NEXT";
this._cargoContractsDisplay(false);
} else if (choice === "04_LIST") {
// display the summary page
this._cargoContractsDisplay(true);
} else if (choice === "05_ACCEPT") {
this._acceptContract();
// do not leave the setting as accept for the next contract!
this.$lastChoice = "03_NEXT";
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.
These must remain unchanged:
cargo.destination
cargo.route
cargo.deadline = clock.adjustedSeconds + Math.floor(daysUntilDeparture*86400)+(cargo.route.time*3600);
cargo.commodity
*/
// Split cargo contract into 2 half-sized contracts if the deadline is >6 days or cargo size is >99 TC
// if(cargo.deadline > (clock.adjustedSeconds + 6*86400) || (cargo.size > 99 && system.mainStation.market[cargo.commodity].quantityUnit == 0)) {
if(cargo.route.time > 6*(24-player.contractReputationPrecise) || (cargo.size > 99 && system.mainStation.market[cargo.commodity].quantityUnit == 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) {
// pay the deposit
player.credits -= cargo.deposit;
// remove the contract from the station list
this.$contracts.splice(this.$contractIndex,1);
// update the interface description
this._updateMainStationInterfacesList();
this.$helper._soundSuccess();
} else {
// else must have had manifest change recently (unlikely, but another OXP could have done it)
this.$helper._soundFailure();
}
}
// removes any expired contracts
this._validateContracts = function()
{
var c = this.$contracts.length-1;
var removed = false;
// iterate downwards so we can safely remove as we go
for (var i=c;i>=0;i--) {
// 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.
if (this.$helper._timeRemainingSeconds(this.$contracts[i]) < this.$helper._timeEstimateSeconds(this.$contracts[i]) / 3) {
// remove it
this.$contracts.splice(i,1);
removed = true;
}
}
if (removed) {
// update the interface description if we removed any
this._updateMainStationInterfacesList();
}
}
/* Utility functions */
// calculates a sample price for a commodity in a distant system
//this._priceForCommodity = function(commodity,economy)
this._priceForCommodity = function(commodity,economy,rnd)
{
// var rnd = Math.floor(Math.random()*256);
var price = 0.4*(Math.floor(parseInt(commodity.marketBasePrice,10) + (rnd & parseInt(commodity.marketMaskPrice,10)) + (economy*parseInt(commodity.marketEcoAdjustPrice,10)))&255);
return price;
}
// description of the cargo
this._descriptionForGoods = function(cargo)
{
var unit = "tons";
if (system.mainStation.market[cargo.commodity].quantityUnit == "1") {
unit = "kilograms";
} else if (system.mainStation.market[cargo.commodity].quantityUnit == "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].quantityUnit == "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].quantityUnit == "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);
} |