/* oolite-contracts-parcels.js Script for managing parcel 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-parcels"; this.author = "cim"; this.copyright = "© 2012-2013 the Oolite team."; this.description = "Parcel delivery contracts."; this.version = "1.79"; /**** Configuration options and API ****/ /* OXPs which wish to add a background to the summary pages should set this value */ this.$parcelSummaryPageBackground = ""; /* OXPs which wish to add an overlay to the parcel mission screens should set this value */ this.$parcelPageOverlay = ""; /* this._addParcelToSystem(parcel) * This function adds the defined parcel to the local main station's * interface list. A parcel definition is an object with the following * parameters, all required: * * destination: system ID of destination system * sender: the name of the sender (max 40 chars) * description: a short description of the parcel contents (max 40 chars) * deadline: the deadline for delivery, in clock seconds * payment: the payment for delivery on time, in credits * * and optionally, the following parameter: * * 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 parcel can be added, false * otherwise. */ this._addParcelToSystem = function(parcel) { if (!system.mainStation) { log(this.name,"Contracts require a main station"); return false; } if (!parcel.sender || parcel.sender.length > 40) { log(this.name,"Rejected parcel: sender missing or too long"); return false; } if (!parcel.description || parcel.description.length > 40) { log(this.name,"Rejected parcel: description missing or too long"); return false; } if (parcel.destination < 0 || parcel.destination > 255) { log(this.name,"Rejected parcel: destination missing or invalid"); return false; } if (parcel.deadline <= clock.adjustedSeconds) { log(this.name,"Rejected parcel: deadline invalid"); return false; } if (parcel.payment < 0) { log(this.name,"Rejected parcel: payment invalid"); return false; } if (!parcel.route) { var destinationInfo = System.infoForSystem(galaxyNumber,parcel.destination); parcel.route = system.info.routeToSystem(destinationInfo); if (!parcel.route) { log(this.name,"Rejected parcel: route invalid"); return false; } } this.$parcels.push(parcel); 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_parcels) { this.$parcels = JSON.parse(missionVariables.oolite_contracts_parcels); } else { this._initialiseParcelContractsForSystem(); } this._updateMainStationInterfacesList(); } this.shipWillExitWitchspace = function() { if (!system.isInterstellarSpace && !system.sun.hasGoneNova && system.mainStation) { // must be a regular system with a main station this._initialiseParcelContractsForSystem(); this._updateMainStationInterfacesList(); } } this.playerWillSaveGame = function() { // encode the contract list to a string for storage in the savegame missionVariables.oolite_contracts_parcels = JSON.stringify(this.$parcels); } // 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 parcel contract list for the current system this._initialiseParcelContractsForSystem = function() { // clear list this.$parcels = []; // basic range -3 to +3 evenly distributed // parcel contracts require far less investment than cargo or passenger // so fewer are available var numContracts = Math.floor(Math.random()*7) - 3; // larger systems more likely to have contracts, smaller less likely if (system.info.population > 50) { numContracts++; } else if (system.info.population < 30) { numContracts--; } // if the player has a bad reputation, reduce the available contract number if (player.parcelReputation < 0) { numContracts += player.parcelReputation; } // if they have a very good reputation, increase the numbers else if (player.parcelReputation > 4) { numContracts += Math.floor(Math.random()*(player.parcelReputation - 3)); } // always have at least two available for new Jamesons if (!missionVariables.oolite_contracts_parcels && numContracts < 2) { numContracts = 2; } // reduce number of places with none whatsoever else if (numContracts < 1 && player.parcelReputation >= 0 && Math.random() < 0.5) { numContracts = 1; } for (var i = 0; i < numContracts; i++) { var parcel = new Object; // pick a random system to take the parcel to var destination = Math.floor(Math.random()*256); // discard if chose the current system if (destination === system.ID) { continue; } // get the SystemInfo object for the destination var destinationInfo = System.infoForSystem(galaxyNumber,destination); // check that a route to the destination exists var routeToDestination = system.info.routeToSystem(destinationInfo); // if the system cannot be reached, discard the parcel if (!routeToDestination) { continue; } // we now have a valid destination, so generate the rest of // the parcel details parcel.destination = destination; // we'll need this again later, and route calculation is slow parcel.route = routeToDestination; parcel.sender = randomName()+" "+randomName(); parcel.description = expandDescription("[parcel-description]"); // time allowed for delivery is time taken by "fewest jumps" // route, plus 10-110%, plus four hours to make sure all routes // are "in time" for a reasonable-length journey in-system. parcel.deadline = clock.adjustedSeconds + Math.floor((routeToDestination.time * 3600 * (1.1+(Math.random())))) + 14400; // total payment is small for these items. parcel.payment = Math.floor( // 2-3 credits per LY of route (routeToDestination.distance * (2+Math.random())) + // additional income for route length based on reputation (Math.pow(routeToDestination.route.length,1+(0.2*player.parcelReputation))) + // small premium for delivery to more dangerous systems (2 * Math.pow(7-destinationInfo.government,1.5)) ); // add parcel to contract list this._addParcelToSystem(parcel); } } // 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.$parcels.length === 0) { // no contracts, remove interface if it exists system.mainStation.setInterface("oolite-contracts-parcels",null); } else { var title = expandMissionText("oolite-contracts-parcels-interface-title",{ "oolite-contracts-parcels-interface-title-count": this.$parcels.length }); system.mainStation.setInterface("oolite-contracts-parcels",{ title: title, category: expandMissionText("oolite-contracts-parcels-interface-category"), summary: expandMissionText("oolite-contracts-parcels-interface-summary"), callback: this._parcelContractsScreens.bind(this) // could alternatively use "cbThis: this" parameter instead of bind() }); } } // if the interface is activated, this function is run. this._parcelContractsScreens = function(interfaceKey) { // the interfaceKey parameter is not used here, but would be useful if // this callback managed more than one interface entry this._validateParcels(); // 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.$parcels.length > 1); this._parcelContractsDisplay(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._parcelContractsDisplay = function(summary) { // Again. Has to be done on every call to this function, but also // has to be done at the start. this._validateParcels(); // if there are no parcels (usually because the player has taken // the last one) display a message and quit. if (this.$parcels.length === 0) { var missionConfig = {titleKey: "oolite-contracts-parcels-none-available-title", messageKey: "oolite-contracts-parcels-none-available-message", allowInterrupt: true, screenID: "oolite-contracts-parcels-none", exitScreen: "GUI_SCREEN_INTERFACES"}; if (this.$parcelSummaryPageBackground != "") { missionConfig.background = this.$parcelSummaryPageBackground; } if (this.$parcelPageOverlay != "") { missionConfig.overlay = this.$parcelPageOverlay; } 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.$parcels.length) { this.$contractIndex = 0; } else if (this.$contractIndex < 0) { this.$contractIndex = this.$parcels.length - 1; } // sub functions display either summary or detail screens if (summary) { this._parcelContractSummaryPage(); } else { this._parcelContractSinglePage(); } } // display the mission screen for the summary page this._parcelContractSummaryPage = function() { // column 'tab stops' var columns = [14,21,28]; // column header line var headline = expandMissionText("oolite-contracts-parcels-column-cargo"); // pad to correct length to give a table-like layout headline += this.$helper._paddingText(headline,columns[0]); headline += expandMissionText("oolite-contracts-parcels-column-destination"); headline += this.$helper._paddingText(headline,columns[1]); headline += expandMissionText("oolite-contracts-parcels-column-within"); headline += this.$helper._paddingText(headline,columns[2]); headline += expandMissionText("oolite-contracts-parcels-column-fee"); // required because of way choices are displayed. headline = " "+headline; // setting options dynamically; one contract per line var options = new Object; var i; for (i=0; i 1) { options["02_BACK"] = expandMissionText("oolite-contracts-parcels-command-back"); options["03_NEXT"] = expandMissionText("oolite-contracts-parcels-command-next"); options["04_LIST"] = expandMissionText("oolite-contracts-parcels-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-parcels-title-detail",{ "oolite-contracts-parcels-title-detail-number": this.$contractIndex+1, "oolite-contracts-parcels-title-detail-total": this.$parcels.length }); // finally, after all that setup, actually create the mission screen var missionConfig = { title: title, message: message, allowInterrupt: true, screenID: "oolite-contracts-parcels-details", exitScreen: "GUI_SCREEN_INTERFACES", backgroundSpecial: backgroundSpecial, choices: options, initialChoicesKey: this.$lastChoice }; if (this.$parcelPageOverlay != "") { missionConfig.overlay = this.$parcelPageOverlay; } mission.runScreen(missionConfig,this._processParcelChoice, this); } this._processParcelChoice = function(choice) { this._resetViews(); if (choice === null) { // can occur if ship launches mid mission screen return; } // 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._parcelContractsDisplay(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._parcelContractsDisplay(false); } else if (choice === "02_BACK") { // reduce contract index (parcelContractsDisplay manages wraparound) this.$contractIndex--; this.$lastChoice = "02_BACK"; this._parcelContractsDisplay(false); } else if (choice === "03_NEXT") { // increase contract index (parcelContractsDisplay manages wraparound) this.$contractIndex++; this.$lastChoice = "03_NEXT"; this._parcelContractsDisplay(false); } else if (choice === "04_LIST") { // display the summary page this._parcelContractsDisplay(true); } else if (choice === "05_ACCEPT") { this._acceptContract(); // do not leave the setting as accept for the next contract! this.$lastChoice = "03_NEXT"; this._parcelContractsDisplay(false); } // if we get this far without having called parcelContractsDisplay // that means either 'exit' or an unrecognised option was chosen } // move a parcel from the contracts list to the player's ship this._acceptContract = function() { var parcel = this.$parcels[this.$contractIndex]; // give the parcel to the player player.ship.addParcel(parcel.sender+"'s "+this._formatPackageName(parcel.description),system.ID,parcel.destination,parcel.deadline,parcel.payment); // remove the parcel from the station list this.$parcels.splice(this.$contractIndex,1); // update the interface description this._updateMainStationInterfacesList(); this.$helper._soundSuccess(); } // removes any expired parcels this._validateParcels = function() { var c = this.$parcels.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.$parcels[i]) < this.$helper._timeEstimateSeconds(this.$parcels[i]) / 3) { // remove it this.$parcels.splice(i,1); removed = true; } } if (removed) { // update the interface description if we removed any this._updateMainStationInterfacesList(); } } /* Utility functions */ // lower-cases the initial letter of the package contents this._formatPackageName = function(name) { return name.charAt(0).toLowerCase() + name.slice(1); }