Back to Index Page generated: Nov 12, 2024, 11:02:04 PM

Expansion GalCop Missions

Content

Warnings

  1. No version in dependency reference to oolite.oxp.phkb.BlackMarket:null
  2. Optional Expansions mismatch between OXP Manifest and Expansion Manager at character position 0218 (DIGIT ZERO vs LATIN SMALL LETTER N)

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Implements some randomised missions via the BB system Implements some randomised missions via the BB system
Identifier oolite.oxp.phkb.GalCopMissions oolite.oxp.phkb.GalCopMissions
Title GalCop Missions GalCop Missions
Category Missions Missions
Author phkb phkb
Version 0.8.2 0.8.2
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
  • oolite.oxp.phkb.BulletinBoardSystem:1.9
  • oolite.oxp.phkb.EmailSystem:1.7.2
  • oolite.oxp.Svengali.GNN:1.0
  • oolite.oxp.phkb.BroadcastCommsMFD:1.2.3
  • oolite.oxp.phkb.BulletinBoardSystem:1.9
  • oolite.oxp.phkb.EmailSystem:1.7.2
  • oolite.oxp.Svengali.GNN:1.0
  • oolite.oxp.phkb.BroadcastCommsMFD:1.2.3
  • Optional Expansions
  • oolite.oxp.Svengali.Snoopers:2.5
  • oolite.oxp.phkb.BlackMarket:0
  • oolite.oxp.stormrider.manifestScanner:1.2
  • oolite.oxp.Okti.CargoScanner:1.11
  • oolite.oxp.Rorschachhamster.Satellites:1.08
  • oolite.oxp.Svengali.Snoopers:2.5
  • oolite.oxp.phkb.BlackMarket:
  • oolite.oxp.stormrider.manifestScanner:1.2
  • oolite.oxp.Okti.CargoScanner:1.11
  • oolite.oxp.Rorschachhamster.Satellites:1.08
  • Conflict Expansions
    Information URL https://wiki.alioth.net/index.php/GalCop_Missions n/a
    Download URL https://wiki.alioth.net/img_auth.php/d/d1/GalCopMissions_0.8.2.oxz n/a
    License CC-BY-NC-SA 4.0 CC-BY-NC-SA 4.0
    File Size n/a
    Upload date 1716770182

    Documentation

    Also read http://wiki.alioth.net/index.php/GalCop%20Missions

    readme.txt

    GalCop Local Missions
    By Nick Rogers
    
    Overview
    ========
    This OXP adds some local missions to the Bulletin Board system, to add more things to do in and around each system. Very little by way of new ships, stations, equipment or AI's has been added to the game. The idea was to simply make use of the existing assets in new and interesting ways. 
    
    There are a number of different missions available, and the availability of these missions depends on a number of factors (score, system tech level, government type, installed equipment etc). Missions can be offered in a number of ways: via the Bulletin Board, from scooped escape pods, from passing ships, or from emails.
    
    This OXP also includes the concept of "mission chains", in that completing one mission may trigger a second mission, which could then trigger a third mission, and so on. Sometimes the player is given an option as to whether they want to perform the secondary mission, and sometimes they won't be.
    
    Generally, all the missions are optional. If you don't visit the Bulletin board, you won't see what's on offer, and very little in the game will change if you don't do the missions. You may occasionally be offered a mission when scooping escape capsules, and sometimes passing ships will pass on requests. 
    
    Gameplay impacts
    ----------------
    Some of the missions in this pack can impact on other areas of the game. For instance, one type of mission, if completed successfully, will result in a higher frequency of mis-jumps to a particular system. Some missions can effect the commodities of a station, or the availability of equipment at a station. It will hopefully be obvious when these changes take place (ie as a result of completing a mission).
    
    Disease Outbreaks
    -----------------
    Another potential impact is on systems that are reported to have disease outbreaks. If a major outbreak occurs, GalCop might choose to lock down the system, preventing the disease from spreading off-world. When this happens, you might find you can’t jump to a particular system without a permit. Some pilots have reported there are ways around GalCop’s restrictions, but even if you do, you might find that GalCop stations in that system will physically prevent you from docking, and inattentive pilots might find their ship taking damage rather than entering the docking slot.
    
    When a disease outbreak restriction is in effect, there might be missions you can undertake that will grant you the required permissions and passcodes to successfully enter the system and dock at the main station. These missions will likely be in systems close to where the outbreak is occurring. 
    
    Required Equipment
    ==================
    Fuel/Cargo scoops and the Advanced Space Compass are required for many of the missions. If your ship lacks these items, certain missions will be unavailable.
    
    Broadcast Comms MFD is also a requirement for many of the missions, as it provides an interface for ship-to-ship communications. If you have not purchased this item, some of the missions will be unavailable.
    
    New Equipment
    =============
    Some new equipment has been added with this OXP.
    
    Range Finder MFD
    ----------------
    When this item is purchased, installed and primed, you can press the "b" key (Mode) to switch the mode between escape capsules, black boxes, asteroids, ships, communication relays and cargo pods. Pressing the "n" (Activate) key will initiate a scan of the local area, up to a distance of 200km. The scan will take approximately 2 seconds to run, but will be run continuously until deactivated with the "n" key again. Any objects of the selected type within range will be displayed, along with the distance to the object. A "+" or "-" indicator will be displayed next to each item, showing whether you are moving away from (+), or towards (-), the target. When the target is less than 50km away, a simplified directional indicator will be shown instead, but outside this distance, the range finder does not provide directional information. The information returned by the scan is displayed in an MFD, so you will need to select an MFD slot (using the ":" key) and then select the Range Finder MFD (using the ";" key).
    
    Direction indicators when distance < 50km:
    	O - Target is generally ahead of you
    	o - Target is ahead of you.
    	X - Target is directly ahead of you.
    	< - Target is to the left
    	> - Target is to the right
    	^ - Target is above you
    	v - Target is below you
    	A - Target is behind you (ie Aft)
    
    The Range Finder MFD is available from all TL5 and greater systems for 250 cr.
    
    Note: When the Range Finder is activated, there will be a drain on your ships energy. If the Ship Configuration OXP is installed, engaging the Range Finder will also generate cabin heat.
    
    Range Finder Extender
    ---------------------
    An additional device, a Range Finder Extender, which increases the range of the scanner to 500km, as well as boosting the range for the simplified directional indicator to 75km, is available from all TL9 systems for 2650 cr.
    
    Comms Relay Beacon Switch
    -------------------------
    This device will be issued to the player when attempting particular missions, although it can also be purchased independently from TL3 systems for 60cr. It is a primable piece of equipment that will communicate with any communication relays in the system and request the beacon to be switched on for a short period of time. The device can be used 3 times before needing to be recharged. Using the device will incur a 5% penalty on whatever mission the communication relay relates to.
    
    Comms Relay Beacon Switch Recharge
    ----------------------------------
    This item will replace all the degraded parts of the switch, allowing it to be used 3 more times. The cost will vary, based on how degraded the current parts are. It is available in all systems.
    
    Cargo Stopper
    -------------
    This device makes use of the tractor beam which forms part of the cargo scoops. When your ship is aimed at cargo cannisters that are floating away, it will automatically use the tractor beam to bring the cargo to a stop. Cargo must be within approximately 2km of your ship, and in the centre area of your front viewscreen, otherwise the tractor beam can't reach it.
    
    Available from TL6 systems for 30cr.
    
    Ejection Damper
    ---------------
    This device attaches to the end of the cargo scoop, and when engaged, will apply some force to any ejected item so that it will slow to a stop within a short period of time, rather than floating away at a constant rate. 
    
    Once purchased, the unit must be turned on before any cargo will be impacted. Prime the "Ejection Damper" equipment, and press either the "Mode" (b) or "Activate" (n) key to turn the unit on. Pressing those keys a second time will turn the unit off. The unit is automatically turned off whenever your ship docks. 
    
    When the unit is on, all cargo ejected will be slowed to a stop. The device can be used on 50 ejected items before it needs to be replaced. 
    
    Available from TL7 systems for 100cr.
    
    Required OXP's
    ==============
    The following OXP's are listed as required:
    
    - Bulletin Board System (1.9)
    	Required as all missions are accessed through this interface.
    
    - Broadcast Comms MFD (v1.2.3)
    	Required for missions that need ship-to-ship communications to take place. Also used for accepting or declining secondary missions.
    
    - Email System (v1.7.2)
    	Provides another interface for missions to be offered.
    
    - GNN (v1.0)
    	Provides text randomisation system used for mission descriptions. Some news items will be published periodically, which can add some flavour to game events. 
    
    Recommended OXP's
    =================
    The following OXP's are listed as recommended, in that additional text and/or gameplay options will become available if they are installed.
    
    - Manifest Scanner
    	Can aid with finding cargo in other ships, which can be very important if you are on a mission to steal certain cargo.
    
    - Cargo Scanner
    	Can help with some missions to identify the contents of cargo containers.
    	
    - Black Market
    	Some additional opportunities are available through the Black Market interface in this OXP.
    
    - Satellites 
    	Some missions are only available with this pack installed.
    
    What else is in the box?
    ========================
    Reputations: The missions available to the player originate from a number of different entities - governments, corporations, factions of every stripe. As missions are performed for these entities your reputation will increase. However, given the number of entities involved, a new F4 interface screen has been created that combines all reputation information into an easy to read display. Also, any awards received can be displayed as well.
    
    If you have the Library OXP installed, you additionally have the option of importing a variety of internal and OXP reputations into the screen, rather than having those reputations displayed on the F5F5 Manifest screen. 
    
    License
    =======
    This work is licensed under the Creative Commons Attribution-Noncommercial-Share Alike 4.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/
    
    Thanks to Svengali and Disembodied for their PhraseGen tool.
    Thanks to Ramirez for his remote mine model (from the "Missiles and Bombs" OXP).
    Thanks to these forum members who have submitted bug reports or suggestions that have helped make the OXP better: Nite Owl, herodotus, Astrobe, Damocles Edge, Cody, Bogatyr, pagroove, gsagostinho, Cmd. Northgate, Balisongdalo, javirodriguez, lohcek, and MrFlibble.
    Special thanks to cim who has very kindly given permission to include his "Black Box" model from Rescue Stations in this OXP.
    
    No entry sign image from http://simpleicon.com/no_entry_sign_1.html
    Warning sign image from http://simpleicon.com/warning-2.html
    Tick image from http://simpleicon.com/ok_1.html
    Pill image from http://simpleicon.com/pill_3.html
    Star badge from https://www.iconfinder.com/icons/1031361/award_badge_best_prize_reward_icon#size=512 (Free for commercial use)
    Round badge from https://www.iconfinder.com/icons/465072/achievement_award_label_medal_politic_win_winner_icon#size=512 (Free for commercial use)
    Star medal from https://www.iconfinder.com/icons/111092/medal_icon#size=512  (CC BY-SA 3.0)
    Star medal on ribbon from https://www.iconfinder.com/icons/316245/medal_icon#size=512 (Free for commercial use)
    Trophy image from http://simpleicon.com/trophy_1.html
    Cup image from http://simpleicon.com/cup.html
    Candle image from http://simpleicon.com/candle-2.html
    Family image from http://simpleicon.com/chat_10.html
    Education image from http://simpleicon.com/wp-content/uploads/knowledge.png
    Head image from https://www.shareicon.net/download/2016/04/17/751132_people_512x512.png
    Raining image from http://simpleicon.com/wp-content/uploads/lightning__raining_2.png
    Biohazard image from https://www.freepik.com/free-vector/biohazard-yellow-circle-sign-graphic-illustration_2631298.htm#fromView=search&page=1&position=0&uuid=040f7aab-af6b-4231-9d3d-7f20cc869cd0" Image by rawpixel.com on Freepik
    Tractor beam sound effect by Finnolia Productions Inc from http://www.soundeffectsplus.com (license https://www.soundeffectsplus.com/content/license/)
    Sound effect from https://freesound.org/people/jmorrisoncafe330/sounds/110097/ (license https://creativecommons.org/licenses/sampling+/1.0/)
    Sound effect from https://freesound.org/people/klankbeeld/sounds/169334/ (license https://creativecommons.org/licenses/by/3.0/)
    Sound effect from https://freesound.org/people/klankbeeld/sounds/239633/ (license https://creativecommons.org/licenses/by/3.0/)
    Sound effect from https://freesound.org/people/flint10/sounds/197547/ (license https://creativecommons.org/publicdomain/zero/1.0/)
    Sound effect from https://freesound.org/people/Robinhood76/sounds/219853/ (license https://creativecommons.org/licenses/by-nc/3.0/)
    Sound effect from https://freesound.org/people/Robinhood76/sounds/333230/ (license https://creativecommons.org/licenses/by-nc/3.0/)
    Sound effect from https://freesound.org/people/staticpony1/sounds/249597/ (license https://creativecommons.org/publicdomain/zero/1.0/)
    Sound effect from https://freesound.org/people/TheBillinator3000/sounds/333845/ (license https://creativecommons.org/publicdomain/zero/1.0/)
    Sound effect from https://freesound.org/people/InSintesi/sounds/346295/ (license https://creativecommons.org/licenses/by/3.0/)
    Sound effect from https://freesound.org/people/lennyboy/sounds/275186/ (license https://creativecommons.org/publicdomain/zero/1.0/)
    Sound effect from https://freesound.org/people/AlienXXX/sounds/168511/ (license https://creativecommons.org/licenses/by/3.0/)
    
    Discussion
    ==========
    This OXP is discussed at this forum link: http://aegidian.org/bb/viewtopic.php?f=4&t=18745
    
    Version History
    ===============
    0.8.2
    - Fixed referencing errors in spawning scripts for "Recover special cargo" (type 24) and "Black Box recovery" (type 22) missions.
    - Fixed issue with mission state not being correctly recorded when the derelict is destroyed in black box recovery missions (types 22/23).
    - Fixed issue with invalid functions calls for meet ship missions (type 30).
    - Fixed issue with the meet ship mission, delivered by email (type 30), that wouldn't generate a second mission when you meet the target.
    - Added clarity to "Computer Delivery" mission (type 42), so when the target ship is destroyed, the mission is failed.
    - Removed "Computer Delivery" from being a follow-up mission.
    - Fixed reference error bugs with investigation missions (types 130-138).
    - Fixed issue with email missions causing a JS exception.
    - Fixed invalid descriptions lookup key.
    - Grammar corrections.
    
    0.8.1
    - Fixed referencing error in data cache missions (type 41) that was preventing the mission from being completable.
    
    0.8
    - Added a 10% penalty to cargo recovery missions (type 6) for each cargo pod the player destroys.
    - Destroying a cargo pod in a "Cargo Recovery" mission will now trigger a mission status update.
    - Fixed multiple issues with the descriptive text for "Cargo Recovery" missions (type 6).
    - For type 6/7 missions, changed reward calculation to be based on the value of the commodity being collected.
    - Excluded escape pods from being considered in the "Pirates for Hire" mission (type 8) when slaves are being sought.
    - Adjusted spawning rules for ship with stolen items (type 33), to ensure they always spawn in scanner range of the player.
    - Fixed bug in special delivery mission (type 40) that was preventing key information from being updated.
    - Tweaked special delivery mission (type 40) so mission complete percentage doesn't go above 100%.
    - Added missing position to a "Computer delivery" mission (type 42) description.
    - Bug fixes in disease outbreak script (types 50-52).
    - Fixed issue with disease outbreak systems, where system traffic wasn't being reduced, and GalCop station docking wasn't being prevented, in the case where the player doesn't have a valid permit.
    - Added a screen displayed when docking at a station to inform player of disease lockdown systems with 7LY.
    - Fixed grammar in descriptive text for "Hacking the witchpoint" mission (type 60).
    - Fixed issues with mission details and grammar for station disrupt missions (type 63).
    - Fixed issues preventing the disrupt station missions (type 63/64) from being completable.
    - Reduced eject distance for the "Acquire GalCop Software" mission (type 65).
    - Fixed issues preventing the "Acquire GalCop Software" mission (type 65) from being completable.
    - Beacon labels are now turned off after collecting data from Solar Monitors (type 71).
    - Updated manifest and BB status entries for donation missions (types 80-87) so that after a partial donation the status shows how much is remaining to donate.
    - Removed waypoints from terminated or failed Seismic Scan missions (type 100).
    - Fixed issue with manifest entry for "Asteroids in low orbit" mission (type 102), where it did not show progress towards completion.
    - Fixed issue with completing the "Deliver Seismic Scan Data" mission (type 104), where the equipment item wasn't being removed.
    - Destroying a satellite will now update the mission objectives (types 110-124).
    - Corrected issue preventing some satellite missions from being completable if the Galactic Almanac OXP is installed.
    - Fixed bug when spawning satellite defenders that was preventing the AI from being switched.
    - Tweaked how far you have to go past the reference point before the unidentified signal is started in an Investigation mission (type 130-138).
    - Tweaked descriptions for investigation missions to include a "reference point" past which a fair distance must be travelled in order to find the signal.
    - Tweaked the location of unidentified signals in Investigation missions (type 130-138).
    - Fixed spelling in "Defend station from attack" mission (type 152) description.
    - Added some lurking pirates outside pirate bases.
    - Added some extra details to manifest entry for missions that send to you a specific location in a system.
    - Ejection damper now includes "uses left" in a console message when turned on and again when used.
    - Switched a few mission types to stop the clock when complete.
    - Switched a lot of mission types to be completable at any main station.
    - Fixed issue that was preventing the Range Finder equipment from detecting mission-based escape pods.
    - Fixed issue that was preventing the Range Finder equipment from detecting derelict ships.
    - Added a simplified directional indicator to the Range Finder for when scanned items are less then 50km distant.
    - Fixed issue where cargo collected for missions wasn't being handed over correctly and removed from the player's hold.
    - Fixed issue where scooping more cargo than the mission requirement would cause some display and calculation irregularities.
    - Fixed issue where the F5F5 manifest page wasn't getting updated correctly for missions that have multiple items to collect if items had been destroyed.
    - Fixed various other grammar issues in mission text.
    - Fixed grammar in readme.txt.
    - Set global debug flag to false.
    - Code refactoring.
    
    0.7.0
    - Fixed bug in "Extract telescope data" mission, where the wrong satellite types were being used in the calculations.
    
    0.6.9
    - Fixed issue where scooped Thargoid metal fragments were not being counted towards mission objective (type 13).
    - Code refactoring.
    
    0.6.8
    - Fixed referencing errors with hit team docking notifications.
    - Fixed JS error when in interstellar space while a slave rescue mission is active.
    - Fixed JS error for disease outbreak systems.
    - Fixed description for blackbox secondary mission that wasn't putting location into the details.
    - Will now try up to 10 times to create a secondary mission after meeting a ship.
    - When no secondary mission can be created after meeting a ship, a message providing some explanation is now given.
    - Improved process for detecting and flagging alloys as having come from Thargoid vessels (type 13).
    - Hit teams bug fixes.
    - Bug fixes.
    
    0.6.7
    - Added mission type 18 to available missions.
    - Fixed spelling error in function names for Collect ingredients mission (type 53).
    - Fixed issue with possibly changing the status of the "defend the anaconda" mission (type 150) after completing it successfully.
    
    0.6.6
    - Turned off debug flag on investigation missions.
    
    0.6.5
    - Completing some missions will now reduce the players bounty.
    
    0.6.4
    - Fixed issue with the Reputations screen not showing some items.
    
    0.6.3
    - Removed customised PhraseGen code, made GNN a requirement for the OXP.
    
    0.6.2
    - Fixed a bug with donation missions and integration with Market Observer.
    
    0.6.1
    - Fixed error when repairing the Range Finder.
    - Tweak to cargo stopper process for mild performance improvements when selecting targets.
    - Fixes to the descriptions for slave repatriation missions (type 84).
    - Fixes to the description for the Destroy Satellites mission (type 122).
    - Donating cargo or slaves to charities will now update Market Observer (if installed).
    - Adjusted time limit for satellite download missions.
    - Tweaked manifest descriptions on satellite missions to better communicate distinct requirements for missions.
    - Range Finder now compatible with GalTech Escape Pods.
    
    0.6.0
    - Cargo stopper is now activated automatically when cargo is targeted, in front of the player ship and in range, negating the need for priming and activating the equipment.
    - Fixed stealing cargo missions, where it was possible to hand in cargo at stations other than the originating station.
    - Improved mission status messages for stealing cargo missions to show how much has been collected.
    - Bug fixes.
    
    0.5.9
    - Fixed some instances of timers being recycled while running.
    
    0.5.8
    - Fixed issue with delivery missions, where comms relay was not being spawned due to a Javascript error.
    
    0.5.7
    - Fixed some issues with retrieving data from stored settings used when the system populator runs.
    - Cleaned up the process of setting ship scripts in all script files to prevent scripts not being present when required.
    
    0.5.6
    - Bug fix in spawning routine for derelict ships for black box missions (type 22).
    - Fix to (hopefully) prevent scenario where mission ships were not being spawned.
    
    0.5.5
    - Small text adjustments for game consistency.
    - Fix JS error in satellite missions.
    
    0.5.4
    - Improved logic for adding ships to a system.
    - Fixed invalid reference error when generating missions.
    - Better use of new functions available in later versions of Oolite.
    - Corrected use of samplePrice method.
    
    0.5.3
    - The number of asteroids required to complete the Asteroid Removal missions (type 1) is now restricted to be less than the number of asteroids currently in the system.
    - Fixed bug in the Hit Teams script where the definition of the "$initiateInvestigation" routine was inaccurately declared.
    
    0.5.2
    - Fixed invalid variable reference error.
    
    0.5.1
    - An email will now be sent to the player when they accept their first mission to Interstellar space, offering tips on how to enter interstellar space, and what to expect when they get there.
    
    0.5.0
    - Added some civil warzone missions (types 90, 91).
    - Added defence missions (types 150, 152, 154, 156).
    - Added a new delivery mission (type 46), which involves collecting cargo from a waypoint, and dropping it at another waypoint.
    - Added a new investigation mission (136). Be afraid...
    - Added secondary mission (type 74) to solar activity missions (70, 71, 73).
    - Fixed error in description for solar activity mission (74).
    - Fixed issue preventing the "gather ingredients" mission (53) from being created.
    - Fixed issue with the "Remove Ejection Damper" equipment item, which wasn't removing the ejection damper equipment.
    - Fixed issue with cargo recovery missions (6, 7), where ejecting and re-scooping cargo pods would throw the mission status out of kilter.
    - Fixed manifest entry for the upload surveillance data mission (118) which was showing the wrong destination.
    - Fixed some missions that were not including a player-rated bonus in their payment amounts.
    - Defend satellite missions (124) were not being created for players with more than 64 kills.
    - Upgrade satellite firmware missions (115) now have proper pass codes to transmit to the satellites.
    - Tweaked the payment amount, and completion time requirement for satellite firmware update missions (115).
    - Completing the delivery of personal messages for a dead pilot (141, secondary mission after investigation mission 132) now done manually.
    - Limited some Thargoid missions to only be available in frontier systems.
    - Updated description of Range Finder MFD equipment on F3 screen to include mention of cargo pods mode.
    - Tweaks to the custom assassin AI routines to eliminate null target conditions.
    - Various adjustments to the manifest entry for many missions.
    - Spelling corrections.
    - Bug fixes and code refactoring.
    - Added a method to main script to allow for any mission to be easily tested during runtime ($testMissionType).
    
    0.4.0
    - Added "Cargo pod" mode to Range Finder.
    - Fixed error with Range Finder when scanning for Black Boxes. If non-ship entities were in range (eg planet, sun), a Javascript error would occur.
    - Changing the Range Finder mode will now clear the MFD of previous scan results.
    
    0.3.7
    - Fixed bug with missing variable declaration in failed mission function call.
    
    0.3.6
    - Removed debug flag that would cause a particular mission to always be generated.
    
    0.3.5
    - Fixed issue with secondary mission after investigation mission (132) not having correct destination on manifest.
    - Code refactoring.
    
    0.3.4
    - Fixed title in manifest.plist file.
    - Fixed missing reference error when completing data cache missions.
    - Fixed missing reference error when completing black box missions.
    - Fixed missing reference error when completing meet ship missions.
    - Fixed missing reference error when completing hacking/restoring witchpoint beacon missions.
    - Fixed missing reference error when completing some solar activity missions.
    - Fixed missing reference error when completing some disease outbreak missions.
    
    0.3.3
    - Added some fallback code in case cargo pods are not spawned with our preferred barrel type.
    
    0.3.2
    - Fixed incorrect worldScript name references in Pirate Bases script.
    - Added extra qualification to mission destination texts that refer to main planet, to help distinguish the main planet from any additional planets.
    - Tweaked one investigation mission to limit its availability to more experienced players with an upgraded ship.
    - Fixed issue with the Thargoid wreckage mission (13) which wasn't recognising wreckage collected in interstellar space.
    - Removed debug log messages.
    
    0.3.1
    - Fixed spawning issues for investigation missions (types 130-139).
    - Tweaked the power of the proximity mine.
    
    0.3.0
    - Added investigation missions (types 130-139).
    - Switched from using a dummy alloy ship entity as a navigational beacon to using a visual effect.
    - Code refactoring, improving code reuse.
    - Split more mission specific functions out of general script file to associated script file.
    - Fixed issue with some missions having a decimal amount as part of the payment. 
    - Performance improvements when generating new missions for a system.
    - Fixed status message with drug dealer mission (15).
    - Bug fixes.
    
    0.2.4
    - Linked the MFD to Broadcast Comms MFD in the Auto-Prime Equipment OXP.
    - Makes use of GNN OXP if installed.
    - Improved handling of player ship death scenarios.
    - Fixed linkage to Home System.
    
    0.2.3
    - Added the Ejection Damper equipment.
    - Reduced the effective range of the Cargo Stopper to approximately 2km to make it slightly less OP.
    - Added a sound effect for the tractor beam when activated.
    
    0.2.2
    - Tweaks to the positioning of ships for "Special Delivery" missions (40/41).
    - Fixed issue with "Special Delivery" mission (40), where the target ship was not responding to the "I don't have any more" comms message.
    - Tweaks to the scavenger AI, so the target ship doesn't jump out of the system prematurely.
    - Added Advanced Space Compass as a requirement for "Special Delivery" missions (40).
    - Removed some debug messages.
    
    0.2.1
    - Fixed issue with escape pod rescue missions (21, 22, 25) not being completable.
    - Moved more interstellar populator routines into the correct methods.
    - Removed some hard-coded scanner range values.
    - Spelling corrections.
    
    0.2.0
    - Fixed issues with the "Collect cargo" (6/7) missions, where scooped cargo was being counted as destroyed instead of collected.
    - Fixed issue with "Special Delivery" (40) missions where, if cargo was dumped and then re-scooped because it was drifting out of range, when the cargo was re-dumped the target ship was ignoring it.
    - Fixed issues with missions that have multiple stages (types 41, 63, 64, 65, and 73) that weren't being initialised correctly and so would never start.
    - Fixed issues with escape pod rescue missions (20) and runaway escape pod missions (25) that was preventing the escape pods from being spawned.
    - Added a new device, a Comms Relay Beacon Switch, that is issued to the player for any "Collect data cache" (31) or "Special delivery" (41) mission. This device will turn on the comms relay beacon again after the initial transmission period has expired, although using it will incur a 5% payment reduction.
    - Added a new device, the "Cargo Stopper", prime-able equipment available from TL6 systems, which, when pointed at moving cargo and then activated, will bring the cargo to a stop.
    - Fixed issue where I.T.H.A. reputation was not being removed from the F5 page correctly when reputations were set to be moved to the "Reputations and Awards" page. 
    - Forced bonus payment calculations to return an integer, rather than a decimal.
    - Some mission text tweaks.
    
    0.1.5
    - Adjustments to the population routines for interstellar space.
    - Fixed bug where reputation was not being applied correctly for missions where multiple reputation entities were defined.
    - Fixed bug with distance calculation in the Disease Outbreak script.
    - Adjusted time calculation for travel between threatened satellites during a satellite defend mission.
    - Added reputation links to Home System OXP.
    - Better handling of Combat Simulator.
    
    0.1.4
    - Adjusted calculation of the number of deaths from a disease outbreak.
    - Fixes to secondary missions sourced from black boxes or escape pods or passing ships having a termination penalty.
    
    0.1.3
    - Reworked all the cargo collection mission types (8,9,10,11,13,14,15) so that mission updates happen when cargo is handed in, not when it is scooped (although a notification will still be given to the player that the cargo was suitable for the mission). Expiry time has also been removed, and maximum quantity can now be greater than the player's available cargo hold.
    - Fixed issues with Thargoid wreckage collection missions, so alloys can now be successfully handed in, and (hopefully) alloys from any Thargoid warship (including OXP Thargoids) should now be acceptable.
    - Applied some fixes for the "Stranded ship" mission to stop the ship from moving before receiving help.
    - Code refactoring.
    - Bug fixes.
    
    0.1.2
    - Tweaked the energy drain of the Range Finder.
    - Added sound for when the Range Finder scan is stopped.
    - Removed debug flag from "Thargoid wreckage" missions, so they shouldn't appear as frequently.
    - Fixed cargo donation charity missions that were only being made available if the local market had slaves.
    - Code refactoring and (hopefully) performance improvements.
    
    0.1.1
    - Fixed bug with Thargoid wreckage mission that was preventing the wreckage from being recognised.
    - Made the "Collect Thargoid wreckage" mission have no expiry, and provided a means by which you can hand over incomplete amounts and continue.
    - Removed debug flag from "Interstellar escape pod rescue" missions, so they shouldn't appear as frequently.
    
    0.1.0
    - Added some missions using Satellites OXP (types 110, 111, 113, 115, 122, 124).
    - Fixed issue where emailed missions, or missions delivered via another ship, were not checking all the conditions, giving the player a mission they couldn't complete.
    - Fixed issue with "Request for meeting" missions, where the target would not respond when the passcode was transmitted.
    - Added additional text to mission briefing for missions that require an interstellar space destination to make it clearer this is required.
    - Added additional text to mission briefing to make it clearer when a mission requires the player to return to the source system in order to complete it.
    - Applied distance and player score bonuses to Special delivery missions (type 40).
    - Fixed issue with the WBSA device not being start-able when the beacon is targeted.
    - Fixed issue with the Range Finder not creating an energy drain.
    - Mission text tweaks.
    
    0.0.9
    - Added "Gather ingredients" mission (type 53).
    - Fixed issue where missions with a deposit could have a negative net payment if the player's reputation is low.
    - Fixed issue with manifest entries not reflecting the state of progress correctly.
    - Reworked how textures are specified in mission definitions.
    - Added overlay images to charity missions.
    - Added routines to control the creation of asteroid fields, so they will be created in the same place in each system, and reused for future missions.
    - Fixed issues with mission descriptions missing some text elements.
    - Fixed issue with cargo recovery mission text containing ">undefined".
    - Fixed issue with RRS reputations causing a JavaScript error when opening the "Reputation and Awards" F4 screen.
    - Headings on the reputations page will now be suppressed if there are no related items to display.
    - Converted text for mission types 74, 100, 101 and 102 to use the PhraseGen tool.
    - Some tweaks/fixes to mission text.
    - Code refactoring.
    
    0.0.8
    - Better logic for determining what is a witchpoint buoy.
    - Better client name generation using PhraseGen tool.
    - Fixed minor text issue with the "decline mission" for escape pod messages.
    - No mission accept/complete confirmation emails will be sent for secondary missions when the source of the mission is an NPC from an escape pod or a ship.
    - If player ejects escape pod while pod occupant is communicating, conversation will stop and mission offering will be correctly terminated.
    - Fixed error when scooping escape pod with no "crew" set (ie equivalent to scooping 1t slaves)
    - Improved robustness of "Rescue Stricken Ship" missions.
    - Fixed issue where stricken ships were not communicating with player correctly.
    - Fixed error when firing on the stricken ship after it was repaired.
    
    0.0.7
    - Better logic for determining if a pirate ship has just been destroyed.
    - Added a "+/-" indicator on the Range Finder MFD, to make it clearer in which direction the player is moving in relation to the target.
    
    0.0.6
    - Prevented Cargo Shepherd magnets from being targeted by delivery ships.
    - Corrected issues with integrating to the Smugglers Black Market script.
    
    0.0.5
    - Fixed mess left after code refactoring. :(
    
    0.0.4
    - Fixed issue with escape pod missions not being complete-able.
    - Turned off lights on derelict ships.
    - Code refactoring to reduce the chance of scripts for various mission types conflicting with another.
    - More robust logic for connecting with Smugglers Black Market.
    - Bug fixes.
    
    0.0.3
    - Made compatible with v1.84 of Oolite.
    
    0.0.2
    - Initial public release.
    

    Equipment

    Name Visible Cost [deci-credits] Tech-Level
    Recovered Black Box yes 1500 1+
    Cargo Stopper yes 300 6+
    Remove Cargo Stopper no 50 5+
    Comms Relay Beacon Switch yes 600 3+
    Comms Relay Beacon Switch Recharge no 200 1+
    Remove Comms Relay Beacon Switch no 50 5+
    Ejection Damper yes 1000 7+
    Remove Ejection Damper no 50 5+
    Escape Pod yes 10000 1+
    Medical Supplies yes 1 1+
    Mass Passenger Transport Module yes 1 1+
    Patient Transport Module yes 1 1+
    Range Finder Extender yes 26500 9+
    Remove Range Finder MFD no 15000 3+
    Range Finder MFD yes 2500 5+
    Remove Range Finder MFD no 1000 3+
    Recovered cargopod yes 20000 1+
    Seismic Resonance Scanner yes 150000 1+
    GalCop Station Security Software yes 250000 1+
    Solar Radiation Disrupter Missile yes 2500 1+
    Solar Activity Scanner yes 150000 1+
    Station Killer Missile yes 125300 15+
    Recovered High-Level Security Codes yes 130000 1+
    Recovered Classified Documents yes 130000 1+
    Recovered Prototype Ship Schematics yes 130000 1+
    Recovered Military Weapons Designs yes 130000 1+
    Unprocessed Scan Data yes 2000 1+
    Virus Specimens yes 1 1+
    Witchpoint Beacon Security Access yes 1 1+

    Ships

    Name
    Communications relay
    gcm-blackbox
    Black Box
    Pirate Base
    Pirate Base
    gcm-pirate-base-turret
    Mine
    gcm-runaway-pod
    gcm-software-package
    Solar Radiation Disrupter Missile
    gcm-special-cargo
    Station Killer Missile
    gcm-stolen-items
    gcm-stricken-pod
    Escape capsule (no life signs)
    Unknown signal source

    Models

    This expansion declares no models. This may be related to warnings.

    Scripts

    Path
    Scripts/galcopbb_asteroids.js
    "use strict";
    this.name = "GalCopBB_AsteroidFields";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Looks after the spawning, and subsequent re-creation, of asteroid fields";
    this.license = "CC BY-NC-SA 4.0";
    
    // array of dictionaries, containing
    //      systemID        system ID where asteroid field is found
    //      factor
    //      seed
    //      locationType
    this._asteroids = [];
    this._currentSystem = []; // list of asteroid field positions created in the current system
    this._populateComplete = false;
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
        if (missionVariables.GalCopBBMissions_AsteroidFields) {
            this._asteroids = JSON.parse(missionVariables.GalCopBBMissions_AsteroidFields);
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillSaveGame = function () {
        missionVariables.GalCopBBMissions_AsteroidFields = JSON.stringify(this._asteroids);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillEnterWitchspace = function (cause, destination) {
        this._currentSystem.length = 0;
        if (cause === "galactic jump") this._asteroids.length = 0;
        this._populateComplete = false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // scripts that will need an asteroid field should manually call this function from their own "systemWillPopulate" function.
    // this ensures any asteroid fields will be created before they need to use them
    this.systemWillPopulate = function () {
        // make sure we don't run this twice
        if (this._populateComplete === true) return;
        this.$fixAsteroidData();
        // theoretically there will only be 1 field per system, but just in case that changes, we'll store everything in an array
        var a = this._asteroids;
        for (var i = 0; i < a.length; i++) {
            if (a[i].systemID === system.ID) {
                var position = Vector3D(a[i].x, a[i].y, a[i].z).fromCoordinateSystem(a[i].coordSystem)
                //var info = worldScripts.GalCopBB_Missions.$getRandomPosition(a[i].locationType, a[i].factor, a[i].seed);
                // add an asteroid field
                system.setPopulator("gcm-asteroids-" + i, {
                    callback: function (pos) {
                        system.addShips("asteroid", 60, pos, 40E3);
                    }.bind(this),
                    location: "COORDINATES",
                    coordinates: position
                });
    
                this._currentSystem.push({pos:position, locationType:a[i].locationType});
            }
        }
        this._populateComplete = true;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // asteroid fields are added with factor and seed values
    // once they are actually spawned, though, they will instead have x,y,z values to keep the field in roughly the same place
    this.$addAsteroidField = function $addAsteroidField(sysID, factor, seed, locationType) {
        this._asteroids.push({
            systemID: sysID,
            factor: factor,
            seed: seed,
            locationType: locationType
        });
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$addAsteroidFieldDirect = function $addAsteroidFieldDirect(sysID, x, y, z, coord, locationType) {
        this._asteroids.push({
            systemID: sysID,
            x: x,
            y: y,
            z: z,
            coordSystem: coord,
            locationType: locationType
        });
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$systemHasAsteroidField = function $systemHasAsteroidField(sysID, locationType) {
        for (var i = 0; i < this._asteroids.length; i++) {
            if (this._asteroids[i].systemID === sysID && (!locationType || this._asteroids[i].locationType === locationType)) return this._asteroids[i];
        }
        return null;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$getSystemAsteroidField = function $getSystemAsteroidField(locationType) {
        if (this._currentSystem.length === 0) return null;
        for (var i = 0; i < this._currentSystem.length; i++) {
            if (this._currentSystem[i].locationType == locationType) return this._currentSystem[i].pos;
        }
        return null;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$fixAsteroidData = function $fixAsteroidData() {
        var gcm = worldScripts.GalCopBB_Missions;
        var a = this._asteroids;
        for (var i = 0; i < a.length; i++) {
            if (a[i].systemID === system.ID && a[i].hasOwnProperty("factor") === true) {
                var info = gcm.$getRandomPosition(a[i].locationType, a[i].factor, a[i].seed);
                a[i].x = info.x;
                a[i].y = info.y;
                a[i].z = info.z;
                a[i].coordSystem = info.coordSystem;
                delete a[i].factor;
                delete a[i].seed;
            }
        }
    }
    Scripts/galcopbb_cargomonitor.js
    "use strict";
    this.name = "GalCopBB_CargoMonitor";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Monitors collected cargo to make sure mission-related items aren't lost/misplaced";
    this.license = "CC BY-NC-SA 4.0";
    
    this._smugglersInstalled = false; // indicates whether Smugglers is installed
    this._igtInstalled = false; // indicates whether Illegal Goods Tweak is installed
    this._monitor = []; // array of cargo types to monitor
    this._commodityIDList = []; // list of all commodity ID's (use for type 8/9 missions)
    this._commodityType = {};
    this._docking = false; // flag to indicate when the player is about to dock with the station
    this._slavesBefore = 0; // number of slaves player had before docking (based on IGT values)
    this._holdCargo = []; // holding array of firearms or narcotics cargo (type 14/15 missions)
    // pulls cargo out of the way of IGT/smugglers so it can be handled here
    
    //-------------------------------------------------------------------------------------------------------------
    this.$addMonitor = function $addMonitor(missID, commodity, sysID, thargoid, pods) {
        this._monitor.push({
            ID: missID,
            commodity: commodity,
            thargoid: thargoid,
            system: sysID,
            containers: [],
            specific_pods: pods
        });
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$removeMonitor = function $removeMonitor(missID) {
        for (var i = this._monitor.length - 1; i >= 0; i--) {
            if (this._monitor[i].ID === missID) this._monitor.splice(i, 1);
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function () {
        if (missionVariables.GalCopBBMissions_CargoMonitor) this._monitor = JSON.parse(missionVariables.GalCopBBMissions_CargoMonitor);
    
        var types = ["t", "kg", "g"];
    
        // build a list of available commodities
        var m = null;
        if (system.mainStation) {
            m = system.mainStation.market;
        } else if (player.ship.dockedStation) {
            m = player.ship.dockedStation.market;
        } else if (system.stations.length > 0) {
            m = system.stations[0].market;
        }
        if (m) {
            var c = Object.keys(m);
            for (var i = 0; i < c.length; i++) {
                this._commodityIDList.push(c[i]);
                this._commodityType[c[i]] = types[m[c[i]].quantity_unit];
            }
        }
    
        // set a flag if the illegal goods tweak is installed
        if (worldScripts["illegal_goods_tweak"]) this._igtInstalled = true;
    
        // monkey patch smugglers so we can do things in the right order (ie remove illegal cargo to hide it before Smugglers sees it)
        if (worldScripts.Smugglers_Equipment) {
            this._smugglersInstalled = true;
            log(this.name, "monkey patching 'Smugglers' to ensure correct order of events");
            worldScripts.Smugglers_Equipment.$gcm_hold_shipWillDockWithStation = worldScripts.Smugglers_Equipment.shipWillDockWithStation;
            delete worldScripts.Smugglers_Equipment.shipWillDockWithStation;
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillSaveGame = function () {
        missionVariables.GalCopBBMissions_CargoMonitor = JSON.stringify(this._monitor);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.missionScreenOpportunity = function () {
        // put back any held cargo, but only if the IGT and Smugglers processes have finished
        if (this._holdCargo.length > 0) {
            if ((this._smugglersInstalled === true && worldScripts.Smugglers_Equipment._doCargoCheck === false) ||
                (this._igtInstalled === true && (!worldScripts.illegal_goods_tweak._runContrabandCheck || worldScripts.illegal_goods_tweak._runContrabandCheck === false))) {
                for (var i = 0; i < this._holdCargo.length; i++) {
                    player.ship.manifest[this._holdCargo[i].commodity] += this._holdCargo[i].amount;
                }
                this._holdCargo.length = 0;
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipScoopedOther = function (whom) {
        /*log(this.name, "got here - shipScoopedOther - " + whom);
    	log(this.name, "_delivery = " + whom.script._delivery);
    	log(this.name, "_missionID = " + whom.script._missionID);
    	log(this.name, "_checkMissionID = " + whom.script._checkMissionID);
        log(this.name, "has shipWasDumped = " + whom.script.shipWasDumped);*/
    
        // ignore escape pods
        if (whom.hasRole("escape-capsule")) return;
    
        // delete any scooped cargo that is being monitored, so that we can catch the dumping process correctly
        // this may lead to (sometimes) an issue where the look of an ejected cargo pod doesn't match what was scooped
        var text = "Cargo collected for mission.";
        for (var i = 0; i < this._monitor.length; i++) {
            var process = false;
            var idx = -1;
            // looking for any cargo of a particular type
            if (!this._monitor[i].specific_pods && this._monitor[i].commodity === whom.commodity && (this._monitor[i].system === 256 || this._monitor[i].system === system.ID)) process = true;
            // looking for a specific group of cargo pods
            if (process == false && this._monitor[i].specific_pods && this._monitor[i].specific_pods.indexOf(whom) >= 0) {
                process = true;
                idx = this._monitor[i].specific_pods.indexOf(whom);
            }
    
            if (process) {
                var reprocessed = false;
                if (this._monitor[i].thargoid === true) {
                    // if this is a thargoid wreckage mission, but the cargo hasn't got the flag, just continue
                    if (whom.script.hasOwnProperty("_fromThargoid") === false || whom.script._fromThargoid === false) continue;
                    // otherwise...
                    // to explain why it won't look like wreckage when ejected
                    text += " Reprocessed for storage.";
                    reprocessed = true;
                }
                if (this._commodityType[whom.commodity] != "t") {
                    // to explain why gold/platinum/gem_stones can't be ejected again
                    text += " Transferred to ship safe.";
                    reprocessed = true;
                }
                var amt = whom.commodityAmount;
                var cmdty = whom.commodity;
    
                var bb = worldScripts.BulletinBoardSystem;
    			if (whom.script && whom.script._missionID && whom.script._missionID > 0) {
    				var item = bb.$getItem(whom.script._missionID);
    				if (item) {
    					// clear out the mission ID on the pod, so we don't end up back here again.
    					// if the player tries ejecting and re-scooping the pod, nothing should now happen
    					whom.script._missionID = 0;
    					// remove this pod from the monitoring list
    					//this.$removeSpecialCargo(whom, item.ID);
    					// increment the collected quantity
                        // but only if we haven't reached the target
                        if (item.data.quantity < (item.data.targetQuantity - item.data.destroyedQuantity)) {
                            item.data.quantity += 1;
                            // update the bulletin board
                            bb.$updateBBMissionPercentage(item.ID, (item.data.quantity / (item.data.targetQuantity - item.data.destroyedQuantity)));
    
                            worldScripts.GalCopBB_Missions.$logMissionData(item.ID);
                            // tell the player something happened on our mission
                            player.consoleMessage(expandDescription("[goal_updated]"));
    
                            // special case for type 46
                            if (item.data.missionType === 46) {
                                // attach our dump script, in case this actual cargo pod is dumped at the site
                                // would only happen if the player got to the site without docking (which is possible)
                                if (whom.script.shipWasDumped && !whom.script.$gcm_hold_shipWasDumped) {
                                    whom.script.$gcm_hold_shipWasDumped = whom.script.shipWasDumped;
                                }
                                whom.script.shipWasDumped = worldScripts.GalCopBB_Delivery.$gcd_cargo_shipWasDumped2;
    
                                if (Math.round(item.percentComplete * 10) === 10 && item.data.stage === 0) {
                                    bb.$removeChartMarker(item.ID);
                                    //this._cargoMonitor.length = 0;
                                    // reset mission for stage 2
                                    item.destination = item.data.destinationA;
                                    item.destinationName = System.systemNameForID(item.destination);
                                    item.additionalMarkers = [];
    
                                    item.data.stage = 1;
                                    item.data.quantity = 0;
                                    bb.$updateBBMissionPercentage(item.ID, (item.data.quantity / (item.data.targetQuantity - item.data.destroyedQuantity)));
                                    bb.$addManifestEntry(item.ID);
                                }
                            }
                        }
    				}
                }
                // only remove/re-add cargo that needs reprocessing
                if (reprocessed == true) {
                    whom.remove(true);
                    player.ship.manifest[cmdty] += amt;
                    // remove specific pod from array
                    if (idx >= 0) this._monitor[i].specific_pods.splice(idx, 1);
                }
                this._monitor[i].containers.push(amt);
                player.consoleMessage(text, 4);
                break;
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDumpedCargo = function (cargo) {
        /*log(this.name, "got here - shipDumpedCargo - " + cargo);
    	log(this.name, "_delivery = " + cargo.script._delivery);
    	log(this.name, "_missionID = " + cargo.script._missionID);
    	log(this.name, "_checkMissionID = " + cargo.script._checkMissionID);*/
    
        // we don't need to worry about kg/g here, because it's being transferred to the safe straight away
        // so we can always just remove the last item
        for (var i = 0; i < this._monitor.length; i++) {
            if (this._monitor[i].commodity === cargo.commodity && this._monitor[i].containers.length > 0) {
                this._monitor[i].containers.pop();
                // put the thargoid flag variables on the cargo entity so it can be rescooped correctly
                if (this._monitor[i].thargoid === true) cargo.script._fromThargoid = true;
                break;
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerSoldCargo = function (commodity, units, price) {
        var remain = units;
        for (var i = 0; i < this._monitor.length; i++) {
            if (this._monitor[i].commodity === commodity && this._monitor[i].containers.length > 0) {
                // reduce amount recorded
                var c = this._monitor[i].containers;
                for (var j = c.length - 1; j >= 0; j--) {
                    if (c[j] > 0) {
                        c[j] -= remain;
                        if (c[j] < 0) {
                            remain = Math.abs(c[j]);
                            c[j] = 0;
                        } else {
                            remain = 0;
                        }
                    }
                    if (remain === 0) break;
                }
                var finished = false;
                do {
                    finished = false;
                    if (c[c.length - 1] === 0) {
                        c.pop();
                    } else {
                        finished = true;
                    }
                } while (finished === false);
    
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerBoughtNewShip = function (ship, price) {
        // check our monitoring array for any shortfalls
        for (var i = 0; i < this._monitor.length; i++) {
            if (this._monitor[i].currentAmount > 0) {
                this._monitor[i].currentAmount = 0;
                this._monitor[i].containers = [];
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillLaunchFromStation = function (station) {
        // grab the value of the ig rescued slaves prior to IGT running
        if (missionVariables.ig_rescued_slaves && missionVariables.ig_rescued_slaves > 0) this._slavesBefore = missionVariables.ig_rescued_slaves
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillDockWithStation = function (station) {
        this._docking = true;
        var gcm = worldScripts.GalCopBB_Missions;
        // check for other mission types being completed
        var list = gcm.$getListOfMissions(true, [14, 15]);
        for (var i = 0; i < list.length; i++) {
            // special cases for 14/15: because they are authorised contracts, they aren't illegal to bring in (possibly, depending on station)
            if (system.ID === list[i].source) {
                // only move cargo if the stationKeys match
                var stnkey = expandDescription("[missionType" + list[i].data.missionType + "_stationKeys]");
                if (worldScripts.BulletinBoardSystem.$checkMissionStationKey(this.name, station, stnkey) === true) {
                    // hide the authorised quantity from being picked up in any illegal commodity sweep
                    if (gcm.$isCommodityIllegal(system.ID, station, list[i].data.commodity) === true) {
                        var amt = list[i].data.quantity;
                        if (player.ship.manifest[list[i].data.commodity] < amt) amt = player.ship.manifest[list[i].data.commodity];
                        this._holdCargo.push({
                            commodity: list[i].data.commodity,
                            amount: amt
                        });
                        player.ship.manifest[list[i].data.commodity] -= amt;
                        player.addMessageToArrivalReport(expandDescription("[gcm_customs_wavethrough]", {
                            amount: amt,
                            commodity: displayNameForCommodity(list[i].data.commodity).toLowerCase()
                        }));
                    }
                }
            }
        }
    
        // call our monkey-patched shipWillDockWithStation from Smugglers
        if (this._smugglersInstalled) worldScripts.Smugglers_Equipment.$gcm_hold_shipWillDockWithStation(station);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.guiScreenChanged = function (to, from) {
        var ignorelist = ["GUI_SCREEN_STATUS", "GUI_SCREEN_REPORT", "GUI_SCREEN_MISSION"];
        // run a check after docking in case any cargo was removed by some OXP docking process (eg IGT, Smugglers)
        if (this._docking === true && ignorelist.indexOf(guiScreen) === -1) {
            this._docking = false;
            // special case for type 12 (slaves) mission
            if (this._igtInstalled === true) {
                var slavesAfter = 0;
                // grab the value of the ig rescued slaves prior to IGT running
                if (missionVariables.ig_rescued_slaves && missionVariables.ig_rescued_slaves > 0) slavesAfter = missionVariables.ig_rescued_slaves;
    
                var gcm = worldScripts.GalCopBB_Missions;
                var bb = worldScripts.BulletinBoardSystem;
                var rescued = slavesAfter - this._slavesBefore;
                var onBoard = this.$countCargoByType("slaves");
                if (rescued > 0 && onBoard > 0) {
                    // run this number through any active type 12 missions and see if it applies - but only if the slaves
                    // were scooped in a destination system
                    var list = gcm.$getListOfMissions(true, 12);
                    if (list.length > 0) {
                        do {
                            // to get here we must have slaves that were rescued as part of a type 12 mission
                            // we have now handed over slaves to AI, so we need to update the mission
                            var pre = rescued;
                            for (var i = 0; i < list.length; i++) {
                                var rec = list[i].data;
                                var removed = 0;
                                if (rec.quantity < rec.targetQuantity) {
                                    rec.quantity += rescued;
                                    removed = rescued;
                                    if (rec.quantity > rec.targetQuantity) {
                                        rescued = rec.targetQuantity - rec.quantity;
                                        removed = removed - rescued;
                                        rec.quantity = rec.targetQuantity;
                                    } else {
                                        rescued = 0;
                                    }
                                    // update the mission percent complete
                                    bb.$updateBBMissionPercentage(list[i].ID, (list[i].data.quantity / list[i].data.targetQuantity));
                                    // however, we won't send a "Mission updated" message for this one - we'll just do it silently.
    
                                    this.$removeCargoForMission(list[i].ID, removed);
                                    onBoard -= removed;
                                    break;
                                }
                            }
                            // check for something going wrong - no change to rescued means invalid/incorrect data
                            if (pre === rescued) break;
                        } while (rescued > 0 && onBoard > 0);
                    }
                }
            }
    
            // make sure we haven't lost anything along the way (via damage or something else)
            for (var i = 0; i < this._commodityIDList.length; i++) {
                var pa = player.ship.manifest[this._commodityIDList[i]];
                var amt = this.$countCargoByType(this._commodityIDList[i]);
                var lost = pa - amt;
                if (lost < 0) {
                    lost = Math.abs(lost);
                    do {
                        // cycle through the monitoring list, deducting amounts until our difference is zero
                        for (var j = this._monitor.length - 1; j >= 0; j--) {
                            var item = this._monitor[j];
                            if (item.commodity === this._commodityIDList[i] && item.containers.length > 0) {
                                do {
                                    item.containers[item.containers.length - 1] -= lost;
                                    if (item.containers[item.containers.length - 1] < 0) {
                                        lost = Math.abs(item.containers[item.containers.length - 1])
                                        item.containers[item.containers.length - 1] = 0;
                                    } else {
                                        lost = 0;
                                    }
                                    if (item.containers[item.containers.length - 1] === 0) {
                                        item.containers.pop();
                                    }
                                } while (lost > 0 && item.containers.length > 0);
                            }
                        }
                    } while (lost > 0);
                }
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns the number of containers for a mission
    this.$countCargoForMission = function $countCargoForMission(missID) {
        var amount = 0;
        for (var i = 0; i < this._monitor.length; i++) {
            if (this._monitor[i].ID === missID) amount += this._monitor[i].containers.length;
        }
        return amount;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns the total cargo of a particular type
    this.$countCargoByType = function $countCargoByType(cmdty) {
        var amount = 0;
        for (var i = 0; i < this._monitor.length; i++) {
            if (this._monitor[i].commodity === cmdty) {
                amount += this.$sumContainers(this._monitor[i]);
            }
        }
        return amount;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // remove cargo by containers, not by cargo quantity
    this.$removeCargoForMission = function $removeCargoForMission(missID, amount) {
        //log(this.name, "ID " + missID + ", amount " + amount);
        var cargo = {};
        cargo.commodity = "";
        cargo.quantity = 0;
        for (var i = this._monitor.length - 1; i >= 0; i--) {
            var item = this._monitor[i];
            if (item.ID === missID && item.containers.length > 0) {
                cargo.commodity = item.commodity;
                do {
                    cargo.quantity += item.containers[item.containers.length - 1];
                    item.containers.pop();
                    amount -= 1;
                } while (amount > 0);
            }
            if (amount == 0) break;
        }
        //log(this.name, "found " + cargo.quantity + " of " + cargo.commodity);
        return cargo;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$sumContainers = function $sumContainers(item) {
        var amount = 0;
        for (var i = 0; i < item.containers.length; i++) {
            amount += item.containers[i];
        }
        return amount;
    }
    Scripts/galcopbb_cargostopper.js
    "use strict";
    this.name = "GalCopBB_CargoStopper";
    this.author = "phkb";
    this.copyright = "2018 phkb";
    this.description = "Equipment that can slow down cargo that is running away.";
    this.license = "CC BY-NC-SA 4.0";
    
    this._cargoList = [];
    this._resetTimer = null;
    this._cargoToCheck = null;
    this._checkTimer = null;
    this._ready = false;
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerBoughtEquipment = function (equipmentKey) {
        if (equipmentKey === "EQ_GCM_CARGO_STOPPER_REMOVE") {
            player.ship.removeEquipment(equipmentKey);
            player.ship.removeEquipment("EQ_GCM_CARGO_STOPPER");
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipTargetAcquired = function (cargo) {
        if (this._ready === false) return;
        if (cargo.isCargo && cargo.hasRole("escape-capsule") === false && this._cargoList.indexOf(cargo) === -1 && (cargo.velocity && cargo.velocity.magnitude() > 0)) {
            this._cargoToCheck = cargo;
            if (!this._checkTimer || this._checkTimer.isRunning == false)
                this._checkTimer = new Timer(this, this.$checkCargo, 0.25, 0.25);
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipLaunchedFromStation = function(station) {
        this._ready = false;
        var p = player.ship;
        if (p.hasEquipmentProviding("EQ_CARGO_SCOOPS") === true && p.equipmentStatus("EQ_GCM_CARGO_STOPPER") == "EQUIPMENT_OK") {
            this._ready = true;
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.equipmentDamaged = function(equipmentKey) {
        if (equipmentKey == "EQ_FUEL_SCOOPS" || equipmentKey == "EQ_GCM_CARGO_STOPPER") this._ready = false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.equipmentRepaired = function(equipmentKey) {
        var p = player.ship;
        if (p.hasEquipmentProviding("EQ_CARGO_SCOOPS") === true && p.equipmentStatus("EQ_GCM_CARGO_STOPPER") == "EQUIPMENT_OK") {
            this._ready = true;
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDockedAtStation = this.shipWillExitWitchspace = this.shipDied = function() {
        if (this._checkTimer && this._checkTimer.isRunning) {
            this._checkTimer.stop();
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$checkCargo = function $checkCargo() {
        var cargo = this._cargoToCheck;
        if (!cargo.isValid || !cargo.isInSpace || player.ship.target != cargo) {
            this._checkTimer.stop();
            this._cargoToCheck = null;
            return;
        }
        // check position
        var p = player.ship;
        var eq = EquipmentInfo.infoForKey("EQ_GCM_CARGO_STOPPER");
        if (p.position.distanceTo(cargo) < (p.scannerRange * eq.scriptInfo.tractor_range_factor)) {
            var deviation = p.vectorForward.angleTo(cargo.position.subtract(p.position));
            if (deviation < 0.2) {
                this.$playSound("on");
                player.consoleMessage("Activating tractor beam", 3);
                cargo.maxThrust = eq.scriptInfo.tractor_apply_thrust;
                cargo.thrust = eq.scriptInfo.tractor_apply_thrust;
                this._cargoList.push(cargo);
                this._checkTimer.stop();
                if (this._resetTimer == null || this._resetTimer.isRunning === false) {
                    this._resetTimer = new Timer(this, this.$resetCargo, 2, 2);
                }
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    /*this.mode = function () {
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.activated = function () {
        var p = player.ship;
        if (p.hasEquipmentProviding("EQ_CARGO_SCOOPS") === false) {
            player.consoleMessage("Tractor-beam offline - unable to start", 5);
            return;
        }
        var cs = worldScripts.GalCopBB_CargoStopper;
        var ships = p.checkScanner(false);
        var msg = false;
        if (ships) {
            for (var i = 0; i < ships.length; i++) {
                var cargo = ships[i];
                if (cargo.isCargo && cargo.hasRole("escape-capsule") === false && cs._cargoList.indexOf(cargo) === -1) {
                    // check position
                    var eq = EquipmentInfo.infoForKey("EQ_GCM_CARGO_STOPPER");
                    if (p.position.distanceTo(cargo) < (p.scannerRange * eq.scriptInfo.tractor_range_factor)) {
                        var deviation = p.vectorForward.angleTo(cargo.position.subtract(p.position));
                        if (deviation < 0.2) {
                            // it's in scope 
                            if (msg === false) {
                                msg = true;
                                this.$playSound("on");
                                player.consoleMessage("Activating tractor beam", 3);
                            }
                            cargo.maxThrust = eq.scriptInfo.tractor_apply_thrust;
                            cargo.thrust = eq.scriptInfo.tractor_apply_thrust;
                            cs._cargoList.push(cargo);
                            if (cs._resetTimer == null || cs._resetTimer.isRunning === false) {
                                cs._resetTimer = new Timer(cs, cs.$resetCargo, 2, 2);
                            }
                        }
                    }
                }
            }
        }
        if (msg === false) {
            player.consoleMessage("No cargo in range of tractor beam", 5);
        }
    }*/
    
    //-------------------------------------------------------------------------------------------------------------
    this.$resetCargo = function $resetCargo() {
        if (this._cargoList.length === 0) {
            this._resetTimer.stop();
            this._resetTimer = null;
            return;
        }
        for (var i = this._cargoList.length - 1; i >= 0; i--) {
            var cargo = this._cargoList[i];
            if (!cargo.velocity || cargo.velocity.magnitude() === 0) {
                if (cargo.isValid && cargo.isInSpace) {
                    cargo.maxThrust = 0;
                    cargo.thrust = 0;
                }
                this._cargoList.splice(i, 1);
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // play the buy/sell sound effects
    this.$playSound = function $playSound(soundtype) {
        var mySound = new SoundSource;
        switch (soundtype) {
            case "on":
                mySound.sound = "tractor-beam.ogg";
                break;
        }
        mySound.loop = false;
        mySound.play();
    }
    
    Scripts/galcopbb_charity.js
    "use strict";
    this.name = "GalCopBB_Charity";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Adds charity missions to the Bulletin Board (80-88)";
    this.license = "CC BY-NC-SA 4.0";
    
    /*
        type 80: donation to AI
        type 81: donation to TWOS
        type 82: donation to MCEP
        type 83: donation to PMHS
        type 84: slave cargo donation to AI
        type 85: cargo donation to TWOS
        type 86: cargo donation to MCEP
        type 87: cargo donation to PMHS
        type 88: cargo donation to AI
        type 89: donation for liberation front of system
            how to apply system changes for this "charity"
                - multiple items (eg commodities and equipment)
        type 90: donation to religious group
            how to apply system changes for this charity
                - bounty
    */
    
    this._debug = true;
    this._trueValues = ["yes", "1", 1, "true", true];
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
    	var gcm = worldScripts.GalCopBB_Missions;
    	this._debug = gcm._debug;
    	// add these mission types into the main control
    	var list = [80, 81, 82, 83, 84, 85, 86, 87];
    	gcm._availableMissionTypes = gcm._availableMissionTypes.concat(list);
    	// add our reputation entities
    	var ent = worldScripts.GalCopBB_Reputation._entities;
    	ent["Amnesty Intergalactic"] = {
    		scope: "galactic",
    		display: true,
    		getValueWS: "",
    		getValueFN: "",
    		maxValue: 210,
    		minValue: 0,
    		degrade: "1|30",
    		impact: "bounty",
    		rewardGrid: [{
    				value: 0,
    				description: "[gcmAIRep0]",
    				impactValue: 0
    			},
    			{
    				value: 1,
    				description: "[gcmAIRep1]",
    				impactValue: 0
    			},
    			{
    				value: 20,
    				description: "[gcmAIRep2]",
    				impactValue: 0,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$AI_Rewards"
    			},
    			{
    				value: 35,
    				description: "[gcmAIRep3]",
    				impactValue: 0,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$AI_Rewards"
    			},
    			{
    				value: 60,
    				description: "[gcmAIRep4]",
    				impactValue: 0.25,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$AI_Rewards"
    			},
    			{
    				value: 90,
    				description: "[gcmAIRep5]",
    				impactValue: 0.5,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$AI_Rewards"
    			},
    			{
    				value: 130,
    				description: "[gcmAIRep6]",
    				impactValue: 0.75,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$AI_Rewards"
    			},
    			{
    				value: 200,
    				description: "[gcmAIRep7]",
    				impactValue: 1,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$AI_Rewards"
    			},
    		]
    	};
    	// Traders Widows and Orphans
    	ent["gcmTWOSTitle$G"] = {
    		scope: "system",
    		display: true,
    		getValueWS: "",
    		getValueFN: "",
    		maxValue: 210,
    		minValue: 0,
    		degrade: "1|30",
    		impact: "markets",
    		impactAllegiance: "galcop",
    		rewardGrid: [{
    				value: 0,
    				description: "[gcmTWOSRep0]",
    				impactValue: 0
    			},
    			{
    				value: 1,
    				description: "[gcmTWOSRep1]",
    				impactValue: 0
    			},
    			{
    				value: 20,
    				description: "[gcmTWOSRep2]",
    				impactValue: 0,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$TWOS_Rewards"
    			},
    			{
    				value: 35,
    				description: "[gcmTWOSRep3]",
    				impactValue: 0.01,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$TWOS_Rewards"
    			},
    			{
    				value: 60,
    				description: "[gcmTWOSRep4]",
    				impactValue: 0.02,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$TWOS_Rewards"
    			},
    			{
    				value: 90,
    				description: "[gcmTWOSRep5]",
    				impactValue: 0.04,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$TWOS_Rewards"
    			},
    			{
    				value: 130,
    				description: "[gcmTWOSRep6]",
    				impactValue: 0.07,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$TWOS_Rewards"
    			},
    			{
    				value: 200,
    				description: "[gcmTWOSRep7]",
    				impactValue: 0.1,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$TWOS_Rewards"
    			}
    		]
    	};
    	// mechanics childrens education fund
    	ent["gcmMCEPTitle$G"] = {
    		scope: "system",
    		display: true,
    		getValueWS: "",
    		getValueFN: "",
    		maxValue: 210,
    		minValue: 0,
    		degrade: "1|30",
    		impact: "equipment",
    		impactAllegiance: "galcop",
    		rewardGrid: [{
    				value: 0,
    				description: "[gcmMCEPRep0]",
    				impactValue: 0
    			},
    			{
    				value: 1,
    				description: "[gcmMCEPRep1]",
    				impactValue: 0
    			},
    			{
    				value: 20,
    				description: "[gcmMCEPRep2]",
    				impactValue: 0,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$MCEP_Rewards"
    			},
    			{
    				value: 35,
    				description: "[gcmMCEPRep3]",
    				impactValue: 0.01,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$MCEP_Rewards"
    			},
    			{
    				value: 60,
    				description: "[gcmMCEPRep4]",
    				impactValue: 0.02,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$MCEP_Rewards"
    			},
    			{
    				value: 90,
    				description: "[gcmMCEPRep5]",
    				impactValue: 0.04,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$MCEP_Rewards"
    			},
    			{
    				value: 130,
    				description: "[gcmMCEPRep6]",
    				impactValue: 0.07,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$MCEP_Rewards"
    			},
    			{
    				value: 200,
    				description: "[gcmMCEPRep7]",
    				impactValue: 0.1,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$MCEP_Rewards"
    			}
    		]
    	};
    	// pilots mental health service
    	ent["gcmPMHSTitle$G"] = {
    		scope: "system",
    		display: true,
    		getValueWS: "",
    		getValueFN: "",
    		maxValue: 210,
    		minValue: 0,
    		degrade: "1|30",
    		impact: "ships",
    		impactAllegiance: "galcop",
    		rewardGrid: [{
    				value: 0,
    				description: "[gcmPMHSRep0]",
    				impactValue: 0
    			},
    			{
    				value: 1,
    				description: "[gcmPMHSRep1]",
    				impactValue: 0
    			},
    			{
    				value: 20,
    				description: "[gcmPMHSRep2]",
    				impactValue: 0,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$PMHS_Rewards"
    			},
    			{
    				value: 35,
    				description: "[gcmPMHSRep3]",
    				impactValue: 0.01,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$PMHS_Rewards"
    			},
    			{
    				value: 60,
    				description: "[gcmPMHSRep4]",
    				impactValue: 0.02,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$PMHS_Rewards"
    			},
    			{
    				value: 90,
    				description: "[gcmPMHSRep5]",
    				impactValue: 0.04,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$PMHS_Rewards"
    			},
    			{
    				value: 130,
    				description: "[gcmPMHSRep6]",
    				impactValue: 0.07,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$PMHS_Rewards"
    			},
    			{
    				value: 200,
    				description: "[gcmPMHSRep7]",
    				impactValue: 0.1,
    				achievementWS: "GalCopBB_Charity",
    				achievementFN: "$PMHS_Rewards"
    			}
    		]
    	};
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // executed if the player selected the "give cargo and continue" option
    this.$partialComplete = function $partialComplete(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var amt = player.ship.manifest[item.data.commodity];
    	if (amt > 0 && (item.data.targetQuantity - item.data.quantity) >= 0) {
    		if (amt > (item.data.targetQuantity - item.data.quantity)) amt = (item.data.targetQuantity - item.data.quantity);
    		item.data.quantity += amt;
    		player.ship.manifest[item.data.commodity] -= amt;
    		bb.$updateBBMissionPercentage(item.ID, (item.data.quantity / item.data.targetQuantity));
    	}
    	// add a new custom menu (same as the old one which will be autoremoved)
    	// but only if there is a possibility the player could do another partial complete
    	if ((item.data.targetQuantity - item.data.quantity) >= 1) {
    		var mnu = expandDescription("[missionType" + item.data.missionType + "_customMenu]");
    		var itms = mnu.split("|");
    		item.customMenuItems.push({
    			text: itms[0],
    			worldScript: itms[1],
    			callback: itms[2],
    			condition: itms[3],
    			autoRemove: (this._trueValues.indexOf(itms[4]) >= 0 ? true : false)
    		});
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // checks if the partial load and continue menu can be selected by the player
    this.$partialCondition = function $partialCondition(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var amt = player.ship.manifest[item.data.commodity];
    	var result = "";
    	if (amt === 0) result = "No " + displayNameForCommodity(item.data.commodity).toLowerCase() + " in cargo hold";
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // add additional reputation value for each 2000 cr in the donation
    // so a donation of 10000 credits will result in a reputation increase of 5
    this.$completeDonationMission = function $completeDonationMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var amt = item.data.targetQuantity;
    	if (amt > player.credits) {
    		amt = player.credits;
    	}
    	// if the player has a bounty, doing these missions will reduce it
    	if (player.ship.bounty > 0) {
    		player.ship.setBounty(player.ship.bounty - 1, "community service");
    		if (amt > 10000 && player.ship.bounty >= 2) player.ship.setBounty(player.ship.bounty - 2, "community service");;
    		player.ship.consoleMessage("Your bounty has been reduced.");
    	}
    	var base = parseInt(expandDescription("[missionType" + item.data.missionType + "_reputationSuccess]"));
    	// rep will increase by 1 anyway, so only increase if target is > 2000
    	player.credits -= parseInt(amt);
    	var rep = parseInt(amt) / 2000;
    	if (rep <= 1) return;
    	// increase the rep amount by the base amount
    	for (var i = 1; i < rep; i++) {
    		worldScripts.GalCopBB_Reputation.$adjustReputationSuccess(item.data.missionType, item.source, item.destination, base);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // add additional reputation value for each t/kg/g in the donation
    // so a donation of 6 t will result in a reputation increase of 3
    this.$completeCargoMission = function $completeCargoMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var amt = item.data.targetQuantity;
    	var base = parseInt(expandDescription("[missionType" + item.data.missionType + "_reputationSuccess]"));
    	var rep = parseInt(amt / 2);
    	if (rep < 1) return;
    	// increase the rep amount by the base amount
    	for (var i = 1; i < rep; i++) {
    		worldScripts.GalCopBB_Reputation.$adjustReputationSuccess(item.data.missionType, item.source, item.destination, base);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$TWOS_Rewards = function $TWOS_Rewards(entity, sysID, repLevel) {
    	// apart from medals, TWOS rewards player by increasing/decreasing value of commodities
    	// commodity price adjustments made by reputation script
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$AI_Rewards = function $AI_Rewards(entity, sysID, repLevel) {
    	// apart from medals, AI rewards player by reducing any bounty
    	worldScripts.GalCopBB_Reputation.$performBountyAdjustment(entity);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$MCEP_Rewards = function $MCEP_Rewards(entity, sysID, repLevel) {
    	// apart from medals, MCEP rewards players by providing a cash-back on items purchased on F3 screen
    	// commodity price adjustments made by reputation script
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$PMHS_Rewards = function $PMHS_Rewards(entity, sysID, repLevel) {
    	// apart from medals, PMHS rewards players by providing a cash-back on new ship purchases
    	// commodity price adjustments made by reputation script
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptedMission = function $acceptedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if (this._debug) log(this.name, "accepted mission id = " + missID);
    
    	if (!item) {
    		log(this.name, "!!ERROR: BB returned null value from $getItem on mission ID " + missID);
    		return;
    	}
    	gcm.$updateLastMissionDate(item.source, item.data.missionType);
    	gcm.$updateGeneralSettings(item);
    
    	// missions 80 - 83 donation missions	
    	if (item.data.missionType >= 80 && item.data.missionType <= 83) {
    		// automatically set this item to "complete" status
    		bb.$updateBBMissionPercentage(item.ID, 1);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$completedMission = function $completedMission(missID) {
    	var p = player.ship;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// update mission history
    	gcm.$updateSuccessHistoryReputation(item);
    
    	// mission types 80-83 (donation missions)
    	if (item.data.missionType >= 80 && item.data.missionType <= 83) {
    		this.$completeDonationMission(item.ID);
    	}
    	// mission types 84-87 (cargo donation missions)
    	if (item.data.missionType >= 84 && item.data.missionType <= 87) {
    		this.$completeCargoMission(item.ID);
    	}
    	// mission type 84 - repatriate slaves on station
    	// mission types 85/86/87 - donate cargo
    	if (item.data.missionType === 84 || item.data.missionType === 85 || item.data.missionType === 86 || item.data.missionType === 87) {
    		var amt = 0;
    		// we can partially complete these, so only take out what the player has
    		if (p.manifest[item.data.commodity] >= item.data.targetQuantity) {
    			p.manifest[item.data.commodity] -= item.data.targetQuantity;
    			amt = item.data.targetQuantity;
    		} else {
    			amt = p.manifest[item.data.commodity];
    			p.manifest[item.data.commodity] = 0;
    		}
    		if (worldScripts.market_observer3 && amt > 0) {
    			var mo = worldScripts.market_observer3;
    			mo.playerSoldCargo(item.data.commodity, amt, 0);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$confirmCompleted = function $confirmCompleted(missID) {
    	var p = player.ship;
    	var m = p.manifest;
    	var result = "";
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (item) {
    		// mission types 80-83 (donation missions)
    		if (item.data.missionType >= 80 && item.data.missionType <= 83) {
    			if (player.credits < item.data.target) {
    				//result += (result === "" ? "" : "\n") + "Insufficient funds to meet the donation amount.";
    			}
    		}
    		// *** type 84 - repatriate slaves
    		// *** type 85/85/87 - donate cargo
    		if (item.data.missionType === 84 || item.data.missionType === 85 || item.data.missionType === 86 || item.data.missionType === 87) {
    			if (m[item.data.commodity] < (item.data.targetQuantity - item.data.quantity)) {
    				var unit = worldScripts.GalCopBB_Missions.$getCommodityUnit(item.data.commodity);
    				result += (result === "" ? "" : "\n") + "Insufficient amount of " + displayNameForCommodity(item.data.commodity).toLowerCase() + " - expected " + item.data.targetQuantity + " t";
    			}
    		}
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$terminateMission = function $terminateMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// adjust reputation only when the terminatePenalty flag is set to true 
    	if (item.data.terminatePenalty === true) {
    		// update mission history
    		gcm.$addMissionHistory(item.source, item.data.missionType, 0, 1);
    
    		// reputation only goes in one way (up or down)
    		// so even if the player completes 90% of a mission and then terminates it, their reputation will go down even if they received most of the payment credits
    		var rep = worldScripts.GalCopBB_Reputation;
    		if (rep._disabled === false) {
    			rep.$adjustReputationFailure(item.data.missionType, item.source, item.destination, item.percentComplete);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$failedMission = function $failedMission(missID) {
    	this.$terminateMission(missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateManifestEntry = function $updateManifestEntry(missID) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	gcm.$updateManifestEntry(missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
    	return worldScripts.GalCopBB_Missions.$missionAvailability(missID, missType, origSysID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // cash donations
    this.$missionType80818283_Values = function $missionType80818283_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	if (player.score < 1000) return null;
    
    	var factor = Math.floor(Math.round(player.score / 1000));
    	var amt = (Math.floor(Math.random() * (6 + factor)) + 1) * 1000;
    	if (amt === 0) return null;
    
    	var result = {};
    	result["quantity"] = amt;
    	result["expiry"] = -1;
    	result["donation"] = amt;
    
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 84 - give slaves to AI
    this.$missionType84_Values = function $missionType84_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	if (!system.mainStation) return null;
    	var slaves = system.mainStation.market["slaves"].quantity;
    	if (slaves === 0) return null;
    	var result = {};
    	result["quantity"] = slaves;
    	result["commodity"] = "slaves";
    	result["price"] = 0;
    	result["expiry"] = -1;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 85/86/87 - cargo donation to entity
    this.$missionType858687_Values = function $missionType858687_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	if (system.ID === -1) return null;
    	// we're leaving out food, textiles 
    	var c_types = ["luxuries", "computers", "furs", "liquor_wines", "alloys", "radioactives", "alien_items", "machinery", "gold", "platinum", "gem_stones"];
    	var result = {};
    	var tries = 0;
    	result["quantity"] = 0;
    	// make sure we don't create missions where the quantity of cargo is more than the player ship
    	//var max = player.ship.cargoSpaceCapacity;
    	var max = 128;
    	// work out mission details - commodity and quantity
    	do {
    		result["commodity"] = c_types[Math.floor(Math.random() * c_types.length)];;
    		result["quantity"] = max;
    		//if ("gold platinum gem_stones".indexOf(result.commodity) >= 0) result.quantity = system.mainStation.market[result.commodity].quantity;
    		// make sure we only set a quantity the local system can provide
    		if (system.mainStation.market[result.commodity].quantity < result.quantity) result.quantity = parseInt(system.mainStation.market[result.commodity].quantity * (Math.random() * 0.4 + 0.5));
    		tries += 1;
    	} while (result.quantity > max || result.quantity <= 0 || tries < 5);
    	// if we aborted the loop, just return null
    	if (tries >= 5 && (result.quantity > max || result.quantity === 0)) return null;
    	// otherwise, just set a token reward - player will also receive bonus from waiting ship
    	// make sure the deposit amount is included in the price so the BB knows how to display it
    	result["price"] = 0;
    	result["expiry"] = -1;
    	result["penalty"] = 0;
    	return result;
    }
    Scripts/galcopbb_conditions.js
    "use strict";
    this.name = "GalCopBBMissions_Conditions";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Condition script for equipment.";
    this.license = "CC BY-NC-SA 4.0";
    
    //-------------------------------------------------------------------------------------------------------------
    this.allowAwardEquipment = function (equipment, ship, context) {
        if (context === "scripted") return true;
        if (equipment === "EQ_GCM_COMMS_RELAY_SWITCH_RECHARGE" && worldScripts.GalCopBB_DataCache._usageCount > 0) return true;
        return false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.updateEquipmentPrice = function (equipment, price) {
        var newprice = price;
        if (equipment === "EQ_GCM_COMMS_RELAY_SWITCH_RECHARGE") {
            newprice = worldScripts.GalCopBB_DataCache._usageCount * price;
        }
        return newprice;
    }
    Scripts/galcopbb_datacache.js
    "use strict";
    this.name = "GalCopBB_DataCache";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Control code for data cache entities (missions 31/41/71)";
    this.license = "CC BY-NC-SA 4.0";
    
    this._maxLocations = 0;
    this._dataCache = null; // holding object, containing data cache that is about to transfer data to the player
    this._dataCacheTimer = null; // timer to control data transfer to player
    this._dataDelivery = 0; // counter that controls the percentage of data transferred
    this._dataReceived = 0; // counter that controls the percentage of data received by player
    this._selfDestructShip = null; // data-cache entity that will be destroyed by self-destruct timer
    this._selfDestructTimer = null; // data-cache self-destruct timer
    this._selfDestructCounter = 0; // data-cache self-destruct counter
    this._selfDestructMessage = ["Self destruct sequence activated", "Self destruct in 10 seconds", "Self destruct in 9 seconds", "Self destruct in 8 seconds",
    	"Self destruct in 7 seconds", "Self destruct in 6 seconds", "Self destruct in 5 seconds", "Self destruct in 4 seconds", "Self destruct in 3 seconds",
    	"Self destruct in 2 seconds", "Self destruct in 1 seconds"
    ];
    this._defenceTimer = null; // timer to control when the data cache will detonate after being attacked
    this._cacheBeaconTimer = null; // timer to control when the data cache waypoint timer will disengage
    this._setData = [];
    this._usageCount = 0;
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
    	var gcm = worldScripts.GalCopBB_Missions;
    	// add these mission types into the main control
    	gcm._availableMissionTypes.push(31);
    	// type 41 is being added through galcopbb_delivery.js file
    	// type 71 is being added through galcopbb_solaractivity.js file
    
    	// position 7 is not used in any of these mission types, so limit the possibilities 
    	this._maxLocations = gcm._positions.length - 1;
    
    	if (missionVariables.GalCopBBMissions_CommsRelaySwitchCount) this._usageCount = missionVariables.GalCopBBMissions_CommsRelaySwitchCount;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillSaveGame = function () {
    	missionVariables.GalCopBBMissions_CommsRelaySwitchCount = this._usageCount;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.systemWillPopulate = function () {
    	this._setData.length = 0;
    	var list = worldScripts.GalCopBB_Missions.$getListOfMissions(true, [31, 41]);
    
    	if (list.length > 0) {
    		// loop through all active missions and see if any need to be set up for this system
    		for (var i = 0; i < list.length; i++) {
    
    			// *** type 31/41 - get data cache
    			if ((list[i].data.missionType === 31 || (list[i].data.missionType === 41 && list[i].data.stage === 0)) &&
    				list[i].destination === system.ID &&
    				list[i].data.quantity === 0 &&
    				list[i].data.destroyedQuantity === 0 &&
    				list[i].expiry > clock.adjustedSeconds) {
    				if (list[i].data.hasOwnProperty("spawned") === false) list[i].data.spawned = false;
    
    				// based on danger level, add some pirate ships around the comms relay
    				var goonCount = Math.floor(Math.random() * (8 - system.info.government)) + 2;
    				this._setData.push({
    					missionType: 3141,
    					trueMissionType: list[i].data.missionType,
    					missionID: list[i].ID,
    					source: list[i].source,
    					goons: goonCount,
    					quantity: 0,
    					target: list[i].data.targetQuantity,
    					repeat: list[i].data.spawned
    				});
    				list[i].data.spawned = true;
    
    				// add comms relay with populator
    				system.setPopulator("gcm-info-cache-" + list[i].ID, {
    					callback: function (pos) {
    						var g = worldScripts.GalCopBB_Missions;
    						var gns = worldScripts.GalCopBB_GoonSquads;
    						var dc = worldScripts.GalCopBB_DataCache;
    						var cr = null;
    						var checkShips = null;
    						for (var j = 0; j <= 5; j++) {
    							checkShips = system.addShips("commsrelay", 1, pos, 1000);
    							if (checkShips) cr = checkShips[0];
    							if (cr) break;
    						}
    						if (cr) {
    							var missData = dc.$getMissionData(3141);
    							cr.setScript("oolite-default-ship-script.js");
    							if (missData.repeat === false) {
    								cr.beaconCode = "D";
    								cr.beaconLabel = "Communications Relay";
    							}
    
    							cr.script._missionID = missData.missionID;
    							cr.script._passcode = expandDescription("[gcm_passcode]");
    
    							// add our defense mechanism to the cache
    							if (cr.script.shipBeingAttacked) cr.script.$gcm_hold_shipBeingAttacked = cr.script.shipBeingAttacked;
    							cr.script.shipBeingAttacked = dc.$gcm_cache_shipBeingAttacked;
    							if (cr.script.shipBeingAttackedByCloaked) cr.script.$gcm_hold_shipBeingAttackedByCloaked = cr.script.shipBeingAttackedByCloaked;
    							cr.script.shipBeingAttackedByCloaked = dc.$gcm_cache_shipBeingAttackedByCloaked;
    							if (cr.script.shipAttackedWithMissile) cr.script.$gcm_hold_shipAttackedWithMissile = cr.script.shipAttackedWithMissile;
    							cr.script.shipAttackedWithMissile = dc.$gcm_cache_shipAttackedWithMissile;
    
    							// add our shipDied event to the relay
    							if (cr.script.shipDied) cr.script.$gcm_hold_shipDied = cr.script.shipDied;
    							cr.script.shipDied = g.$gcm_entity_shipDied;
    
    							// set up broadcast comms interface
    							var bcc = worldScripts.BroadcastCommsMFD;
    							if (bcc.$checkMessageExists("gcm_transmit_seccode") === false) {
    								bcc.$createMessage({
    									messageName: "gcm_transmit_seccode",
    									callbackFunction: dc.$transmitSecurityCode.bind(dc),
    									displayText: "Transmit security code",
    									messageText: expandDescription("[gcm_transmitting_securitycode]"),
    									ship: cr,
    									transmissionType: "target",
    									deleteOnTransmit: true,
    									delayCallback: 5,
    									hideOnConditionRed: true
    								});
    							}
    
    							if (missData.repeat === false && (dc._cacheBeaconTimer == null || dc._cacheBeaconTimer.isRunning === false)) {
    								dc._cacheBeaconTimer = new Timer(dc, dc.$endDataCacheBeacon, Math.floor(Math.random() * 30) + 120, 0);
    							}
    
    							gns.$createGoonSquad(missData.goons, cr.position, player.ship.scannerRange * 0.5);
    
    							if (g._debug) g.$setWaypoint(pos, [0, 0, 0, 0], "Z", "Debug position (" + missData.trueMissionType + ")", missData.trueMissionType);
    
    						} else {
    							log("galcopBB_data_cache", "!!ERROR: Data cache not spawned!");
    						}
    					},
    					location: "INNER_SYSTEM_OFFPLANE",
    					locationSeed: list[i].ID
    				});
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillEnterWitchspace = function (cause, destination) {
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = function (whom, why) {
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerBoughtEquipment = function (equipmentKey) {
    	if (equipmentKey === "EQ_GCM_COMMS_RELAY_SWITCH_RECHARGE") {
    		player.ship.removeEquipment(equipmentKey);
    		worldScripts.GalCopBB_DataCache._usageCount = 0;
    	}
    	if (equipmentKey === "EQ_GCM_COMMS_RELAY_SWITCH_REMOVE") {
    		player.ship.removeEquipment(equipmentKey);
    		player.ship.removeEquipment("EQ_GCM_COMMS_RELAY_SWITCH");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.mode = function () {
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.activated = function () {
    	var dc = worldScripts.GalCopBB_DataCache;
    	var beacons = system.shipsWithRole("commsrelay");
    	if (!beacons || beacons.length === 0) {
    		player.consoleMessage("No communication relays found in system.", 5);
    		return;
    	}
    	if (dc._cacheBeaconTimer != null && dc._cacheBeaconTimer.isRunning === true) {
    		player.consoleMessage("Communication relay beacon is already active.", 5);
    		return;
    	}
    	if (dc._usageCount === 3) {
    		player.consoleMessage("Comms Relay Beacon Switch is non-operational.", 5)
    		return;
    	}
    	var bcn = beacons[0]; // there should only ever be one
    	if (bcn.beaconCode === "D") {
    		// this shouldn't happen, because the beacon code should only be present when the timer is running
    		// it's on already, so we'll just pretend nothing happened...
    	} else {
    		dc._usageCount += 1;
    		bcn.beaconCode = "D";
    		bcn.beaconLabel = "Communications Relay";
    		var bb = worldScripts.BulletinBoardSystem;
    		var item = bb.$getItem(bcn.script._missionID);
    		item.payment = Math.floor(item.payment * 0.95);
    		dc._cacheBeaconTimer = new Timer(dc, dc.$endDataCacheBeacon, 30, 0);
    		player.consoleMessage("Communication relay beacon re-activated", 5);
    		//if (dc._usageCount === 3) {
    		//dc._usageCount = 0;
    		//player.ship.removeEquipment("EQ_GCM_COMMS_RELAY_SWITCH");
    		//}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_cache_shipBeingAttacked = function $gcm_cache_shipBeingAttacked(attacker) {
    	if (this.ship.script.$gcm_hold_shipBeingAttacked) this.ship.script.$gcm_cache_shipBeingAttacked(attacker);
    	worldScripts.GalCopBB_DataCache.$cacheExplosion(this.ship);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_cache_shipBeingAttackedByCloaked = function $gcm_cache_shipBeingAttackedByCloaked() {
    	if (this.ship.script.$gcm_hold_shipBeingAttackedByCloaked) this.ship.script.$gcm_cache_shipBeingAttackedByCloaked();
    	worldScripts.GalCopBB_DataCache.$cacheExplosion(this.ship);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_cache_shipAttackedWithMissile = function $gcm_cache_shipAttackedWithMissile(missile, whom) {
    	if (this.ship.script.$gcm_hold_shipAttackedWithMissile) this.ship.script.$gcm_cache_shipAttackedWithMissile(missile, whom);
    	worldScripts.GalCopBB_DataCache.$cacheExplosion(this.ship);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$cacheExplosion = function $cacheExplosion(ship) {
    	this.ship.commsMessage("Alert! Under attack! Data integrity compromised! Activating defense system!");
    	this._dataCache = ship;
    	if (this._dataCacheTimer && this._dataCacheTimer.isRunning) this._dataCacheTimer.stop();
    	this._defenceTimer = new Timer(this, this.$detonateCache, 3, 0);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$detonateCache = function $detonateCache() {
    	this._dataCache.becomeCascadeExplosion();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // gets mission specific data for the populator routines
    // this works on a first in/first out basis - if there are multiple missions of the same type being populated, the mission specific data would
    // get pushed in to the setData array in order, and then this routine pulls that data out in the same order
    // that's the theory, anyway!
    this.$getMissionData = function $getMissionData(missionType) {
    	for (var i = 0; i < this._setData.length; i++) {
    		if (this._setData[i].missionType === missionType) {
    			var result = {
    				missionID: this._setData[i].missionID,
    				trueMissionType: (this._setData[i].trueMissionType ? this._setData[i].trueMissionType : missionType),
    				source: this._setData[i].source,
    				goons: this._setData[i].goons,
    				quantity: this._setData[i].quantity,
    				target: this._setData[i].target,
    				repeat: this._setData[i].repeat
    			};
    			this._setData.splice(i, 1);
    			return result;
    		}
    	}
    	return null;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$stopTimers = function $stopTimers() {
    	if (this._dataCacheTimer && this._dataCacheTimer.isRunning) this._dataCacheTimer.stop();
    	delete this._dataCacheTimer;
    	if (this._selfDestructTimer && this._selfDestructTimer.isRunning) this._selfDestructTimer.stop();
    	delete this._selfDestructTimer;
    	if (this._defenceTimer && this._defenceTimer.isRunning) this._defenceTimer.stop();
    	delete this._defenceTimer;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // works the time delay between transmitting the security code and receiving the data
    this.$checkRangeToDataCache = function $checkRangeToDataCache() {
    	if (this._dataCache.isValid === false) {
    		this._dataCacheTimer.stop();
    		return;
    	}
    	if (player.ship.position.distanceTo(this._dataCache) < player.ship.scannerRange) {
    		this._dataReceived += 1;
    		player.consoleMessage("Data transfer " + (this._dataReceived * 10) + "% complete.");
    		if (this._dataReceived >= 10) {
    			this._dataReceived = 0;
    			this._dataCacheTimer.stop();
    			delete this._dataCacheTimer;
    			this.$dataCacheComplete();
    			// remove the beacon, if it's been added
    			if (this._dataCache.beaconCode == "M") {
    				this._dataCache.beaconCode = "";
    				this._dataCache.beaconLabel = "";
    			}
    		}
    	} else {
    		// reset the data cache BCC setup
    		this._dataCacheTimer.stop();
    		delete this._dataCacheTimer;
    		player.consoleMessage("Out of range. Data transmission interrupted.");
    		var item = this._bb.$getItem(this._dataCache.script._missionID);
    		// check the mission type
    		var msgKey = "";
    		if (item.data.missionType === 31) msgKey = "gcm_transmit_seccode";
    		if (item.data.missionType === 71) {
    			msgKey = "gcm_sm_transmit_seccode_1";
    			if (this._dataCache.position.distanceTo(system.mainPlanet) > sun.position.distanceTo(system.mainPlanet)) msgKey = "gcm_sm_transmit_seccode_2";
    		}
    		if (msgKey != "") {
    			// set up broadcast comms interface
    			var bcc = worldScripts.BroadcastCommsMFD;
    			if (bcc.$checkMessageExists(msgKey) === false) {
    				bcc.$createMessage({
    					messageName: msgKey,
    					callbackFunction: this.$transmitSecurityCode.bind(this),
    					displayText: "Transmit security code",
    					messageText: expandDescription("[gcm_transmitting_securitycode]"),
    					ship: this._dataCache,
    					transmissionType: "target",
    					deleteOnTransmit: true,
    					delayCallback: 5,
    					hideOnConditionRed: true
    				});
    			}
    		}
    		this._dataCache = null;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // completes the data cache collection mission
    this.$dataCacheComplete = function $dataCacheComplete() {
    	if (this._dataCache.isInSpace) {
    
    		this._dataCache.commsMessage("Data transfer complete.", player.ship);
    		var missID = this._dataCache.script._missionID;
    		var bb = worldScripts.BulletinBoardSystem;
    		var item = bb.$getItem(missID);
    		item.data.quantity += 1;
    		// the mission can now be completed 
    		bb.$updateBBMissionPercentage(missID, item.data.quantity / item.data.targetQuantity);
    
    		worldScripts.GalCopBB_Missions.$logMissionData(item.ID);
    		player.consoleMessage(expandDescription("[goal_updated]"));
    
    		// set up self-destruct timer (mission type 31 only)
    		if (item.data.missionType === 31) {
    			this._selfDestructCounter = 0;
    			this._selfDestructTimer = new Timer(this, this.$selfDestruct, 10, 1);
    		} else {
    			this._dataCache = null;
    		}
    
    		var id = worldScripts.GalCopBB_Missions.$createSecondaryMission(missID);
    		if (id !== -1) {
    			// we set the pending mission orig id here, although missions received via a comms relay will be auto accepted
    			worldScripts.GalCopBB_Missions._pendingMissionOrigID = missID;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // response after player has transmitted security code to a comms relay
    this.$transmitSecurityCode = function $transmitSecurityCode() {
    	var bb = worldScripts.BulletinBoardSystem;
    	var targets = system.shipsWithPrimaryRole("commsrelay");
    	for (var i = 0; i < targets.length; i++) {
    		if (player.ship.position.distanceTo(targets[i]) < player.ship.scannerRange) {
    			targets[i].commsMessage(expandDescription("[gcm_transmit_code]"), player.ship);
    
    			var item = bb.$getItem(targets[i].script._missionID);
    			if (item.data.missionType === 31 || item.data.missionType === 71) {
    				// start a timer to control data transfer to player
    				this._dataCache = targets[i];
    				this._dataCacheTimer = new Timer(this, this.$checkRangeToDataCache.bind(this), 2, 2);
    			} else if (item.data.missionType === 41) {
    				this._dataCache = targets[i];
    				// work out new destination
    				var dta = worldScripts.GalCopBB_Delivery.$missionType41Updated_Values(targets[i], targets[i].script._missionID);
    				// tell player if something went wrong
    				if (dta === false) {
    					// !! Argh! error ... 
    					targets[i].commsMessage("Error! Data corruption detected! Unable to comply.", player.ship);
    					// turn off penalty so player doesn't get dinged for something not his fault
    					item.penalty = 0;
    				}
    				// start the self-destruct
    				this._selfDestructCounter = 0;
    				this._selfDestructTimer = new Timer(this, this.$selfDestruct, 10, 1);
    
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // does the self destruct sequence on a data cache
    this.$selfDestruct = function $selfDestruct() {
    	if (this._selfDestructCounter <= (this._selfDestructMessage.length - 1)) {
    		this._dataCache.commsMessage(this._selfDestructMessage[this._selfDestructCounter], player.ship);
    		this._selfDestructCounter += 1;
    	} else {
    		this._selfDestructTimer.stop();
    		delete this._dataCache.script.shipDied;
    		this._dataCache.explode();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // turns off the data cache beacon (it's only available for a short period of time)
    this.$endDataCacheBeacon = function $endDataCacheBeacon() {
    	var targets = system.shipsWithPrimaryRole("commsrelay");
    	if (targets.length > 0) {
    		for (var i = 0; i < targets.length; i++) {
    			targets[i].beaconCode = "";
    			targets[i].beaconLabel = "";
    		}
    		player.consoleMessage("Lost contact with communications relay" + (targets.length > 1 ? "s" : "") + ".");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptedMission = function $acceptedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if (this._debug) log(this.name, "accepted mission id = " + missID);
    
    	if (!item) {
    		log(this.name, "!!ERROR: BB returned null value from $getItem on mission ID " + missID);
    		return;
    	}
    	gcm.$updateLastMissionDate(item.source, item.data.missionType);
    
    	if (item.data.missionType === 31) {
    		player.ship.awardEquipment("EQ_GCM_COMMS_RELAY_SWITCH");
    		worldScripts.GalCopBB_DataCache._usageCount = 0;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$completedMission = function $completedMission(missID) {
    	var p = player.ship;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    
    	// update mission history
    	gcm.$updateSuccessHistoryReputation(item);
    	// remove any data-type black market sales
    	if (item.data.missionType === 31) {
    		if (sbm) sbm.$removeSaleItem("DTA_DATA_PACKAGE:" + missID);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$confirmCompleted = function $confirmCompleted(missID) {
    	var p = player.ship;
    	var result = "";
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (item) {
    		// get data from data cache
    		if (item.data.missionType === 31) {
    			if (item.data.targetQuantity === 0) {
    				result += (result === "" ? "" : "\n") + "The data package collected from the cache is no longer on your computer.";
    			}
    		}
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$terminateMission = function $terminateMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// adjust reputation only when the terminatePenalty flag is set to true 
    	if (item.data.terminatePenalty === true) {
    		// update mission history
    		gcm.$updateFailedHistoryReputation(item);
    	}
    	// remove any data-type black market sales
    	if (item.data.missionType === 31) {
    		if (sbm) sbm.$removeSaleItem("DTA_DATA_PACKAGE:" + missID);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$failedMission = function $failedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var eq = "";
    
    	if (!sbm) {
    		// if there's no blackmarket option, just remove the equipment
    		this.$terminateMission(missID);
    		return;
    	}
    
    	var item = bb.$getItem(missID);
    	// update mission history
    	gcm.$updateFailedHistoryReputation(item);
    
    	if (item.data.missionType === 31 && item.data.quantity === 1 && item.data.targetQuantity > 0) eq = "DTA_DATA_PACKAGE";
    
    	if (eq != "") gcm._equipmentFromFailedMissions.push({
    		missionType: item.data.missionType,
    		source: item.source,
    		equip: eq,
    		date: clock.adjustedSeconds
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateManifestEntry = function $updateManifestEntry(missID) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	gcm.$updateManifestEntry(missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
    	return "";
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 31 - data cache
    this.$missionType31_Values = function $missionType31_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	// pick a location
    	result["locationType"] = Math.floor(Math.random() * this._maxLocations);
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 40) + 20) / 10) * 10 + (7 - destSysInfo.government) * 20 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(200) // plus a possible bonus price, based on player score 
    		+
    		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 20); // plus a distance bonus
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price / 2);
    	return result;
    }
    Scripts/galcopbb_defend.js
    "use strict";
    this.name = "GalCopBB_Defend";
    this.author = "phkb";
    this.copyright = "2018 phkb";
    this.description = "Controls all defend missions (150-159)";
    this.license = "CC BY-NC-SA 4.0";
    
    /*
    	*150 - defend anaconda group from pirates
    	151 - defend liner from pirates (need liners OXP installed)
    		>> need to check if any liners spawned - if so, pick one and make it the target
    				Q: how to tell one liner is the target ship?
    			if not, spawn one and make it the target
    	*152 - defend main station from pirates
    		>> make mission text relate to "pirates attempting to disrupt station operations"
    		>> need a longer play on this one 
    			where did the station killer missile come from? how did the pirates come to have it?
    	153 - defend other galcop station from pirates (OXP required)
    		> how to tell what stations will be available at destination? or only tell player on arrival in system
    	*154 - defend main station from thargoids
    	155 - defend other station from thargoids (OXP's required)
    		> how to tell what stations will be available at destination? or only tell player on arrival in system
    	*156 - defend witchpoint beacon from thargoids
    */
    
    this._spawnTimer = null; // timer to control for how long attackers will be spawned
    this._defenceTarget = null; // object reference to defence target
    this._attackers = []; // array of ships who are attacking target
    this._playerCount = 0; // number of attacking ships destroyed by player
    this._playerMissileCount = 0; // number of station killer missiles destroyed by player
    this._endTime = 0; // time at which the attack will cease
    this._monitorTimer = null; // timer to monitor when attack is finished
    this._endTimeRemaining = 0; // holding value of time remaining if player saves game at station during an attack
    this._missionTypeActive = 0; // holding value of active mission type if player saves game at station during attack
    this._startTimer = null; // timer to check for when the mission will start
    this._setupAttackerTarget = null; // timer to control the setup of attacker targets in the AI
    this._alertMissileTimer = null; // timer to control an alert to the player when a station killer missile is launched
    
    this._initConfig = {
    	150: {spawnCount:3, initialSpawnTime:30, reSpawnTime:60, attackPeriod:300, maxCount:5},
    	152: {spawnCount:5, initialSpawnTime:30, reSpawnTime:15, attackPeriod:600, maxCount:12},
    	154: {spawnCount:3, initialSpawnTime:45, reSpawnTime:30, attackPeriod:600, maxCount:8},
    	156: {spawnCount:2, initialSpawnTime:45, reSpawnTime:60, attackPeriod:300, maxCount:3},
    };
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function () {
    	var gcm = worldScripts.GalCopBB_Missions;
    	// add these mission types into the main control
    	var list = [150,152,154,156];
    	//if (worldScripts["liners_populator_script.js"]) list.push(151);
    	gcm._availableMissionTypes = gcm._availableMissionTypes.concat(list);
    
    	// if we're in the middle of a 152/154 mission, set it up again
    	if (missionVariables.GalCopBBMissions_DefendActive) {
    		this._defenceTarget = system.mainStation;
    		this._missionTypeActive = missionVariables.GalCopBBMissions_DefendActive;
    		this._endTimeRemaining = missionVariables.GalCopBBMissions_EndTimeRemaining;
    		this._playerCount = missionVariables.GalCopBBMissions_PlayerCount;
    		this._playerMissileCount = missionVariables.GalCopBBMissions_PlayerMissileCount;
    		// there should only be one
    		var missList = gcm.$getListOfMissions(true, [152,154]);
    		for (var i = 0; i < missList.length; i++) {
    			var item = missList[i];
    			if (item.destination === system.ID && 
    				item.expiry > clock.adjustedSeconds &&
    				item.data.quantity < item.data.targetQuantity) {
    
    				this._defenceTarget.script._missionID = item.ID;
    				this._defenceTarget.script._missionType = this._missionTypeActive;
    				this._defenceTarget.script._attackerCount = 0;
    			}
    		}
    		this.shipWillDockWithStation = this.$gcm_shipWillDockWithStation;
    		this.shipLaunchedFromStation = this.$gcm_shipLaunchedFromStation;
    
    		delete missionVariables.GalCopBBMissions_DefendActive;
    		delete missionVariables.GalCopBBMissions_EndTimeRemaining;
    		delete missionVariables.GalCopBBMissions_PlayerCount;
    		delete missionVariables.GalCopBBMissions_PlayerMissileCount;
    		this._missionTypeActive = 0;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipExitedWitchspace = function () {
    	var list = worldScripts.GalCopBB_Missions.$getListOfMissions(true, [150,152,154,156]);
    	if (list.length > 0) {
    		for (var i = 0; i < list.length; i++) {
    			var item = list[i];
    
    			if (item.data.missionType === 150 && item.destination === system.ID &&
    				item.data.quantity === 0 &&
    				item.data.destroyedQuantity === 0 &&
    				item.expiry > clock.adjustedSeconds) {
    
    				// start monitoring for player getting to station
    				this.shipWillDockWithStation = this.$gcm_shipWillDockWithStation2;
    				
    				var checkShips = system.addShips("[anaconda]", 1, Vector3D(0, 0, 0.2).fromCoordinateSystem("wpu"), 1000);
    				var s = null;
    				if (checkShips) s = checkShips[0];
    				if (s) {
    					//log(this.name, "anaconda target vessel created successfully");
    					var g = worldScripts.GalCopBB_Missions;
    					if (g._rsnInstalled) s.shipUniqueName = item.data.targetShipName;
    
    					s.setScript("oolite-default-ship-script.js");
    					s.script._missionID = item.ID;
    					s.script._missionType = item.data.missionType;
    					s.script._attackerCount = 0;
    					// attach scripts
    					if (s.script.shipDied) s.script.$gcm_hold_shipDied = s.script.shipDied;
    					s.script.shipDied = this.$gcm_shipDied;
    					if (s.script.shipWillEnterWormhole) s.script.$gcm_hold_shipWillEnterWormhole = s.script.shipWillEnterWormhole;
    					s.script.shipWillEnterWormhole = this.$gcm_shipWillEnterWormhole;
    
    					s.switchAI("oolite-shuttleAI.js");
    
    					s.script.$initialise = this.$initialise;
    					s.script.$setDestination = this.$setDestination;
    					s.script.$initialise();
    					s.destination = system.mainStation;
    					s.destinationSystem = system.ID;
    					s.fuel = 0; // prevent any chance of jumping out
    
    					this._defenceTarget = s;
    					this._playerCount = 0;
    					this._playerMissileCount = 0;
    					this._startTimer = new Timer(this, this.$startAttack, 10, 10);
    				} else {
    					log(this.name, "ERROR: anaconda target vessel not created");
    				}
    			}
    			
    			if ((item.data.missionType === 152 || item.data.missionType === 154) && item.destination === system.ID &&
    				item.data.quantity === 0 &&
    				item.data.destroyedQuantity === 0 &&
    				item.expiry > clock.adjustedSeconds) {
    
    				// start monitoring for player getting to station
    				this.shipEnteredStationAegis = this.$gcm_shipEnteredStationAegis;
    				this.shipWillDockWithStation = this.$gcm_shipWillDockWithStation;
    				// for testing only
    				//system.mainStation.script.shipTakingDamage = this.$station_shipTakingDamage;
    
    				this._defenceTarget = system.mainStation;
    				this._defenceTarget.script._missionID = item.ID;
    				this._defenceTarget.script._missionType = item.data.missionType;
    				this._defenceTarget.script._attackerCount = 0;
    				// attach scripts
    				if (this._defenceTarget.script.shipDied) this._defenceTarget.script.$gcm_hold_shipDied = this._defenceTarget.script.shipDied;
    				this._defenceTarget.script.shipDied = this.$gcm_shipDied;
    				this._playerCount = 0;
    				this._playerMissileCount = 0;
    			}
    
    			if (item.data.missionType === 156 && item.destination === system.ID &&
    				item.data.quantity === 0 &&
    				item.data.destroyedQuantity === 0 &&
    				item.expiry > clock.adjustedSeconds) {
    				var ships = system.shipsWithRole("buoy-witchpoint");
    				if (ships.length > 0) {
    					this._defenceTarget = ships[0];
    					// bump up the energy stats a bit so the player has a chance to defend something
    					this._defenceTarget.maxEnergy = 1000;
    					this._defenceTarget.energy = 1000;
    					this._defenceTarget.script._missionID = item.ID;
    					this._defenceTarget.script._missionType = item.data.missionType;
    					this._defenceTarget.script._attackerCount = 0;
    					// attach scripts
    					if (this._defenceTarget.script.shipDied) this._defenceTarget.script.$gcm_hold_shipDied = this._defenceTarget.script.shipDied;
    					this._defenceTarget.script.shipDied = this.$gcm_shipDied;
    					var conf = this._initConfig[item.data.missionType];
    					this._spawnTimer = new Timer(this, this.$spawnThargoidAttackers, conf.initialSpawnTime, conf.reSpawnTime);
    					this._endTime = clock.adjustedSeconds + conf.attackPeriod; // 5 minutes
    					this._monitorTimer = new Timer(this, this.$monitorAttackers, 30, 30);
    					this._playerCount = 0;
    					this._playerMissileCount = 0;
    				}
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillSaveGame = function () {
    	if (this._missionTypeActive != 0) {
    		missionVariables.GalCopBBMissions_DefendActive = this._missionTypeActive;
    		missionVariables.GalCopBBMissions_EndTimeRemaining = this._endTimeRemaining;
    		missionVariables.GalCopBBMissions_PlayerCount = this._playerCount;
    		missionVariables.GalCopBBMissions_PlayerMissileCount = this._playerMissileCount;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // for mission types 152/154
    this.$gcm_shipWillDockWithStation = function $gcm_shipWillDockWithStation(station) {
    	// is the spawn timer still running?
    	if (this._spawnTimer && this._spawnTimer.isRunning && this._endTime > clock.seconds) {
    		// calc home much time is still to run
    		this._endTimeRemaining = this._endTime - clock.seconds;
    		this._missionTypeActive = this._defenceTarget.script._missionType;
    		// stop, but don't delete, the timers
    		this._spawnTimer.stop();
    		this._monitorTimer.stop();
    		// set up the relaunch 
    		this.shipLaunchedFromStation = this.$gcm_shipLaunchedFromStation;
    	}
    	delete this.shipWillDockWithStation;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // for mission types 150
    this.$gcm_shipWillDockWithStation2 = function $gcm_shipWillDockWithStation2(station) {
    	// is any timers running?
    	if ((this._startTimer && this._startTimer.isRunning) || (this._spawnTimer && this._spawnTimer.isRunning)) {
    		// fail mission by destroying the defence ship
    		this._defenceTarget.explode();
    		this.$stopTimers();
    	}
    	delete this.shipWillDockWithStation;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_shipLaunchedFromStation = function $gcm_shipLaunchedFromStation(station) {
    	this._endTime = clock.adjustedSeconds + this._endTimeRemaining;
    	if (this._spawnTimer == null) {
    		var conf = this._initConfig[this._defenceTarget.script._missionType];
    		conf.initialSpawnTime = 2;
    		switch (this._defenceTarget.script._missionType) {
    			case 152:
    				this._spawnTimer = new Timer(this, this.$spawnPirateAttackers, conf.initialSpawnTime, conf.reSpawnTime);
    				break;
    			case 154:
    				this._spawnTimer = new Timer(this, this.$spawnThargoidAttackers, conf.initialSpawnTime, conf.reSpawnTime);
    				break;
    		}
    		this._monitorTimer = new Timer(this, this.$monitorAttackers, 30, 30);
    	} else {
    		this._spawnTimer.start();
    		this._monitorTimer.start();
    	}
    
    	delete this.shipLaunchedFromStation;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_shipEnteredStationAegis = function $gcm_shipEnteredStationAegis(station){
    	if (station.isValid && station.isInSpace && station == this._defenceTarget) {
    		var conf = this._initConfig[this._defenceTarget.script._missionType];
    		switch (this._defenceTarget.script._missionType) {
    			case 152:
    				this._spawnTimer = new Timer(this, this.$spawnPirateAttackers, conf.initialSpawnTime, conf.reSpawnTime);
    				break;
    			case 154:
    				this._spawnTimer = new Timer(this, this.$spawnThargoidAttackers, conf.initialSpawnTime, conf.reSpawnTime);
    				break;
    		}
    		this._endTime = clock.adjustedSeconds + conf.attackPeriod; // 5 minutes
    		this._monitorTimer = new Timer(this, this.$monitorAttackers, 30, 30);
    		delete this.shipEnteredStationAegis;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillEnterWitchspace = function (cause, destination) {
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = function (whom, why) {
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipLaunchedEscapePod = function (escapepod) {
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_shipDied = function $gcm_shipDied(whom, why) {
    	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);
    	if (this.ship.script._setDest && this.ship.script._setDest.isRunning) this.ship.script._setDest.stop();
    	//log(this.name, "target ship died " + why + ", " + whom);
    	if (this.ship.script._missionID && this.ship.script._missionID > 0) {
    		var bb = worldScripts.BulletinBoardSystem;
    		var item = bb.$getItem(this.ship.script._missionID);
    		if (item.percentComplete < 1) {
    			item.data.destroyedQuantity = 1;
    			switch (this.ship.script._missionType) {
    				case 150:
    					player.consoleMessage(expandDescription("[gcm_anaconda_died]"), 5);
    					break;
    				case 152:
    				case 154:
    					player.consoleMessage(expandDescription("[gcm_station_died]"), 5);
    					break;
    				case 156:
    					player.consoleMessage(expandDescription("[gcm_beacon_died]"), 5);
    					break;
    			}
    			if (item.data.quantity === 1) {
    				item.data.quantity = 0;
    				player.consoleMessage(expandDescription("[goal_updated]"));
    			}
    		}
    		worldScripts.GalCopBB_Defend.$stopTimers();
    		this.ship.script._missionID = 0;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_shipWillEnterWormhole = function $gcm_shipWillEnterWormhole() {
    	if (this.ship.script.$gcm_hold_shipWillEnterWormhole) this.ship.script.$gcm_hold_shipWillEnterWormhole();
    	if (this.ship.script._setDest && this.ship.script._setDest.isRunning) this.ship.script._setDest.stop();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_badGuy_shipDied = function $gcm_badGuy_shipDied(whom, why) {
    	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);
    	if (this.ship.script._missionID && this.ship.script._missionID > 0) {
    		this.ship.script.$processBadGuyEnd(whom);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_badGuy_shipTakingDamage = function $gcm_badGuy_shipTakingDamage(amount, whom, type) {
    	if (this.ship.script.$gcm_hold_shipTakingDamage) this.ship.script.$gcm_hold_shipTakingDamage(amount, whom, type);
    	// make a note of who was our last attacker, so we can tell if the pilot ejects
    	// may not always correspond to the correct ship, but it's close enough
    	this.ship.script._attackedByWhom = whom;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_badGuy_shipLaunchedEscapePod = function $gcm_badGuy_shipLaunchedEscapePod(escapePod) {
    	if (this.ship.script.$gcm_hold_shipLaunchedEscapePod) this.ship.script.$gcm_hold_shipLaunchedEscapePod(escapePod);
    	if (this.ship.script._missionID && this.ship.script._missionID > 0) {
    		// disable the shipDied function
    		delete this.ship.script.shipDied;
    		if (this.ship.script.$gcm_hold_shipDied) {
    			this.ship.script.shipDied = this.ship.script.$gcm_hold_shipDied;
    			delete this.ship.script.$gcm_hold_shipDied;
    		}
    		this.ship.script.$processBadGuyEnd(this.ship.script._attackedByWhom);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$processBadGuyEnd = function $processBadGuyEnd(whom) {
    	this.ship.script._missionID = 0;
    	var w = worldScripts.GalCopBB_Defend;
    	if (w._defenceTarget && w._defenceTarget.isValid && w._defenceTarget.isInSpace) {
    		w._defenceTarget.script._attackerCount -= 1;
    		if (whom && whom.isPlayer) w._playerCount += 1;
    		if (w._defenceTarget.script._attackerCount <= 0 && w._spawnTimer === null) {
    			// yay!
    			w.$finishMission(w._defenceTarget.script._missionID, (this.ship.isThargoid ? 500 : 200));
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$stopTimers = function $stopTimers() {
    	if (this._monitorTimer && this._monitorTimer.isRunning) this._monitorTimer.stop();
    	this._monitorTimer = null;
    	if (this._spawnTimer && this._spawnTimer.isRunning) this._spawnTimer.stop();
    	this._spawnTimer = null;
    	if (this._startTimer && this._startTimer.isRunning) this._startTimer.stop();
    	this._startTimer = null;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$spawnThargoidAttackers = function $spawnThargoidAttackers() {
    	if (this._defenceTarget.isValid && this._defenceTarget.isInSpace) {
    		var tgt = this._defenceTarget;
    		var g = worldScripts.GalCopBB_Missions;
    		var conf = this._initConfig[tgt.script._missionType];
    		if (Math.random() > 0.5 && tgt.script._attackerCount < conf.maxCount) {
    			// get the initial number of ships to spawn from the config
    			var num = conf.spawnCount;
    			var pos = null;
    			var range = 0;
    			switch (tgt.script._missionType) {
    				case 154:
    					pos = g.$findPosition(tgt.position);
    					range = 200;
    					break;
    				case 156:
    					pos = tgt.position;
    					range = 25000;
    					break;
    			}
    			var thargoids = system.addShips("thargoid", num, pos, range);
    			// reset the count to 1 for any future respawns
    			conf.spawnCount = 1;
    			// make sure all the attackers have the target
    			for (var i = 0; i < thargoids.length; i++) {
    				this._attackers.push(thargoids[i]);
    				thargoids[i].script._missionID = tgt.script._missionID;
    				// attach scripts
    				if (thargoids[i].script.shipDied) thargoids[i].script.$gcm_hold_shipDied = thargoids[i].script.shipDied;
    				thargoids[i].script.shipDied = this.$gcm_badGuy_shipDied;
    				// attach completion script
    				thargoids[i].script.$processBadGuyEnd = this.$processBadGuyEnd;
    				
    				tgt.script._attackerCount += 1;
    				thargoids[i].target = tgt;
    				thargoids[i].performAttack();
    			}
    		}
    		if (clock.adjustedSeconds > this._endTime) {
    			this._spawnTimer.stop();
    			this._spawnTimer = null;
    			// have we finished?
    			if (tgt.script._attackerCount <= 0) {
    				// yay!
    				this.$finishMission(tgt.script._missionID, 500);
    			}
    		}
    	} else {
    		this._spawnTimer.stop();
    		this._spawnTimer = null;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$spawnPirateAttackers = function $spawnPirateAttackers() {
    	if (this._defenceTarget.isValid && this._defenceTarget.isInSpace) {
    		var tgt = this._defenceTarget;
    		var g = worldScripts.GalCopBB_Missions;
    		var conf = this._initConfig[tgt.script._missionType];
    		if (Math.random() > 0.5 && tgt.script._attackerCount < conf.maxCount) {
    			// get the initial number of ships to spawn from the config
    			var pop = worldScripts["oolite-populator"];
    			var pos = g.$findPosition(tgt.position);
    			var types = ["pirate-medium-fighter","pirate-medium-fighter","pirate-heavy-fighter"];
    			var pirates = system.addShips(types[Math.floor(Math.random() * types.length)], conf.spawnCount, pos, 200);
    			if (pirates) {
    				// decrease the count by 1 for any future respawns
    				conf.spawnCount -= 1;
    				if (conf.spawnCount < 1) conf.spawnCount = 1;
    				// make sure all the attackers are equipped, have names, and bounties
    				for (var i = 0; i < pirates.length; i++) {
    					var pr = pirates[i];
    					if (g._rsnInstalled) pr.shipUniqueName = g.$getRandomShipName(pr, "pirate");
    					pr.setScript("oolite-default-ship-script.js");
    					pr.script._configDone = false;
    					// configure our attackers
    					pr.setBounty(60 + system.government + Math.floor(Math.random() * 8), "setup actions");
    					pr.setCrew({
    						name: randomName() + " " + randomName(),
    						bounty: pr.bounty,
    						insurance: 0
    					});
    					if (pr.hasHyperspaceMotor) {
    						pop._setWeapons(pr, 1.75); // bigger ones sometimes well-armed
    					} else {
    						pop._setWeapons(pr, 1.3); // rarely well-armed
    					}
    					pop._setSkill(pr, 4 - system.info.government);
    					if (Math.random() * 16 < system.info.government) pop._setMissiles(pr, -1);
    					pr.script._missionID = tgt.script._missionID;
    					// attach monkey scripts
    					if (pr.script.shipDied) pr.script.$gcm_hold_shipDied = pr.script.shipDied;
    					pr.script.shipDied = this.$gcm_badGuy_shipDied;
    					if (pr.script.shipLaunchedEscapePod) pr.script.$gcm_hold_shipLaunchedEscapePod = pr.script.shipLaunchedEscapePod;
    					pr.script.shipLaunchedEscapePod = this.$gcm_badGuy_shipLaunchedEscapePod;
    					if (pr.script.shipTakingDamage) pr.script.$gcm_hold_shipTakingDamage = pr.script.shipTakingDamage;
    					pr.script.shipTakingDamage = this.$gcm_badGuy_shipTakingDamage;
    					// attach completion script
    					pr.script.$processBadGuyEnd = this.$processBadGuyEnd;
    
    					tgt.script._attackerCount += 1;
    					// make sure the AI is switched
    					pr.switchAI("gcm-attackerAI.js");
    					this._attackers.push(pr);
    				}
    				// get ready to set up the target
    				if (this._setupAttackerTarget == null || this._setupAttackerTarget.isRunning === false) {
    					this._setupAttackerTarget = new Timer(this, this.$giveAttackersTarget, 1, 0);
    				}
    			}
    		}
    		if (clock.adjustedSeconds > this._endTime) {
    			this._spawnTimer.stop();
    			this._spawnTimer = null;
    			// have we finished?
    			if (tgt.script._attackerCount <= 0) {
    				// yay!
    				this.$finishMission(tgt.script._missionID, 200);
    			}
    		}
    	} else {
    		this._spawnTimer.stop();
    		this._spawnTimer = null;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$startAttack = function $startAttack() {
    	var p = player.ship;
    	var tgt = this._defenceTarget;
    	if (p.position.distanceTo(tgt) < 24000) {
    		this._startTimer.stop();
    		this._startTimer = null;
    		// add a beacon to the ship for easy navigation
    		tgt.beaconCode = "A";
    		tgt.beaconLabel = "Anaconda Transport";
    		player.consoleMessage(expandDescription("[gcm_anaconda_located]"), 5);
    		tgt.commsMessage(expandDescription("[gcm_anaconda_greeting]"), player.ship);
    
    		// start time
    		var conf = this._initConfig[tgt.script._missionType];
    		this._spawnTimer = new Timer(this, this.$spawnPirateAttackers, conf.initialSpawnTime, conf.reSpawnTime);
    		this._endTime = clock.adjustedSeconds + conf.attackPeriod; // 5 minutes
    		this._monitorTimer = new Timer(this, this.$monitorAttackers, 30, 30);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$monitorAttackers = function $monitorAttackers() {
    	if (this._defenceTarget.isValid === false || this._defenceTarget.isInSpace === false) {
    		this._monitorTimer.stop();
    		this._monitorTimer = null;
    		return;
    	}
    	var tgt = this._defenceTarget;
    	// auto-destruct ship if the player gets too far away
    	if (player.ship.position.distanceTo(tgt) > 60000) {
    		this._monitorTimer.stop();
    		this._monitorTimer = null;
    		tgt.explode();
    		return;
    	}
    	for (var i = 0; i < this._attackers.length; i++) {
    		var atk = this._attackers[i];
    		if (atk.isValid && atk.isInSpace) {
    			// if they're running away and out of range, remove them
    			if (atk.position.distanceTo(tgt.position) > 100000 && atk.position.distanceTo(player.ship) > 100000) {
    				atk.remove();
    			}
    			// make sure all our attackers have their target set
    			if (atk.isThargoid === false && atk.AIScript && atk.AIScript.oolite_priorityai && atk.AIScript.oolite_priorityai.getParameter("oolite_attackTarget") != tgt) {
    				atk.AIScript.oolite_priorityai.setParameter("oolite_attackTarget", tgt);
    				atk.AIScript.oolite_priorityai.configurationResetWaypoint();
    				atk.AIScript.oolite_priorityai.reconsiderNow();
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptedMission = function $acceptedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if (this._debug) log(this.name, "accepted mission id = " + missID);
    
    	if (!item) {
    		log(this.name, "!!ERROR: BB returned null value from $getItem on mission ID " + missID);
    		return;
    	}
    	gcm.$updateLastMissionDate(item.source, item.data.missionType);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$completedMission = function $completedMission(missID) {
    	var p = player.ship;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if ([152, 154, 156].indexOf(item.data.missionType) >= 0) {
    		// if the player has a bounty, doing these missions will reduce it
    		if (player.ship.bounty > 0) {
    			for (var i = 0; i < 10; i++) {
    				if (player.ship.bounty > 0) player.ship.setBounty(player.ship.bounty - 1, "community service");
    			}
    			player.ship.consoleMessage("Your bounty has been reduced.");
    		}
    	}
    	// update mission history
    	gcm.$addMissionHistory(item.source, item.data.missionType, 1, 0);
    
    	gcm.$checkForFollowupMission(item);
    
    	// adjust reputation with entities
    	var rep = worldScripts.GalCopBB_Reputation;
    	if (rep._disabled === false) {
    		rep.$adjustReputationSuccess(item.data.missionType, item.source, item.destination, item.percentComplete);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$confirmCompleted = function $confirmCompleted(missID) {
    	var p = player.ship;
    	var result = "";
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (item) {
    
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$terminateMission = function $terminateMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// adjust reputation only when the terminatePenalty flag is set to true 
    	if (item.data.terminatePenalty === true) {
    		// update mission history
    		gcm.$updateFailedHistoryReputation(item);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$failedMission = function $failedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var eq = "";
    
    	if (!sbm) {
    		// if there's no blackmarket option, just remove the equipment
    		this.$terminateMission(missID);
    		return;
    	}
    
    	var item = bb.$getItem(missID);
    	// update mission history
    	gcm.$updateFailedHistoryReputation(item);
    
    	// cleanup any residual attackers
    	for (var i = 0; i < this._attackers.length; i++) {
    		this._attackers[i].remove(true);
    	}
    
    	if (eq != "") gcm._equipmentFromFailedMissions.push({
    		missionType: item.data.missionType,
    		source: item.source,
    		equip: eq,
    		date: clock.adjustedSeconds
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateManifestEntry = function $updateManifestEntry(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (item.data.destroyedQuantity === 0) {
    		var gcm = worldScripts.GalCopBB_Missions;
    		gcm.$updateManifestEntry(missID);
    	} else {
    		var rep = worldScripts.GalCopBB_Reputation;
    		// update the manifest
    		bb.$updateBBManifestText(
    			missID,
    			rep.$transformText(expandDescription("[missionType" + item.data.missionType + "_failedManifest]", {
    				system: System.systemNameForID(item.source),
    				expiry: ""
    			}), item.source, item.destination)
    		);
    		bb.$updateBBStatusText(
    			missID,
    			rep.$transformText(expandDescription("[missionType" + item.data.missionType + "_failedStatus]", {
    				system: System.systemNameForID(item.source)
    			}), item.source, item.destination)
    		);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
    	return worldScripts.GalCopBB_Missions.$missionAvailability(missID, missType, origSysID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 150 - defend transport from pirates
    this.$missionType150_Values = function $missionType150_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var missValues = worldScripts.GalCopBB_CoreMissionValues;
        var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 1300) + 1300) / 10) * 10 + (7 - destSysInfo.government) * 20 + missValues.$calcDistanceBonus(routeDistance, 10);
    	result["expiry"] = clock.adjustedSeconds + routeTime + 1800; 
    	result["penalty"] = 0;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 152 - defend main station from pirates
    this.$missionType152_Values = function $missionType152_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var missValues = worldScripts.GalCopBB_CoreMissionValues;
        var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 1000) + 1000) / 10) * 10 + (7 - destSysInfo.government) * 20 + missValues.$calcDistanceBonus(routeDistance, 10);
    	result["expiry"] = clock.adjustedSeconds + routeTime + 1800; 
    	result["penalty"] = 0;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 154 - defend main station from thargoids
    this.$missionType154_Values = function $missionType154_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var missValues = worldScripts.GalCopBB_CoreMissionValues;
        var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 1500) + 1500) / 10) * 10 + (7 - destSysInfo.government) * 20 + missValues.$calcDistanceBonus(routeDistance, 10);
    	result["expiry"] = clock.adjustedSeconds + routeTime + 1800; 
    	result["penalty"] = 0;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 156 - defend wp beacon from thargoids
    this.$missionType156_Values = function $missionType156_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var missValues = worldScripts.GalCopBB_CoreMissionValues;
        var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 1500) + 1500) / 10) * 10 + (7 - destSysInfo.government) * 20 + missValues.$calcDistanceBonus(routeDistance, 10);
    	result["expiry"] = clock.adjustedSeconds + routeTime + 1800; 
    	result["penalty"] = 0;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$finishMission = function(missID, bonusType) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (item.data.quantity === 0) {
    		// send a message to the player (types 150,152,154 only)
    		if (this._defenceTarget && this._defenceTarget.isValid && this._defenceTarget.isInSpace) {
    			if ([150,152,154].indexOf(this._defenceTarget.script._missionType) >= 0 && player.ship.position.distanceTo(this._defenceTarget) < player.ship.scannerRange) {
    				this._defenceTarget.commsMessage(expandDescription("[gcm_defend_finish_comms]"), player.ship);
    			}
    		}
    
    		item.data.quantity += 1;
    		bb.$updateBBMissionPercentage(item.ID, 1);
    		gcm.$logMissionData(item.ID);
    		// add bonus for any ships killed
    		item.payment += (bonusType * this._playerCount);
    		// add bonus for any station killer missiles destroyed
    		if (this._playerMissileCount > 0) item.payment += (500 * this._playerMissileCount);
    		// reset variables
    		this._playerCount = 0;
    		this._playerMissileCount = 0;
    		player.consoleMessage(expandDescription("[goal_updated]"));
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // attached to a spawned anaconda vessel to force the main station to be the destination
    this.$initialise = function $initialise() {
    	// run a timer so the ai controller has a chance to startup before we try and set the destination
    	this.ship.script._retryCounter = 0;
    	this.ship.script._setDest = new Timer(this, this.ship.script.$setDestination, 2, 0);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$setDestination = function $setDestination() {
    	// do we have a ai controller yet?
    	if (!this.ship.AIScript || !this.ship.AIScript.oolite_priorityai) {
    		// start counting after ship is in flight
    		if (this.ship.status !== "STATUS_IN_FLIGHT") this.ship.script._retryCounter += 1;
    		// give up after ten tries
    		if (this.ship.script._retryCounter === 10) return;
    
    		// otherwise, reset the timer and try again later
    		delete this.ship.script._setDest;
    		this.ship.script._setDest = new Timer(this, this.ship.script.$setDestination, 2, 0);
    		return;
    	}
    
        this.ship.AIScript.oolite_priorityai.setParameter("oolite_selectedPlanet", null);
        this.ship.AIScript.oolite_priorityai.setParameter("oolite_selectedStation", system.mainStation);
    	this.ship.AIScript.oolite_priorityai.reconsiderNow();
    	delete this.ship.script._setDest;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // makes the attackers attack a particular target
    this.$giveAttackersTarget = function $giveAttackersTarget() {
    	var retry = false;
    	for (var i = 0; i < this._attackers.length; i++) {
    		var shp = this._attackers[i];
    		if (shp.AIScript && shp.AIScript.oolite_priorityai) {
    			if (shp.script._configDone === false) {
    				shp.AIScript.oolite_priorityai.setParameter("oolite_pirateLurk", this._defenceTarget.position.add(this._defenceTarget.vectorForward.multiply(5000)));
    				shp.AIScript.oolite_priorityai.setParameter("oolite_attackTarget", this._defenceTarget);
    				shp.AIScript.oolite_priorityai.configurationResetWaypoint();
    				shp.AIScript.oolite_priorityai.reconsiderNow();
    				if (shp.missileCapacity > 0) {
    					shp.script.$attackStation = this.$attackStation;
    					shp.script._attackTimer = new Timer(shp, shp.script.$attackStation, 5, 5);
    				}
    				shp.script._configDone = true;
    			}
    		} else {
    			retry = true;
    			break;
    		}
    		if (retry === true) break;
    	}
    	if (retry === true) {
    		this._setupAttackerTarget = new Timer(this, this.$giveAttackersTarget, 1, 0);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$station_shipTakingDamage = function(amount, whom, type) {
    	log(this.name, "station hit - " + amount + ", " + type + ": " + this.ship.energy);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$attackStation = function $attackStation() {
    	if (!this) return;
    	if (!system.mainStation) {
    		this.script._attackTimer.stop();
    		return;
    	}
    	var num = system.countShipsWithRole("EQ_GCM_STATIONKILLER_MISSILE");
    	var dist = this.position.distanceTo(system.mainStation);
    	if (num < 2 && dist > 18000 && dist < 23000 && Math.random() > 0.5) {
    		this.script._attackTimer.stop();
    		// target station
    		var holdTarget = this.target;
    		this.target = system.mainStation;
    		// load up the station killer missile
    		// make sure there's a free slot
    		if (this.missiles.length == this.missileCapacity) {
    			this.removeEquipment(this.missiles[this.missiles.length - 1].equipmentKey);
    		}
    		var result = this.awardEquipment("EQ_GCM_STATIONKILLER_MISSILE");
    		if (result) {
    			var ms = this.fireMissile("EQ_GCM_STATIONKILLER_MISSILE");
    			if (ms.script.shipDied) ms.script.$gcm_hold_shipDied = ms.script.shipDied;
    			ms.script.shipDied = this.$gcm_missile_shipDied;
    
    			var gcd = worldScripts.GalCopBB_Defend;
    			if (gcd._alertMissileTimer == null || gcd._alertMissileTimer.isRunning === false) {
    				gcd._alertMissileTimer = new Timer(gcd, gcd.$missileAlert, 2, 10);
    			}
    		}
    		this.target = holdTarget;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missileAlert = function $missileAlert() {
    	var num = system.countShipsWithRole("EQ_GCM_STATIONKILLER_MISSILE");
    	if (num > 0 && player.ship.position.distanceTo(system.mainStation) < player.ship.scannerRange) {
    		system.mainStation.commsMessage(expandDescription("[gcm_station_missile_alert]"), player.ship);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_missile_shipDied = function $gcm_missile_shipDied(whom, why) {
    	if (this.ship.script.$gcm_hold_shipDied) this.$gcm_hold_shipDied(whom, why);
    	if (whom && whom.isPlayer) {
    		var w = worldScripts.GalCopBB_Defend;
    		w._playerMissileCount += 1;
    	}
    }
    Scripts/galcopbb_delivery.js
    "use strict";
    this.name = "GalCopBB_Delivery";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Controls all delivery missions (40-46)";
    this.license = "CC BY-NC-SA 4.0";
    
    /*
    	Variation for 40 and 41 (part2): after two thirds or three quarters of the cargo has been delivered, 
    	spawn a pirate group at 27000 from player (above or below), and give them a lurk position of the player (or the delivery target)
    	Then, when they get in range, have some specific comms from the delivery target to indicate these ships are after them
    	like "Oh no! They've found me!"
    	Also have some specific comms from the pirates, like "There he is, get him!" (like an assassin)
    	Pirate group size will be related to Elite ranking, from 2 to a max of 8 ships
    
    	Process
    		check if ship is under attack
    			switch to trader AI for fight or flee
    		if fleeing, dump a part payment before jumping
    		if fighting, monitor when it's safe again, and switch back to scavenger AI to collect the rest of the drop
    
    	TODO: add occasional pirate incursions at waypoints for type 46
    */
    
    this._debug = false;
    this._maxLocations = 0;
    this._strickenFixTimer = null; // timer to control when stricken ships are fixed
    this._delayToReceiveFixTimer = null; // timer to control the delay between receiving the fix, and actually being fixed.
    this._strickenShip = null; // holding object, containing the stricken ship the player is trying to rescue
    this._fixCargo = null; // holding object, containing the cargo canister ejected by the player
    this._strickenScoopTimer = null;
    this._deliverShip = null; // reference to the ship we are in the process of delivering cargo to
    this._deliverCargo = []; // array of cargo items for the target ship to scoop
    this._deliverShipPointCounter = 0;
    this._deliverShipLastSpeed = 0;
    this._deliverShipLastDirection = "";
    this._deliverShipDataKey = "";
    this._deliverShipName = "";
    this._deliverShipPersonality = -1;
    this._checkPlayerNearbyTimer = null; // timer to check if the player is nearby
    this._waypointMissionID = 0;
    this._badCargoTimer = null;
    this._setData = []; // hold value for populator routine
    this._requestSpecialCargo = ""; // text of commodity, indicating that the player is due to receive 1t of commodity upon docking at GalCop station
    this._simulator = false;
    this._monitorCargoDump = null;
    this._monitorCargo = [];
    this._attackers = [];
    this._setupAttackerTarget = null;
    this._ambushTimer = null;
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
    	var gcm = worldScripts.GalCopBB_Missions;
    	// add these mission types into the main control
    	var list = [40, 41, 42, 43, 44, 46];
    	gcm._availableMissionTypes = gcm._availableMissionTypes.concat(list);
    	gcm._multiStageMissionTypes.push(41);
    	gcm._multiStageMissionTypes.push(46);
    	gcm._interstellarMissionTypes.push(44);
    	// position 7 is not used in any of these mission types, so limit the possibilities 
    	this._maxLocations = gcm._positions.length - 1;
    	this._debug = gcm._debug;
    	// load any stored values
    	if (missionVariables.GalCopBBMissions_DeliverShipKey) this._deliverShipDataKey = missionVariables.GalCopBBMissions_DeliverShipKey;
    	if (missionVariables.GalCopBBMissions_DeliverShipName) this._deliverShipName = missionVariables.GalCopBBMissions_DeliverShipName;
    	if (missionVariables.GalCopBBMissions_DeliverShipPersonality) this._deliverShipPersonality = missionVariables.GalCopBBMissions_DeliverShipPersonality;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillSaveGame = function () {
    	// save any data we currently have
    	if (this._deliverShip != null) {
    		missionVariables.GalCopBBMissions_DeliverShipKey = this._deliverShip.dataKey;
    		missionVariables.GalCopBBMissions_DeliverShipName = this._deliverShip.name;
    		missionVariables.GalCopBBMissions_DeliverShipPersonality = this._deliverShip.entityPersonality;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.systemWillPopulate = function () {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var list = gcm.$getListOfMissions(true, [40, 41, 42, 43, 46]);
    
    	if (list.length > 0) {
    		// loop through all active missions and see if any need to be set up for this system
    		for (var i = 0; i < list.length; i++) {
    			var item = list[i];
    
    			// *** type 40/41 - special delivery - cargo to ship at waypoint
    			if ((item.data.missionType === 40 || (item.data.missionType === 41 && item.data.stage === 1)) &&
    				item.destination === system.ID &&
    				item.data.quantity === 0 &&
    				item.data.destroyedQuantity === 0 &&
    				item.expiry > clock.adjustedSeconds) {
    
    				this._setData.push({
    					missionType: 4041,
    					missionID: item.ID,
    					source: item.source,
    					goons: 0,
    					target: item.data.targetQuantity,
    					destroyed: 0,
    					quantity: 0
    				});
    				//var mypos = Vector3D(0, 0, -0.1).fromCoordinateSystem("wpu"); // on the other side of the planet from the witchpoint
    
    				system.setPopulator("gcm-delivery-" + item.ID, {
    					callback: function (pos) {
    						var g = worldScripts.GalCopBB_Missions;
    						var gcd = worldScripts.GalCopBB_Delivery;
    						var gns = worldScripts.GalCopBB_GoonSquads;
    						var missData = gcd.$getMissionData(4041);
    
    						// 3 possibilities:
    						// 1 - ship is there, ready to receive (types, 1,2,3,4,5,7,9,10,11,12,13,14,15)
    						// 2 - it's a trap! player is attacked by pirates (type 6)
    						// 3 - no ship at all - mission is a bust (type 8)
    						var type = parseInt(Math.random() * 15) + 1;
    						//var type = 1;
    						switch (type) {
    							case 1:
    							case 2:
    							case 3:
    							case 4:
    							case 5:
    							case 7:
    							case 9:
    							case 10:
    							case 11:
    							case 12:
    							case 13:
    							case 14:
    							case 15:
    								// create the ship, using saved specs if present
    								var key = (gcd._deliverShipDataKey != "" ? gcd._deliverShipDataKey : g.$getRandomShipKey(parseInt(missData.missionID), missData.target));
    								var nam = (gcd._deliverShipName != "" ? gcd._deliverShipName : g.$getRandomShipName(tgt, "trader"));
    								var tgt = null;
    								var checkShips = null;
    								for (var j = 1; j <= 5; j++) {
    									checkShips = system.addShips("[" + key + "]", 1, pos, player.ship.scannerRange * 0.75);
    									if (checkShips) tgt = checkShips[0];
    									if (tgt) break;
    								}
    								if (tgt) {
    									if (g._rsnInstalled) tgt.shipUniqueName = nam;
    									if (gcd._deliverShipPersonality != -1) tgt.entityPersonality = gcd._deliverShipPersonality;
    
    									tgt.bounty = 0;
    									// set script to default, to avoid a special script for the trader doing stuff. (like setting a new AI)
    									tgt.setScript("oolite-default-ship-script.js");
    									// now switch off the AI
    									tgt.switchAI("oolite-nullAI.js");
    									tgt.script._missionID = missData.missionID;
    
    									tgt.fuel = 7;
    									tgt.setCrew({
    										name: randomName() + " " + randomName(),
    										origin: missData.source,
    										seed: "0 0 0 0 " + missData.source + " 2"
    									});
    									// give the ship a unique role so we can look for it later
    									tgt.script._savedPrimaryRole = tgt.primaryRole;
    									tgt.primaryRole = "gcm_delivery_ship";
    									// make sure they have fuel scoops, otherwise they can't pick up the delivery
    									tgt.awardEquipment("EQ_FUEL_SCOOPS");
    									// make sure they have a destination system set that isn't our originating system
    									var sysSet = false;
    									var minGov = 4;
    									var pop = worldScripts["oolite-populator"];
    									do {
    										var sys = pop._nearbySafeSystem(minGov);
    										if (sys !== system.ID) {
    											tgt.destinationSystem = sys;
    											sysSet = true;
    										}
    										minGov -= 1;
    									} while (sysSet === false && minGov >= 0);
    
    									// remove any escorts that came with the ship
    									if (tgt.escorts && tgt.escorts.length > 0) {
    										for (var j = tgt.escorts.length - 1; j >= 0; j--) {
    											tgt.escorts[j].remove(true);
    										}
    									}
    									// monkey patch if necessary
    									// add our shipDied event to the ship
    									if (tgt.script.shipDied) tgt.script.$gcm_hold_shipDied = tgt.script.shipDied;
    									tgt.script.shipDied = gcd.$gcd_entity_shipDied;
    									// add our shipBeingAttacked event to the ship
    									if (tgt.script.shipBeingAttacked) tgt.script.$gcm_hold_shipBeingAttacked = tgt.script.shipBeingAttacked;
    									tgt.script.shipBeingAttacked = gcd.$gcd_contact_shipBeingAttacked;
    
    									if (tgt.script.shipScoopedOther) tgt.script.$gcm_hold_shipScoopedOther;
    									tgt.script.shipScoopedOther = gcd.$gcd_delivery_shipScoopedOther;
    									tgt.script._missionID = missData.missionID;
    
    									if (gcd._checkPlayerNearbyTimer == null || gcd._checkPlayerNearbyTimer.isRunning === false) {
    										gcd._checkPlayerNearbyTimer = new Timer(gcd, gcd.$isPlayerNearby, 5, 5);
    									}
    								} else {
    									log("galcobBB_delivery", "!!ERROR: Target ship not spawned!");
    								}
    								break;
    
    							case 6:
    								gns.$createGoonSquad(parseInt((Math.random() * 4) + 3), pos, player.ship.scannerRange * 0.5);
    								/*var grp = system.addGroup("pirate", parseInt((Math.random() * 4) + 3), pos, player.ship.scannerRange * 0.5);
    								var gn = grp.ships;
    								if (gn && gn.length > 0) {
    									//var grp = new ShipGroup;
    									for (var j = 0; j < gn.length; j++) {
    										// configure our pirates
    										var pop = worldScripts["oolite-populator"];
    										gn[j].setBounty(20 + system.info.government + missData.goons + Math.floor(Math.random() * 8), "setup actions");
    										// make sure the pilot has a bounty
    										gn[j].setCrew({name:randomName() + " " + randomName(), bounty:gn[j].bounty, insurance:0});
    										if (gn[j].hasHyperspaceMotor) {
    											pop._setWeapons(gn[j], 1.75); // bigger ones sometimes well-armed
    										} else {
    											pop._setWeapons(gn[j], 1.3); // rarely well-armed
    										}
    										// in the safer systems, rarely highly skilled (the skilled ones go elsewhere)
    										pop._setSkill(gn[j], 4 - system.info.government);
    										if (Math.random() * 16 < system.info.government) {
    											pop._setMissiles(gn[j], -1);
    										}
    										// make sure the AI is switched
    										gn[j].switchAI("gcm-pirateAI.js"); 
    									}
    									gns._goonSquads.push({group:grp, position:pos});
    								}*/
    								// monitor player's position - when in range of waypoint, set any penalty to 0 
    								if (gcd._checkPlayerNearbyTimer == null || gcd._checkPlayerNearbyTimer.isRunning === false) {
    									gcd._checkPlayerNearbyTimer = new Timer(gcd, gcd.$isPlayerNearWaypoint, 5, 5);
    								}
    								break;
    							case 8:
    								// monitor player's position - when in range of waypoint, set any penalty to 0 
    								if (gcd._checkPlayerNearbyTimer == null || gcd._checkPlayerNearbyTimer.isRunning === false) {
    									gcd._checkPlayerNearbyTimer = new Timer(gcd, gcd.$isPlayerNearWaypoint, 5, 5);
    								}
    								break;
    						}
    
    						// we'll set the waypoint up regardless of the random roll above, so the player won't know what the result is
    						g.$setWaypoint(pos, [0, 0, 0, 0], "M", "Meeting point", "meeting_" + missData.missionID);
    						gcd._waypointMissionID = missData.missionID;
    					}.bind(this),
    					location: "INNER_SYSTEM",
    					locationSeed: 0
    				});
    			}
    
    			// *** type 42 - special delivery - special computers to ship at random location
    			if (item.data.missionType === 42 &&
    				item.destination === system.ID &&
    				item.data.quantity === 0 &&
    				item.data.destroyedQuantity === 0 &&
    				item.expiry > clock.adjustedSeconds) {
    
    				// add the ship
    				var position = gcm.$getRandomPosition(item.data.locationType, 0, item.ID).position;
    
    				this._setData.push({
    					missionType: 42,
    					missionID: item.ID,
    					source: item.source,
    					goons: 0,
    					target: item.data.targetQuantity,
    					destroyed: 0,
    					quantity: 0
    				});
    
    				system.setPopulator("gcm-delivery-" + item.ID, {
    					callback: function (pos) {
    						// derelict ship creation code from Eric Walsh's DeepSpaceDredger OXP
    						var g = worldScripts.GalCopBB_Missions;
    						var gcd = worldScripts.GalCopBB_Delivery;
    						var missData = gcd.$getMissionData(42);
    						var tgt = null;
    						var checkShips = null;
    						for (var j = 1; j <= 5; j++) {
    							checkShips = system.addShips("[" + g.$getRandomShipKey(parseInt(missData.missionID), 1) + "]", 1, pos, player.ship.scannerRange * 0.75);
    							if (checkShips) tgt = checkShips[0];
    							if (tgt) break;
    						}
    						if (tgt) {
    							if (g._rsnInstalled) tgt.shipUniqueName = g.$getRandomShipName(tgt, "trader");
    
    							tgt.bounty = 0;
    							// make sure they have fuel scoops, otherwise they can't pick up the delivery
    							tgt.awardEquipment("EQ_FUEL_SCOOPS");
    							// set script to default, to avoid a special script for the trader doing stuff. (like setting a new AI)
    							tgt.setScript("oolite-default-ship-script.js");
    							// now switch off the AI
    							tgt.switchAI("oolite-nullAI.js");
    							tgt.script._missionID = missData.missionID;
    
    							tgt.fuel = 7;
    							tgt.setCrew({
    								name: randomName() + " " + randomName(),
    								origin: missData.source,
    								seed: "0 0 0 0 " + missData.source + " 2"
    							});
    							// give the ship a unique role so we can look for it later
    							tgt.script._savedPrimaryRole = tgt.primaryRole;
    							tgt.primaryRole = "gcm_delivery_ship";
    							tgt.awardEquipment("EQ_FUEL_SCOOPS");
    							// make sure they have a destination system set that isn't our originating system
    							var sysSet = false;
    							var minGov = 4;
    							var pop = worldScripts["oolite-populator"];
    							do {
    								var sys = pop._nearbySafeSystem(minGov);
    								if (sys !== system.ID) {
    									tgt.destinationSystem = sys;
    									sysSet = true;
    								}
    								minGov -= 1;
    							} while (sysSet === false && minGov >= 0);
    
    							// remove any escorts that came with the ship
    							if (tgt.escorts && tgt.escorts.length > 0) {
    								for (var j = tgt.escorts.length - 1; j >= 0; j--) {
    									tgt.escorts[j].remove(true);
    								}
    							}
    							// monkey patch if necessary
    							// add our shipDied event to the ship
    							if (tgt.script.shipDied) tgt.script.$gcm_hold_shipDied = tgt.script.shipDied;
    							tgt.script.shipDied = gcd.$gcd_entity_shipDied;
    							// add our shipBeingAttacked event to the ship
    							if (tgt.script.shipBeingAttacked) tgt.script.$gcm_hold_shipBeingAttacked = tgt.script.shipBeingAttacked;
    							tgt.script.shipBeingAttacked = gcd.$gcd_contact_shipBeingAttacked;
    
    							if (tgt.script.shipScoopedOther) tgt.script.$gcm_hold_shipScoopedOther;
    							tgt.script.shipScoopedOther = gcd.$gcd_delivery_shipScoopedOther;
    						} else {
    							log(this.name, "!!ERROR: Target ship not spawned!");
    						}
    
    					}.bind(this),
    					location: "COORDINATES",
    					coordinates: position
    				});
    				if (this._checkPlayerNearbyTimer == null || this._checkPlayerNearbyTimer.isRunning === false) {
    					this._checkPlayerNearbyTimer = new Timer(this, this.$isPlayerNearby, 5, 5);
    				}
    
    				if (gcm._debug) gcm.$setWaypoint(position, [0, 0, 0, 0], "D", "Debug position (42)", "42");
    			}
    
    			// *** type 43 - equipment for stricken ship
    			if (item.data.missionType === 43 &&
    				item.destination === system.ID &&
    				item.data.quantity === 0 &&
    				item.data.targetQuantity > 0 &&
    				item.data.destroyedQuantity === 0 &&
    				item.expiry > clock.adjustedSeconds) {
    
    				// add the ship
    				var position = gcm.$getRandomPosition(item.data.locationType, 0, item.ID).position;
    
    				this._setData.push({
    					missionType: 43,
    					missionID: item.ID,
    					source: item.source,
    					goons: 0,
    					target: 1,
    					destroyed: 0,
    					quantity: 0
    				});
    
    				system.setPopulator("gcm-stricken-" + item.ID, {
    					callback: function (pos) {
    						var g = worldScripts.GalCopBB_Missions;
    						var gcd = worldScripts.GalCopBB_Delivery;
    						var missData = gcd.$getMissionData(43);
    						var stricken = null;
    						var checkShips = null;
    						for (var j = 1; j <= 5; j++) {
    							checkShips = system.addShips("[" + g.$getRandomShipKey(parseInt(missData.missionID), 1) + "]", 1, pos, player.ship.scannerRange * 0.75);
    							if (checkShips) stricken = checkShips[0];
    							if (stricken) break;
    						}
    						if (stricken) {
    							if (g._rsnInstalled) stricken.shipUniqueName = g.$getRandomShipName(stricken, "trader");
    
    							stricken.bounty = 0;
    							// make sure they have fuel scoops, otherwise they can't get help from the player
    							stricken.awardEquipment("EQ_FUEL_SCOOPS");
    							// set script to default, to avoid a special script for the trader doing stuff. (like setting a new AI)
    							stricken.setScript("oolite-default-ship-script.js");
    							// now switch off the AI
    							stricken.switchAI("oolite-nullAI.js");
    							stricken.script._missionID = missData.missionID;
    							stricken.fuel = 7;
    							// make sure this ship is not going anywhere.
    							stricken.script._maxSpeed = stricken.maxSpeed;
    							stricken.maxSpeed = 0;
    							stricken.desiredSpeed = 0;
    
    							if (missData.target === 1) {
    								stricken.setCrew({
    									name: randomName() + " " + randomName(),
    									origin: missData.source,
    									seed: "0 0 0 0 " + missData.source + " 2"
    								});
    								// give the stricken ship a unique role so we can look for it later
    								stricken.script._savedPrimaryRole = stricken.primaryRole;
    								stricken.primaryRole = "gcm_stricken_ship";
    								stricken.script._sentDistress = false;
    								// add our shipBeingAttacked event to the stricken ship
    								if (stricken.script.shipBeingAttacked) stricken.script.$gcm_hold_shipBeingAttacked = stricken.script.shipBeingAttacked;
    								stricken.script.shipBeingAttacked = gcd.$gcd_stricken_shipBeingAttacked;
    							} else {
    								// create as a derelict
    								stricken.script.shipLaunchedEscapePod = worldScripts.GalCopBB_Derelict.$gcm_derelict_shipLaunchedEscapePod; // function to remove the escapepod after launch.
    								if (stricken.equipmentStatus("EQ_ESCAPE_POD") === "EQUIPMENT_UNAVAILABLE") stricken.awardEquipment("EQ_ESCAPE_POD");
    								stricken.abandonShip(); // make sure no pilot is left behind and this command turns the ship into cargo.
    								stricken.primaryRole = "gcm_derelict"; // to avoid pirate attacks
    								stricken.displayName = stricken.displayName + " (no life signs)";
    							}
    							// remove any escorts that came with the ship
    							if (stricken.escorts && stricken.escorts.length > 0) {
    								for (var j = stricken.escorts.length - 1; j >= 0; j--) {
    									stricken.escorts[j].remove(true);
    								}
    							}
    							// monkey patch if necessary
    							// add our shipDied event to the stricken ship
    							if (stricken.script.shipDied) stricken.script.$gcm_hold_shipDied = stricken.script.shipDied;
    							stricken.script.shipDied = gcd.$gcd_entity_shipDied;
    
    							if (missData.target === 1) {
    								worldScripts.GalCopBB_LifeSupport.$addShip({
    									ent: stricken,
    									type: "stranded ship",
    									remaining: Math.floor(Math.random() * 120 + worldScripts.GalCopBB_LifeSupport._lifeSupportDefaultTime),
    									last: 0,
    									comms: false
    								});
    							}
    						} else {
    							log(this.name, "!!ERROR: Stricken ship not spawned!");
    						}
    					}.bind(this),
    					location: "COORDINATES",
    					coordinates: position
    				});
    
    				if (gcm._debug) gcm.$setWaypoint(position, [0, 0, 0, 0], "D", "Debug position (43)", "43");
    
    			}
    
    			// *** type 46 - special delivery - cargo from/to waypoint stage 0 (collect)
    			if ((item.data.missionType === 46) &&
    				item.destination === system.ID &&
    				item.data.quantity < (item.data.targetQuantity - item.data.destroyedQuantity) &&
    				item.data.stage === 0 &&
    				item.expiry > clock.adjustedSeconds) {
    
    				this._setData.push({
    					missionType: 46,
    					missionID: item.ID,
    					source: item.source,
    					goons: 0,
    					quantity: (item.data.targetQuantity - item.data.destroyedQuantity) - item.data.quantity,
    					target: item.data.targetQuantity
    				});
    				/*gcm._cargoMonitor.push({
    					quantity: (item.data.targetQuantity - item.data.destroyedQuantity) - item.data.quantity,
    					missionID: item.ID
    				});*/
    
    				// add the cargo canisters
    				system.setPopulator("gcm-delivery-" + item.ID, {
    					callback: function (pos) {
    						var missData = worldScripts.GalCopBB_Delivery.$getMissionData(46);
    						var g = worldScripts.GalCopBB_Missions;
    
    						// only create the ones that haven't been collected
    						var pd = g._preferredCargoPods[Math.floor(Math.random() * g._preferredCargoPods.length)];
    						var cg = system.addShips("[" + pd + "]", missData.quantity, pos, 5000);
    						// if we couldn't create them with our preferred pod type, use the default
    						if (!cg) {
    							cg = system.addShips("[barrel]", missData.quantity, pos, 5000);
    						}
    						if (cg && cg.length === missData.quantity) {
    							var bb = worldScripts.BulletinBoardSystem;
    							var item = bb.$getItem(missData.missionID);
    							for (var j = 0; j < cg.length; j++) {
    								cg[j].switchAI("oolite-nullAI.js"); // dumbAI.plist
    								cg[j].setScript("oolite-default-ship-script.js");
    
    								item.data.expected += 1;
    								cg[j].setCargo(item.data.commodity, 1);
    								cg[j].script._missionID = missData.missionID;
    								cg[j].script._gcmSpecial = true;
    
    								// monkey patch if necessary
    								// add our shipDied event to the cargo
    								if (cg[j].script.shipDied) cg[j].script.$gcm_hold_shipDied = cg[j].script.shipDied;
    								cg[j].script.shipDied = g.$gcm_cargo_shipDied;
    
    								cg[j].primaryRole = "special_cargo";
    								cg[j].name = "Cargo container";
    							}
    							g.$setWaypoint(pos, [0, 0, 0, 0], "C", "Cargo dump location", "46");
    							// start a timer to watch what happens to our special cargo
    							/*if (g._cargoMonitorTimer == null || g._cargoMonitorTimer.isRunning === false) {
    								g._cargoMonitorTimer = new Timer(g, g.$checkSpecialCargo, 3, 3);
    							}*/
    							worldScripts.GalCopBB_CargoMonitor.$addMonitor(missData.missionID, item.data.commodity, system.ID, false, cg);
    						} else {
    							log(this.name, "!!ERROR: Cargo not spawned!");
    						}
    					}.bind(this),
    					location: "INNER_SYSTEM",
    					locationSeed: 0
    				});
    				if (Math.random() < parseFloat(expandDescription("[missionType46_assassinChance]"))) {
    					//log(this.name, "setting up ambush");
    					this._ambushTimer = new Timer(this, this.$setupAmbush, 10, 10);
    				}
    			}
    			// *** type 46 - special delivery - cargo from/to waypoint stage 1 (dump)
    			if ((item.data.missionType === 46) &&
    				item.destination === system.ID &&
    				item.data.quantity < (item.data.targetQuantity - item.data.destroyedQuantity) &&
    				item.data.stage === 1 &&
    				item.expiry > clock.adjustedSeconds) {
    
    				// add the waypoint
    				system.setPopulator("gcm-delivery-" + item.ID, {
    					callback: function (pos) {
    						var g = worldScripts.GalCopBB_Missions;
    						g.$setWaypoint(pos, [0, 0, 0, 0], "C", "Cargo dump location", "46");
    					}.bind(this),
    					location: "INNER_SYSTEM",
    					locationSeed: 0
    				});
    				this._monitorCargoMission = item.ID;
    				this._monitorCargoDump = new Timer(this, this.$monitorCargoDump, 5, 5);
    				if (Math.random() < parseFloat(expandDescription("[missionType46_assassinChance]"))) {
    					//log(this.name, "setting up ambush");
    					this._ambushTimer = new Timer(this, this.$setupAmbush, 10, 10);
    				}
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.systemWillRepopulate = function () {
    	function gcm_findstricken(entity) {
    		return (entity.primaryRole === "gcm_stricken_ship");
    	}
    
    	var gcm = worldScripts.GalCopBB_Missions;
    	var list = gcm.$getListOfMissions(true, 43);
    
    	// if expiry time for type 43 mission expires, convert the stranded ship into a derelict
    	for (var i = 0; i < list.length; i++) {
    		var item = list[i];
    		if (item.data.quantity !== 1 && item.expiry < clock.adjustedSeconds) {
    			// turn stranded ship into derelict
    			var ss = gcm_findstricken();
    			for (var j = 0; j < ss.length; j++) {
    				if (ss[j].script._missionID === item.ID) {
    					// set script to default, to avoid a special script for the trader doing stuff. (like setting a new AI)
    					ss[j].setScript("oolite-default-ship-script.js");
    					ss[j].switchAI("oolite-nullAI.js");
    
    					ss[j].script.shipLaunchedEscapePod = worldScripts.GalCopBB_Derelict.$gcm_derelict_shipLaunchedEscapePod; // function to remove the escapepod after launch.
    					if (ss[j].equipmentStatus("EQ_ESCAPE_POD") === "EQUIPMENT_UNAVAILABLE") ss[j].awardEquipment("EQ_ESCAPE_POD");
    					ss[j].abandonShip(); // make sure no pilot is left behind and this command turns the ship into cargo.
    					ss[j].primaryRole = "gcm_derelict";
    					ss[j].displayName = ss[j].displayName + " (derelict)";
    					item.data.targetQuantity = 0;
    					break;
    				}
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // set up interstellar space mission entities here
    this.interstellarSpaceWillPopulate = function () {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var list = gcm.$getListOfMissions(true, 44);
    	if (list.length > 0) {
    		// loop through all active missions and see if any need to be set up for this system
    		for (var i = 0; i < list.length; i++) {
    			var item = list[i];
    			// *** type 44 - equipment for stricken ship in interstellar space
    			if (((gcm._fromSystem === item.destination || gcm._fromSystem === item.source) &&
    					(gcm._toSystem === item.destination || gcm._toSystem === item.source)) &&
    				item.data.quantity === 0 &&
    				item.data.destroyedQuantity === 0 &&
    				item.expiry > clock.adjustedSeconds) {
    
    				// add the ship somewhere just inside scanner range
    				var dist = (Math.random() * (player.ship.scannerRange * 0.2)) + (player.ship.scannerRange * 0.75);
    				var dir = Vector3D.randomDirection(dist);
    				var position = Vector3D(0, 0, 0).add(dir);
    
    				this._setData.push({
    					missionType: 44,
    					missionID: item.ID,
    					source: item.source,
    					target: item.data.targetQuantity,
    					goons: 0,
    					quantity: 0
    				});
    
    				// add pod with populator
    				system.setPopulator("gcm-stricken-" + item.ID, {
    					callback: function (pos) {
    						var missData = worldScripts.GalCopBB_Delivery.$getMissionData(44);
    						var g = worldScripts.GalCopBB_Missions;
    
    						// derelict ship creation code from Eric Walsh's DeepSpaceDredger OXP
    						var stricken = null;
    						var checkShips = null;
    						for (var j = 1; j <= 5; j++) {
    							checkShips = system.addShips("[" + g.$getRandomShipKey(missData.missionID, 10) + "]", 1, pos, 1);
    							if (checkShips) stricken = checkShips[0];
    							if (stricken) break;
    						}
    						if (stricken) {
    							if (g._rsnInstalled) stricken.shipUniqueName = g.$getRandomShipName(stricken, "trader");
    
    							stricken.setCrew({
    								name: randomName() + " " + randomName(),
    								origin: missData.source,
    								seed: "0 0 0 0 " + missData.source + " 2"
    							});
    
    							// make sure they have fuel scoops, otherwise they can't get help from the player
    							stricken.awardEquipment("EQ_FUEL_SCOOPS");
    							// set script to default, to avoid a special script for the trader doing stuff. (like setting a new AI)
    							stricken.setScript("oolite-default-ship-script.js");
    							// now switch off the AI
    							stricken.switchAI("oolite-nullAI.js");
    							stricken.script._missionID = missData.missionID;
    							stricken.script._maxSpeed = stricken.maxSpeed;
    							stricken.maxSpeed = 0;
    							stricken.desiredSpeed = 0;
    
    							// remove any escorts that came with the ship
    							if (stricken.escorts && stricken.escorts.length > 0) {
    								for (var j = stricken.escorts.length - 1; j >= 0; j--) {
    									stricken.escorts[j].remove(true);
    								}
    							}
    
    							if (missData.target === 1) {
    								stricken.setCrew({
    									name: randomName() + " " + randomName(),
    									origin: missData.source,
    									seed: "0 0 0 0 " + missData.source + " 2"
    								});
    								// give the stricken ship a unique role so we can look for it later
    								stricken.script._savedPrimaryRole = stricken.primaryRole;
    								stricken.primaryRole = "gcm_stricken_ship";
    								// "disable" their engines
    								stricken.script._sentDistress = false;
    								// configure the life support countdown for this ship
    								worldScripts.GalCopBB_LifeSupport.$addShip({
    									ent: stricken,
    									type: "stranded ship",
    									remaining: Math.floor(Math.random() * 120 + worldScripts.GalCopBB_LifeSupport._lifeSupportDefaultTime / 2),
    									last: 0,
    									comms: false
    								});
    							} else {
    								// create as a derelict
    								stricken.script.shipLaunchedEscapePod = worldScripts.GalCopBB_Derelict.$gcm_derelict_shipLaunchedEscapePod; // function to remove the escapepod after launch.
    								if (stricken.equipmentStatus("EQ_ESCAPE_POD") === "EQUIPMENT_UNAVAILABLE") stricken.awardEquipment("EQ_ESCAPE_POD");
    								stricken.abandonShip(); // make sure no pilot is left behind and this command turns the ship into cargo.
    								stricken.primaryRole = "gcm_derelict"; // to avoid pirate attacks
    								stricken.displayName = stricken.displayName + " (no life signs)";
    							}
    
    							// monkey patch if necessary
    							// add our shipDied event to the stricken ship
    							if (stricken.script.shipDied) stricken.script.$gcm_hold_shipDied = stricken.script.shipDied;
    							stricken.script.shipDied = worldScripts.GalCopBB_Delivery.$gcd_entity_shipDied;
    
    						} else {
    							log(this.name, "!!ERROR: Stricken ship not spawned!");
    						}
    					}.bind(this),
    					location: "COORDINATES",
    					coordinates: position
    				});
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillEnterWitchspace = function () {
    	this._attackers.length = 0;
    	this._monitorCargo.length = 0;
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDockedWithStation = function (station) {
    	if (this._simulator === true) {
    		this._simulator = false;
    		return;
    	}
    	// check for other mission types being completed
    	var gcm = worldScripts.GalCopBB_Missions;
    	var list = gcm.$getListOfMissions(true, 42);
    	for (var i = 0; i < list.length; i++) {
    		var item = list[i];
    		if (station.allegiance === "galcop" && item.data.quantity === 0 && item.data.destroyedQuantity === 0) {
    			// fail - we docked before delivery
    			item.data.destroyedQuantity = 1;
    			// remove the special computer payload
    			player.ship.manifest["computers"] -= 1;
    			player.addMessageToArrivalReport(expandDescription("[missionType42_arrivalReport_fail]"));
    			// remove the delivery ship so we can't just turn up with any old computers
    			// another option would be to leave the ship there and have them attack the player or possibly use strong language
    			// if the player tried to give them any old computers.
    			if (system.countShipsWithPrimaryRole("gcm_delivery_ship") !== 0) {
    				var dvy = system.shipsWithPrimaryRole("gcm_delivery_ship");
    				for (var j = 0; j < dvy.length; j++) {
    					if (dvy[j].script._missionID === item.ID) {
    						dvy[j].remove(true);
    						break;
    					}
    				}
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipLaunchedFromStation = function (station) {
    	if (worldScripts.GalCopBB_Missions.$simulatorRunning() === true) this._simulator = true;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    /*this.shipSpawned = function (ship) {
    	var p = player.ship;
    	// did the player just dump cargo?
    	if (p && ship && ship.isCargo && ship.hasRole("EQ_CARGO_SHEPHERD_MINE") === false && ship.position && p.position && p.position.distanceTo(ship) < 300) {
    		this.$checkForStrickenShip(ship)
    		this.$checkForSpecialDeliveryShip(ship);
    		this.$checkForCargoDump(ship);
    	}
    }*/
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDumpedCargo = function (cargo) {
    	this.$checkForStrickenShip(cargo)
    	this.$checkForSpecialDeliveryShip(cargo);
    	this.$checkForCargoDump(cargo);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipScoopedOther = function (cargo) {
    	/*log(this.name, "got here - shipScoopedOther - cargo = " + cargo);
    	if (cargo.script) {
    		log(this.name, "_delivery = " + cargo.script._delivery);
    		log(this.name, "_missionID = " + cargo.script._missionID);
    		log(this.name, "_checkMissionID = " + cargo.script._checkMissionID);
        	log(this.name, "has shipWasDumped = " + cargo.script.shipWasDumped);
    	}*/
    	// clean up any properties we might have added
    	if (cargo.script && cargo.script._delivery) {
    		if (cargo.script.$gcm_hold_shipDied) {
    			cargo.script.shipDied = cargo.script.$gcm_hold_shipDied;
    			delete cargo.script.$gcm_hold_shipDied;
    		}
    		if (cargo.script._missionID) delete cargo.script._missionID;
    	}
    	if (cargo.script && cargo.script._checkMissionID) {
    		// we just re-scooped a mission-related cargo item
    		// undo the mission status
    		var gcm = worldScripts.GalCopBB_Missions;
    		var bb = worldScripts.BulletinBoardSystem;
    		var item = bb.$getItem(cargo.script._checkMissionID);
    		// don't reduce the quantity to less than zero
    		if (item.data.quantity > 0) {
    			item.data.quantity -= 1;
    			bb.$updateBBMissionPercentage(item.ID, (item.data.quantity / (item.data.targetQuantity - item.data.destroyedQuantity)));
    			gcm.$updateManifestEntry(cargo.script._checkMissionID);
    			gcm.$logMissionData(item.ID);
    			player.consoleMessage(expandDescription("[goal_updated]"));
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = function (whom, why) {
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$stopTimers = function $stopTimers() {
    	if (this._checkPlayerNearbyTimer && this._checkPlayerNearbyTimer.isRunning) this._checkPlayerNearbyTimer.stop();
    	delete this._checkPlayerNearbyTimer;
    	if (this._delayToReceiveFixTimer && this._delayToReceiveFixTimer.isRunning) this._delayToReceiveFixTimer.stop();
    	delete this._delayToReceiveFixTimer;
    	if (this._strickenFixTimer && this._strickenFixTimer.isRunning) this._strickenFixTimer.stop();
    	delete this._strickenFixTimer;
    	if (this._deliverScoopTimer && this._deliverScoopTimer.isRunning) this._deliverScoopTimer.stop();
    	delete this._deliverScoopTimer;
    	if (this._strickenScoopTimer && this._strickenScoopTimer.isRunning) this._strickenScoopTimer.stop();
    	delete this._strickenScoopTimer
    	if (this._badCargoTimer && this._badCargoTimer.isRunning) this._badCargoTimer.stop();
    	delete this._badCargoTimer;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptPendingMission = function $acceptPendingMission(item) {
    	// eject some computers from the contact
    	var target = worldScripts.GalCopBB_Missions_MFD._target;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var pd = gcm._preferredCargoPods[Math.floor(Math.random() * gcm._preferredCargoPods.length)];
    	var barrel = target.ejectItem("[" + pd + "]");
    	barrel.setCargo("computers", 1);
    
    	// attach the default ship script to the barrel
    	barrel.setScript("oolite-default-ship-script.js");
    	// attach our shipDied script, just in case the player messes up the scoop
    	if (barrel.script.shipDied && barrel.script.$gcm_hold_shipDied == null) {
    		barrel.script.$gcm_hold_shipDied = barrel.script.shipDied;
    		barrel.script.shipDied = this.$gcd_cargo_shipDied;
    	}
    	barrel.script._specialisedComputers = 1;
    	barrel.script._missionID = item.ID;
    
    	// notify the player
    	target.commsMessage("Here's the computers. And remember - don't dock at a GalCop station with them.", player.ship);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$checkForSpecialDeliveryShip = function $checkForSpecialDeliveryShip(cargo) {
    	// get all the delivery ships in range - there should only ever be one
    	if (this._deliverShip == null) {
    		var targets = system.shipsWithPrimaryRole("gcm_delivery_ship", cargo, player.ship.scannerRange);
    		if (targets && targets.length > 0) {
    			this._deliverShip = targets[0];
    			var bcc = worldScripts.BroadcastCommsMFD;
    			if (bcc.$checkMessageExists("gcm_player_arrived_cargo") === true) bcc.$removeMessage("gcm_player_arrived_cargo");
    		}
    	}
    
    	var gcm = worldScripts.GalCopBB_Missions;
    	var bb = worldScripts.BulletinBoardSystem;
    	if (this._deliverShip) {
    		var item = bb.$getItem(this._deliverShip.script._missionID);
    
    		// add the cargo to the list even if it's the wrong type
    		// the target won't know what's inside until they scoop
    		if ((item.data.missionType === 40 || item.data.missionType === 41 && item.data.targetQuantity > 1) &&
    			//cargo.commodity === item.data.commodity && 
    			item.destination === system.ID &&
    			item.data.targetQuantity > 0 &&
    			item.data.quantity < item.data.targetQuantity) {
    
    			if (cargo.script.shipDied && cargo.script.$gcm_hold_shipDied == null) {
    				cargo.script.$gcm_hold_shipDied = cargo.script.shipDied;
    				cargo.script.shipDied = this.$gcd_cargo_shipDied;
    			}
    			cargo.script._missionID = item.ID;
    			cargo.script._delivery = true;
    
    			this._deliverCargo.push(cargo);
    
    			// check to see if the ship has switched back to dumb mode because it's waiting for more cargo.
    			if (!this._deliverShip.AIScript || this._deliverShip.AIScript.name != "GCM Scavenger AI") {
    				// target the new cargo barrel
    				this._deliverShip.target = cargo;
    				// switch to the collect loot AI so the ship will scoop the barrel
    				this._deliverShip.switchAI("gcm-scavengerAI.js");
    			}
    		}
    
    		if (item.data.missionType === 42 &&
    			cargo.commodity === item.data.commodity &&
    			item.destination === system.ID &&
    			item.data.targetQuantity === 1) {
    
    			if (cargo.script.shipDied && cargo.script.$gcm_hold_shipDied == null) {
    				cargo.script.$gcm_hold_shipDied = cargo.script.shipDied;
    				cargo.script.shipDied = this.$gcd_cargo_shipDied;
    			}
    			cargo.script._missionID = item.ID;
    			// reset the primary role and ship AI
    			this._deliverShip.primaryRole = "trader";
    			// now target the cargo barrel
    			this._deliverShip.target = cargo;
    			// switch to the collect loot AI so the ship will scoop the barrel
    			this._deliverShip.switchAI("gcm-scavengerAI.js");
    		}
    	} else {
    		// no one there -- check if there is a mission type 42 active
    		if (cargo.commodity === "computers") {
    			var missID = gcm.$getActiveMissionIDByType(42);
    			if (missID !== -1) {
    				if (cargo.script.shipDied && cargo.script.$gcm_hold_shipDied == null) {
    					cargo.script.$gcm_hold_shipDied = cargo.script.shipDied;
    					cargo.script.shipDied = this.$gcd_cargo_shipDied;
    				}
    				if (cargo.script.shipWasDumped && cargo.script.$gcm_hold_shipWasDumped == null) {
    					cargo.script.$gcm_hold_shipWasDumped = cargo.script.shipWasDumped;
    					cargo.script.shipWasDumped = this.$gcd_cargo_shipWasDumped;
    				}
    				cargo.script._missionID = missID;
    				cargo.script._delivery = true;
    				cargo.script._specialisedComputers = 1;
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // looks for any stricken ships within 5km of cargo that was just ejected
    this.$checkForStrickenShip = function $checkForStrickenShip(cargo) {
    	// if we didn't dump machinery, there's no point in checking further
    	// but tell the player about it
    	if (cargo.commodity !== "machinery") {
    		if (system.shipsWithPrimaryRole("gcm_stricken_ship", player.ship, player.ship.scannerRange).length > 0) {
    			this._badCargoTimer = new Timer(this, this.$tellPlayerAboutCargo, 5, 0);
    		}
    		return;
    	}
    	if (this._badCargoTimer && this._badCargoTimer.isRunning) this._badCargoTimer.stop();
    
    	var gcm = worldScripts.GalCopBB_Missions;
    	var targets = system.shipsWithPrimaryRole("gcm_stricken_ship", cargo, 10000);
    	if (targets && targets.length > 0) {
    		var list = gcm.$getListOfMissions(true, [43, 44]);
    		for (var i = 0; i < list.length; i++) {
    			var item = list[i];
    			var check = false;
    			if (item.data.missionType === 43 && item.destination === system.ID && item.data.targetQuantity === 1) check = true;
    			if (item.data.missionType === 44 &&
    				((gcm._fromSystem === item.destination || gcm._fromSystem === item.source) &&
    					(gcm._toSystem === item.destination || gcm._toSystem === item.source))) check = true;
    
    			if (check) {
    				cargo.script.shipDied = this.$gcd_cargo_shipDied;
    				this._strickenShip = targets[0];
    				this._fixCargo = cargo;
    				if (this._delayToReceiveFixTimer && this._delayToReceiveFixTimer.isRunning) this._delayToReceiveFixTimer.stop();
    				this._delayToReceiveFixTimer = new Timer(this, this.$fixStrickenShipsInitiate, 5, 2);
    				break;
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // timer destination after player dumps machinery within range of stricken ship
    this.$fixStrickenShipsInitiate = function $fixStrickenShipsInitiate() {
    	if (this._strickenShip.position.distanceTo(this._fixCargo) < 1000) {
    		this._delayToReceiveFixTimer.stop();
    		// scoop cargo, delay, then switch AI
    		if (this._fixCargo && this._fixCargo.isInSpace) {
    			if (this._strickenScoopTimer && this._strickenScoopTimer.isRunning) this._strickenScoopTimer.stop();
    			this._strickenScoopTimer = new Timer(this, this.$scoopCargo, 0.5, 0);
    		}
    	} else {
    		if (this._strickenShip.position.distanceTo(this._fixCargo) > 20000) {
    			this._delayToReceiveFixTimer.stop();
    			player.consoleMessage("Repair equipment out of range of stranded ship");
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$scoopCargo = function $scoopCargo() {
    	if (this._fixCargo && this._fixCargo.isInSpace) {
    		// check to make sure the cargo didn't get away from the stricken ship
    		if (this._strickenShip.position.distanceTo(this._fixCargo) > 1000) {
    			// tell the player, and revert back to waiting for the cargo to get close
    			this._strickenShip.commsMessage(expandDescription("[gcm_stricken_cantscoop]"), player.ship);
    			if (this._delayToReceiveFixTimer && this._delayToReceiveFixTimer.isRunning) this._delayToReceiveFixTimer.stop();
    			this._delayToReceiveFixTimer = new Timer(this, this.$fixStrickenShipsInitiate, 5, 2);
    		} else {
    			// simulate a tractor beam by moving cargo towards stricken ship
    			this._fixCargo.velocity = this._strickenShip.position.subtract(this._fixCargo.position).direction().multiply(20).add(this._fixCargo.velocity);
    			if (this._strickenShip.position.distanceTo(this._fixCargo) < 100) {
    				this._fixCargo.remove(true);
    				//this._strickenShip.target = this._fixCargo;
    				//this._strickenShip.performCollect();
    				this._strickenShip.script._fixed = true;
    				if (this._strickenFixTimer == null || this._strickenFixTimer.isRunning === false) {
    					this._strickenFixTimer = new Timer(this, this.$fixStrickenShips, 10, 0);
    				}
    			} else {
    				if (this._strickenScoopTimer && this._strickenScoopTimer.isRunning) this._strickenScoopTimer.stop();
    				this._strickenScoopTimer = new Timer(this, this.$scoopCargo, 0.5, 0);
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // timer destination for "repairing" stricken ships
    this.$fixStrickenShips = function $fixStrickenShips() {
    	var targets = system.shipsWithPrimaryRole("gcm_stricken_ship");
    	var bb = worldScripts.BulletinBoardSystem;
    	var gcm = worldScripts.GalCopBB_Missions;
    	if (targets && targets.length > 0) {
    		for (var i = 0; i < targets.length; i++) {
    			if (targets[i].script._fixed === true) {
    
    				targets[i].primaryRole = targets[i].script._savedPrimaryRole;
    				targets[i].maxSpeed = targets[i].script._maxSpeed;
    				targets[i].switchAI("oolite-traderAI.js");
    				targets[i].commsMessage(expandDescription("[thanks_for_help]"), player.ship);
    
    				var item = bb.$getItem(targets[i].script._missionID);
    
    				item.data.quantity = 1;
    				bb.$updateBBMissionPercentage(item.ID, 1);
    
    				gcm.$logMissionData(item.ID);
    				player.consoleMessage(expandDescription("[goal_updated]"));
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // timer destination for stricken ship detecting the wrong dumped cargo
    this.$tellPlayerAboutCargo = function $tellPlayerAboutCargo() {
    	var ships = system.shipsWithPrimaryRole("gcm_stricken_ship", player.ship, player.ship.scannerRange);
    	if (ships.length > 0) {
    		ships[0].commsMessage(expandDescription("[gcm_stricken_bad_cargo]"), player.ship);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcd_delivery_shipScoopedOther = function $gcd_delivery_shipScoopedOther(whom) {
    	if (this.ship.script.$gcm_hold_shipScoopedOther) this.ship.script.$gcm_hold_shipScoopedOther(whom);
    
    	if (whom.isCargo === false) return;
    
    	// update the mission
    	var gcm = worldScripts.GalCopBB_Missions;
    	var gcd = worldScripts.GalCopBB_Delivery;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(this.ship.script._missionID);
    
    	if (whom.commodity === item.data.commodity) {
    		if (item.data.quantity < item.data.targetQuantity) {
    			item.data.quantity += 1;
    			bb.$updateBBMissionPercentage(item.ID, item.data.quantity / item.data.targetQuantity);
    
    			if (item.data.quantity === item.data.targetQuantity) { //  + item.data.destroyedQuantity
    				// switch the AI to an outbound trader so they will hyperspace out
    				this.ship.switchAI("exitingTraderAI.plist");
    				this.ship.commsMessage(expandDescription("[gcm_scoop_success]"), player.ship);
    				// clean up any broadcast comms
    				var bcc = worldScripts.BroadcastCommsMFD;
    				if (bcc.$checkMessageExists("gcm_player_response_1") === true) bcc.$removeMessage("gcm_player_response_1");
    				if (bcc.$checkMessageExists("gcm_player_response_2") === true) bcc.$removeMessage("gcm_player_response_2");
    				// delay spawning loot for a couple of seconds to prevent it being rescooped by the originator
    				if (item.data.missionType === 40 || (item.data.missionType === 41 && item.data.stage === 1)) {
    					if (gcd._deliverScoopTimer && gcd._deliverScoopTimer.isRunning) {
    						gcd._deliverScoopTimer.stop();
    						delete gcd._deliverScoopTimer;
    					}
    					// clean up the array
    					for (var i = gcd._deliverCargo.length - 1; i >= 0; i--) {
    						if (gcd._deliverCargo[i].isValid === false || gcd._deliverCargo[i].status === "STATUS_IN_HOLD") {
    							gcd._deliverCargo.splice(i, 1);
    						}
    					}
    					// give the player some loot
    					this.ship.commsMessage(expandDescription("[gcm_scoop_reward]"), player.ship);
    					var opts = ["gold", "platinum", "gem_stones"];
    					var cmdty = opts[Math.floor(Math.random() * opts.length)];
    					var qty = parseInt(Math.random() * 30 + 10);
    					if (cmdty === "gem_stones") qty *= 2;
    
    					var pd = gcm._preferredCargoPods[Math.floor(Math.random() * gcm._preferredCargoPods.length)];
    					var loot = this.ship.ejectItem("[" + pd + "]");
    					loot.setCargo(cmdty, qty);
    					gcd._deliverShip = null;
    					// give the player a trader role
    					gcd.$addTraderRoleToPlayer(whom.commodity);
    					// update the stage for a type 41 mission
    					if (item.data.missionType === 41) item.data.stage += 1;
    				}
    			} else {
    				// is there any more of the cargo scoop?
    				var found = false;
    				for (var i = 0; i < gcd._deliverCargo.length; i++) {
    					if (gcd._deliverCargo[i].isValid && gcd._deliverCargo[i].status === "STATUS_BEING_SCOOPED") {
    						// we're already scooping this one!
    						found = true;
    						break;
    					}
    					if (gcd._deliverCargo[i].isValid && gcd._deliverCargo[i].status === "STATUS_IN_FLIGHT") {
    						found = true;
    						break;
    					}
    				}
    				if (found === false) {
    					// player didn't provide enough cargo... respond
    					// send player message
    					this.ship.commsMessage(expandDescription("[gcm_delivery_short]"), player.ship);
    					// turn off the loot ai
    					this.ship.switchAI("oolite-nullAI.js");
    					this.ship.desiredSpeed = 0;
    					// give player some response options.
    					var bcc = worldScripts.BroadcastCommsMFD;
    					if (bcc.$checkMessageExists("gcm_player_response_1") === false) {
    						bcc.$createMessage({
    							messageName: "gcm_player_response_1",
    							callbackFunction: gcd.$transmitResponseNoMore.bind(this),
    							displayText: "Say 'There is no more'",
    							messageText: expandDescription("[gcm_player_response_all_out]"),
    							ship: this.ship,
    							transmissionType: "target",
    							delayCallback: 5,
    							deleteOnTransmit: true,
    							hideOnConditionRed: true
    						});
    					}
    					if (bcc.$checkMessageExists("gcm_player_response_2") === false) {
    						bcc.$createMessage({
    							messageName: "gcm_player_response_2",
    							callbackFunction: gcd.$transmitResponseOops.bind(this),
    							displayText: "Say 'Sorry, ejecting more now'",
    							messageText: expandDescription("[gcm_player_response_sorry]"),
    							ship: this.ship,
    							transmissionType: "target",
    							delayCallback: 5,
    							deleteOnTransmit: true,
    							hideOnConditionRed: true
    						});
    					}
    				}
    			}
    		}
    	} else {
    		// did we just scoop the cargo we gave the player?
    		// eject it again
    		if (item.data.quantity + item.data.destroyedQuantity === item.data.targetQuantity) {
    			var pd = gcm._preferredCargoPods[Math.floor(Math.random() * gcm._preferredCargoPods.length)];
    			var loot = this.ship.ejectItem("[" + pd + "]");
    			loot.setCargo(whom.commodity, whom.commodityAmount);
    			return;
    		}
    		// not the right cargo... respond
    		// send player message
    		// check if there are other containers around and scoop them
    		var found = false;
    		if (gcd._deliverCargo.length > 0) {
    			for (var i = 0; i < gcd._deliverCargo.length; i++) {
    				if (gcd._deliverCargo[i].isValid && gcd._deliverCargo[i].status === "STATUS_BEING_SCOOPED") {
    					// we're already scooping this one!
    					this.ship.target = gcd._deliverCargo[i];
    					found = true;
    					break;
    				}
    				if (gcd._deliverCargo[i].isValid && gcd._deliverCargo[i].status === "STATUS_IN_FLIGHT") {
    					this.ship.switchAI("oolite-nullAI.js");
    					this.ship.switchAI("gcm-scavengerAI.js");
    					this.ship.target = gcd._deliverCargo[i];
    					found = true;
    					break;
    				}
    			}
    		}
    		var bcc = worldScripts.BroadcastCommsMFD;
    		// stop the ship from moving if there's no cargo to scoop atm
    		if (found === false) {
    			this.ship.switchAI("oolite-nullAI.js");
    			this.ship.desiredSpeed = 0;
    			if (bcc.$checkMessageExists("gcm_player_response_1") === false) {
    				bcc.$createMessage({
    					messageName: "gcm_player_response_1",
    					callbackFunction: gcd.$transmitResponseNoMore.bind(gcd),
    					displayText: "Say 'There is no more'",
    					messageText: expandDescription("[gcm_player_response_all_out]"),
    					ship: this.ship,
    					transmissionType: "target",
    					delayCallback: 5,
    					deleteOnTransmit: true,
    					hideOnConditionRed: true
    				});
    			}
    		} else {
    			if (bcc.$checkMessageExists("gcm_player_response_1") === true) bcc.$removeMessage("gcm_player_response_1");
    		}
    		this.ship.commsMessage(expandDescription("[gcm_delivery_wrong_cargo]", {
    			commodity: displayNameForCommodity(whom.commodity).toLowerCase()
    		}), player.ship);
    		// give player some response options.
    		if (bcc.$checkMessageExists("gcm_player_response_2") === false) {
    			bcc.$createMessage({
    				messageName: "gcm_player_response_2",
    				callbackFunction: gcd.$transmitResponseOops.bind(gcd),
    				displayText: "Say 'Sorry, ejecting more now'",
    				messageText: expandDescription("[gcm_player_response_sorry]"),
    				ship: this.ship,
    				transmissionType: "target",
    				delayCallback: 5,
    				deleteOnTransmit: true,
    				hideOnConditionRed: true
    			});
    		}
    		return;
    	}
    	gcm.$logMissionData(item.ID);
    	player.consoleMessage(expandDescription("[goal_updated]"));
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // attached to meeting ships, designed to handle what happens if the ship is attacked
    this.$gcd_contact_shipBeingAttacked = function $gcd_contact_shipBeingAttacked(whom) {
    	if (this.ship.script.$gcm_hold_shipBeingAttacked) this.ship.script.$gcm_hold_shipBeingAttacked(whom);
    
    	if (whom.isPlayer) {
    		this.ship.commsMessage(expandDescription("[gcm_contact_attacked]"), whom);
    	} else {
    		this.ship.commsMessage(expandDescription("[distress-call]"), player.ship);
    	}
    
    	var bb = worldScripts.BulletinBoardSystem;
    	var missID = this.ship.script._missionID;
    	if (missID > 0) {
    		var item = bb.$getItem(missID);
    		item.data.targetQuantity = 0;
    
    		if (this.ship.AIScript.name === "Null AI") this.ship.switchAI("oolite-traderAI.js");
    		this.ship.target = whom;
    
    		// turn off any pending responses
    		var g = worldScripts.GalCopBB_Missions_MFD;
    		if (g._subMessageTimer && g._subMessageTimer.isRunning) g._subMessageTimer.stop();
    		if (g._offerTimer && g._offerTimer.isRunning) g.$declineNewMission();
    	}
    	// get the ships response
    	this.ship.target = whom;
    	this.ship.performAttack();
    	this.ship.AIScript.oolite_priorityai.reconsiderNow();
    	// restore the original script (if there was one)
    	if (this.ship.script.$gcm_hold_shipBeingAttacked) {
    		this.ship.script.shipBeingAttacked = this.ship.script.$gcm_hold_shipBeingAttacked;
    		delete this.ship.script.$gcm_hold_shipBeingAttacked;
    	}
    	// remove this script so it doesn't keep running
    	delete this.ship.script.shipBeingAttacked;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcd_stricken_shipBeingAttacked = function $gcd_stricken_shipBeingAttacked(whom) {
    	if (this.ship.script.$gcm_hold_shipBeingAttacked) this.ship.script.$gcm_hold_shipBeingAttacked(whom);
    
    	if (worldScripts.GalCopBB_Missions._debug) log(this.name, "stricken ship being attacked by " + whom);
    
    	// send a distress call 
    	if (this.ship.script._sentUnderAttackMessage == null || this.ship.script._sentUnderAttackMessage === false) {
    		this.ship.script._sentUnderAttackMessage = true;
    		this.ship.broadcastDistressMessage();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcd_cargo_shipWasDumped = function $gcd_cargo_shipWasDumped(dumper) {
    	var gcd = worldScripts.GalCopBB_Delivery;
    	gcd.$checkForSpecialDeliveryShip(this.ship);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcd_cargo_shipWasDumped2 = function $gcd_cargo_shipWasDumped2(dumper) {
    	if (this.ship.script.$gcm_hold_shipWasDumped) this.ship.script.$gcm_hold_shipWasDumped(dumper);
    	worldScripts.GalCopBB_Delivery.$checkForCargoDump(this.ship);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcd_cargo_shipDied = function $gcd_cargo_shipDied(whom, why) {
    	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);
    	var gcm = worldScripts.GalCopBB_Missions;
    	var bb = worldScripts.BulletinBoardSystem;
    
    	if (gcm._debug) log(this.name, "!!OUCH! Special cargo destroyed - " + why + ", " + whom);
    
    	var item = bb.$getItem(this.ship.script._missionID);
    	if (item) {
    		item.data.destroyedQuantity += 1;
    		bb.$updateBBMissionPercentage(item.ID, (item.data.quantity / (item.data.targetQuantity - item.data.destroyedQuantity)));
    		player.consoleMessage(expandDescription("[goal_updated]"));
    		// reduce the payment amount by half
    		if (item.data.missionType === 43 || item.data.missionType == 44) {
    			item.payment *= 0.5;
    			// switch to the alt manifest entry
    			item.data.altManifest = true;
    			gcm.$updateManifestEntry(this.ship.script._missionID);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcd_cargo_shipDied2 = function $gcd_cargo_shipDied2(whom, why) {
    	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);
    	var gcm = worldScripts.GalCopBB_Missions;
    	var bb = worldScripts.BulletinBoardSystem;
    
    	if (gcm._debug) log(this.name, "!!OUCH! Special cargo destroyed - " + why + ", " + whom);
    
    	var item = bb.$getItem(this.ship.script._checkMissionID);
    	if (item) {
    		item.data.destroyedQuantity += 1;
    		bb.$updateBBMissionPercentage(item.ID, (item.data.quantity / (item.data.targetQuantity - item.data.destroyedQuantity)));
    		if (whom.isPlayer) item.payment = parseInt((item.payment * 0.9) * 10) / 10; // 10% penalty for loss cause by the player
    		gcm.$updateManifestEntry(cargo.script._checkMissionID);
    		gcm.$logMissionData(item.ID);
    		player.consoleMessage(expandDescription("[goal_updated]"));
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcd_entity_shipDied = function $gcd_entity_shipDied(whom, why) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var bb = worldScripts.BulletinBoardSystem;
    
    	if (gcm._debug) log(this.name, "running shipDied for " + this.ship + ": reason " + why + ", " + whom);
    	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);
    
    	var item = bb.$getItem(this.ship.script._missionID);
    	if (item) {
    		item.data.destroyedQuantity = 1;
    		// reduce the payment amount by half
    		if (item.data.missionType === 43 || item.data.missionType == 44) {
    			item.payment *= 0.5;
    			// switch to the alt manifest entry
    			item.data.altManifest = true;
    			gcm.$updateManifestEntry(this.ship.script._missionID);
    		}
    		if (item.data.missionType === 42) {
    			// mission failed - target ship destroyed
    			var rep = worldScripts.GalCopBB_Reputation;
    			bb.$updateBBManifestText(
    				item.ID,
    				rep.$transformText(expandDescription("[missionType" + item.data.missionType + "_failedManifest]", {
    					system: System.systemNameForID(item.source),
    					expiry: ""
    				}), item.source, item.destination)
    			);
    			bb.$updateBBStatusText(
    				item.ID,
    				rep.$transformText(expandDescription("[missionType" + item.data.missionType + "_failedStatus]", {
    					system: System.systemNameForID(item.source)
    				}), item.source, item.destination)
    			);
    		}
    		bb.$updateBBMissionPercentage(item.ID, (item.data.quantity / (item.data.targetQuantity - item.data.destroyedQuantity)));
    		gcm.$logMissionData(item.ID);
    		player.consoleMessage(expandDescription("[goal_updated]"));
    	}
    
    	if (gcm._distressMessageTimer && gcm._distressMessageTimer.isRunning) gcm._distressMessageTimer.stop();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$transmitResponseNoMore = function $transmitResponseNoMore() {
    	// player said there is no more
    	// terminate the mission, but give the player a token for how much they did provide.
    	var gcd = worldScripts.GalCopBB_Delivery;
    	if (gcd._deliverShip) {
    		var bb = worldScripts.BulletinBoardSystem
    		var item = bb.$getItem(gcd._deliverShip.script._missionID);
    		// switch the AI to an outbound trader so they will hyperspace out
    		gcd._deliverShip.switchAI("exitingTraderAI.plist");
    		gcd._deliverShip.commsMessage(expandDescription("[gcm_scoop_part_success]"), player.ship);
    		// clean up any broadcast comms
    		var bcc = worldScripts.BroadcastCommsMFD;
    		if (bcc.$checkMessageExists("gcm_player_response_1") === true) bcc.$removeMessage("gcm_player_response_1");
    		if (bcc.$checkMessageExists("gcm_player_response_2") === true) bcc.$removeMessage("gcm_player_response_2");
    		if (item.data.missionType === 40 || (item.data.missionType === 41 && item.data.targetQuantity > 1)) {
    			if (gcd._deliverScoopTimer && gcd._deliverScoopTimer.isRunning) {
    				gcd._deliverScoopTimer.stop();
    				delete gcd._deliverScoopTimer;
    			}
    			// clean up the array
    			for (var i = gcd._deliverCargo.length - 1; i >= 0; i--) {
    				if (gcd._deliverCargo[i].isValid === false || gcd._deliverCargo[i].status === "STATUS_IN_HOLD") {
    					gcd._deliverCargo.splice(i, 1);
    				}
    			}
    			// give the player some loot
    			var opts = ["gold", "platinum", "gem_stones"];
    			var cmdty = opts[Math.floor(Math.random() * opts.length)];
    			var pct = (item.data.quantity + item.data.destroyedQuantity) / item.data.targetQuantity;
    			var qty = parseInt((Math.random() * 30 + 10) * pct);
    			if (cmdty === "gem_stones") qty *= 2;
    			var gcm = worldScripts.GalCopBB_Missions;
    			var pd = gcm._preferredCargoPods[Math.floor(Math.random() * gcm._preferredCargoPods.length)];
    			var loot = gcd._deliverShip.ejectItem("[" + pd + "]");
    			loot.setCargo(cmdty, qty);
    		}
    		gcd._deliverShip = null;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$transmitResponseOops = function $transmitResponseOops() {
    	// player said 'oops'
    	// do nothing  - wait for player to eject more
    	// clean up any broadcast comms
    	var bcc = worldScripts.BroadcastCommsMFD;
    	if (bcc.$checkMessageExists("gcm_player_response_1") === true) bcc.$removeMessage("gcm_player_response_1");
    	if (bcc.$checkMessageExists("gcm_player_response_2") === true) bcc.$removeMessage("gcm_player_response_2");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptedMission = function $acceptedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if (this._debug) log(this.name, "accepted mission id = " + missID);
    
    	if (!item) {
    		log(this.name, "!!ERROR: BB returned null value from $getItem on mission ID " + missID);
    		return;
    	}
    	gcm.$updateLastMissionDate(item.source, item.data.missionType);
    	gcm.$updateGeneralSettings(item);
    
    	// give player cargo for special delivery mission, but only if they're not in space
    	// if they get here from a secondary mission given in space, they'll need to find their own cargo
    	if (item.data.missionType === 40) {
    		if (player.ship.isInSpace === false) {
    			for (var j = 0; j < item.data.targetQuantity; j++) {
    				// free up space if required
    				if (player.ship.cargoSpaceAvailable === 0 && player.ship.cargoSpaceCapacity >= item.data.targetQuantity) gcm.$freeCargoSpace(1, item.data.commodity);
    				// add the commodity to the player's ship
    				player.ship.manifest[item.data.commodity] += 1;
    				// reduce the quantity in the station market, if possible
    				if (player.ship.dockedStation.market[item.data.commodity].quantity > 0) {
    					player.ship.dockedStation.setMarketQuantity(item.data.commodity, player.ship.dockedStation.market[item.data.commodity].quantity - 1);
    				}
    			}
    		}
    	}
    
    	if (item.data.missionType === 41) {
    		player.ship.awardEquipment("EQ_GCM_COMMS_RELAY_SWITCH");
    		worldScripts.GalCopBB_DataCache._usageCount = 0;
    	}
    
    	// give player special computers for mission type 42
    	if (item.data.missionType === 42) {
    		if (player.ship.isInSpace === false) {
    			if (player.ship.cargoSpaceAvailable === 0 && player.ship.cargoSpaceCapacity >= 1) gcm.$freeCargoSpace(1, "computers");
    			player.ship.manifest["computers"] += 1;
    		}
    	}
    	// give player special machinery for mission type 43/44
    	if (item.data.missionType === 43 || item.data.missionType === 44) {
    		if (player.ship.isInSpace === false) {
    			// if the player is docked they can get the cargo straight away
    			if (player.ship.cargoSpaceAvailable === 0 && player.ship.cargoSpaceCapacity >= 1) gcm.$freeCargoSpace(1, "machinery");
    			player.ship.manifest["machinery"] += 1;
    		} else {
    			// otherwise flag it so they player will get it when they next dock
    			gcm._requestSpecialCargo = "machinery";
    			gcm._specialCargoTask[gcm._requestSpecialCargo] = "repair the stranded ship";
    		}
    	}
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$completedMission = function $completedMission(missID) {
    	var p = player.ship;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	worldScripts.GalCopBB_CargoMonitor.$removeMonitor(missID);
    
    	// update mission history
    	gcm.$updateSuccessHistoryReputation(item);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$confirmCompleted = function $confirmCompleted(missID) {
    	return "";
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$terminateMission = function $terminateMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// adjust reputation only when the terminatePenalty flag is set to true 
    	if (item.data.terminatePenalty === true) {
    		// update mission history
    		gcm.$updateFailedHistoryReputation(item);
    	}
    
    	worldScripts.GalCopBB_CargoMonitor.$removeMonitor(missID);
    
    	// *** type 43/44 - machinery for stranded ship
    	if (item.data.missionType === 43 || item.data.missionType === 44) {
    		if (player.ship.manifest["machinery"] > 0 && gcm._requestSpecialCargo === "") player.ship.manifest["machinery"] -= 1;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$failedMission = function $failedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var eq = "";
    
    	if (!sbm) {
    		// if there's no blackmarket option, just remove the equipment
    		this.$terminateMission(missID);
    		return;
    	}
    
    	var item = bb.$getItem(missID);
    	// update mission history
    	gcm.$updateFailedHistoryReputation(item);
    	worldScripts.GalCopBB_CargoMonitor.$removeMonitor(missID);
    
    	if (eq != "") gcm._equipmentFromFailedMissions.push({
    		missionType: item.data.missionType,
    		source: item.source,
    		equip: eq,
    		date: clock.adjustedSeconds
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateManifestEntry = function $updateManifestEntry(missID) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	gcm.$updateManifestEntry(missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
    	return worldScripts.GalCopBB_Missions.$missionAvailability(missID, missType, origSysID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 40 - special cargo delivery to waypoint
    this.$missionType40_Values = function $missionType40_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	if (system.ID === -1) return null;
    	// we're leaving out food, textiles and the precious metals and gems
    	var c_types = ["luxuries", "computers", "furs", "liquor_wines", "alloys", "radioactives", "alien_items", "machinery", "narcotics", "firearms", "slaves"];
    	var result = {};
    	var tries = 0;
    	result["quantity"] = 0;
    	// make sure we don't create missions where the quantity of cargo is more than the player ship
    	var max = player.ship.cargoSpaceCapacity;
    	// also make sure that we don't ever allocate more than any of our target ships can handle
    	if (max > worldScripts.GalCopBB_Missions._maxCargoOfShips) max = worldScripts.GalCopBB_Missions._maxCargoOfShips;
    	// make sure we don't end up with something ridiculous, like 100. make the max possible 25
    	if (max > 25) max = 25;
    	// work out mission details - commodity and quantity
    	do {
    		result["commodity"] = c_types[Math.floor(Math.random() * c_types.length)];;
    		result["quantity"] = parseInt((Math.random() * (max - 3)) + 3);
    		// make sure we only set a quantity the local system can provide
    		if (system.mainStation.market[result.commodity].quantity < result.quantity) result.quantity = system.mainStation.market[result.commodity].quantity;
    		tries += 1;
    	} while ((result.quantity > max || result.quantity <= 0) && tries < 5);
    	// if we aborted the loop, just return null
    	if (tries >= 5 && (result.quantity > max || result.quantity === 0)) return null;
    	// otherwise, just set a token reward - player will also receive bonus from waiting ship
    	// make sure the deposit amount is included in the price so the BB knows how to display it
    	result["price"] = parseInt(((Math.random() * 100) + 50) * 10) / 10 + (result.quantity * (system.mainStation.market[result.commodity].price / 10)) +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(200) + // plus a possible bonus price, based on player score 
    		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 20); // plus a distance bonus
    	result["expiry"] = clock.adjustedSeconds + routeTime + 21600; // transit time + 6 hours to complete 
    	result["penalty"] = 0;
    	result["deposit"] = result.quantity * (system.mainStation.market[result.commodity].price / 10);
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 41 - data cache to new waypoint for delivery of cargo
    this.$missionType41_Values = function $missionType41_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	// make sure we don't create missions where the quantity of cargo is more than the player ship
    	var max = player.ship.cargoSpaceCapacity;
    	// also make sure that we don't ever allocate more than any of our target ships can handle
    	if (max > worldScripts.GalCopBB_Missions._maxCargoOfShips) max = worldScripts.GalCopBB_Missions._maxCargoOfShips;
    	// make sure we don't end up with something ridiculous, like 100. make the max possible 25
    	if (max > 25) max = 25;
    	// we must have at least 2t otherwise sequencing won't work
    	if (max < 2) return null;
    	// pick a location
    	result["locationType"] = Math.floor(Math.random() * this._maxLocations);
    	result["quantity"] = 1; // initial quantity set to 1
    	result["price"] = parseInt((parseInt(Math.random() * 40) + 20) / 10) * 10 + (7 - destSysInfo.government) * 20 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(200) // plus a possible bonus price, based on player score 
    		+
    		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 20); // plus a distance bonus
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price / 2);
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 41 (part 2) - data cache to new waypoint for delivery of cargo
    this.$missionType41Updated_Values = function $missionType41Updated_Values(c_relay, missID, secondary) {
    	if (system.ID === -1) return false;
    	// we'll be doing all the calcs here, as this mission is split into two
    
    	var completed = false;
    	var bb = worldScripts.BulletinBoardSystem;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var item = bb.$getItem(missID);
    
    	// get interstellar range for this mission
    	var rng = parseInt(expandDescription("[missionType41_destRange]"));
    	var sys = System.infoForSystem(galaxyNumber, system.ID).systemsInRange(rng);
    	for (var i = sys.length - 1; i >= 0; i--) {
    		// remove any systems that don't meet conditions
    		if (sys[i].systemID === item.source) {
    			// don't include the original system
    			sys.splice(i, 1);
    			continue;
    		} else {
    			if (gcm.$testMissionConditions(expandDescription("[missionType41_destConditions]"), sys[i].systemID, 41, system.ID, secondary) === false) {
    				sys.splice(i, 1);
    				continue;
    			} else if (sys[i].systemID === system.ID) {
    				// remove the current system if it's there
    				sys.splice(i, 1);
    				continue;
    			}
    		}
    		// make sure the trip distance is still inside our desired range
    		var rt = system.info.routeToSystem(sys[i]);
    		if (rt.distance > rng) {
    			sys.splice(i, 1);
    			continue;
    		}
    		// remove systems for any mission types of: 31
    		var list = gcm.$getListOfMissions(true, 31);
    		for (var j = 0; j < list.length; j++) {
    			if (list[j].destination == sys[i].systemID) {
    				sys.splice(i, 1);
    				continue;
    			}
    		}
    	}
    
    	// if we didn't find any possible destinations, continue
    	if (sys == null || sys.length === 0) {
    		if (this._debug) log(this.name, "no systems found to generate mission type " + selectedMissionType);
    		return false;
    	}
    
    	// get a random sequence of index values, so we don't end up getting the same planets coming up first
    	sys.sort(function (a, b) {
    		return Math.random() - 0.5;
    	});
    
    	var sysSelectType = expandDescription("[missionType41_systemSelectProcess]");
    	var l_start = 0;
    	var l_end = sys.length;
    	var choice = 0; // we'll pick it up in the loop
    
    	for (var i = l_start; i < l_end; i++) {
    		choice = i;
    
    		// get a systemInfo object of the destination system
    		var dest = sys[choice];
    		if (dest == null) continue; // in case this ever happens...
    
    		// get the route information from the current system to the destination system
    		var route = system.info.routeToSystem(dest);
    		if (route == null) continue; // if there's no route to the system
    
    		var rtime = 0;
    		var s_type = "0";
    		var completeType = expandDescription("[missionType41_completionType]");
    		if (completeType.indexOf("|") >= 0) {
    			// check the completion type for a change to the stopTime flag
    			s_type = completeType.split("|")[1];
    			completeType = completeType.split("|")[0];
    		}
    
    		// time is one way only
    		rtime = route.time * 3600;
    		// plus 30 minutes transit time in each system
    		rtime += route.route.length * gcm._transitTime;
    		// if we're adding missions after a witchspace jump, add the extra time now
    		rtime += gcm._initialTime;
    
    		// mission specific config
    		// we're leaving out food, textiles and the precious metals and gems
    		var c_types = ["luxuries", "computers", "furs", "liquor_wines", "alloys", "radioactives", "alien_items", "machinery", "narcotics", "firearms", "slaves"];
    		var result = {};
    		var tries = 0;
    		result["quantity"] = 0;
    		// make sure we don't create missions where the quantity of cargo is more than the player ship
    		var max = player.ship.cargoSpaceCapacity;
    		// also make sure that we don't ever allocate more than any of our target ships can handle
    		if (max > worldScripts.GalCopBB_Missions._maxCargoOfShips) max = worldScripts.GalCopBB_Missions._maxCargoOfShips;
    		// make sure we don't end up with something ridiculous, like 100. make the max possible 25
    		if (max > 25) max = 25;
    		// work out mission details - commodity and quantity
    		do {
    			result["commodity"] = c_types[Math.floor(Math.random() * c_types.length)];;
    			result["quantity"] = parseInt((Math.random() * (max - 3)) + 3);
    			// make sure we only set a quantity the local system can provide
    			if (system.mainStation.market[result.commodity].quantity < result.quantity) result.quantity = system.mainStation.market[result.commodity].quantity;
    			tries += 1;
    		} while ((result.quantity > max || result.quantity < 2) && tries < 10);
    		// if we aborted the loop, just return null
    		if (tries >= 10 && (result.quantity > max || result.quantity < 2)) continue;
    		result["expiry"] = clock.adjustedSeconds + rtime + (result.quantity * 1800) + 21600; // transit time + 30 mins per t + 6 hours to complete (cargo is sometimes hard to find...)
    		result["refund"] = result.quantity * (system.mainStation.market[result.commodity].price / 10);
    
    		// need to update:
    		bb.$removeChartMarker(missID);
    		//	destination
    		item.destination = dest.systemID;
    		//	expiry time
    		item.expiry = result.expiry;
    		//	targetQuantity
    		item.data.targetQuantity = result.quantity;
    		//	quantity
    		item.data.quantity = 0;
    		//	commodity
    		item.data.commodity = result.commodity;
    		//	percentage complete
    		bb.$updateBBMissionPercentage(missID, 0);
    		//	refund
    		//	this enables the player to be paid back for the cost of the merchandise
    		item.customDisplayItems.push({
    			heading: "Refund amount:",
    			value: formatCredits(result.refund, false, true)
    		});
    		item.data.refund = result.refund;
    		item.data.stage += 1;
    
    		bb.$addManifestEntry(missID);
    		gcm.$logMissionData(item.ID);
    		player.consoleMessage(expandDescription("[goal_updated]"));
    
    		var msg = "Updated destination: " + System.systemNameForID(dest.systemID) + "; Cargo: " + result.quantity + "t × " + displayNameForCommodity(result.commodity).toLowerCase() +
    			". Waypoint transferred to ship computer.";
    
    		c_relay.commsMessage(msg, player.ship, 10);
    		completed = true;
    		break;
    	}
    
    	return completed;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 42 - special delivery
    this.$missionType42_Values = function $missionType42_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = 1;
    	result["locationType"] = Math.floor(Math.random() * this._maxLocations);
    	result["commodity"] = "computers";
    	result["price"] = parseInt((parseInt(Math.random() * 50) + 100) / 10) * 10 + (7 - destSysInfo.government) * 50 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 10) + // plus a distance bonus
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price / 2);
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 43 - rescue stranded ship
    this.$missionType43_Values = function $missionType43_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	// pick a location
    	result["locationType"] = Math.floor(Math.random() * this._maxLocations);
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 50) + 100) / 10) * 10 + (7 - destSysInfo.government) * 50 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 10) + // plus a distance bonus
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price / 2);
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 44 - rescue stranded ship interstellar
    this.$missionType44_Values = function $missionType44_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 500) + 500) / 10) * 10 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 10) + // plus a distance bonus
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(1000); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price / 2);
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 46 - cargo pickup from waypoint/delivery to waypoint
    this.$missionType46_Values = function $missionType46_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	if (system.ID === -1) return null;
    	// we're leaving out food, textiles and the precious metals and gems
    	var c_types = ["luxuries", "computers", "furs", "liquor_wines", "alloys", "radioactives", "alien_items", "machinery", "narcotics", "firearms", "slaves"];
    	var result = {};
    	result["quantity"] = 0;
    	// make sure we don't create missions where the quantity of cargo is more than the player ship
    	var max = player.ship.cargoSpaceCapacity;
    	// make sure we don't end up with something ridiculous, like 100. make the max possible 25
    	if (max > 25) max = 25;
    	// work out mission details - commodity and quantity
    	result["commodity"] = c_types[Math.floor(Math.random() * c_types.length)];;
    	result["quantity"] = parseInt((Math.random() * (max - 3)) + 3);
    
    	var rng = parseInt(expandDescription("[missionType46_destRange]"))
    	var sys = destSysInfo.systemsInRange(rng);
    	var rt = null;
    	for (var i = sys.length - 1; i >= 0; i--) {
    		// remove any systems that don't meet conditions
    		if (sys[i].systemID === system.ID) {
    			// don't include the original system
    			sys.splice(i, 1);
    			continue;
    		}
    		// make sure the trip is possible
    		rt = system.info.routeToSystem(sys[i]);
    		if (!rt) {
    			sys.splice(i, 1);
    			continue;
    		}
    	}
    	if (sys.length == 0) return null;
    	// pick one of the remainder
    	var tgtSys = sys[Math.floor(Math.random() * sys.length)];
    	result["destinationA"] = tgtSys.systemID;
    	rt = destSysInfo.routeToSystem(tgtSys, "OPTIMIZED_BY_JUMPS");
    
    	// make sure the deposit amount is included in the price so the BB knows how to display it
    	result["price"] = parseInt(((Math.random() * 250) + 250) / 10) * 10 + (7 - destSysInfo.government) * 50 + (7 - tgtSys.government) * 50 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(200) + // plus a possible bonus price, based on player score 
    		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance + rt.distance, 20); // plus a distance bonus
    	result["expiry"] = clock.adjustedSeconds + routeTime + (rt.time * 3600) + 7200; // transit time + 2hrs to complete - would need more if mission required return to original station
    	result["penalty"] = 0;
    
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$addTraderRoleToPlayer = function $addTraderRoleToPlayer(cmdty) {
    	if (system.ID != -1) {
    		var typ = "trader";
    		if (system.mainStation.market[cmdty].legality_import > 0 || system.mainStation.market[cmdty].legality_export > 0) typ = "trader-smuggler";
    		player.setPlayerRole(typ);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // gets mission specific data for the populator routines
    // this works on a first in/first out basis - if there are multiple missions of the same type being populated, the mission specific data would
    // get pushed in to the setData array in order, and then this routine pulls that data out in the same order
    // that's the theory, anyway!
    this.$getMissionData = function $getMissionData(missionType) {
    	for (var i = 0; i < this._setData.length; i++) {
    		if (this._setData[i].missionType === missionType) {
    			var result = {
    				missionID: this._setData[i].missionID,
    				trueMissionType: (this._setData[i].trueMissionType ? this._setData[i].trueMissionType : missionType),
    				source: this._setData[i].source,
    				goons: this._setData[i].goons,
    				quantity: this._setData[i].quantity,
    				target: this._setData[i].target
    			};
    			this._setData.splice(i, 1);
    			return result;
    		}
    	}
    	return null;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$countDeliveryCargo = function $countDeliveryCargo() {
    	var result = 0;
    	for (var i = 0; i < this._deliverCargo.length; i++) {
    		if (this._deliverCargo[i] && this._deliverCargo[i].isValid && this._deliverCargo[i].status === "STATUS_IN_FLIGHT") result += 1;
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$isPlayerNearby = function $isPlayerNearby() {
    	// don't run this check if the player is busy
    	if (player.alertCondition === 3 || player.alertHostiles === true) return;
    	// get all powered ships in range
    	var ships = player.ship.checkScanner(true);
    	var missTypes = [40, 41, 42];
    	for (var i = 0; i < ships.length; i++) {
    		if (ships[i].script.hasOwnProperty("_missionID") === true && ships[i].hasRole("gcm_delivery_ship") === true) {
    			var item = worldScripts.BulletinBoardSystem.$getItem(ships[i].script._missionID);
    			if (item && missTypes.indexOf(item.data.missionType) >= 0) {
    				if (ships[i].script.hasOwnProperty("_greeted") === false) {
    					this._checkPlayerNearbyTimer.stop();
    					ships[i].script._greeted = true;
    					var bcc = worldScripts.BroadcastCommsMFD;
    					// transmit message
    					ships[i].commsMessage(expandDescription("[gcm_delivery_question]", {
    						commodity: displayNameForCommodity(item.data.commodity).toLowerCase()
    					}), player.ship);
    					if (bcc.$checkMessageExists("gcm_player_arrived_cargo") === false) {
    						bcc.$createMessage({
    							messageName: "gcm_player_arrived_cargo",
    							callbackFunction: this.$transmitNoResponse.bind(this),
    							displayText: "Greet ship",
    							messageText: expandDescription("[gcm_player_arrived_cargo]"),
    							ship: ships[i],
    							transmissionType: "target",
    							deleteOnTransmit: true,
    							hideOnConditionRed: true
    						});
    					}
    					bcc.$addShipToArray(ships[i], bcc._greeted);
    					bcc.$buildMessageList();
    				}
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // clears out the penalty amount from the mission if the mission has to be failed but it's not the player's fault
    this.$isPlayerNearWaypoint = function $isPlayerNearWaypoint() {
    	if (this._waypointMissionID === 0) {
    		this._checkPlayerNearbyTimer.stop();
    		return;
    	}
    	var wp = system.waypoints["meeting_" + this._waypointMissionID];
    	if (!wp) {
    		this._checkPlayerNearbyTimer.stop();
    		return;
    	}
    	if (player.ship.position.distanceTo(wp.position) < player.ship.scannerRange) {
    		var bb = worldScripts.BulletinBoardSystem;
    		var item = bb.$getItem(this._waypointMissionID);
    		// set the penalty to 0
    		item.penalty = 0;
    		// add some text so it doesn't look like a bug
    		bb.$updateBBStatusText(item.ID, "Ship did not arrive at waypoint. Return to main station to cancel mission.");
    		bb.$updateBBManifestText(item.ID, "Ship did not arrive at waypoint. Return to main station to cancel mission.");
    		item.manifestCallback = "";
    
    		// prevent this mission from having an impact on player reputation
    		item.data.terminatePenalty = false;
    		this._checkPlayerNearbyTimer.stop();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$transmitNoResponse = function $transmitNoResponse() {
    	// this function intentionally left blank
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$monitorCargoDump = function $monitorCargoDump() {
    	var mc = this._monitorCargo;
    	if (mc && mc.length > 0) {
    		var pos = this.$findWaypointPosition();
    		var bb = worldScripts.BulletinBoardSystem;
    		var gcm = worldScripts.GalCopBB_Missions;
    		var item = bb.$getItem(this._monitorCargoMission);
    		for (var i = 0; i < mc.length; i++) {
    			var cargo = mc[i];
    			if (cargo && cargo.isValid && cargo.isInSpace) {
    				if (cargo.position.distanceTo(pos) < 21000 && cargo.speed == 0) {
    					if (item.data.quantity < (item.data.targetQuantity - item.data.destroyedQuantity)) {
    						item.data.quantity += 1;
    						bb.$updateBBMissionPercentage(item.ID, (item.data.quantity / (item.data.targetQuantity - item.data.destroyedQuantity)));
    						gcm.$logMissionData(item.ID);
    						player.consoleMessage(expandDescription("[goal_updated]"));
    					}
    					if (item.data.quantity >= (item.data.targetQuantity - item.data.destroyedQuantity)) {
    						this._monitorCargoDump.stop();
    					}
    					// add some scripts to help monitor the status of these cargo pods
    					// we'll be checking if the pod is destroyed, or if it's scooped by the player
    					log(this.name, "adding script events " + cargo);
    					cargo.script._checkMissionID = this._monitorCargoMission;
    					if (cargo.script.$gcm_hold_shipDied) {
    						cargo.script.shipDied = cargo.script.$gcm_hold_shipDied;
    						delete cargo.script.$gcm_hold_shipDied;
    					}
    					if (cargo.script.shipDied) cargo.script.$gcm_hold_shipDied = cargo.script.shipDied;
    					cargo.script.shipDied = this.$gcd_cargo_shipDied2;
    
    					if (cargo.script.$gcm_hold_shipWasDumped) {
    						cargo.script.shipWasDumped = cargo.script.$gcm_hold_shipWasDumped;
    						delete cargo.script.$gcm_hold_shipWasDumped;
    					}
    					if (cargo.script.shipWasDumped) cargo.script.$gcm_hold_shipWasDumped = cargo.script.shipWasDumped;
    					cargo.script.shipWasDumped = this.$gcd_cargo_shipWasDumped2;
    					// take it out of the monitoring array
    					mc[i] = null;
    				}
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$checkForCargoDump = function $checkForCargoDump(cargoPod) {
    	if (this._monitorCargoMission > 0) {
    		var bb = worldScripts.BulletinBoardSystem;
    		var item = bb.$getItem(this._monitorCargoMission);
    		// only add it to the monitor list if it's the right commodity type
    		if (item.data.commodity === cargoPod.commodity) {
    			var pos = this.$findWaypointPosition();
    			if (cargoPod.position.distanceTo(pos) < 21000) this._monitorCargo.push(cargoPod);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$findWaypointPosition = function $findWaypointPosition() {
    	var wps = system.waypoints;
    	var keys = Object.keys(wps);
    	if (wps && keys.length > 0) {
    		for (var i = 0; i < keys.length; i++) {
    			if (wps[keys[i]].beaconLabel = "Cargo dump location") return wps[keys[i]].position;
    		}
    	}
    	return null;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // makes the attackers attack a particular target
    this.$giveAttackersTarget = function $giveAttackersTarget() {
    	var retry = false;
    	var pos = this.$findWaypointPosition();
    	if (!pos) return;
    	for (var i = 0; i < this._attackers.length; i++) {
    		var shp = this._attackers[i];
    		if (shp && shp.isValid && shp.isInSpace) {
    			if (shp.AIScript && shp.AIScript.oolite_priorityai) {
    				if (shp.script._configDone === false) {
    					shp.AIScript.oolite_priorityai.setParameter("oolite_pirateLurk", pos);
    					shp.AIScript.oolite_priorityai.setParameter("oolite_attackTarget", player.ship);
    					shp.AIScript.oolite_priorityai.configurationResetWaypoint();
    					shp.AIScript.oolite_priorityai.reconsiderNow();
    					shp.script._configDone = true;
    				}
    			} else {
    				retry = true;
    				break;
    			}
    		}
    		if (retry === true) break;
    	}
    	if (this._setupAttackerTarget.isRunning === true) this._setupAttackerTarget.stop();
    	if (retry === true) {
    		this._setupAttackerTarget = new Timer(this, this.$giveAttackersTarget, 1, 0);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$setupAmbush = function $setupAmbush() {
    	var pos = this.$findWaypointPosition();
    	if (!pos) {
    		this._ambushTimer.stop();
    		return
    	}
    	if (player.ship.position.distanceTo(pos) < player.ship.scannerRange) {
    		this._ambushTimer.stop();
    		this.$createAmbush();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$createAmbush = function $createAmbush() {
    	var gcm = worldScripts.GalCopBB_Missions;
    	// get the initial number of ships to spawn from the config
    	var pop = worldScripts["oolite-populator"];
    	var pos = gcm.$findPosition(player.ship.position);
    	var types = ["pirate-medium-fighter","pirate-medium-fighter","pirate-heavy-fighter"];
    	var pirates = system.addShips(types[Math.floor(Math.random() * types.length)], Math.floor(Math.random() * 4) + 3, pos, 800);
    	// make sure all the attackers are equipped, have names, and bounties
    	for (var i = 0; i < pirates.length; i++) {
    		var pr = pirates[i];
    		if (gcm._rsnInstalled) pr.shipUniqueName = gcm.$getRandomShipName(pr, "pirate");
    		pr.setScript("oolite-default-ship-script.js");
    		pr.script._configDone = false;
    		// configure our attackers
    		pr.setBounty(60 + system.government + Math.floor(Math.random() * 8), "setup actions");
    		pr.setCrew({
    			name: randomName() + " " + randomName(),
    			bounty: pr.bounty,
    			insurance: 0
    		});
    		if (pr.hasHyperspaceMotor) {
    			pop._setWeapons(pr, 1.75); // bigger ones sometimes well-armed
    		} else {
    			pop._setWeapons(pr, 1.3); // rarely well-armed
    		}
    		pop._setSkill(pr, 4 - system.info.government);
    		if (Math.random() * 16 < system.info.government) pop._setMissiles(pr, -1);
    
    		// make sure the AI is switched
    		pr.switchAI("gcm-attackerAI.js");
    		this._attackers.push(pr);
    	}
    	// get ready to set up the target
    	this._setupAttackerTarget = new Timer(this, this.$giveAttackersTarget, 1, 0);
    }
    Scripts/galcopbb_derelict.js
    "use strict";
    this.name = "GalCopBB_Derelict";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Control code for derelict/blackbox missions (missions 22/23/24)";
    this.license = "CC BY-NC-SA 4.0";
    
    this._intermittentSignalTimer = null; // timer to control when an intermittent signal is sent from the derelict
    this._maxLocations = 0;
    this._setData = [];
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
    	var gcm = worldScripts.GalCopBB_Missions;
    	// add these mission types into the main control
    	var list = [22, 23, 24];
    	gcm._availableMissionTypes = gcm._availableMissionTypes.concat(list);
    	gcm._interstellarMissionTypes.push(23);
    	this._maxLocations = gcm._positions.length;
    
    	// exclude some equipment items from being sold/stored by ShipConfig
    	var sc = worldScripts.ShipConfiguration_Core;
    	if (sc) {
    		sc._noSell.push("EQ_GCM_BLACK_BOX");
    		sc._noSell.push("EQ_GCM_RECOVERED_CARGO");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // set up interstellar space mission entities here
    //this.shipExitedWitchspace = function() {
    this.interstellarSpaceWillPopulate = function () {
    	//if (system.ID === -1) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var list = gcm.$getListOfMissions(true, 23);
    	if (list.length > 0) {
    		// loop through all active missions and see if any need to be set up for this system
    		for (var i = 0; i < list.length; i++) {
    			// *** type 23 - blackbox/derelict ship in interstellar
    			if (((gcm._fromSystem === list[i].destination || gcm._fromSystem === list[i].source) &&
    					(gcm._toSystem === list[i].destination || gcm._toSystem === list[i].source)) &&
    				list[i].data.quantity === 0 &&
    				list[i].data.destroyedQuantity === 0 &&
    				list[i].expiry > clock.adjustedSeconds) {
    
    				// add the derelict ship just inside scanner range
    				var dist = (Math.random() * 5000) + (player.ship.scannerRange - 5200);
    				var dir = Vector3D.randomDirection(dist);
    				//var position = player.ship.position.add(dir);
    				var position = Vector3D(0, 0, 0).add(dir);
    
    				// add derelict ship
    				// derelict ship creation code from Eric Walsh's DeepSpaceDredger OXP
    				var derelict = null;
    				var checkShips = null;
    				for (var j = 1; j <= 5; j++) {
    					checkShips = system.addShips("trader", 1, position, 1);
    					if (checkShips) derelict = checkShips[0];
    					if (derelict) break;
    				}
    				if (derelict) {
    					if (gcm._rsnInstalled) derelict.shipUniqueName = gcm.$getRandomShipName(derelict, "trader");
    					derelict.bounty = 0;
    
    					// set script to default, to avoid a special script for the trader doing stuff. (like setting a new AI)
    					derelict.setScript("oolite-default-ship-script.js");
    					derelict.switchAI("oolite-nullAI.js");
    
    					// remove any escorts that came with the ship
    					if (derelict.escorts) {
    						for (var j = derelict.escorts.length - 1; j >= 0; j--) derelict.escorts[j].remove(true);
    					}
    
    					derelict.script.shipLaunchedEscapePod = this.$gcm_derelict_shipLaunchedEscapePod; // function to remove the escapepod after launch.
    					if (derelict.equipmentStatus("EQ_ESCAPE_POD") === "EQUIPMENT_UNAVAILABLE") derelict.awardEquipment("EQ_ESCAPE_POD");
    					derelict.abandonShip(); // make sure no pilot is left behind and this command turns the ship into cargo.
    					derelict.primaryRole = "gcm_derelict"; // to avoid pirate attacks
    					derelict.displayName = derelict.displayName + " (derelict)";
    					derelict.lightsActive = false;
    					derelict.script._missionID = list[i].ID;
    					// monkey patch if necessary
    					// add our shipDied event to the derelict
    					if (derelict.script.shipDied) derelict.script.$gcm_hold_shipDied = derelict.script.shipDied;
    					derelict.script.shipDied = this.$gcm_derelict_shipDied;
    				} else {
    					log(this.name, "!!ERROR: Derelict ship not spawned!");
    				}
    			}
    		}
    	}
    	//}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.systemWillPopulate = function () {
    
    	// reset the mission populator data array
    	this._setData.length = 0;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var list = gcm.$getListOfMissions(true, [22, 24]);
    
    	if (list.length > 0) {
    		// loop through all active missions and see if any need to be set up for this system
    		for (var i = 0; i < list.length; i++) {
    			// *** type 22 - blackbox/derelict ship
    			if (list[i].data.missionType === 22 &&
    				list[i].destination === system.ID &&
    				list[i].data.quantity === 0 &&
    				list[i].data.destroyedQuantity === 0 &&
    				list[i].expiry > clock.adjustedSeconds) {
    
    				// based on danger level, add some pirate ships around the derelict
    				var goonCount = Math.floor(Math.random() * (9 - system.info.government)) - 1;
    				if (goonCount === 1) goonCount = 2; // make sure we don't have a single pirate
    
    				this._setData.push({
    					missionType: 22,
    					missionID: list[i].ID,
    					source: list[i].source,
    					goons: goonCount,
    					quantity: 0,
    					target: list[i].data.targetQuantity
    				});
    
    				// add derelict ship
    				system.setPopulator("gcm-derelict-" + list[i].ID, {
    					callback: function (pos) {
    						var gcd = worldScripts.GalCopBB_Derelict;
    						var missData = gcd.$getMissionData(22);
    						if (missData.target === 0) {
    							// in this case, the derelict was destroyed, but the blackbox wasn't scooped
    							// so spawn the black box
    							var bb = null;
    							var checkShips = null;
    							for (var j = 1; j <= 5; j++) {
    								checkShips = system.addShips("gcm_blackbox", 1, pos, 0);
    								if (checkShips) bb = checkShips[0];
    								if (bb) break;
    							}
    							if (bb) {
    								bb.setScript("oolite-default-ship-script.js");
    								bb.script._missionID = missData.missionID;
    								bb.script._gcmSpecial = true;
    								bb.script._gcmBlackBox = true;
    								bb.performTumble();
    								// monkey patch if necessary
    								// add our shipDied event to the blackbox
    								if (bb.script.shipDied) bb.script.$gcm_hold_shipDied = bb.script.shipDied;
    								bb.script.shipDied = gcd.$gcm_entity_shipDied;
    							} else {
    								log(this.name, "!!ERROR: Black box not spawned");
    							}
    						} else {
    							// derelict ship creation code from Eric Walsh's DeepSpaceDredger OXP
    							var g = worldScripts.GalCopBB_Missions;
    							var gns = worldScripts.GalCopBB_GoonSquads;
    							var derelict = null;
    							var checkShips = null;
    							for (var j = 1; j <= 5; j++) {
    								checkShips = system.addShips("trader", 1, pos, player.ship.scannerRange);
    								if (checkShips) derelict = checkShips[0];
    								if (derelict) break;
    							}
    							if (derelict) {
    								if (g._rsnInstalled) derelict.shipUniqueName = g.$getRandomShipName(derelict, "trader");
    								derelict.bounty = 0;
    
    								// set script to default, to avoid a special script for the trader doing stuff. (like setting a new AI)
    								derelict.setScript("oolite-default-ship-script.js");
    								derelict.switchAI("oolite-nullAI.js");
    
    								// remove any escorts that came with the ship
    								if (derelict.escorts) {
    									for (var j = derelict.escorts.length - 1; j >= 0; j--) derelict.escorts[j].remove(true);
    								}
    
    								derelict.script.shipLaunchedEscapePod = gcd.$gcm_derelict_shipLaunchedEscapePod; // function to remove the escapepod after launch.
    								if (derelict.equipmentStatus("EQ_ESCAPE_POD") === "EQUIPMENT_UNAVAILABLE") derelict.awardEquipment("EQ_ESCAPE_POD");
    								derelict.abandonShip(); // make sure no pilot is left behind and this command turns the ship into cargo.
    								derelict.primaryRole = "gcm_derelict"; // to avoid pirate attacks
    								derelict.displayName = derelict.displayName + " (derelict)";
    								derelict.lightsActive = false;
    								derelict.script._missionID = missData.missionID;
    								// monkey patch if necessary
    								// add our shipDied event to the derelict
    								if (derelict.script.shipDied) derelict.script.$gcm_hold_shipDied = derelict.script.shipDied;
    								derelict.script.shipDied = gcd.$gcm_derelict_shipDied;
    
    								if (missData.goons > 0) {
    									gns.$createGoonSquad(missData.goons, derelict.position, player.ship.scannerRange * 0.5);
    								}
    								if (g._debug) g.$setWaypoint(pos, [0, 0, 0, 0], "D", "Debug position (22)", "22");
    
    							} else {
    								log(this.name, "!!ERROR: Derelict ship not spawned!");
    							}
    						}
    					}.bind(this),
    					priority: 120,
    					location: "INNER_SYSTEM",
    					locationSeed: list[i].ID
    				});
    
    				// set up the intermittent signal
    				this._intermittentSignalTimer = new Timer(this, this.$sendIntermittentSignal, (Math.random() * 30) + 10, 0);
    			}
    
    			// *** type 24 - special cargo/derelict ship
    			if (list[i].data.missionType === 24 &&
    				list[i].destination === system.ID &&
    				list[i].data.quantity === 0 &&
    				list[i].data.destroyedQuantity === 0 &&
    				list[i].expiry > clock.adjustedSeconds) {
    
    				// based on danger level, add some pirate ships around the derelict
    				var goonCount = Math.floor(Math.random() * (9 - system.info.government)) - 1;
    				if (goonCount === 1) goonCount = 2; // make sure we don't have a single pirate
    
    				var position = gcm.$getRandomPosition(list[i].data.locationType, 0.1, list[i].ID).position;
    
    				this._setData.push({
    					missionType: 24,
    					missionID: list[i].ID,
    					source: list[i].source,
    					goons: goonCount,
    					quantity: 0,
    					target: list[i].data.targetQuantity
    				});
    
    				// add derelict ship
    				system.setPopulator("gcm-derelict2-" + list[i].ID, {
    					callback: function (pos) {
    						var g = worldScripts.GalCopBB_Missions;
    						var gns = worldScripts.GalCopBB_GoonSquads;
    						var gcd = worldScripts.GalCopBB_Derelict;
    						var missData = gcd.$getMissionData(24);
    						if (missData.target === 0) {
    							// in this case, the derelict was destroyed, but the cargo wasn't scooped
    							// so spawn the cargo
    							var cg = null;
    							var checkShips = null;
    							for (var j = 1; j <= 5; j++) {
    								checkShips = system.addShips("gcm_specialcargo", 1, pos, 0);
    								if (checkShips) cg = checkShips[0];
    								if (cg) break;
    							}
    							if (cg) {
    								cg.setScript("oolite-default-ship-script.js");
    								cg.script._missionID = missData.missionID;
    								cg.script._gcmSpecial = true;
    
    								// monkey patch if necessary
    								// add our shipDied event to the special cargo
    								if (cg.script.shipDied) cg.script.$gcm_hold_shipDied = cg.script.shipDied;
    								cg.script.shipDied = gcd.$gcm_entity_shipDied;
    							} else {
    								log("galcopBB_derelict", "!!ERROR: Special cargo not spawned");
    							}
    						} else {
    							// derelict ship creation code from Eric Walsh's DeepSpaceDredger OXP
    							var derelict = null;
    							var checkShips = null;
    							for (var j = 1; j <= 5; j++) {
    								checkShips = system.addShips("trader-courier", 1, pos, player.ship.scannerRange);
    								if (checkShips) derelict = checkShips[0];
    								if (derelict) break;
    							}
    							if (derelict) {
    								if (g._rsnInstalled) derelict.shipUniqueName = g.$getRandomShipName(derelict, "trader");
    								derelict.bounty = 0;
    
    								// set script to default, to avoid a special script for the trader doing stuff. (like setting a new AI)
    								derelict.setScript("oolite-default-ship-script.js");
    								derelict.switchAI("oolite-nullAI.js");
    
    								// remove any escorts that came with the ship
    								if (derelict.escorts) {
    									for (var j = derelict.escorts.length - 1; j >= 0; j--) derelict.escorts[j].remove(true);
    								}
    
    								derelict.script.shipLaunchedEscapePod = gcd.$gcm_derelict_shipLaunchedEscapePod; // function to remove the escapepod after launch.
    								if (derelict.equipmentStatus("EQ_ESCAPE_POD") === "EQUIPMENT_UNAVAILABLE") derelict.awardEquipment("EQ_ESCAPE_POD");
    								derelict.abandonShip(); // make sure no pilot is left behind and this command turns the ship into cargo.
    								derelict.primaryRole = "gcm_derelict2"; // to avoid pirate attacks
    								derelict.displayName = derelict.displayName + " (derelict)";
    								derelict.lightsActive = false;
    								derelict.script._missionID = missData.missionID;
    								// monkey patch if necessary
    								// add our shipDied event to the derelict
    								if (derelict.script.shipDied) derelict.script.$gcm_hold_shipDied = derelict.script.shipDied;
    								derelict.script.shipDied = gcd.$gcm_derelict_shipDied;
    
    								if (missData.goons > 0) {
    									gns.$createGoonSquad(missData.goons, derelict.position, player.ship.scannerRange * 0.5);
    								}
    								if (g._debug) g.$setWaypoint(pos, [0, 0, 0, 0], "D", "Debug position (24)", "24");
    
    							} else {
    								log("galcopBB_derelict", "!!ERROR: Derelict ship not spawned!");
    							}
    						}
    					}.bind(this),
    					priority: 130,
    					location: "COORDINATES",
    					coordinates: position
    				});
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillEnterWitchspace = function (cause, destination) {
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = function (whom, why) {
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipScoopedOther = function (whom) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var bb = worldScripts.BulletinBoardSystem;
    	// black box collected
    	if (whom && whom.isCargo && whom.primaryRole === "gcm_blackbox") {
    		var item = bb.$getItem(whom.script._missionID);
    		if (item) {
    			if (item.data.destroyedQuantity === 0 && item.data.quantity === 0) {
    				item.data.quantity = 1;
    				bb.$updateBBMissionPercentage(item.ID, 1);
    
    				gcm.$logMissionData(item.ID);
    				player.consoleMessage(expandDescription("[goal_updated]"));
    
    				gcm.$postScoopMissionCreation(item.ID, 7);
    			}
    		}
    		// convert blackbox cargo into an equipment item
    		player.ship.manifest["machinery"] -= 1;
    		whom.remove(true);
    		player.ship.awardEquipment("EQ_GCM_BLACK_BOX");
    	}
    	// special cargo collected
    	if (whom && whom.hasRole("gcm_specialcargo") === true) {
    		var item = bb.$getItem(whom.script._missionID);
    		if (item) {
    			item.data.quantity += 1;
    			bb.$updateBBMissionPercentage(item.ID, 1);
    			gcm.$logMissionData(item.ID);
    			player.consoleMessage(expandDescription("[goal_updated]"));
    		}
    		whom.remove(true);
    		player.ship.awardEquipment("EQ_GCM_RECOVERED_CARGO");
    	}
    	// leveraging the generic black boxes added by rescue stations
    	// actual black box missions in RS use the "rescue_blackbox" primary role
    	if (whom && whom.isCargo && whom.primaryRole === "rescue_blackbox_generic" && whom.script && whom.script.hasOwnProperty("_gcm_already_asked") === false) {
    		log(this.name, "found a generic blackbox");
    		if (Math.random() > 0.75) {
    			log(this.name, "adding post-scoop mission to blackbox");
    			gcm.$postScoopMissionCreation(-22, 7);
    		}
    		whom.script._gcm_already_asked = true;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // stop all timers
    this.$stopTimers = function $stopTimers() {
    	if (this._intermittentSignalTimer && this._intermittentSignalTimer.isRunning) this._intermittentSignalTimer.stop();
    	delete this._intermittentSignalTimer;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_derelict_shipDied = function $gcm_derelict_shipDied(whom, why) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var bb = worldScripts.BulletinBoardSystem;
    
    	var item = bb.$getItem(this.ship.script._missionID);
    	if (item.data.missionType === 22 || item.data.missionType === 23) {
    		if (gcm._debug) log(this.name, "Derelict destroyed - spawning blackbox");
    
    		// spawn the black box
    		//var bkb = system.addShips("gcm_blackbox", 1, this.ship.position.add(this.ship.vectorUp.multiply((this.ship.boundingBox.y + 25) * -1)), 0)[0]; 
    		var bkb = this.ship.spawnOne("gcm_blackbox");
    		if (bkb) {
    			bkb.setScript("oolite-default-ship-script.js");
    			bkb.script._missionID = item.ID;
    			bkb.script._gcmSpecial = true;
    			bkb.script._gcmBlackBox = true;
    
    			item.data.targetQuantity === 0;
    
    			// monkey patch if necessary
    			// add our shipDied event to the blackbox
    			if (bkb.script.shipDied && bkb.script.$gcm_hold_shipDied == null) {
    				bkb.script.$gcm_hold_shipDied = bkb.script.shipDied;
    				bkb.script.shipDied = worldScripts.GalCopBB_Derelict.$gcm_entity_shipDied;
    			}
    
    			if (whom && whom.isPlayer) {
    				// give the bb some velocity
    				if (Math.random() > 0.1) {
    					bkb.maxSpeed = player.ship.maxSpeed; // / ((Math.random() * 3) + 2);
    				} else {
    					bkb.maxSpeed = Math.random() * 20;
    				}
    				bkb.maxThrust = 10;
    			} else {
    				bkb.maxThrust = 1;
    				bkb.thrust = 1;
    			}
    			// give the blackbox a heading and velocity
    			bkb.orientation = Quaternion.random();
    			bkb.desiredSpeed = bb.maxSpeed;
    
    		} else {
    			log(this.name, "!!ERROR: Black box not spawned");
    		}
    	} else if (item.data.missionType === 24) {
    		var sc = this.ship.spawnOne("gcm_specialcargo");
    		//var sc = system.addShips("gcm_specialcargo", 1, this.ship.position.add(this.ship.vectorUp.multiply((this.ship.boundingBox.y + 15) * -1)), 0)[0];
    		if (sc) {
    			sc.setScript("oolite-default-ship-script.js");
    			sc.script.shipDied = gcm.$gcm_cargo_shipDied;
    			sc.script._missionID = item.ID;
    		} else {
    			log(this.name, "!!ERROR: Special cargo not spawned")
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_derelict_shipLaunchedEscapePod = function $gcm_derelict_shipLaunchedEscapePod(pod, passengers) {
    	pod.remove(true); // we don't want floating escapepods around but need them initially to create the derelict.
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_entity_shipDied = function $gcm_entity_shipDied(whom, why) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var bb = worldScripts.BulletinBoardSystem;
    
    	if (gcm._debug) log(this.name, "running shipDied for " + this.ship + ": reason " + why + ", " + whom);
    	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);
    
    	var item = bb.$getItem(this.ship.script._missionID);
    	if (item) item.data.destroyedQuantity = 1;
    	if (this.ship.hasRole("gcm_blackbox") === false) {
    		if (gcm._distressMessageTimer && gcm._distressMessageTimer.isRunning) gcm._distressMessageTimer.stop();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // gets mission specific data for the populator routines
    // this works on a first in/first out basis - if there are multiple missions of the same type being populated, the mission specific data would
    // get pushed in to the setData array in order, and then this routine pulls that data out in the same order
    // that's the theory, anyway!
    this.$getMissionData = function $getMissionData(missionType) {
    	for (var i = 0; i < this._setData.length; i++) {
    		if (this._setData[i].missionType === missionType) {
    			var result = {
    				missionID: this._setData[i].missionID,
    				trueMissionType: (this._setData[i].trueMissionType ? this._setData[i].trueMissionType : missionType),
    				source: this._setData[i].source,
    				goons: this._setData[i].goons,
    				quantity: this._setData[i].quantity,
    				target: this._setData[i].target
    			};
    			this._setData.splice(i, 1);
    			return result;
    		}
    	}
    	return null;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // turns on an intermittent navigational beacon the player can use to find the derelict
    this.$sendIntermittentSignal = function $sendIntermittentSignal() {
    	// we should only ever get 1 ship in this array, but just in case we'll do the loop
    	var targets = system.shipsWithPrimaryRole("gcm_derelict");
    	if (targets && targets.length > 0) {
    		for (var i = 0; i < targets.length; i++) {
    			if (targets[i].isValid) {
    				if (worldScripts.GalCopBB_Missions._debug) log(this.name, "activating intermittent signal");
    				// get the distance to the derelict, in number of scanner ranges
    				var dist = player.ship.position.distanceTo(targets[i]);
    				if (dist > player.ship.scannerRange) {
    					var err_dist = dist / 3;
    					var pos = targets[i].position.add(Vector3D.randomDirection(err_dist));
    					if (worldScripts.GalCopBB_Missions._debug) {
    						log(this.name, "player dist = " + dist + ", err_dist = " + err_dist);
    						log(this.name, "sigsrc = " + targets[i]);
    						log(this.name, "pos = " + pos);
    						log(this.name, "pos dist from derelict " + targets[i].position.distanceTo(pos));
    					}
    
    					// create dummy object that has a beacon on it
    					system.addVisualEffect("gcm_unidentified_signal", pos);
    					if (player.ship.isInSpace === true) {
    						player.consoleMessage("Unrecognised navigational beacon detected", 3);
    					}
    					// setup timer to remove intermittent signal
    					this._intermittentSignalTimer = new Timer(this, this.$removeIntermittentSignal, (Math.random() * 15) + 5, 0);
    					break;
    				} else {
    					var replay = parseInt(Math.random() * 60 + 40);
    					this._intermittentSignalTimer = new Timer(this, this.$sendIntermittentSignal, replay, 0);
    					break;
    				}
    				break;
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // turns off the intermittent signal
    this.$removeIntermittentSignal = function $removeIntermittentSignal() {
    	var targets = system.allVisualEffects;
    	if (targets.length > 0) {
    		for (var i = targets.length - 1; i >= 0; i--) {
    			if (targets[i].dataKey === "gcm_unidentified_signal") targets[i].remove(true);
    		}
    	}
    	if (worldScripts.GalCopBB_Missions._debug) log(this.name, "deactivating intermittent signal");
    	if (player.ship.isInSpace) {
    		player.consoleMessage("Lost contact with unrecognised navigational beacon", 3);
    	}
    	// set up a timer to show the intermittent signal again
    	var derelict = system.shipsWithPrimaryRole("gcm_derelict");
    	for (var i = 0; i < derelict.length; i++) {
    		if (derelict[i].isValid && player.ship.position.distanceTo(derelict[i]) > player.ship.scannerRange) {
    			var replay = parseInt(Math.random() * 60 + 40);
    			this._intermittentSignalTimer = new Timer(this, this.$sendIntermittentSignal, replay, 0);
    			break;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptedMission = function $acceptedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if (this._debug) log(this.name, "accepted mission id = " + missID);
    
    	if (!item) {
    		log(this.name, "!!ERROR: BB returned null value from $getItem on mission ID " + missID);
    		return;
    	}
    	gcm.$updateLastMissionDate(item.source, item.data.missionType);
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$completedMission = function $completedMission(missID) {
    	var p = player.ship;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    
    	// update mission history
    	gcm.$updateSuccessHistoryReputation(item);
    
    	// *** type 22 & 23 - Black box recovery
    	if (item.data.missionType === 22 || item.data.missionType === 23) {
    		p.removeEquipment("EQ_GCM_BLACK_BOX");
    		if (sbm) sbm.$removeSaleItem("EQ_GCM_BLACK_BOX:" + missID);
    	}
    	// *** type 24 - recovery special cargo
    	if (item.data.missionType === 24) {
    		p.removeEquipment("EQ_GCM_RECOVERED_CARGO");
    	}
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$confirmCompleted = function $confirmCompleted(missID) {
    	var p = player.ship;
    	var result = "";
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (item) {
    		// *** type 22/23 - black box recovery - make sure we still have it
    		if (item.data.missionType === 22 || item.data.missionType === 23) {
    			var chk = p.equipmentStatus("EQ_GCM_BLACK_BOX");
    			if (chk !== "EQUIPMENT_OK" && chk !== "EQUIPMENT_DAMAGED") {
    				result += (result === "" ? "" : "\n") + "The recovered black box is not on board your ship.";
    			}
    			if (chk === "EQUIPMENT_DAMAGED") {
    				result += (result === "" ? "" : "\n") + "The recovered black box has been damaged and the data cannot be accessed.";
    			}
    		}
    		// *** type 24 - special cargo - make sure we still have it
    		if (item.data.missionType === 24) {
    			var chk = p.equipmentStatus("EQ_GCM_RECOVERED_CARGO");
    			if (chk !== "EQUIPMENT_OK" && chk !== "EQUIPMENT_DAMAGED") {
    				result += (result === "" ? "" : "\n") + "The recovered cargo is not on board your ship.";
    			}
    			if (chk === "EQUIPMENT_DAMAGED") {
    				result += (result === "" ? "" : "\n") + "The recovered cargo has been damaged the contents are worthless.";
    			}
    		}
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$terminateMission = function $terminateMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// adjust reputation only when the terminatePenalty flag is set to true 
    	if (item.data.terminatePenalty === true) {
    		// update mission history
    		gcm.$updateFailedHistoryReputation(item);
    	}
    
    	// *** type 22 & 23 - Black box recovery
    	if (item.data.missionType === 22 || item.data.missionType === 23) {
    		var eqsts = player.ship.equipmentStatus("EQ_GCM_BLACK_BOX");
    		if (eqsts === "EQUIPMENT_OK" || eqsts === "EQUIPMENT_DAMAGED") {
    			player.ship.removeEquipment("EQ_GCM_BLACK_BOX");
    			if (sbm) sbm.$removeSaleItem("EQ_GCM_BLACK_BOX:" + missID);
    		}
    	}
    	if (item.data.missionType === 24) {
    		var eqsts = player.ship.equipmentStatus("EQ_GCM_RECOVERED_CARGO");
    		if (eqsts === "EQUIPMENT_OK" || eqsts === "EQUIPMENT_DAMAGED") {
    			player.ship.removeEquipment("EQ_GCM_RECOVERED_CARGO");
    			if (sbm) sbm.$removeSaleItem("EQ_GCM_RECOVERED_CARGO:" + missID);
    		}
    	}
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$failedMission = function $failedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if (!sbm) {
    		// if there's no blackmarket option, just remove the equipment
    		this.$terminateMission(missID);
    		return;
    	}
    
    	var item = bb.$getItem(missID);
    	// update mission history
    	gcm.$updateFailedHistoryReputation(item);
    
    	// we need to record the equipment/data element that the player will have in their possession, and the system that wanted it
    	var eq = "";
    	if ((item.data.missionType === 22 || item.data.missionType === 23) && item.data.quantity === 1) {
    		var eqsts = player.ship.equipmentStatus("EQ_GCM_BLACK_BOX");
    		if (eqsts === "EQUIPMENT_OK" || eqsts === "EQUIPMENT_DAMAGED") eq = "EQ_GCM_BLACK_BOX";
    	}
    	if (item.data.missionType === 24 && item.data.quantity === 1) {
    		var eqsts = player.ship.equipmentStatus("EQ_GCM_RECOVERED_CARGO");
    		if (eqsts === "EQUIPMENT_OK" || eqsts === "EQUIPMENT_DAMAGED") eq = "EQ_GCM_RECOVERED_CARGO";
    	}
    
    	if (eq != "") gcm._equipmentFromFailedMissions.push({
    		missionType: item.data.missionType,
    		source: item.source,
    		equip: eq,
    		date: clock.adjustedSeconds
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateManifestEntry = function $updateManifestEntry(missID) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	gcm.$updateManifestEntry(missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
    	return "";
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 22 - blackbox recovery
    this.$missionType22_Values = function $missionType22_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 50) + 100) / 10) * 10 + (7 - destSysInfo.government) * 20 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 10) // plus a distance bonus
    		+
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price / 5);
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 23 - blackbox recovery interstellar
    this.$missionType23_Values = function $missionType23_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 500) + 500) / 10) * 10 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 10) // plus a distance bonus
    		+
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(1000); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + (routeTime * 1.5) + workTime; // 1.5 transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price / 5);
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 24 - recover special cargo
    this.$missionType24_Values = function $missionType24_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["locationType"] = Math.floor(Math.random() * this._maxLocations);
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 500) + 500) / 10) * 10 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 10) // plus a distance bonus
    		+
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price / 5);
    	return result;
    }
    Scripts/galcopbb_diseaseoutbreak.js
    "use strict";
    this.name = "GalCopBB_DiseaseOutbreak";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Controls all disease-system mission settings (50-52), including locking down a system by removing all traffic and limiting docking";
    this.license = "CC BY-NC-SA 4.0";
    
    /*
    Apart from controlling the mission specific code, the goal of this code is to lock down a system from most incoming and insystem traffic
    Player can only validly enter the system if they've been issued a security pass as part of a mission (mission type 50)
    
    Ideas For disease systems:
    	- add how much outbreak was contained by player input
    	- change system 
    		- monitor outgoing ships in nearby systems to prevent them heading for disease system
    		- change police logic - player needs to send confirmation code to police vessels otherwise they will fine the player and attack
    	- outbreak spreading could be caused by player buying stuff at non-galcop stations in an outbreak system then selling those items on another system
    		- penalties could be monetary, legal, or in the form of trade restrictions
    */
    
    this._systemID = -1;
    this._periodicBroadcastTimer = null;
    this._allowDocking = null;
    this._sysPopulated = 0;
    this._resetStations = false;
    
    this._diseaseSystems = []; // systems where there is major disease
    this._diseaseEvents = []; // list of systemID's that are having a major disease outbreak
    this._outbreakCheck = false; // flag to indicate the outbreak check routine is currently active
    this._galJump = false;
    this._debug = true;
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
    	var gcm = worldScripts.GalCopBB_Missions;
    	// add these mission types into the main control
    	var list = [50, 51, 52, 53];
    	gcm._availableMissionTypes = gcm._availableMissionTypes.concat(list);
    	this._debug = gcm._debug;
    	// add our reputation entities
    	var ent = worldScripts.GalCopBB_Reputation._entities;
    	ent["Angel Of Mercy"] = {
    		scope: "chart",
    		display: true,
    		getValueWS: "",
    		getValueFN: "",
    		maxValue: 7,
    		minValue: 0,
    		degrade: "0.1|30",
    		rewardGrid: [{
    				value: 0,
    				description: ""
    			},
    			{
    				value: 4,
    				description: "",
    				achievementWS: "GalCopBB_DiseaseOutbreak",
    				achievementFN: "$angelOfMercyReward"
    			},
    			{
    				value: 6,
    				description: "",
    				achievementWS: "GalCopBB_DiseaseOutbreak",
    				achievementFN: "$angelOfMercyReward"
    			},
    			{
    				value: 7,
    				description: "",
    				achievementWS: "GalCopBB_DiseaseOutbreak",
    				achievementFN: "$angelOfMercyReward"
    			}
    		]
    	};
    	ent["Galactic Centre for Disease Control"] = {
    		scope: "chart",
    		display: true,
    		getValueWS: "",
    		getValueFN: "",
    		maxValue: 7,
    		minValue: 0,
    		degrade: "0.5|30",
    		rewardGrid: []
    	};
    
    	// exclude some equipment items from being sold/stored by ShipConfig
    	var sc = worldScripts.ShipConfiguration_Core;
    	if (sc) {
    		sc._noSell.push("EQ_GCM_MEDICAL_SUPPLIES");
    		sc._noSell.push("EQ_GCM_VIRUS_SPECIMENS");
    		sc._noSell.push("EQ_GCM_PATIENT_TRANSPORT");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function () {
    	this._diseaseSystems = this.$findDiseaseSystems();
    	if (missionVariables.GalCopBBMissions_DiseaseOutbreaks) {
    		this._diseaseEvents = JSON.parse(missionVariables.GalCopBBMissions_DiseaseOutbreaks);
    		delete missionVariables.GalCopBBMissions_DiseaseOutbreaks;
    	}
    	if (missionVariables.GalCopBBMissions_SystemLockdown) {
    		this._systemID = parseInt(missionVariables.GalCopBBMissions_SystemLockdown);
    		delete missionVariables.GalCopBBMissions_SystemLockdown;
    	}
    	// make sure the populator function runs after the startUpComplete
    	if (this._sysPopulated === 1 && this._systemID >= 0) {
    		this.systemWillPopulate();
    	}
    	this._sysPopulated = -1;
    	this.shipExitedWitchspace();
    
    	worldScripts.BulletinBoardSystem.$registerBBEvent("GalCopBB_DiseaseOutbreak", "$custom_shipWillDockWithStation", "shipWillDockWithStation_start");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillSaveGame = function () {
    	if (this._systemID >= 0) missionVariables.GalCopBBMissions_SystemLockdown = this._systemID;
    	if (this._diseaseEvents.length > 0) missionVariables.GalCopBBMissions_DiseaseOutbreaks = JSON.stringify(this._diseaseEvents);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerEnteredNewGalaxy = function (galaxyNumber) {
    	this._diseaseEvents.length = 0;
    	this._diseaseSystems.length = 0;
    	this._galJump = true;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.dayChanged = function (newday) {
    	this.$checkDiseaseOutbreaks();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerStartedJumpCountdown = function (type, seconds) {
    	// no check from interstellar space
    	if (system.ID === -1) return;
    	// check if the player has a reason to visit a disease system
    	var gcm = worldScripts.GalCopBB_Missions;
    	var bb = worldScripts.BulletinBoardSystem;
    	var dest = gcm.$playerTargetSystem();
    	if (type == "standard" && this.$systemHasDiseaseOutbreak(dest) === true) {
    		this._systemID = dest;
    		// have we been given clearance to go there?
    		var id = gcm.$getActiveMissionIDByType(50);
    		var found = false;
    		if (id >= 0) {
    			var item = bb.$getItem(id);
    			// is the mission destination the same as our jump destination
    			if (item.destination === dest) found = true;
    		}
    		if (found === false) {
    			// check distance from nearest galcop entity (witchpoint beacon, station)
    			// if they aren't, the jump can continue
    			if (this.$checkGalCopDistance() === false) return;
    
    			// we don't have a reason, so cancel the jump
    			player.ship.cancelHyperspaceCountdown();
    			player.consoleMessage("GalCop quarantine override enforced: " + System.systemNameForID(dest) + " is currently locked down due to a disease outbreak.", 5);
    		} else {
    			player.consoleMessage("GalCop quarantine override disabled: Valid system security code found.", 5);
    		}
    	} else {
    		this._systemID = -1;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipExitedWitchspace = function () {
    	function getGalCop(entity) {
    		return (entity.isShip && (entity.hasRole("buoy-witchpoint") || (entity.isStation && entity.allegiance === "galcop")));
    	}
    	this._galCopTargets = system.filteredEntities(this, getGalCop);
    
    	if (system.sun && system.mainPlanet) {
    		this._sun_planet_dist = system.sun.position.distanceTo(system.mainPlanet);
    	} else {
    		this._sun_planet_dist = 0;
    	}
    
    	// set up the timer to transmit warning messages
    	if (this._systemID >= 0 && system.ID === this._systemID) {
    		this._periodicBroadcastTimer = new Timer(this, this.$transmitWarning, 10, 120);
    
    		this.$removeShips();
    
    		var gcm = worldScripts.GalCopBB_Missions;
    		var bb = worldScripts.BulletinBoardSystem;
    		// have we been given clearance to come here?
    		var id = gcm.$getActiveMissionIDByType(50);
    		var item = bb.$getItem(id);
    		if (item && item.data.quantity === 0) {
    			// set up broadcast comms interface
    			var bcc = worldScripts.BroadcastCommsMFD;
    			var stns = system.stations;
    			for (var i = 0; i < stns.length; i++) {
    				// clearance only required at galcop stations
    				if (stns[i].allegiance === "galcop") {
    					if (bcc.$checkMessageExists("gcm_transmit_dockcode_" + i) === false) {
    						bcc.$createMessage({
    							messageName: "gcm_transmit_dockcode_" + i,
    							callbackFunction: this.$transmitSecurityCode.bind(this),
    							displayText: "Transmit security code",
    							messageText: expandDescription("[gcm_transmitting_dockcode]"),
    							ship: system.mainStation,
    							transmissionType: "target",
    							deleteOnTransmit: true,
    							delayCallback: 5,
    							hideOnConditionRed: true
    						});
    					}
    				}
    			}
    		}
    	}
    	if (this._galJump === true) {
    		this._galJump = false;
    		this._diseaseSystems = this.$findDiseaseSystems();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillEnterWitchspace = function (cause, destination) {
    	this._resetStations = false;
    	// stop the timer
    	if (this._periodicBroadcastTimer && this._periodicBroadcastTimer.isRunning) {
    		this._periodicBroadcastTimer.stop();
    		this._periodicBroadcastTimer = null;
    	}
    	if (system.ID === this._systemID) {
    		// turn on queues again in SDC as we leave
    		var sdc = worldScripts.StationDockControl;
    		if (sdc) sdc._disableQueues = false;
    		this.$enableBigShips();
    	}
    	if (destination === this._systemID) {
    		this.$disableBigShips();
    		// tell planetfall not to spawn any planetary locations
    		if (worldScripts.PlanetFall2) {
    			worldScripts.PlanetFall2.disable = true;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = function (whom, why) {
    	// stop the timer
    	if (this._periodicBroadcastTimer && this._periodicBroadcastTimer.isRunning) {
    		this._periodicBroadcastTimer.stop();
    		this._periodicBroadcastTimer = null;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$custom_shipWillDockWithStation = function $custom_shipWillDockWithStation(station) {
    	if (this._simulator === true) {
    		this._simulator = false;
    		return;
    	}
    	// turn off the docking fees, if it's installed
    	if (this._systemID >= 0 && system.ID === this._systemID) {
    		if (worldScripts["Docking Fees"]) worldScripts["Docking Fees"]._simulator = true;
    	}
    	if (this._periodicBroadcastTimer && this._periodicBroadcastTimer.isRunning) {
    		this._periodicBroadcastTimer.stop();
    		this._periodicBroadcastTimer = null;
    	}
    
    	// check for other mission types being completed
    	var gcm = worldScripts.GalCopBB_Missions;
    	var bb = worldScripts.BulletinBoardSystem;
    	var list = gcm.$getListOfMissions(true, [50, 51, 52]);
    	for (var i = 0; i < list.length; i++) {
    		if (station.isMainStation) {
    			if (list[i].data.missionType === 50 && list[i].destination === system.ID) {
    				if (player.ship.equipmentStatus("EQ_GCM_MEDICAL_SUPPLIES") === "EQUIPMENT_OK" && list[i].expiry >= clock.adjustedSeconds) {
    					list[i].arrivalReportText = expandDescription("[missionType50_arrivalReport_complete]", {
    						payment: formatCredits(list[i].payment, false, true)
    					});
    				} else {
    					list[i].arrivalReportText = expandDescription("[missionType50_arrivalReport_late]");
    				}
    
    				list[i].data.quantity = 1;
    				bb.$updateBBMissionPercentage(list[i].ID, 1);
    
    				gcm.$logMissionData(list[i].ID);
    			}
    			if (list[i].data.missionType === 51 && list[i].destination === system.ID) {
    				if (player.ship.equipmentStatus("EQ_GCM_PATIENT_TRANSPORT") === "EQUIPMENT_OK" && list[i].expiry >= clock.adjustedSeconds) {
    					list[i].arrivalReportText = expandDescription("[missionType51_arrivalReport_complete]", {
    						payment: formatCredits(list[i].payment, false, true)
    					});
    				} else {
    					list[i].arrivalReportText = expandDescription("[missionType51_arrivalReport_late]");
    				}
    
    				list[i].data.quantity = 1;
    				bb.$updateBBMissionPercentage(list[i].ID, 1);
    
    				gcm.$logMissionData(list[i].ID);
    			}
    			if (list[i].data.missionType === 52 && list[i].destination === system.ID) {
    				if (player.ship.equipmentStatus("EQ_GCM_VIRUS_SPECIMENS") === "EQUIPMENT_OK" && list[i].expiry >= clock.adjustedSeconds) {
    					list[i].arrivalReportText = expandDescription("[missionType52_arrivalReport_complete]", {
    						payment: formatCredits(list[i].payment, false, true)
    					});
    				} else {
    					list[i].arrivalReportText = expandDescription("[missionType52_arrivalReport_late]");
    				}
    
    				list[i].data.quantity = 1;
    				bb.$updateBBMissionPercentage(list[i].ID, 1);
    
    				gcm.$logMissionData(list[i].ID);
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDockedWithStation = function (station) {
    	if (this._simulator === true) this._simulator = false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipLaunchedFromStation = function (station) {
    	if (worldScripts.GalCopBB_Missions.$simulatorRunning() === true) this._simulator = true;
    	if (this._systemID >= 0 && system.ID === this._systemID) {
    		this.$removeShips();
    		if (station === system.mainStation) this.$lockStation(station);
    		this._periodicBroadcastTimer = new Timer(this, this.$transmitWarning, 10, 120);
    	}
    	if (this._simulator == false) this._reported = false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.systemWillPopulate = function () {
    	// set the flag to indicate we've been here already
    	if (this._sysPopulated !== -1) this._sysPopulated += 1;
    	if (this._systemID >= 0 && system.ID === this._systemID) {
    		// zero the populator values so no ships are coming in
    		var pr = worldScripts["oolite-populator"];
    		var k = Object.keys(pr.$repopulatorFrequencyIncoming);
    		for (var i = 0; i < k.length; i++) {
    			pr.$repopulatorFrequencyIncoming[k[i]] = 0;
    		}
    		k = Object.keys(pr.$repopulatorFrequencyOutgoing);
    		for (i = 0; i < k.length; i++) {
    			pr.$repopulatorFrequencyOutgoing[k[i]] = 0;
    		}
    		// for SDC, make sure all docks are empty
    		var sdc = worldScripts.StationDockControl;
    		if (sdc) {
    			sdc._disableQueues = true;
    			sdc.$emptyAllQueues();
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.systemWillRepopulate = function () {
    	if (this._systemID > 0 && system.ID === this._systemID) {
    		if (this._resetStations === false) {
    			this._resetStations = true;
    			var stns = system.stations;
    			for (var i = 0; i < stns.length; i++) {
    				if (stns[i].allegiance === "galcop") {
    					// turn off ability to dock at station
    					this.$lockStation(stns[i]);
    				}
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.guiScreenChanged = function (to, from) {
    	if (this._systemID >= 0 && system.ID === this._systemID && player.ship.alertCondition === 0 && player.ship.dockedStation.allegiance === "galcop" && to === "GUI_SCREEN_MARKET") {
    		mission.runScreen({
    			screenID: "oolite-gcm-marketlockout-summary",
    			title: "Market Services Suspended",
    			message: "The commodities market on this station has been suspended due to the quarantine restrictions put in place by GalCop authorities. This has been done in order to prevent the spread of the disease off-world.",
    			overlay: {name: "gcm-noentry.png", height: 546},
    			exitScreen: "GUI_SCREEN_STATUS"
    		});
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.missionScreenOpportunity = function() {
    	if (this._simulator == true) return;
    	if (this._reported == true) return;
    	// tell the player about local systems that are in lockdown
    	this._reported = true;
    	var sys = System.infoForSystem(galaxyNumber, system.ID);
    	var report = [];
    	for (var i = 0; i < this._diseaseEvents.length; i++) {
    		var dist = sys.distanceToSystem(System.infoForSystem(galaxyNumber, this._diseaseEvents[i].systemID));
    		if (dist <= 7) report.push(this._diseaseEvents[i].systemID);
    	}
    	if (report.length > 0) {
    		var systems = "";
    		for (var i = 0; i < report.length; i++) {
    			systems += (systems == "" ? "" : ", ") + System.systemNameForID(report[i]) + " (" + sys.distanceToSystem(System.infoForSystem(galaxyNumber, report[i])).toFixed(1) + "ly)";
    		}
    		mission.runScreen({
    			title: "Galactic Centre for Disease Control",
    			message: "***** WARNING! *****\n\nAttention all pilots. The GCDC is working with GalCop to limit the spread of disease in the following systems:\n\n-- " + systems + "\n\nThese systems are in a lockdown state, and as a result, you will not be able to initiate a witchjump into them without appropriate permits.\n\nThe GCDC apologies for any inconvenience this may cause, and thanks you for your understanding.",
    			overlay: {name: "gcm-biohazard.png", height: 546},
    			exitScreen: "GUI_SCREEN_STATUS"
    		});
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$transmitWarning = function $transmitWarning() {
    	function getWPBuoy(entity) {
    		return (entity.isShip && (entity.hasRole("buoy-witchpoint") || (entity.isStation && entity.allegiance === "galcop")));
    	}
    	var targets = system.filteredEntities(this, getWPBuoy);
    	if (targets.length > 0) {
    		for (var i = 0; i < targets.length; i++) {
    			targets[i].commsMessage(expandDescription("[gcm_disease_warning]"), player.ship);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // response after player has transmitted security code to a station (via BroadcastCommsMFD)
    this.$transmitSecurityCode = function $transmitSecurityCode() {
    	if (player.ship.position.distanceTo(system.mainStation) < player.ship.scannerRange) {
    		system.mainStation.commsMessage(expandDescription("[gcm_transmit_dockcode]"), player.ship);
    		this._allowDocking = new Timer(this, this.$allowDocking.bind(this), 10, 0);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$populatorPreventer = function $populatorPreventer() {
    	// this function intentionally left blank
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$allowDocking = function $allowDocking() {
    	system.mainStation.commsMessage(expandDescription("[gcm_dockcode_success]"), player.ship);
    	this.$unlockStation(system.mainStation);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$enableBigShips = function $enableBigShips() {
    	// restore bigShips populator on the way out
    	var bship = worldScripts["bigShips_populator"];
    	if (bship) {
    		if (!bship.$gcm_hold_systemWillPopulate) {
    			if (bship.$gcm_hold_bship_populator) {
    				bship.populator = bship.$gcm_hold_bship_populator;
    				delete bship.$gcm_hold_bship_populator;
    			}
    		} else {
    			bship.systemWillPopulate = bship.$gcm_hold_systemWillPopulate;
    			delete bship.$gcm_hold_systemWillPopulate;
    		}
    	} else {
    		var liners = worldScripts["liners_populator_script.js"];
    		if (liners) {
    			if (liners.$gcm_hold_liners_populator) {
    				liners.populator = liners.$gcm_hold_liners_populator;
    				delete liners.$gcm_hold_liners_populator;
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$disableBigShips = function $disableBigShips() {
    	// disable bigShips populator on the way in
    	var bship = worldScripts["bigShips_populator"];
    	if (bship) {
    		if (!bship.systemWillPopulate) {
    			if (!bship.$gcm_hold_bship_populator) {
    				bship.$gcm_hold_bship_populator = bship.populator;
    				bship.populator = this.$populatorPreventer;
    			}
    		} else {
    			bship.$gcm_hold_systemWillPopulate = bship.systemWillPopulate;
    			delete bship.systemWillPopulate;
    		}
    	} else {
    		var liners = worldScripts["liners_populator_script.js"];
    		if (liners) {
    			if (!liners.$gcm_hold_liners_populator) {
    				liners.$gcm_hold_liners_populator = liners.populator;
    				liners.populator = this.$populatorPreventer;
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$lockStation = function $lockStation(stn) {
    	stn.requiresDockingClearance = false;
    	stn.hasNPCTraffic = false;
    	for (var j = 0; j < stn.subEntities.length; j++) {
    		if (stn.subEntities[j].isDock) {
    			stn.subEntities[j].allowsDocking = false;
    			stn.subEntities[j].disallowedDockingCollides = true;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$unlockStation = function $unlockStation(stn) {
    	for (var j = 0; j < stn.subEntities.length; j++) {
    		if (stn.subEntities[j].isDock) {
    			stn.subEntities[j].allowsDocking = true;
    			stn.subEntities[j].disallowedDockingCollides = false;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // forcibly remove any ships that are already in the system (in case the core populator runs before this script)
    this.$removeShips = function $removeShips() {
    	for (var i = system.allShips.length - 1; i >= 0; i--) {
    		var shp = system.allShips[i];
    		if (shp.scanClass === "CLASS_NEUTRAL") {
    			shp.remove(true);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // checks the players distance from GalCop entities that could stop a witchspace countdown
    this.$checkGalCopDistance = function $checkGalCopDistance() {
    	if (!player.ship) return false;
    	var p = player.ship;
    	var inrange = false;
    	if (this._galCopTargets.length > 0) {
    		for (var i = 0; i < this._galCopTargets.length; i++) {
    			if (p.position.distanceTo(this._galCopTargets[i]) < this._sun_planet_dist) {
    				inrange = true;
    				break;
    			}
    		}
    	}
    	return inrange;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // perform the disease outbreak checks
    this.$checkDiseaseOutbreaks = function $checkDiseaseOutbreaks(override) {
    	// make sure we only come here once per day
    	if (this._outbreakCheck) return;
    	this._outbreakCheck = true;
    
    	if (this._debug) log(this.name, "checking disease outbreaks (override = " + override + ")");
    
    	// are any going to stop today?
    	for (var i = this._diseaseEvents.length - 1; i >= 0; i--) {
    		var event = this._diseaseEvents[i];
    		if (event.endDate < clock.adjustedSeconds) {
    			if (this._debug) log(this.name, "disease outbreak stopped on " + event.name + " (" + event.systemID + ")");
    			// send a snoopers message (if installed)
    			// propagation = how fast it spreads
    			// virulence = how fast it kills
    			// the total damage (loss of life) = (number of days * ((100 * virulence) * (1 + propagation)));
    			var numDeaths = ((
    				((clock.adjustedSeconds - event.startDate) / 86400) *
    				((100 * event.virulence) * (1 + event.propagation))
    			) / 1000).toFixed(1);
    			var news = {
    				ID: this.name,
    				Message: expandDescription("[gcm_disease_contained]", {
    					system: this._diseaseSystems[i].name,
    					deaths: numDeaths
    				}),
    				Agency: 1,
    				Priority: 1
    			};
    			// send a GNN/snoopers message (if installed)
    			var g = worldScripts.GNN;
    			if (g) g._insertNews(news);
    			if (!g) {
    				var w = worldScripts.snoopers;
    				if (w) w.insertNews(news);
    			}
    
    			// remove the extra description from the planet description
    			var sys = System.infoForSystem(galaxyNumber, event.systemID);
    			sys.setProperty(2, "description", sys.description.replace("\n" + expandDescription("[gcm_disease_description]"), ""));
    			this._diseaseEvents.splice(i, 1);
    		}
    	}
    
    	// for our existing outbreaks, are any going to propagate today?
    	for (var i = 0; i < this._diseaseEvents.length; i++) {
    		var event = this._diseaseEvents[i];
    		var chance = event.propagagtion;
    		if (Math.random() < chance && Math.random() < chance && Math.random() < chance) {
    			if (this._debug) log(this.name, "diease outbreak on " + event.name + " (" + event.systemID + ") spreading...");
    			// yep - we're going offworld
    			var info = System.infoForSystem(galaxyNumber, event.systemID);
    			var sys = info.systemsInRange(4);
    			if (sys.length > 0) {
    				// pick a random item
    				var dest = sys[Math.floor(Math.random() * sys.length)];
    				// make sure this dest doesn't already have the disease
    				var found = false;
    				for (var j = 0; j < this._diseaseEvents.length; j++) {
    					if (this._diseaseEvents[j].systemID === dest.systemID) {
    						found = true;
    						break;
    					}
    				}
    				if (found === false) {
    					if (this._debug) log(this.name, "... to " + dest.name + " (" + dest.systemID + ")");
    					// we've got a hit - calculate the end point in seconds
    					var months = event.endDate + (((Math.random() * 2) + 1) * 30) * 86400;
    					this._diseaseEvents.push({
    						systemID: dest.systemID,
    						name: dest.name,
    						startDate: clock.adjustedSeconds,
    						endDate: months,
    						propagation: (event.propagation / 2),
    						virulence: (event.virulence / 2),
    						origin: event.origin
    					});
    
    					var news = {
    						ID: this.name,
    						Message: expandDescription("[gcm_disease_spread]", {
    							system: System.systemNameForID(dest.systemID),
    							origin: System.systemNameForID(this._diseaseEvents[i].origin)
    						}),
    						Agency: 1,
    						Priority: 1
    					};
    					// send a GNN/snoopers message (if installed)
    					var g = worldScripts.GNN;
    					if (g) g._insertNews(news);
    					if (!g) {
    						var w = worldScripts.snoopers;
    						if (w) w.insertNews(news)
    					}
    					break;
    				}
    			}
    		}
    	}
    
    	// how many outbreaks do we have?
    	if (this._diseaseEvents.length < 3) {
    		// how recent was the last one?
    		var newdis = true;
    		for (var i = 0; i < this._diseaseEvents.length; i++) {
    			if (parseInt((clock.adjustedSeconds - this._diseaseEvents[i].startDate) / 86400) < 30) {
    				newdis = false;
    				break;
    			}
    		}
    		// it's been more than 30 days since the last one - work out if today is the day
    		if (newdis === true) {
    			if (Math.random() > 0.96 || (override && override === true)) {
    				// bingo - create a new outbreak
    				// find an unused system
    				// resort the array so we don't start with the same system each time
    				this._diseaseSystems.sort(function (a, b) {
    					return Math.random() - 0.5;
    				});
    				for (var i = 0; i < this._diseaseSystems.length; i++) {
    					//log(this.name, "sys" + i + ", " + this._diseaseSystems[i].systemID + " " + this._diseaseSystems[i].name);
    					var found = false;
    					for (var j = 0; j < this._diseaseEvents.length; j++) {
    						if (this._diseaseSystems[i].systemID === this._diseaseEvents[j].systemID) {
    							found = true;
    							break;
    						}
    					}
    					if (found === false) {
    						// we've got a hit - calculate the end point in seconds
    						var months = clock.adjustedSeconds + (((Math.random() * 3) + 1) * 30) * 86400;
    						var sys = this._diseaseSystems[i].systemsInRange(4);
    						var prop = sys.length / 10;
    						if (prop > 0.8) prop = 0.8;
    						var vir = Math.random() * 0.5 + 0.3;
    						if (this._debug) log(this.name, "new disease outbreak starting on " + this._diseaseSystems[i].name + " (" + this._diseaseSystems[i].systemID + ") - current end date " + months);
    
    						this._diseaseEvents.push({
    							systemID: this._diseaseSystems[i].systemID,
    							name: this._diseaseSystems[i].name,
    							startDate: clock.adjustedSeconds,
    							endDate: months,
    							propagation: prop,
    							virulence: vir,
    							origin: this._diseaseSystems[i].systemID
    						});
    
    						var news = {
    							ID: this.name,
    							Message: expandDescription("[gcm_disease_outbreak]", {
    								system: this._diseaseSystems[i].name
    							}),
    							Agency: 1,
    							Priority: 1
    						};
    						// send a GNN/snoopers message (if installed)
    						var g = worldScripts.GNN;
    						if (g) g._insertNews(news);
    						if (!g) {
    							var w = worldScripts.snoopers;
    							if (w) w.insertNews(news);
    						}
    
    						// update the planet description
    						this._diseaseSystems[i].setProperty(2, "description", this._diseaseSystems[i].description += "\n" + expandDescription("[gcm_disease_description]"));
    						break;
    					}
    				}
    			}
    		}
    	}
    
    	this._outbreakCheck = false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns a list of sysInfos where disease is prevalent
    this.$findDiseaseSystems = function $findDiseaseSystems() {
    	return SystemInfo.filteredSystems(this, function (other) {
    		return (other.description.indexOf("disease") >= 0 && other.techlevel < 11);
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$systemHasDiseaseOutbreak = function $systemHasDiseaseOutbreak(sysID) {
    	// if there are no disease events in play, it's a no
    	if (this._diseaseEvents.length === 0) {
    		return false;
    	} else {
    		// otherwise, loop through the current disease events list
    		var found = false;
    		for (var j = 0; j < this._diseaseEvents.length; j++) {
    			if (this._diseaseEvents[j].systemID === sysID) found = true;
    		}
    		// return what we found
    		return found;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // reduce the end point for the outbreak on a particular system
    this.$reduceDiseaseTime = function $reduceDiseaseTime(sysID) {
    	for (var i = 0; i < this._diseaseEvents.length; i++) {
    		if (this._diseaseEvents[i].systemID === sysID && this._diseaseEvents[i].endDate - 86400 > clock.adjustedSeconds) this._diseaseEvents -= 86400;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$angelOfMercyReward = function $angelOfMercyReward(entity, sysID, rep) {
    	var reputation = worldScripts.GalCopBB_Reputation;
    	if (rep >= 4 && rep < 6) {
    		if (reputation.$checkForAward(entity, "Wings of Clemency", 0) === false) {
    			reputation.$addAward({
    				title: "Wings of Clemency",
    				description: "Awarded for the exceptional help and support provided during medical emergencies in Galactic Chart " + (galaxyNumber + 1) + ".",
    				entity: entity,
    				system: 0,
    				imageType: 1
    			});
    		}
    	}
    	if (rep >= 6 && rep < 7) {
    		if (reputation.$checkForAward(entity, "Wings of Compassion", 0) === false) {
    			reputation.$addAward({
    				title: "Wings of Compassion",
    				description: "Awarded for providing help above and beyond the call of duty during medical emergencies in Galactic Chart " + (galaxyNumber + 1) + ".",
    				entity: entity,
    				system: 0,
    				imageType: 2
    			});
    		}
    	}
    	if (rep >= 7) {
    		if (reputation.$checkForAward(entity, "Wings of Benevolence", 0) === false) {
    			reputation.$addAward({
    				title: "Wings of Benevolence",
    				description: "Awarded for providing a level of help and support rarely ever seen in the enitre galaxy, let along in Galactic Chart " + (galaxyNumber + 1) + ".",
    				entity: entity,
    				system: 0,
    				imageType: 3
    			});
    		}
    	}
    }
    
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptedMission = function $acceptedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if (this._debug) log(this.name, "accepted mission id = " + missID);
    
    	if (!item) {
    		log(this.name, "!!ERROR: BB returned null value from $getItem on mission ID " + missID);
    		return;
    	}
    	gcm.$updateLastMissionDate(item.source, item.data.missionType);
    	gcm.$updateGeneralSettings(item);
    
    	// give player medical supplies for mission type 50
    	if (item.data.missionType === 50) {
    		if (player.ship.cargoSpaceAvailable === 0 && player.ship.cargoSpaceCapacity >= 1) this.$freeCargoSpace(1, "");
    		player.ship.awardEquipment("EQ_GCM_MEDICAL_SUPPLIES");
    	}
    	// give player patient transport for mission type 51
    	if (item.data.missionType === 51) {
    		var eq = EquipmentInfo.infoForKey("EQ_GCM_PATIENT_TRANSPORT");
    		if (player.ship.cargoSpaceAvailable < eq.requiredCargoSpace) this.$freeCargoSpace(eq.requiredCargoSpace, "");
    		player.ship.awardEquipment("EQ_GCM_PATIENT_TRANSPORT");
    	}
    	// give player virus specimens for mission type 52
    	if (item.data.missionType === 52) {
    		player.ship.awardEquipment("EQ_GCM_VIRUS_SPECIMENS");
    	}
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$completedMission = function $completedMission(missID) {
    	var p = player.ship;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    
    	// if the player has a bounty, doing these missions will reduce it
    	if (player.ship.bounty > 0) {
    		for (var i = 0; i < 5; i++) {
    			if (player.ship.bounty > 0) player.ship.setBounty(player.ship.bounty - 1, "community service");
    		}
    		player.ship.consoleMessage("Your bounty has been reduced.");
    	}
    
    	// update mission history
    	gcm.$updateSuccessHistoryReputation(item);
    
    	// *** type 50 - medical delivery (shouldn't need this, but just in case)
    	if (item.data.missionType === 50) {
    		worldScripts.GalCopBB_DiseaseOutbreak.$reduceDiseaseTime(system.ID);
    		p.removeEquipment("EQ_GCM_MEDICAL_SUPPLIES");
    		if (sbm) sbm.$removeSaleItem("EQ_GCM_MEDICAL_SUPPLIES:" + missID);
    	}
    	// *** type 51 - patient transport (shouldn't need this, but just in case)
    	if (item.data.missionType === 51) {
    		p.removeEquipment("EQ_GCM_PATIENT_TRANSPORT");
    	}
    	// *** type 52 - virus specimens (shouldn't need this, but just in case)
    	if (item.data.missionType === 52) {
    		p.removeEquipment("EQ_GCM_VIRUS_SPECIMENS");
    		if (sbm) sbm.$removeSaleItem("EQ_GCM_VIRUS_SPECIMENS:" + missID);
    	}
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$confirmCompleted = function $confirmCompleted(missID) {
    	var p = player.ship;
    	var result = "";
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (item) {
    		// *** type 50 - medical delivery
    		if (item.data.missionType === 50) {
    			var chk = p.equipmentStatus("EQ_GCM_MEDICAL_SUPPLIES");
    			if (chk !== "EQUIPMENT_OK" && chk !== "EQUIPMENT_DAMAGED") {
    				result += (result === "" ? "" : "\n") + "The medical supplies are not on board your ship.";
    			}
    			if (chk === "EQUIPMENT_DAMAGED") {
    				result += (result === "" ? "" : "\n") + "The medical supplies have been damaged and the contents ruined.";
    			}
    		}
    		// *** type 51 - patient transport
    		if (item.data.missionType === 51) {
    			var chk = p.equipmentStatus("EQ_GCM_PATIENT_TRANSPORT");
    			if (chk !== "EQUIPMENT_OK" && chk !== "EQUIPMENT_DAMAGED") {
    				result += (result === "" ? "" : "\n") + "The emergency patient transport unit is not on board your ship.";
    			}
    			if (chk === "EQUIPMENT_DAMAGED") {
    				result += (result === "" ? "" : "\n") + "The emergency patient transport unit has been damaged. You can't determine the condition of the patients inside.";
    			}
    		}
    		// *** type 52 - virus specimen transport
    		if (item.data.missionType === 52) {
    			var chk = p.equipmentStatus("EQ_GCM_VIRUS_SPECIMENS");
    			if (chk !== "EQUIPMENT_OK" && chk !== "EQUIPMENT_DAMAGED") {
    				result += (result === "" ? "" : "\n") + "The virus specimens are not on board your ship.";
    			}
    			if (chk === "EQUIPMENT_DAMAGED") {
    				result += (result === "" ? "" : "\n") + "The virus specimen transportation unit has been damaged. The contents have been sterilised.";
    			}
    		}
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$terminateMission = function $terminateMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// adjust reputation only when the terminatePenalty flag is set to true 
    	if (item.data.terminatePenalty === true) {
    		// update mission history
    		gcm.$updateFailedHistoryReputation(item);
    	}
    
    	// *** type 50 - medical delivery
    	if (item.data.missionType === 50) {
    		var eqsts = player.ship.equipmentStatus("EQ_GCM_MEDICAL_SUPPLIES");
    		if (eqsts === "EQUIPMENT_OK" || eqsts === "EQUIPMENT_DAMAGED") {
    			player.ship.removeEquipment("EQ_GCM_MEDICAL_SUPPLIES");
    			if (sbm) sbm.$removeSaleItem("EQ_GCM_MEDICAL_SUPPLIES:" + missID);
    		}
    	}
    	// *** type 51 - patient transport
    	if (item.data.missionType === 51) {
    		player.ship.removeEquipment("EQ_GCM_PATIENT_TRANSPORT");
    	}
    	// *** type 52 - virus specimens
    	if (item.data.missionType === 52) {
    		var eqsts = player.ship.equipmentStatus("EQ_GCM_VIRUS_SPECIMENS");
    		if (eqsts === "EQUIPMENT_OK" || eqsts === "EQUIPMENT_DAMAGED") {
    			player.ship.removeEquipment("EQ_GCM_VIRUS_SPECIMENS");
    			if (sbm) sbm.$removeSaleItem("EQ_GCM_VIRUS_SPECIMENS:" + missID);
    		}
    	}
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$failedMission = function $failedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var eq = "";
    
    	if (!sbm) {
    		// if there's no blackmarket option, just remove the equipment
    		this.$terminateMission(missID);
    		return;
    	}
    
    	var item = bb.$getItem(missID);
    	// update mission history
    	gcm.$updateFailedHistoryReputation(item);
    
    	if (item.data.missionType === 50) {
    		var eqsts = player.ship.equipmentStatus("EQ_GCM_MEDICAL_SUPPLIES");
    		if (eqsts === "EQUIPMENT_OK" || eqsts === "EQUIPMENT_DAMAGED") eq = "EQ_GCM_MEDICAL_SUPPLIES";
    	}
    	if (item.data.missionType === 51) {
    		// we'll just remove this one
    		player.ship.removeEquipment("EQ_GCM_PATIENT_TRANSPORT");
    		eq = "";
    	}
    	if (item.data.missionType === 52) {
    		var eqsts = player.ship.equipmentStatus("EQ_GCM_VIRUS_SPECIMENS");
    		if (eqsts === "EQUIPMENT_OK" || eqsts === "EQUIPMENT_DAMAGED") eq = "EQ_GCM_VIRUS_SPECIMENS";
    	}
    
    	if (eq != "") gcm._equipmentFromFailedMissions.push({
    		missionType: item.data.missionType,
    		source: item.source,
    		equip: eq,
    		date: clock.adjustedSeconds
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateManifestEntry = function $updateManifestEntry(missID) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	gcm.$updateManifestEntry(missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
    	return worldScripts.GalCopBB_Missions.$missionAvailability(missID, missType, origSysID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 50 - deliver medical equipment
    // 52 - virus specimen transport
    this.$missionType5052_Values = function $missionType5052_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var missValues = worldScripts.GalCopBB_CoreMissionValues;
    	var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 50) + 75) / 10) * 10 + (7 - destSysInfo.government) * 20 + missValues.$calcDistanceBonus(routeDistance, 50);
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = result.price / 2;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 51 - emergency patient transport
    this.$missionType51_Values = function $missionType51_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var missValues = worldScripts.GalCopBB_CoreMissionValues;
    	var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 50) + 75) / 10) * 10 + (7 - destSysInfo.government) * 20 + missValues.$calcDistanceBonus(routeDistance, 10);
    	result["expiry"] = clock.adjustedSeconds + routeTime + 600; // we want the time to be tight for the patient transport -- transit time + 10 mins
    	result["penalty"] = result.price / 2;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 53 - collect 3 parts of experimental antidote
    this.$missionType53_Values = function $missionType53_Values(workTime, routeTime, routeDistance, destSysInfo) {
    
    	var textlist = ["edible moth", "tree grub", "solar", "tree wolf", "spotted wolf", "deadly tree ant", "deadly edible grubs",
    		"yaks", "monkey", "mountain goat", "tulip", "banana", "lobstoid", "volcano"
    	];
    	var typelist = ["edible_moths", "tree_grubs", "coronal_plasma", "tree_wolf", "spotted_wolf", "tree_ants", "deadly_grubs",
    		"yak_milk", "monkey_glands", "goat_hair", "tulip_petals", "banana_syrup", "lobstoid_eggs", "magma"
    	];
    
    	var resultset = [];
    	for (var i = 0; i < textlist.length; i++) {
    		var list = this.$getSystemsByDescription(textlist[i]);
    		if (list.length > 0) resultset.push(typelist[i]);
    	}
    
    	if (resultset.length < 3) return null;
    
    	for (var i = 0; i < 5; i++)
    		resultset.sort(function (a, b) {
    			return Math.random() - 0.5;
    		}); // shuffle order so it isn't always the same variant being checked first
    
    	var result = {};
    	result["ingredients"] = [resultset[0], resultset[1], resultset[2]];
    	result["quantity"] = 3;
    	// pay should be good for these (between 1500 and 7500)
    	result["price"] = parseInt((parseInt(Math.random() * 3000 + 500) + parseInt(Math.random() * 2000 + 500) + parseInt(Math.random() * 1000 + 500)) / 10) * 10;
    	result["expiry"] = -1; // no expiry for these missions
    	result["penalty"] = 0;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$getSystemsByDescription = function $getSystemsByDescription(options) {
    	var planets = SystemInfo.filteredSystems(this, function (other) {
    		return (other.description.indexOf(options) >= 0 && other.systemID != system.ID);
    	});
    	return planets;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_confirm_tree_grubs = function $type53_confirm_tree_grubs(missID) {
    	return this.$itemValidation("tree grub", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_collect_tree_grubs = function $type53_collect_tree_grubs(missID) {
    	this.$update53Mission("tree_grubs", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_confirm_edible_moths = function $type53_confirm_edible_moths(missID) {
    	return this.$itemValidation("edible moth", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_collect_edible_moths = function $type53_collect_edible_moths(missID) {
    	this.$update53Mission("edible_moths", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_confirm_coronal_plasma = function $type53_confirm_coronal_plasma(missID) {
    	return this.$itemValidation("solar", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_collect_coronal_plasma = function $type53_collect_coronal_plasma(missID) {
    	this.$update53Mission("coronal_plasma", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_confirm_tree_wolf = function $type53_confirm_tree_wolf(missID) {
    	return this.$itemValidation(["tree wolf", "tree wolves"], missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_collect_tree_wolf = function $type53_collect_tree_wolf(missID) {
    	this.$update53Mission("tree_wolf", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_confirm_spotted_wolf = function $type53_confirm_spotted_wolf(missID) {
    	return this.$itemValidation(["spotted wolf", "spotted wolves"], missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_collect_spotted_wolf = function $type53_collect_spotted_wolf(missID) {
    	this.$update53Mission("spotted_wolf", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_confirm_tree_ants = function $type53_confirm_tree_ants(missID) {
    	return this.$itemValidation("deadly tree ant", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_collect_tree_ants = function $type53_collect_tree_ants(missID) {
    	this.$update53Mission("tree_ants", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_confirm_deadly_grubs = function $type53_confirm_deadly_grubs(missID) {
    	return this.$itemValidation("deadly edible grub", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_collect_deadly_grubs = function $type53_collect_deadly_grubs(missID) {
    	this.$update53Mission("deadly_grubs", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_confirm_yak_milk = function $type53_confirm_yak_milk(missID) {
    	return this.$itemValidation("yak", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_collect_yak_milk = function $type53_collect_yak_milk(missID) {
    	this.$update53Mission("yak_milk", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_confirm_monkey_glands = function $type53_confirm_monkey_glands(missID) {
    	return this.$itemValidation("monkey", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_collect_monkey_glands = function $type53_collect_monkey_glands(missID) {
    	this.$update53Mission("monkey_glands", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_confirm_goat_hair = function $type53_confirm_goat_hair(missID) {
    	return this.$itemValidation("mountain goat", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_collect_goat_hair = function $type53_collect_goat_hair(missID) {
    	this.$update53Mission("goat_hair", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_confirm_tulip_petals = function $type53_confirm_tulip_petals(missID) {
    	return this.$itemValidation("tulip", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_collect_tulip_petals = function $type53_collect_tulip_petals(missID) {
    	this.$update53Mission("tulip_petals", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_confirm_banana_syrup = function $type53_confirm_banana_syrip(missID) {
    	return this.$itemValidation("banana", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_collect_banana_syrup = function $type53_collect_banana_syrip(missID) {
    	this.$update53Mission("banana_syrip", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_confirm_lobstoid_eggs = function $type53_confirm_lobstoid_eggs(missID) {
    	return this.$itemValidation("lobstoid", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_collect_lobstoid_eggs = function $type53_collect_lobstoid_eggs(missID) {
    	this.$update53Mission("lobstoid_eggs", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_confirm_magma = function $type53_confirm_magma(missID) {
    	return this.$itemValidation("volcano", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type53_collect_magma = function $type53_collect_magma(missID) {
    	this.$update53Mission("magma", missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$itemValidation = function $itemValidation(ingr, missID) {
    	var desc = system.info.description;
    	if (Array.isArray(ingr) === true) {
    		var found = false;
    		for (var i = 0; i < ingr.length; i++) {
    			if (desc.indexOf(ingr[i]) >= 0) found = true;
    		}
    		if (found === false) return "Unavailable in this system";
    	} else {
    		if (desc.indexOf(ingr) === -1) return "Unavailable in this system";
    	}
    	if (player.ship.dockedStation.isMainStation === false) return "Unavailable at this station";
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (system.ID === item.source) return "Local supply unsuitable";
    	return "";
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$update53Mission = function $update53Mission(ingrType, missID) {
    	var text = expandDescription("[gcm_ingredient_" + ingrType + "_collected]", {
    		destSystem: system.name
    	});
    	var bb = worldScripts.BulletinBoardSystem;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var item = bb.$getItem(missID);
    
    	// add text to mission details
    	if (item.details.indexOf("*") === -1) item.details += "\n";
    	item.details += text;
    
    	// remove this ingredient from the list
    	for (var i = 0; i < item.data.ingredients.length; i++) {
    		if (item.data.ingredients[i] === ingrType) {
    			item.data.ingredients.splice(i, 1);
    			break;
    		}
    	}
    
    	// update the percentage complete
    	item.data.quantity += 1;
    	bb.$updateBBMissionPercentage(item.ID, (item.data.quantity / item.data.targetQuantity));
    	gcm.$logMissionData(item.ID);
    	player.consoleMessage(expandDescription("[goal_updated]"));
    }
    Scripts/galcopbb_earthquake.js
    "use strict";
    this.name = "GalCopBB_Earthquake";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Controls all earthquake-system mission settings (missions 100-106)";
    this.license = "CC BY-NC-SA 4.0";
    
    /*
    	100-110 Earthquake system missions
    	*100 - Use scan equipment (seismic resonance scanner) to scan planet in 3-4 locations
    		player must get in low enough orbit to perform scan successfully
    		have some way of informing the player where the scan points are - waypoints, must be within 2km of waypoints
    	*101 - transport injured people to hi-techlevel system for treatment
    	*102 - destroy a number of asteroids around the planet. The gravitational effect of the asteroids means instability on the surface
    	103 - collect and deliver special equipment
    	*104 - take seismic data to hi-techlevel system for data analysis
    */
    
    this._debug = false;
    this._earthquakeSystems = []; // systems where there are earthquakes
    this._scanTimer = null; // timer object used when scanning
    this._scanPosition = 0; // current position of the scan
    this._outOfRangeCount = 0; // counts number of times scan attempted to start but was out of range
    this._energy = null; // timer to deduct energy
    this._energyReduction = 3; // amount of energy to deduct each quarter second
    this._waypoints = [];
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
    	var gcm = worldScripts.GalCopBB_Missions;
    	// add these mission types into the main control
    	var list = [100, 101, 102, 104];
    	gcm._availableMissionTypes = gcm._availableMissionTypes.concat(list);
    	this._debug = gcm._debug;
    
    	// exclude some equipment items from being sold/stored by ShipConfig
    	var sc = worldScripts.ShipConfiguration_Core;
    	if (sc) {
    		sc._noSell.push("EQ_GCM_SEISMIC_SCANNER");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function () {
    	this._earthquakeSystems = this.$findEarthquakeSystems();
    	worldScripts.BulletinBoardSystem.$registerBBEvent("GalCopBB_Earthquake", "$custom_shipWillDockWithStation", "shipWillDockWithStation_start");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillLaunchFromStation = function (station) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var seq = ["pwp", "-pwp", "psp", "-psp"];
    	var list = gcm.$getListOfMissions(true, [100, 102]);
    	if (list.length === 0) return;
    	for (var i = 0; i < list.length; i++) {
    		if (list[i].data.missionType === 100 &&
    			list[i].destination === system.ID &&
    			list[i].expiry > clock.adjustedSeconds) {
    			// create waypoints
    			this._waypoints.length = 0;
    			for (var j = 1; j <= 4; j++) {
    				var typ = seq[j - 1];
    				var mult = 1;
    				if (typ.indexOf("-") >= 0) {
    					mult = -1;
    					typ = typ.replace("-", "");
    				}
    				var pos = Vector3D(0, 0, 1.03 * mult).fromCoordinateSystem(typ);
    				system.setWaypoint(this.name + "_" + j, pos, system.mainPlanet.orientation, {
    					size: 50,
    					beaconCode: j.toString(),
    					beaconLabel: "Scan Point " + j
    				});
    				this._waypoints.push(pos);
    			}
    			break;
    		}
    		if (list[i].data.missionType === 102 &&
    			list[i].destination === system.ID &&
    			list[i].expiry > clock.adjustedSeconds) {
    			var checkShips = null;
    			for (var j = 0; j < ((list[i].data.targetQuantity - list[i].data.destroyedQuantity) - list[i].data.quantity); j++) {
    				for (var k = 1; k <= 5; k++) {
    					checkShips = system.addShips("asteroid", 1, system.mainPlanet.position.add(new Vector3D.randomDirection(system.mainPlanet.radius * (1 + (Math.random() * 0.4) + 0.2))), 1)
    					if (checkShips) break;
    				}
    				if (checkShips) {
    					var aster = checkShips[0];
    					aster.setScript("oolite-default-ship-script.js");
    					aster.script.shipDied = this.$aster_shipDied;
    					aster.script._missionID = list[i].ID;
    				}
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$custom_shipWillDockWithStation = function $custom_shipWillDockWithStation(station) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	if (gcm._simulator === true) return;
    	var bb = worldScripts.BulletinBoardSystem;
    	var list = gcm.$getListOfMissions(true, [101, 104]);
    	for (var i = 0; i < list.length; i++) {
    		if (list[i].data.missionType === 101 && station.isMainStation && list[i].destination === system.ID) {
    			if (player.ship.equipmentStatus("EQ_GCM_PATIENT_TRANSPORT") === "EQUIPMENT_OK" && list[i].expiry >= clock.adjustedSeconds) {
    				list[i].arrivalReportText = expandDescription("[missionType101_arrivalReport_complete]", {
    					payment: formatCredits(list[i].payment, false, true)
    				});
    			} else {
    				list[i].arrivalReportText = expandDescription("[missionType101_arrivalReport_late]");
    			}
    
    			list[i].data.quantity = 1;
    			bb.$updateBBMissionPercentage(list[i].ID, 1);
    
    			gcm.$logMissionData(list[i].ID);
    		}
    		if (list[i].data.missionType === 104 && station.isMainStation && list[i].destination === system.ID) {
    			if (list[i].expiry >= clock.adjustedSeconds) {
    				if (player.ship.equipmentStatus("EQ_GCM_UNPROCESSED_SCAN_DATA") === "EQUIPMENT_OK") {
    					list[i].arrivalReportText = expandDescription("[missionType104_arrivalReport_complete]", {
    						payment: formatCredits(list[i].payment, false, true)
    					});
    				} else {
    					list[i].arrivalReportText = expandDescription("[missionType104_arrivalReport_corrupt]", {
    						payment: formatCredits(list[i].payment, false, true)
    					});
    				}
    			} else {
    				list[i].arrivalReportText = expandDescription("[missionType104_arrivalReport_late]");
    			}
    
    			list[i].data.quantity = 1;
    			bb.$updateBBMissionPercentage(list[i].ID, 1);
    
    			gcm.$logMissionData(list[i].ID);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.equipmentDamaged = function (equipmentKey) {
    	if (equipmentKey === "EQ_GCM_UNPROCESSED_SCAN_DATA") {
    		var list = worldScripts.GalCopBB_Missions.$getListOfMissions(true, 104);
    		for (var i = 0; i < list.length; i++) {
    			if (list[i].data.destroyedQuantity === 0) {
    				list[i].data.destroyedQuantity = 1;
    				list[i].payment /= 2;
    				break;
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$aster_shipDied = function $aster_shipDied(whom, why) {
    	//log(this.name, "whom " + whom + ", why " + why);
    	var gcm = worldScripts.GalCopBB_Missions;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(this.ship.script._missionID);
    	if (whom.isPlayer) {
    		item.data.quantity += 1;
    		bb.$updateBBMissionPercentage(item.ID, item.data.quantity / item.data.targetQuantity);
    
    		gcm.$logMissionData(item.ID);
    		player.consoleMessage(expandDescription("[goal_updated]"));
    	} else {
    		item.data.destroyedQuantity += 1;
    		bb.$updateBBMissionPercentage(item.ID, item.data.quantity / item.data.targetQuantity);
    		gcm.$logMissionData(item.ID);
    		player.consoleMessage(expandDescription("[goal_updated]"));
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.mode = function () {}
    
    //-------------------------------------------------------------------------------------------------------------
    this.activated = function () {
    	worldScripts.GalCopBB_Earthquake.$startScanner();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$startScanner = function $startScanner() {
    	if (this._scanTimer && this._scanTimer.isRunning) {
    		player.consoleMessage("Seismic scan process stopped.");
    		worldScripts.GalCopBB_Missions.$playSound("stop");
    		this.$stopTimer();
    		return;
    	}
    	player.consoleMessage("Seismic scan started.");
    	this._scanTimer = new Timer(this, this.$performScan, 1, 1);
    	if (this._energy && this._energy.isRunning) this._energy.stop();
    	this._energy = new Timer(this, this.$depleteEnergy, 0.25, 0.25);
    	worldScripts.GalCopBB_Missions.$playSound("activate");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$performScan = function $performScan() {
    	var p = player.ship;
    	if (p.equipmentStatus("EQ_GCM_SEISMIC_SCANNER") === "EQUIPMENT_DAMAGED") {
    		player.consoleMessage("Scanner equipment is inoperable.");
    		this.$stopTimer();
    		return;
    	}
    	if (this.$isPlayerWithinRange() === false) {
    		this._outOfRangeCount += 1;
    		if (this._outOfRangeCount === 10) this._outOfRangeCount = 0;
    		if (this._outOfRangeCount === 1) player.consoleMessage("Out of range of waypoint. Get within 1km range to continue scan.");
    		return;
    	}
    	this._outOfRangeCount = 0;
    	this._scanPosition += 1;
    	if (parseInt(this._scanPosition / 5) === this._scanPosition / 5) {
    		player.consoleMessage("Seismic scan: " + parseInt((this._scanPosition / 60) * 100) + "% complete");
    	}
    	// check for complete
    	if (this._scanPosition >= 60) {
    		this._scanPosition = 0;
    		this.$stopTimer();
    		var gcm = worldScripts.GalCopBB_Missions;
    		var bb = worldScripts.BulletinBoardSystem;
    		var list = gcm.$getListOfMissions(true, 100);
    		for (var i = 0; i < list.length; i++) {
    			if (system.ID === list[i].source &&
    				list[i].expiry > clock.adjustedSeconds) {
    
    				// remove the waypoint we've finished with
    				for (var j = 0; j < this._waypoints.length; j++) {
    					if (player.ship.position.distanceTo(this._waypoints[j]) < 1000) {
    						system.setWaypoint(this.name + "_" + (j + 1));
    						this._waypoints[j] = system.sun.position;
    						break;
    					}
    				}
    
    				list[i].data.quantity += 1;
    				bb.$updateBBMissionPercentage(list[i].ID, list[i].data.quantity / list[i].data.targetQuantity);
    
    				gcm.$logMissionData(list[i].ID);
    				player.consoleMessage(expandDescription("[goal_updated]"));
    				break;
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$depleteEnergy = function $depleteEnergy() {
    	var p = player.ship;
    	p.energy -= this._energyReduction;
    	if (p.energy < 64) {
    		this.$stopTimer()
    		player.consoleMessage("Insufficient energy to continue scanning.");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns true if player is within 2km of one of the waypoints, otherwise false
    this.$isPlayerWithinRange = function $isPlayerWithinRange() {
    	var inRange = false;
    	for (var i = 0; i < this._waypoints.length; i++) {
    		if (player.ship.position.distanceTo(this._waypoints[i]) < 1000) inRange = true;
    	}
    	return inRange;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$stopTimer = function $stopTimer() {
    	if (this._scanTimer && this._scanTimer.isRunning) {
    		this._scanTimer.stop();
    	}
    	delete this._scanTimer;
    	if (this._energy && this._energy.isRunning) {
    		this._energy.stop();
    	}
    	delete this._energy;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$removeAllWaypoints = function() {
    	// remove any waypoints still left
    	for (var i = 0; i < this._waypoints.length; i++) {
    		system.setWaypoint(this.name + "_" + (i + 1));
    		this._waypoints[i] = system.sun.position;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns a list of sysInfos where earthquakes are a problem
    this.$findEarthquakeSystems = function $findEarthquakeSystems() {
    	return SystemInfo.filteredSystems(this, function (other) {
    		return (other.description.indexOf("earthquake") >= 0 && other.techlevel < 11);
    	});
    }
    
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptedMission = function $acceptedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if (this._debug) log(this.name, "accepted mission id = " + missID);
    
    	if (!item) {
    		log(this.name, "!!ERROR: BB returned null value from $getItem on mission ID " + missID);
    		return;
    	}
    	gcm.$updateLastMissionDate(item.source, item.data.missionType);
    	gcm.$updateGeneralSettings(item);
    
    	// give the player the seismic scanner
    	if (item.data.missionType === 100) {
    		player.ship.awardEquipment("EQ_GCM_SEISMIC_SCANNER");
    	}
    	// give player patient transport for mission type 51
    	if (item.data.missionType === 101) {
    		var eq = EquipmentInfo.infoForKey("EQ_GCM_PATIENT_TRANSPORT");
    		if (player.ship.cargoSpaceAvailable < eq.requiredCargoSpace) this.$freeCargoSpace(eq.requiredCargoSpace, "");
    		player.ship.awardEquipment("EQ_GCM_PATIENT_TRANSPORT");
    	}
    	// give player the unprocessed data
    	if (item.data.missionType === 104) {
    		player.ship.awardEquipment("EQ_GCM_UNPROCESSED_SCAN_DATA");
    	}
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$completedMission = function $completedMission(missID) {
    	var p = player.ship;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// update mission history
    	gcm.$updateSuccessHistoryReputation(item);
    
    	var sbm = worldScripts.Smugglers_BlackMarket;
    
    	// *** type 100 - seismic scanner
    	if (item.data.missionType === 100) {
    		p.removeEquipment("EQ_GCM_SEISMIC_SCANNER");
    		if (sbm) sbm.$removeSaleItem("EQ_GCM_SEISMIC_SCANNER:" + missID);
    	}
    	// *** type 101 - patient transport 
    	if (item.data.missionType === 101) {
    		// if the player has a bounty, doing these missions will reduce it
    		if (player.ship.bounty > 0) {
    			for (var i = 0; i < 5; i++) {
    				if (player.ship.bounty > 0) player.ship.setBounty(player.ship.bounty - 1, "community service");
    			}
    			player.ship.consoleMessage("Your bounty has been reduced.");
    		}
    		// just in case
    		p.removeEquipment("EQ_GCM_PATIENT_TRANSPORT");
    	}
    	// *** type 104 - seismic scan data
    	if (item.data.missionType === 104) {
    		p.removeEquipment("EQ_GCM_UNPROCESSED_SCAN_DATA");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$confirmCompleted = function $confirmCompleted(missID) {
    	var p = player.ship;
    	var result = "";
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (item) {
    		// *** type 101 - patient transport
    		if (item.data.missionType === 101) {
    			var chk = p.equipmentStatus("EQ_GCM_PATIENT_TRANSPORT");
    			if (chk !== "EQUIPMENT_OK" && chk !== "EQUIPMENT_DAMAGED") {
    				result += (result === "" ? "" : "\n") + "The emergency patient transport unit is not on board your ship.";
    			}
    			if (chk === "EQUIPMENT_DAMAGED") {
    				result += (result === "" ? "" : "\n") + "The emergency patient transport unit has been damaged. You can't determine the condition of the patients inside.";
    			}
    		}
    		if (item.data.missionType === 104) {
    			var chk = p.equipmentStatus("EQ_GCM_UNPROCESSED_SCAN_DATA")
    			if (chk !== "EQUIPMENT_OK" && chk !== "EQUIPMENT_DAMAGED") result += (result === "" ? "" : "\n") + "Seismic scan data not on board";
    		}
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$terminateMission = function $terminateMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	this.$removeAllWaypoints();
    
    	// adjust reputation only when the terminatePenalty flag is set to true 
    	if (item.data.terminatePenalty === true) {
    		// update mission history
    		gcm.$updateFailedHistoryReputation(item);
    	}
    
    	// *** type 100 - seismic scanner
    	if (item.data.missionType === 100) {
    		var eqsts = player.ship.equipmentStatus("EQ_GCM_SEISMIC_SCANNER");
    		if (eqsts === "EQUIPMENT_OK" || eqsts === "EQUIPMENT_DAMAGED") {
    			player.ship.removeEquipment("EQ_GCM_SEISMIC_SCANNER");
    			if (sbm) sbm.$removeSaleItem("EQ_GCM_SEISMIC_SCANNER:" + missID);
    		}
    	}
    	// *** type 101 - patient transport
    	if (item.data.missionType === 101) {
    		player.ship.removeEquipment("EQ_GCM_PATIENT_TRANSPORT");
    	}
    	// *** type 104 - seismic data transfer
    	if (item.data.missionType === 104) {
    		player.ship.removeEquipment("EQ_GCM_UNPROCESSED_SCAN_DATA");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$failedMission = function $failedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var eq = "";
    
    	if (!sbm) {
    		// if there's no blackmarket option, just remove the equipment
    		this.$terminateMission(missID);
    		return;
    	}
    
    	this.$removeAllWaypoints();
    
    	var item = bb.$getItem(missID);
    	gcm.$updateFailedHistoryReputation(item);
    
    	// *** type 100 - seismic scanner
    	if (item.data.missionType === 100) {
    		var eqsts = player.ship.equipmentStatus("EQ_GCM_SEISMIC_SCANNER");
    		if (eqsts === "EQUIPMENT_OK" || eqsts === "EQUIPMENT_DAMAGED") {
    			eq = "EQ_GCM_SEISMIC_SCANNER";
    		}
    	}
    	if (item.data.missionType === 101) {
    		// we'll just remove this one
    		player.ship.removeEquipment("EQ_GCM_PATIENT_TRANSPORT");
    		eq = "";
    	}
    
    	if (eq != "") gcm._equipmentFromFailedMissions.push({
    		missionType: item.data.missionType,
    		source: item.source,
    		equip: eq,
    		date: clock.adjustedSeconds
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateManifestEntry = function $updateManifestEntry(missID) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	gcm.$updateManifestEntry(missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
    	return worldScripts.GalCopBB_Missions.$missionAvailability(missID, missType, origSysID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 100
    this.$missionType100_Values = function $missionType100_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var missValues = worldScripts.GalCopBB_CoreMissionValues;
    	var result = {};
    	result["quantity"] = 4;
    	result["price"] = 100 + parseInt((8 - destSysInfo.government) * (Math.random() * 50));
    	result["expiry"] = clock.adjustedSeconds + (3600 * 1.5); // 1.5 hours to complete
    	result["penalty"] = result.price / 4;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 101 - emergency patient transport
    this.$missionType101_Values = function $missionType101_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var missValues = worldScripts.GalCopBB_CoreMissionValues;
    	var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 50) + 75) / 10) * 10 + (7 - destSysInfo.government) * 20 + missValues.$calcDistanceBonus(routeDistance, 10);
    	result["expiry"] = clock.adjustedSeconds + routeTime + 600; // we want the time to be tight for the patient transport -- transit time + 10 mins
    	result["penalty"] = result.price / 2;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 102
    this.$missionType102_Values = function $missionType102_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var missValues = worldScripts.GalCopBB_CoreMissionValues;
    	var result = {};
    	result["quantity"] = Math.floor(Math.random() * 20) + 20;
    	result["price"] = result.quantity * 8 + (Math.floor(Math.random() * 40) + 20);
    	result["expiry"] = clock.adjustedSeconds + (3600 * 3); // 3 hours to complete
    	result["penalty"] = 0;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 104
    this.$missionType104_Values = function $missionType104_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var missValues = worldScripts.GalCopBB_CoreMissionValues;
    	var result = {};
    	result["quantity"] = 1;
    	result["price"] = 500 + parseInt((8 - destSysInfo.government) * (Math.random() * 90)) + missValues.$calcDistanceBonus(routeDistance, 10);
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime;
    	result["penalty"] = result.price / 2;
    	return result;
    }
    Scripts/galcopbb_ejectiondamper.js
    "use strict";
    this.name = "GalCopBB_EjectionDamper";
    this.author = "phkb";
    this.copyright = "2018 phkb";
    this.description = "Equipment to cause ejected cargo to slow down to a stop, rather than to float away at a constant speed.";
    this.license = "CC BY-NC-SA 4.0";
    
    this._cargoList = [];
    this._resetTimer = null;
    this._usesLeft = 0;
    this._unitOn = false;
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function () {
        if (missionVariables.GalCopBBMissions_EjectionDamper) this._usesLeft = parseInt(missionVariables.GalCopBBMissions_EjectionDamper);
        if (player.ship.equipmentStatus("EQ_GCM_Ejection_REMOVE") === "EQUIPMENT_OK") player.ship.removeEquipment("EQ_GCM_Ejection_REMOVE");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillSaveGame = function () {
        missionVariables.GalCopBBMissions_EjectionDamper = this._usesLeft;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillDockWithStation = function (station) {
        this._unitOn = false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = function (whom, why) {
        if (this._resetTimer && this._resetTimer.isRunning) this._resetTimer.stop();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerBoughtEquipment = function (equipmentKey) {
        if (equipmentKey === "EQ_GCM_EJECTION_DAMPER") {
            var eq = EquipmentInfo.infoForKey(equipmentKey);
            this._usesLeft = eq.scriptInfo.max_uses;
        }
        if (equipmentKey === "EQ_GCM_EJECTION_DAMPER_REMOVE") {
            player.ship.removeEquipment(equipmentKey);
            player.ship.removeEquipment("EQ_GCM_EJECTION_DAMPER");
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.mode = this.activated = function () {
        var ws = worldScripts.GalCopBB_EjectionDamper;
        ws._unitOn = !ws._unitOn;
        switch (ws._unitOn) {
            case true:
                player.consoleMessage("Ejection damper is on. " + ws._usesLeft + " uses remaining", 3);
                this.$playSound("on");
                break;
            case false:
                player.consoleMessage("Ejection damper is off.", 3);
                this.$playSound("off");
                break;
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDumpedCargo = function (cargo) {
        if (this._unitOn === true) {
            this._usesLeft -= 1;
            player.consoleMessage("Ejection dampening applied. " + this._usesLeft + " uses remaining.", 2);
            cargo.maxThrust = 1;
            cargo.thrust = 1;
            this._cargoList.push(cargo);
            if (this._resetTimer == null || this._resetTimer.isRunning === false) {
                this._resetTimer = new Timer(this, this.$resetCargo, 2, 2);
            }
            if (this._usesLeft <= 0) {
                this._usesLeft = 0;
                this._unitOn = false;
                this.$playSound("off");
                player.ship.removeEquipment("EQ_GCM_EJECTION_DAMPER");
                player.consoleMessage("Ejection damper expended and removed.", 5);
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$resetCargo = function $resetCargo() {
        if (this._cargoList.length === 0) {
            this._resetTimer.stop();
            this._resetTimer = null;
            return;
        }
        for (var i = this._cargoList.length - 1; i >= 0; i--) {
            var cargo = this._cargoList[i];
            if (!cargo.velocity || cargo.velocity.magnitude() === 0) {
                if (cargo.isValid && cargo.isInSpace) {
                    cargo.maxThrust = 0;
                    cargo.thrust = 0;
                }
                this._cargoList.splice(i, 1);
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // play the buy/sell sound effects
    this.$playSound = function $playSound(soundtype) {
        var mySound = new SoundSource;
        switch (soundtype) {
            case "on":
                mySound.sound = "[@switch-on]";
                break;
            case "off":
                mySound.sound = "[@switch-off]";
                break;
        }
        mySound.loop = false;
        mySound.play();
    }
    Scripts/galcopbb_escapepods.js
    "use strict";
    this.name = "GalCopBB_EscapePods";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Control code for derelict/blackbox missions (missions 20/21/25)";
    this.license = "CC BY-NC-SA 4.0";
    
    this._periodicWaypointTimer = null; // timer to control the updating of periodic waypoints
    this._maxLocations = 0;
    this._setData = [];
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
    	var gcm = worldScripts.GalCopBB_Missions;
    	// add these mission types into the main control
    	var list = [20, 21, 25];
    	gcm._availableMissionTypes = gcm._availableMissionTypes.concat(list);
    	gcm._interstellarMissionTypes.push(21);
    	// position 7 is not used in any of these mission types, so limit the possibilities 
    	this._maxLocations = gcm._positions.length - 1;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.systemWillPopulate = function () {
    	// reset the mission populator data array
    	this._setData.length = 0;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var list = gcm.$getListOfMissions(true, [20, 25]);
    
    	if (list.length > 0) {
    		// loop through all active missions and see if any need to be set up for this system
    		for (var i = 0; i < list.length; i++) {
    			// *** type 20 - escape pod
    			if (list[i].data.missionType === 20 &&
    				list[i].destination === system.ID &&
    				list[i].data.quantity === 0 &&
    				list[i].data.destroyedQuantity === 0 &&
    				list[i].expiry > clock.adjustedSeconds) {
    
    				// add the escape pod
    				var position = gcm.$getRandomPosition(list[i].data.locationType, 0.1, list[i].ID).position;
    
    				this._setData.push({
    					missionType: 20,
    					missionID: list[i].ID,
    					source: list[i].source,
    					target: list[i].data.targetQuantity,
    					goons: 0,
    					quantity: 0
    				});
    
    				// add pod with populator
    				system.setPopulator("gcm-escape-pod-" + list[i].ID, {
    					callback: function (pos) {
    						var missData = worldScripts.GalCopBB_EscapePods.$getMissionData(20);
    						var ep = null;
    						var checkShips = null;
    						for (var j = 1; j <= 5; j++) {
    							checkShips = system.addShips((missData.target === 1 ? "gcm_stricken_pod" : "gcm_stricken_pod_dead"), 1, pos, 1000);
    							if (checkShips) ep = checkShips[0];
    							if (ep) break;
    						}
    						if (ep) {
    							// make the escape pod look broken
    							ep.maxSpeed = 0;
    							ep.desiredSpeed = 0;
    							ep.reactToAIMessage("START_TUMBLING");
    							ep.script._missionID = missData.missionID;
    							ep.script._sentDistress = false;
    							// monkey patch if necessary
    							// add our shipDied event to the escape pod
    							if (ep.script.shipDied) ep.script.$gcm_hold_shipDied = ep.script.shipDied;
    							ep.script.shipDied = worldScripts.GalCopBB_EscapePods.$gcm_entity_shipDied;
    
    							if (missData.target === 1) {
    								// add occupant
    								ep.setCrew({
    									name: randomName() + " " + randomName(),
    									insurance: parseInt(Math.random() * 100 + 50),
    									bounty: 0,
    									origin: missData.source,
    									seed: "0 0 0 0 " + missData.source + " 2"
    								});
    								ep.script._missionID_lifeSupport = ep.script._missionID;
    								worldScripts.GalCopBB_LifeSupport.$addShip({
    									ent: ep,
    									type: "damaged escape pod",
    									remaining: Math.floor(Math.random() * 120 + worldScripts.GalCopBB_LifeSupport._lifeSupportDefaultTime),
    									last: 0,
    									comms: false
    								});
    							} else {
    								ep.setCrew(null);
    							}
    						} else {
    							log("galcopBB_escapepods", "!!ERROR: Escape pod not spawned!");
    						}
    						// spawn some alloy wreckage as well
    						system.addShips("scarred-alloy", (Math.floor(Math.random() * 5) + 2), pos, 5000);
    
    					}.bind(this),
    					location: "COORDINATES",
    					coordinates: position
    				});
    
    				if (gcm._debug) gcm.$setWaypoint(position, [0, 0, 0, 0], "D", "Debug position (20)", "20");
    			}
    
    			// *** type 25 - runaway escape pod
    			if (list[i].data.missionType === 25 &&
    				list[i].destination === system.ID &&
    				list[i].data.quantity === 0 &&
    				list[i].data.destroyedQuantity === 0 &&
    				list[i].expiry > clock.adjustedSeconds) {
    
    				this._setData.push({
    					missionType: 25,
    					missionID: list[i].ID,
    					source: list[i].source,
    					goons: 0,
    					target: list[i].data.targetQuantity,
    					quantity: 0
    				});
    
    				// add pod with populator
    				system.setPopulator("gcm-escape-pod-" + list[i].ID, {
    					callback: function (pos) {
    						var missData = worldScripts.GalCopBB_EscapePods.$getMissionData(25);
    						var ep = null;
    						var checkShips = null;
    						for (var j = 1; j <= 5; j++) {
    							checkShips = system.addShips((missData.target === 1 ? "gcm_runaway_pod" : "gcm_stricken_pod_dead"), 1, pos, player.ship.scannerRange);
    							if (checkShips) ep = checkShips[0];
    							if (ep) break;
    						}
    						if (ep) {
    							// give the escape pod some serious speed
    							ep.maxSpeed = player.ship.maxSpeed * player.ship.injectorSpeedFactor - 100;
    							ep.maxThrust = 0; // make the pod frictionless
    							// give the escape pod a heading and velocity
    							ep.orientation = Quaternion.random();
    							ep.desiredSpeed = ep.maxSpeed;
    
    							ep.script._missionID = missData.missionID;
    							ep.script._sentDistress = false;
    							// add our shipDied event to the escape pod
    							// monkey patch if necessary
    							if (ep.script.shipDied) ep.script.$gcm_hold_shipDied = ep.script.shipDied;
    							ep.script.shipDied = worldScripts.GalCopBB_EscapePods.$gcm_entity_shipDied;
    
    							// add occupant
    							if (missData.target === 1) {
    								ep.setCrew({
    									name: randomName() + " " + randomName(),
    									insurance: parseInt(Math.random() * 100 + 50),
    									bounty: 0,
    									origin: missData.source,
    									seed: "0 0 0 0 " + missData.source + " 2"
    								});
    								ep.script._missionID_lifeSupport = ep.script._missionID;
    								worldScripts.GalCopBB_LifeSupport.$addShip({
    									ent: ep,
    									type: "runaway escape pod",
    									remaining: Math.floor(Math.random() * 120 + worldScripts.GalCopBB_LifeSupport._lifeSupportDefaultTime * 0.75),
    									last: 0,
    									comms: false
    								});
    							} else {
    								ep.setCrew(null);
    							}
    						} else {
    							log("galcopBB_escapepods", "!!ERROR: Escape pod not spawned!");
    						}
    					}.bind(this),
    					location: "INNER_SYSTEM_OFFPLANE",
    					locationSeed: list[i].ID
    				});
    
    				// start periodic waypoint
    				this._periodicWaypointTimer = new Timer(this, this.$startPeriodicWaypoint, 10, 0);
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.interstellarSpaceWillPopulate = function () {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var list = gcm.$getListOfMissions(true, 21);
    	if (list.length > 0) {
    		// loop through all active missions and see if any need to be set up for this system
    		for (var i = 0; i < list.length; i++) {
    			// *** type 21 - escape pod in interstellar
    			if (list[i].data.missionType === 21 &&
    				((gcm._fromSystem === list[i].destination || gcm._fromSystem === list[i].source) &&
    					(gcm._toSystem === list[i].destination || gcm._toSystem === list[i].source)) &&
    				list[i].data.quantity === 0 &&
    				list[i].data.destroyedQuantity === 0 &&
    				list[i].expiry > clock.adjustedSeconds) {
    
    				// add the escape pod somewhere just inside scanner range
    				var dist = (Math.random() * (player.ship.scannerRange * 0.2)) + (player.ship.scannerRange * 0.75);
    				var dir = Vector3D.randomDirection(dist);
    				var position = Vector3D(0, 0, 0).add(dir);
    
    				this._setData.push({
    					missionType: 21,
    					missionID: list[i].ID,
    					source: list[i].source,
    					target: list[i].data.targetQuantity,
    					goons: 0,
    					quantity: 0
    				});
    
    				// add pod with populator
    				system.setPopulator("gcm-escape-pod-" + list[i].ID, {
    					callback: function (pos) {
    						var missData = worldScripts.GalCopBB_EscapePods.$getMissionData(21);
    						var ep = null;
    						var checkShips = null;
    						for (var j = 1; j <= 5; j++) {
    							checkShips = system.addShips((missData.target === 1 ? "gcm_stricken_pod" : "gcm_stricken_pod_dead"), 1, pos, 1)
    							if (checkShips) ep = checkShips[0];
    							if (ep) break;
    						}
    						if (ep) {
    							// make the escape pod look broken
    							ep.maxSpeed = 0;
    							ep.desiredSpeed = 0;
    							ep.script._missionID = missData.missionID;
    							ep.script._sentDistress = false;
    							ep.reactToAIMessage("START_TUMBLING");
    							// monkey patch if necessary
    							// add our shipDied event to the escape pod
    							if (ep.script.shipDied) ep.script.$gcm_hold_shipDied = ep.script.shipDied;
    							ep.script.shipDied = worldScripts.GalCopBB_EscapePods.$gcm_entity_shipDied;
    
    							// add occupant
    							if (missData.target === 1) {
    								ep.setCrew({
    									name: randomName() + " " + randomName(),
    									insurance: parseInt(Math.random() * 100 + 50),
    									bounty: 0,
    									origin: missData.source,
    									seed: "0 0 0 0 " + missData.source + " 2"
    								});
    								// configure the life support countdown for this ship
    								ep.script._missionID_lifeSupport = ep.script._missionID;
    								worldScripts.GalCopBB_LifeSupport.$addShip({
    									ent: ep,
    									type: "damaged escape pod",
    									remaining: Math.floor(Math.random() * 60 + worldScripts.GalCopBB_LifeSupport._lifeSupportDefaultTime / 5),
    									last: 0,
    									comms: false
    								});
    							} else {
    								ep.setCrew(null);
    							}
    
    						} else {
    							log("galcopBB_escapepods", "!!ERROR: Escape pod not spawned!");
    						}
    						// spawn some alloy wreckage as well
    						system.addShips("scarred-alloy", (Math.floor(Math.random() * 5) + 2), pos, 5000);
    					}.bind(this),
    					location: "COORDINATES",
    					coordinates: position
    				});
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillEnterWitchspace = function () {
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = function (whom, why) {
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipScoopedOther = function (whom) {
    	if (!whom || !whom.isValid) return;
    	var gcm = worldScripts.GalCopBB_Missions;
    	if (whom.hasRole("gcm_special_pod") === true) {
    		var bb = worldScripts.BulletinBoardSystem;
    		var item = bb.$getItem(whom.script._missionID);
    
    		if (item && item.data.targetQuantity === 1) {
    			item.data.quantity = 1;
    			bb.$updateBBMissionPercentage(item.ID, 1);
    
    			gcm.$logMissionData(item.ID);
    			player.consoleMessage(expandDescription("[goal_updated]"));
    
    			gcm._storedClientName = whom.crew[0].name;
    			if (worldScripts.GalCopBB_Missions_MFD._escapePod == null) {
    				gcm.$postScoopMissionCreation(whom.script._missionID, 7);
    				if (whom.script.shipWasDumped) whom.script.$gcmovr_shipWasDumped = whom.script.shipWasDumped;
    				whom.script.shipWasDumped = worldScripts.GalCopBB_Missions_MFD.$gcmmfd_shipWasDumped;
    				worldScripts.GalCopBB_Missions_MFD._escapePod = whom;
    			}
    		}
    		return;
    	}
    	if (whom.primaryRole === "escape-capsule" && whom.script.hasOwnProperty("_gcm_already_asked") == false) {
    		// this is a generic escape capsule
    		// work out what type of rescue it is (bounty or insurance)
    		// straight insurance job
    		if (whom.crew && whom.crew[0].insuranceCredits > 0 && whom.crew[0].legalStatus === 0 &&
    			(gcm._newMissionDelayTimer == null || gcm._newMissionDelayTimer.isRunning === false)) {
    			// consider adding a secondary mission
    			// use a negative to make the routine run as if a type 20 (escape pod) mission has just been completed
    			if (Math.random() > 0.8 && worldScripts.GalCopBB_Missions_MFD._escapePod == null) { // 0.8
    				gcm._storedClientName = whom.crew[0].name;
    				gcm.$postScoopMissionCreation(-20, 7);
    				if (whom.script.shipWasDumped) whom.script.$gcmovr_shipWasDumped = whom.script.shipWasDumped;
    				whom.script.shipWasDumped = worldScripts.GalCopBB_Missions_MFD.$gcmmfd_shipWasDumped;
    				worldScripts.GalCopBB_Missions_MFD._escapePod = whom;
    			}
    		}
    		// straight bounty claim
    		if (whom.crew && whom.crew[0].insuranceCredits === 0 && whom.crew[0].legalStatus > 0) {
    			// do we have any secondary missions a pirate would want to initiate?
    		}
    		// set a flag so that if the player dumps and then rescoops the capsule we don't keep getting new mission offers.
    		whom.script._gcm_already_asked = true;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // handles shipDied for escape pod (20) missions
    this.$gcm_entity_shipDied = function $gcm_entity_shipDied(whom, why) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var bb = worldScripts.BulletinBoardSystem;
    
    	if (gcm._debug) log(this.name, "running shipDied for " + this.ship + ": reason " + why + ", " + whom);
    	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);
    
    	var item = bb.$getItem(this.ship.script._missionID);
    	if (item) item.data.destroyedQuantity = 1;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // stop all timers
    this.$stopTimers = function $stopTimers() {
    	if (this._periodicWaypointTimer && this._periodicWaypointTimer.isRunning) this._periodicWaypointTimer.stop();
    	delete this._periodicWaypointTimer;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // turns on an intermittent waypoint at the last known location of the runaway escape pod
    this.$startPeriodicWaypoint = function $startPeriodicWaypoint() {
    	var pods = system.shipsWithPrimaryRole("gcm_runaway_pod");
    	if (pods && pods.length > 0) {
    		for (var i = 0; i < pods.length; i++) {
    			worldScripts.GalCopBB_Missions.$setWaypoint(pods[i].position, pods[i].orientation, "E", "Runaway escape pod", "runaway_" + pods[i].script._missionID);
    		}
    		this._periodicWaypointTimer = new Timer(this, this.$endPeriodicWaypoint, Math.floor(Math.random() * 10) + 10, 0);
    		player.consoleMessage("Escape pod navigation beacon detected")
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // turns off the intermittent waypoint
    this.$endPeriodicWaypoint = function $endPeriodicWaypoint() {
    	if (worldScripts.GalCopBB_Missions.$removeSpecialWaypoint("_runaway") === true) {
    		player.consoleMessage("Escape pod navigation beacon lost")
    		// set timer to turn them on again
    		this._periodicWaypointTimer = new Timer(this, this.$startPeriodicWaypoint, Math.floor(Math.random() * 10) + 10, 0);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // gets mission specific data for the populator routines
    // this works on a first in/first out basis - if there are multiple missions of the same type being populated, the mission specific data would
    // get pushed in to the setData array in order, and then this routine pulls that data out in the same order
    // that's the theory, anyway!
    this.$getMissionData = function $getMissionData(missionType) {
    	for (var i = 0; i < this._setData.length; i++) {
    		if (this._setData[i].missionType === missionType) {
    			var result = {
    				missionID: this._setData[i].missionID,
    				trueMissionType: (this._setData[i].trueMissionType ? this._setData[i].trueMissionType : missionType),
    				source: this._setData[i].source,
    				goons: this._setData[i].goons,
    				quantity: this._setData[i].quantity,
    				target: this._setData[i].target
    			};
    			this._setData.splice(i, 1);
    			return result;
    		}
    	}
    	return null;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 20 - rescue damaged escape pod
    this.$missionType20_Values = function $missionType20_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	// pick a location
    	result["locationType"] = Math.floor(Math.random() * this._maxLocations);
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 50) + 100) / 10) * 10 + (7 - destSysInfo.government) * 50 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 10) // plus a distance bonus
    		+
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price / 2);
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 21 - rescue stranded escape pod interstellar
    this.$missionType21_Values = function $missionType21_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 500) + 500) / 10) * 10 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 10) // plus a distance bonus
    		+
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(1000); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price / 2);
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 25 - rescue runaway escape pod
    this.$missionType25_Values = function $missionType25_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 50) + 100) / 10) * 10 + (7 - destSysInfo.government) * 50 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 10) // plus a distance bonus
    		+
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price / 2);
    	return result;
    }
    Scripts/galcopbb_goonsquads.js
    "use strict";
    this.name = "GalCopBB_GoonSquads";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Looks after all investigation missions (130-139)";
    this.license = "CC BY-NC-SA 4.0";
    
    this._goonSquads = []; // array of goon squad and lurk positions
    this._goonSquadSetup = null; // timer used to configure goon squads
    
    //-------------------------------------------------------------------------------------------------------------
    this.systemWillRepopulate = function () {
    	// give good squad their lurk position
    	if (this._goonSquads.length > 0 && (this._goonSquadSetup == null || this._goonSquadSetup.isRunning === false)) {
    		this._goonSquadSetup = new Timer(this, this.$giveGoonSquadLurkPosition, 1, 0);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$createGoonSquad = function $createGoonSquad(goons, pos, spread) {
    	var pop = worldScripts["oolite-populator"];
    	var grp = system.addGroup("pirate", goons, pos, spread);
    	var gn = grp.ships;
    	if (gn && gn.length > 0) {
    		for (var j = 0; j < gn.length; j++) {
    			// configure our pirates
    			gn[j].setBounty(20 + system.info.government + goons + Math.floor(Math.random() * 8), "setup actions");
    			// make sure the pilot has a bounty
    			gn[j].setCrew({
    				name: randomName() + " " + randomName(),
    				bounty: gn[j].bounty,
    				insurance: 0
    			});
    			if (gn[j].hasHyperspaceMotor) {
    				pop._setWeapons(gn[j], 1.75); // bigger ones sometimes well-armed
    			} else {
    				pop._setWeapons(gn[j], 1.3); // rarely well-armed
    			}
    			// in the safer systems, rarely highly skilled (the skilled ones go elsewhere)
    			pop._setSkill(gn[j], 4 - system.info.government);
    			if (Math.random() * 16 < system.info.government) {
    				pop._setMissiles(gn[j], -1);
    			}
    			// make sure the AI is switched
    			gn[j].switchAI("gcm-pirateAI.js");
    		}
    		this._goonSquads.push({
    			group: grp,
    			position: pos
    		});
    	} else {
    		log(this.name, "!!ERROR: Goon squad not spawned!");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // makes the goon squads lurk in a particular position
    this.$giveGoonSquadLurkPosition = function $giveGoonSquadLurkPosition() {
    	var retry = false;
    	for (var i = 0; i < this._goonSquads.length; i++) {
    		var grp = this._goonSquads[i].group;
    		var pos = this._goonSquads[i].position;
    		if (grp.leader) {
    			if (grp.leader.AIScript.oolite_priorityai) {
    				grp.leader.AIScript.oolite_priorityai.setParameter("oolite_pirateLurk", pos);
    				grp.leader.AIScript.oolite_priorityai.reconsiderNow();
    				for (var j = 0; j < grp.ships.length; j++) {
    					if (grp.leader !== grp.ships[j]) {
    						var shp = grp.ships[j];
    						if (shp.AIScript.oolite_priorityai) {
    							shp.AIScript.oolite_priorityai.setParameter("oolite_pirateLurk", pos);
    							shp.AIScript.oolite_priorityai.reconsiderNow();
    						} else {
    							retry = true;
    							break;
    						}
    					}
    				}
    			} else {
    				retry = true;
    				break;
    			}
    		}
    		if (retry === true) break;
    	}
    	if (retry === true) {
    		this._goonSquadSetup = new Timer(this, this.$giveGoonSquadLurkPosition, 1, 0);
    	} else {
    		this._goonSquads.length = 0;
    	}
    }
    Scripts/galcopbb_hitteams.js
    "use strict";
    this.name = "GalCopBB_HitTeams";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Controls movement and deployment of hit teams";
    this.license = "CC BY-NC-SA 4.0";
    
    /*
    99 times out of 100 this code will never run. If players do the missions as prescribed, even if they fail the mission, everything will be fine and we'll never get here.
    
    We will only get here if the player attempted to sell on the black market any special equipment or cargo given to them as part of the mission. Players will have to go out of their way to experience this code.
    However, with "open"-style play, the player should have the freedom to do whatever they want, and that's where this code comes into play. The player can sell the equipment, but if they do the source system is likely to be quite upset and send a "hit team" after them.
    
    Hit teams will be larger than a normal assassin team, 8-15 ships based on type of mission, as an incentive to do the right thing.
    
    If the hit team is spawned, and the player manages to evade them, they will keep turning up at each witchpoint until they are all destroyed
    
    process overview
    1. set up team size, ship config, location, and maxPursuitDistance (how far they will go in pursuit)
    	distance is decided upon based on the projected impact of the betrayal.
    		selling cargo is low, selling medical supplies or virus samples is high
    		
    2. set up the process of movement, executed with each witchspace jump by the player
    	head to the players last known position via the fastest route.
    	ie. a. get a route from player system to team's current position optimised for time
    		b. based on the time taken by the players jump, work out which system the team will reach
    		c. if the team hits their travel limit, drop the team from pursuit and just put them at the witchpoint of the source system, waiting for the player's arrival sometime in the future. 
    			Player will be flagged as a fugitive in this system
    		d. if the team can arrive ahead of the player, put the team at the witchpoint with orders to kill. drop the team from the master list - they've served their purpose
    
    TODO: create a F4 interface screen for disposal of equipment from failed missions
    
    */
    
    this._preferredAssassinShips = [];
    this._lastSystem = -1;
    this._lastGal = -1;
    this._startTime = 0;
    this._doArrivalMessages = false;
    this._hitTeams = []; // array of dictionary objects defining hit teams
    /*		source					(int) system ID of the original mission
    		location				(int) system ID of the teams current position
    		reason					text string containing reason player is being pursued (eg "selling data on Black Market)
    		ships 					[array of ship defs] basic ship definitions for each of the team members (assumuption that index 0 is leader)
    									dataKey
    									name
    									entityPersonality
    		distance				(float) current distance travelled
    		maxPursuitDistance		(int) how far the team is willing to go to get the player
    */
    this._createTeams = []; // array of index values relating to _hitTeams - these teams will be created during system population
    this._suppressMissions = []; // array of system ID's and dates where the player is persona non grata therefore no missions
    this._warnPlayer = false;
    this._warnPlayerSource = 0;
    this._warnPlayerCurrent = 0;
    
    this._sellInfo = {
    	"DTA_DATA_PACKAGE": {
    		cost: 1500,
    		description: "Sensitive data package"
    	},
    	"DTA_DATA_DELIVERY": {
    		cost: 1500,
    		description: "Sensitive data package"
    	},
    	"DTA_GALCOP_SEC_SOFTWARE": {
    		cost: 2000,
    		description: "GalCop station monitoring software"
    	},
    	"EQ_GCM_BLACK_BOX": {
    		cost: 800,
    		description: "Black box data recorder"
    	},
    	"EQ_GCM_STOLEN_SCHEMATICS": {
    		cost: 13000,
    		description: "Stolen prototype ship schematics"
    	},
    	"EQ_GCM_STOLEN_CODES": {
    		cost: 13000,
    		description: "Stolen high-level security codes"
    	},
    	"EQ_GCM_STOLEN_DOCUMENTS": {
    		cost: 13000,
    		description: "Stolen classified documents"
    	},
    	"EQ_GCM_STOLEN_WEAPONDESIGNS": {
    		cost: 13000,
    		description: "Stolen military weapon designs"
    	},
    	"EQ_GCM_MEDICAL_SUPPLIES": {
    		cost: 2000,
    		description: "Biological medical supplies"
    	},
    	"EQ_GCM_VIRUS_SPECIMENS": {
    		cost: 4000,
    		description: "Pathological virus specimens"
    	},
    	"EQ_GCM_WBSA": {
    		cost: 5000,
    		description: "Witchpoint Beacon Security Access device"
    	},
    	"EQ_GCM_SOFTWARE": {
    		cost: 25000,
    		description: "GalCop station security software"
    	},
    	"EQ_GCM_SOLAR_SCANNER": {
    		cost: 1000,
    		description: "Solar activity scanner"
    	},
    	"EQ_GCM_SEISMIC_SCANNER": {
    		cost: 1500,
    		description: "Seismic resonance scanner"
    	},
    	"EQ_GCM_RECOVERED_CARGO": {
    		cost: 2000,
    		description: "High-value parcel recovered from cargopod"
    	},
    };
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function () {
    	if (missionVariables.GalCopBB_HitTeams) this._hitTeams = JSON.parse(missionVariables.GalCopBB_HitTeams);
    	if (missionVariables.GalCopBB_SuppressSystems) this._suppressMissions = JSON.parse(missionVariables.GalCopBB_SuppressSystems);
    	this.$populateBlackMarketSaleList();
    	this.$getPreferredShipList();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillSaveGame = function () {
    	if (this._hitTeams.length > 0) missionVariables.GalCopBB_HitTeams = JSON.stringify(this._hitTeams);
    	if (this._suppressMissions.length > 0) missionVariables.GalCopBB_SuppressSystems = JSON.stringify(this._suppressMissions);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillDockWithStation = function (station) {
    	if (worldScripts.GalCopBB_Missions._simulator === true) return;
    	this.$populateBlackMarketSaleList();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDockedWithStation = function (station) {
    	if (worldScripts.GalCopBB_Missions._simulator === true) return;
    	if (this._warnPlayer === true) {
    		player.addMessageToArrivalReport(expandDescription("[gcm_hitteam_progress]", {
    			system: System.systemNameForID(this._warnPlayerSource),
    			location: System.systemNameForID(this._warnPlayerCurrent)
    		}));
    		this._warnPlayer = false;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillEnterWitchspace = function (cause, destination) {
    	this._lastSystem = system.ID;
    	this._lastGal = galaxyNumber;
    	this._startTime = clock.seconds;
    	this._doArrivalMessages = true;
    
    	// clean up
    	for (var i = this._suppressMissions.length - 1; i >= 0; i--) {
    		if (this._suppressMissions[i].date < clock.adjustedSeconds) this._suppressMissions.splice(i, 1);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.systemWillPopulate = function () {
    	// how'd we get here? gal jump? reset values and exit
    	if (this._hitTeams.length === 0) return;
    	if (this._lastGal != -1 && this._lastGal != galaxyNumber) {
    		this._hitTeams.length = 0;
    		this._suppressMissions.length = 0;
    		return;
    	}
    	this._createTeams.length = 0;
    	for (var i = this._hitTeams.length - 1; i >= 0; i--) {
    		var team_route = System.infoForSystem(galaxyNumber, this._hitTeams[i].location).routeToSystem(system.info, "OPTIMIZED_BY_TIME");
    		if (team_route == null) {
    			// player has moved to an unreachable location - end the team and flag the player as a fugitive in the source system (if bounty system is installed)
    		} else {
    			// how long did the player take to jump
    			var elapsed_time = clock.adjustedSeconds - this._startTime;
    			// how far along the route would would that take the team?
    			// add up the time for each jump from start point until the time taken is greater than the elapsed time
    			var step_route = null;
    			var gaveup = false;
    			for (var j = 1; j < team_route.route.length; j++) {
    				step_route = System.infoForSystem(galaxyNumber, this._hitTeams[i].location).routeToSystem(System.infoForSystem(galaxyNumber, team_route.route[j]), "OPTIMIZED_BY_TIME");
    				var step_time = step_route.time * 3600; // convert to seconds
    				if (step_time >= elapsed_time) {
    					// this is our destination
    					this._hitTeams[i].location = team_route.route[j];
    					if (this._hitTeams[i].location === system.ID) {
    						this._createTeams.push(i)
    					} else {
    						if (this._doArrivalMessages === true) {
    							// inform the player (sometimes)
    							if (Math.random() > 0.6) {
    								this._warnPlayer = true;
    								this._warnPlayerSource = this._hitTeams[i].source;
    								this._warnPlayerCurrent = this._hitTeams[i].location;
    							}
    						}
    					}
    					break;
    				}
    				if (this._hitTeams[i].maxPursuitDistance > 0 && this._hitTeams[i].distance + step_route.distance > this._hitTeams[i].maxPursuitDistance) {
    					// they're giving up.
    					this._hitTeams.splice(i, 1);
    					gaveup = true;
    					break;
    					// flag the player as a fugitive in the source system (if bounty system is installed)
    				}
    			}
    			if (gaveup === true || !step_route) continue;
    			this._hitTeams[i].distance += step_route.distance;
    		}
    	}
    	if (this._createTeams.length > 0) {
    		system.setPopulator("gcm-hitteams", {
    			priority: 50,
    			location: "WITCHPOINT",
    			callback: this.$createHitTeams.bind(this)
    		});
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$populateBlackMarketSaleList = function $populateBlackMarketSaleList() {
    	var p = player.ship;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if (sbm && sbm.$removeWorldScriptItems) {
    		// clear out dregs
    		sbm.$removeWorldScriptItems("GalCopBB_HitTeams");
    
    		// failed mission sale items
    		for (var i = 0; i < gcm._equipmentFromFailedMissions.length; i++) {
    			// factor in the age of the item
    			var factor = (30 - (clock.adjustedSeconds - gcm._equipmentFromFailedMissions[i].date) / 86400) / 30;
    			// and the government of the system we're currently trying to sell in
    			factor *= ((7 - system.government) / 5);
    			// make sure we're not too crazy
    			if (factor < 0.2) factor = 0.2;
    			if (factor > 1.5) factor = 1.5;
    
    			var eq = "";
    			if (gcm._equipmentFromFailedMissions[i].equip.indexOf("EQ_") >= 0) {
    				var sts = p.equipmentStatus(gcm._equipmentFromFailedMissions[i].equip);
    				if (sts === "EQUIPMENT_OK" || sts === "EQUIPMENT_DAMAGED") {
    					eq = gcm._equipmentFromFailedMissions[i].equip;
    				}
    			} else {
    				eq = gcm._equipmentFromFailedMissions[i].equip;
    			}
    			if (eq !== "") {
    				sbm.$addSaleItem({
    					key: eq + ":" + (i * -1),
    					text: this._sellInfo[eq].description,
    					cost: parseInt(this._sellInfo[eq].cost * factor),
    					worldScript: "GalCopBB_HitTeams",
    					sellCallback: "$sellItemCallback"
    				});
    			}
    		}
    
    		// active mission-based sale items
    		var list = gcm.$getListOfMissions(true);
    		for (var i = 0; i < list.length; i++) {
    			// only add sale items for primary missions, not chained ones
    			if (list[i].data.chained === false) {
    				// factor in the government of the system we're currently trying to sell in
    				var factor = (7 - system.government) / 5;
    				// make sure we're not too crazy
    				if (factor < 0.2) factor = 0.2;
    				if (factor > 1.5) factor = 1.5;
    
    				var eq = "";
    				switch (list[i].data.missionType) {
    					case 22:
    					case 23:
    						if (p.equipmentStatus("EQ_GCM_BLACK_BOX") === "EQUIPMENT_OK" || p.equipmentStatus("EQ_GCM_BLACK_BOX") === "EQUIPMENT_DAMAGED") eq = "EQ_GCM_BLACK_BOX";
    						break;
    					case 24:
    						if (p.equipmentStatus("EQ_GCM_RECOVERED_CARGO") === "EQUIPMENT_OK" || p.equipmentStatus("EQ_GCM_RECOVERED_CARGO") === "EQUIPMENT_DAMAGED") eq = "EQ_GCM_RECOVERED_CARGO";
    						break;
    					case 31: // data cache collection
    						// can only sell it if it's been received
    						if (list[i].data.quantity === 1 && list[i].data.targetQuantity > 0) eq = "DTA_DATA_PACKAGE";
    						break;
    					case 32: // delivery info to ship
    						if (list[i].data.quantity === 0 && list[i].data.targetQuantity > 0) eq = "DTA_DATA_DELIVERY";
    						break;
    					case 33:
    						if (list[i].data.quantity === 1) {
    							switch (list[i].data.stolenItemType) {
    								case "prototype ship schematics":
    									eq = "EQ_GCM_STOLEN_SCHEMATICS";
    									break;
    								case "high-level security codes":
    									eq = "EQ_GCM_STOLEN_CODES";
    									break;
    								case "classified documents":
    									eq = "EQ_GCM_STOLEN_DOCUMENTS";
    									break;
    								case "military weapon designs":
    									eq = "EQ_GCM_WEAPONDESIGNS";
    									break;
    							}
    						}
    						break;
    					case 50:
    						if (p.equipmentStatus("EQ_GCM_MEDICAL_SUPPLIES") === "EQUIPMENT_OK" || p.equipmentStatus("EQ_GCM_MEDICAL_SUPPLIES") === "EQUIPMENT_DAMAGED") eq = "EQ_GCM_MEDICAL_SUPPLIES";
    						break;
    					case 52:
    						if (p.equipmentStatus("EQ_GCM_VIRUS_SPECIMENS") === "EQUIPMENT_OK" || p.equipmentStatus("EQ_GCM_VIRUS_SPECIMENS") === "EQUIPMENT_DAMAGED") eq = "EQ_GCM_VIRUS_SPECIMENS";
    						break;
    					case 60:
    					case 61:
    						if (p.equipmentStatus("EQ_GCM_WBSA") === "EQUIPMENT_OK" || p.equipmentStatus("EQ_GCM_WBSA") === "EQUIPMENT_DAMAGED") eq = "EQ_GCM_WBSA";
    						break;
    					case 62: // security software courtesy of GalCop
    						if (list[i].data.quantity === 0 && list[i].data.targetQuantity > 0) eq = "DTA_GALCOP_SEC_SOFTWARE";
    						break;
    					case 65:
    						if (p.equipmentStatus("EQ_GCM_SOFTWARE") === "EQUIPMENT_OK") "EQ_GCM_SOFTWARE";
    						break;
    					case 70:
    					case 73:
    						if (p.equipmentStatus("EQ_GCM_SOLAR_SCANNER") === "EQUIPMENT_OK" || p.equipmentStatus("EQ_GCM_SOLAR_SCANNER") === "EQUIPMENT_DAMAGED") eq = "EQ_GCM_SOLAR_SCANNER";
    						break;
    					case 100:
    						if (p.equipmentStatus("EQ_GCM_SEISMIC_SCANNER") === "EQUIPMENT_OK" || p.equipmentStatus("EQ_GCM_SEISMIC_SCANNER") === "EQUIPMENT_DAMAGED") eq = "EQ_GCM_SEISMIC_SCANNER";
    						break;
    				}
    				if (eq !== "") {
    					log(this.name, "found " + eq + " ... adding to BM list");
    					sbm.$addSaleItem({
    						key: eq + ":" + list[i].ID,
    						text: this._sellInfo[eq].description,
    						cost: parseInt(this._sellInfo[eq].cost * factor),
    						worldScript: "GalCopBB_HitTeams",
    						sellCallback: "$sellItemCallback"
    					});
    				}
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // handles the process of selling items on the black market (if Smugglers is installed)
    this.$sellItemCallback = function $sellItemCallback(key, amount) {
    
    	if (key.indexOf(":") === -1) {
    		throw "!ERROR: Invalid key data returned from Black Market sell.";
    	}
    	var bb = worldScripts.BulletinBoardSystem;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var typ = key.split(":")[0];
    	var missID = parseInt(key.split(":")[1]);
    
    	if (missID > 0) {
    		var item = bb.$getItem(missID);
    	}
    
    	// remove the equipment from the player ship
    	if (typ.indexOf("EQ_") != -1) {
    		var eqsts = player.ship.equipmentStatus(typ);
    		if (eqsts === "EQUIPMENT_OK" || eqsts === "EQUIPMENT_DAMAGED") {
    			player.ship.removeEquipment(typ);
    		}
    	} else {
    		// its something other than an equipment item
    		if (typ.indexOf("DTA_") >= 0 && missID > 0) {
    			// set the damaged element of the mission to 1-stop
    			item.data.targetQuantity = 0;
    		}
    	}
    
    	var missType = 0;
    	var src = -1;
    	var age = 0;
    	if (missID > 0) {
    		missType = item.data.missionType;
    		src = item.data.source;
    	} else {
    		missType = gcm._equipmentFromFailedMissions[Math.abs(missID)].missionType;
    		src = gcm._equipmentFromFailedMissions[Math.abs(missID)].source;
    		// if this item is from a previous failed mission, extract the age from the key
    		age = parseInt((clock.adjustedSeconds - gcm._equipmentFromFailedMissions[Math.abs(missID)].date) / 86400);
    	}
    
    	if (src >= 0) {
    		// we'll be triggering the hit teams at this point
    		this.$initiateInvestigation(src, missType, age);
    	}
    
    	// remove the equipment from the failed equipment list, if required
    	if (missID < 0) {
    		gcm._equipmentFromFailedMissions.splice(Math.abs(missID), 1);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // create a new "investigation" team, with the source system ID and mission type
    this.$initiateInvestigation = function $initiateInvestigation(systemID, missionType, age) {
    
    	var team = {};
    	team.source = systemID;
    	team.location = systemID;
    	team.distance = 0;
    	team.ships = [];
    
    	var num_ships = 0;
    	var period = 0;
    
    	switch (missionType) {
    		case 22:
    		case 23: // black boxes
    		case 24: // special cargo
    			// possible there will not be a hit team
    			if ((age > 0 && Math.random() < (age / 15)) || Math.random() < 0.3) return;
    			team.maxPursuitDistance = 50;
    			num_ships = parseInt(Math.random() * 3 + 2);
    			period = 120;
    			break;
    		case 31: // data cache collection
    			// possible there will not be a hit team
    			if ((age > 0 && Math.random() < (age / 20)) || Math.random() < 0.2) return;
    			team.maxPursuitDistance = 70;
    			num_ships = parseInt(Math.random() * 3 + 2);
    			period = 180;
    			break;
    		case 32: // delivery info to ship
    			// possible there will not be a hit team
    			if ((age > 0 && Math.random() < (age / 20)) || Math.random() < 0.2) return;
    			team.maxPursuitDistance = 70;
    			num_ships = parseInt(Math.random() * 3 + 2);
    			period = 180;
    			break;
    		case 42: // special computers
    			// possible there will not be a hit team
    			if ((age > 0 && Math.random() < (age / 15)) || Math.random() < 0.3) return;
    			team.maxPursuitDistance = 60;
    			num_ships = parseInt(Math.random() * 3 + 2);
    			period = 150;
    			break;
    		case 50: // medical equipment
    			// possible there will not be a hit team
    			if ((age > 0 && Math.random() < (age / 90)) || Math.random() < 0.05) return;
    			team.maxPursuitDistance = 0; // these guys will be relentless
    			num_ships = parseInt(Math.random() * 4 + 4);
    			period = 365;
    			break;
    		case 52: // virus specimins
    			// possible there will not be a hit team
    			if ((age > 0 && Math.random() < (age / 90)) || Math.random() < 0.05) return;
    			team.maxPursuitDistance = 0; // these guys will be relentless
    			num_ships = parseInt(Math.random() * 4 + 4);
    			period = 365;
    			break;
    		case 60: // 
    		case 61: // wbsa devices
    			// possible there will not be a hit team
    			if ((age > 0 && Math.random() < (age / 60)) || Math.random() < 0.08) return;
    			team.maxPursuitDistance = 0; // these guys will be relentless
    			num_ships = parseInt(Math.random() * 4 + 4);
    			period = 365;
    			break;
    		case 62: // security software courtesy of GalCop
    			// possible there will not be a hit team
    			if ((age > 0 && Math.random() < (age / 90)) || Math.random() < 0.05) return;
    			team.maxPursuitDistance = 0; // these guys will be relentless
    			num_ships = parseInt(Math.random() * 4 + 4);
    			period = 365;
    			break;
    		case 65: // galcop station security software
    			// possible there will not be a hit team
    			if ((age > 0 && Math.random() < (age / 90)) || Math.random() < 0.05) return;
    			team.maxPursuitDistance = 0;
    			num_ships = parseInt(Math.random() * 5 + 4);
    			period = 365;
    			break;
    		case 70: // solar scanner
    		case 73: // solar scanner
    		case 100: // seismic scanner
    			// possible there will not be a hit team
    			if ((age > 0 && Math.random() < (age / 15)) || Math.random() < 0.3) return;
    			team.maxPursuitDistance = 40;
    			num_ships = parseInt(Math.random() * 2 + 2);
    			period = 60;
    			break;
    	}
    
    	var leader = true;
    	for (var i = 1; i <= num_ships; i++) {
    		// make the first ship in the array the leader
    		team.ships.push(this.$createShipDef(leader));
    		leader = false;
    	}
    
    	this._hitTeams.push(team);
    	// add a mission suppression entry
    	this._suppressMissions.push({
    		system: systemID,
    		date: clock.adjustedSeconds + (period * 86400)
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$createShipDef = function $createShipDef(isLeader) {
    	var def = {};
    	var dta = null;
    	do {
    		var dta = this._preferredAssassinShips[Math.floor(Math.random() * this._preferredAssassinShips.length)];
    		if (isLeader === true && dta.isLeader === false) dta = null;
    	} while (dta == null);
    	def.dataKey = dta.key;
    	def.personality = Math.floor(Math.random() * 32000);
    	var gcm = worldScripts.GalCopBB_Missions;
    	if (gcm._rsnInstalled) {
    		def.shipName = gcm.$getRandomShipName(null, "assassin-light");
    	} else {
    		def.shipName = "";
    	}
    	return def;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // creates the hit teams at the witchpoint
    this.$createHitTeams = function $createHitTeams(pos) {
    	// put ship died event handlers on all ships so we can monitor how many of them the player kills
    	// give most of the ships injectors and a large number of beam lasers
    	for (var i = 0; i < this._createTeams.length; i++) {
    		var team = this._hitTeams[this._createTeams[i]];
    		// assume the first ship in the list is the leader
    		var result = system.addShips("[" + team.ships[0].dataKey + "]", 1, pos, 0);
    		var leader = null;
    		if (result) var leader = result[0];
    		if (!leader || leader == null) continue;
    		leader.entityPersonality = team.ships[0].personality;
    		leader.shipUniqueName = team.ships[0].shipName;
    		leader.setScript("oolite-default-ship-script.js");
    		leader.switchAI("gcm-assassinAI.js");
    
    		leader.script._teamIndex = this._createTeams[i];
    		leader.awardEquipment("EQ_FUEL_INJECTION");
    		leader.awardEquipment("EQ_ECM");
    		leader.awardEquipment("EQ_SHIELD_BOOSTER");
    		// assassins don't respect escape pods and won't expect anyone
    		// else to either.
    		leader.removeEquipment("EQ_ESCAPE_POD");
    		leader.fuel = 7;
    		leader.forwardWeapon = "EQ_WEAPON_MILITARY_LASER";
    		if (Math.random() > 0.5) {
    			leader.aftWeapon = "EQ_WEAPON_BEAM_LASER";
    		} else {
    			leader.aftWeapon = "EQ_WEAPON_PULSE_LASER";
    		}
    		leader.accuracy = 4;
    		leader.bounty = 0;
    		// attach our shipDied event
    		if (leader.script.shipDied && leader.script.$gcm_hitteamsovr_shipDied == null) leader.script.$gcm_hitteamsovr_shipDied = leader.script.shipDied;
    		leader.script.shipDied = this.$gcm_hitteams_shipDied;
    
    		// remove any escorts added automatically by the system
    		if (leader.escorts) {
    			for (var j = leader.escorts.length - 1; j >= 0; j--) leader.escorts[j].remove(true);
    		}
    
    		var grp = new ShipGroup("gcm-assassin-group-" + i, leader);
    		leader.group = grp;
    
    		for (var j = 1; j < team.ships.length; j++) {
    			var shpspec = team.ships[j];
    			result = system.addShips("[" + shpspec.dataKey + "]", 1, pos, 3E3);
    			var member = null;
    			if (result) member = result[0];
    			if (!member || member == null) continue;
    			member.entityPersonality = shpspec.personality;
    			member.shipUniqueName = shpspec.shipName;
    			member.setScript("oolite-default-ship-script.js");
    			member.switchAI("gcm-assassinAI.js");
    
    			member.script._teamIndex = this._createTeams[i];
    			member.awardEquipment("EQ_FUEL_INJECTION");
    			member.awardEquipment("EQ_ECM");
    			if (Math.random() > 0.5) member.awardEquipment("EQ_SHIELD_BOOSTER");
    			// assassins don't respect escape pods and won't expect anyone else to either.
    			member.removeEquipment("EQ_ESCAPE_POD");
    			member.bounty = 0;
    			member.fuel = 5;
    			if (Math.random() > 0.3) {
    				member.forwardtWeapon = "EQ_WEAPON_BEAM_LASER";
    				if (Math.random() > 0.5) {
    					member.aftWeapon = "EQ_WEAPON_BEAM_LASER";
    				} else {
    					member.aftWeapon = "EQ_WEAPON_PULSE_LASER";
    				}
    			} else {
    				member.forwardWeapon = "EQ_WEAPON_PULSE_LASER";
    			}
    			member.accuracy = Math.random() * 6 - 3; // between -3 and +3
    
    			if (member.script.shipDied && member.script.$gcm_hitteamsovr_shipDied == null) member.script.$gcm_hitteamsovr_shipDied = member.script.shipDied;
    			member.script.shipDied = this.$gcm_hitteams_shipDied;
    
    			// remove any escorts added automatically by the system
    			if (member.escorts) {
    				for (var k = member.escorts.length - 1; k >= 0; k--) member.escorts[k].remove(true);
    			}
    	
    			member.group = grp;
    			grp.addShip(member);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // populates an array with ship data keys for use by the populator routines
    this.$getPreferredShipList = function $getPreferredShipList() {
    	this._preferredAssassinShips.length = 0;
    	var assassinRoles = ["assassin-light", "assassin-medium", "assassin-heavy"];
    
    	for (var role = 0; role < assassinRoles.length; role++) {
    		var shipkeys = Ship.keysForRole(assassinRoles[role]);
    		for (var i = 0; i < shipkeys.length; i++) {
    			var shipspec = Ship.shipDataForKey(shipkeys[i]);
    			this._preferredAssassinShips.push({
    				key: shipkeys[i],
    				isLeader: (role > 0 ? true : false)
    			});
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns true if missions are suppressed in this system, otherwise false
    this.$areMissionsSuppressed = function $areMissionsSuppressed(sysID) {
    	for (var i = 0; i < this._suppressMissions.length; i++) {
    		if (this._suppressMissions[i].system === sysID && this._suppressMissions[i].date > clock.adjustedSeconds) return true;
    	}
    	return false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_hitteams_shipDied = function $gcm_hitteams_shipDied(whom, why) {
    	if (this.ship.script.$gcm_hitteamsovr_shipDied) this.ship.script.$gcm_hitteamsovr_shipDied(whom, why);
    	var ht = worldScripts.GalCopBB_HitTeams;
    	var idx = this.ship.script._teamIndex;
    	for (var i = 0; i < ht._hitTeams[idx].ships.length; i++) {
    		if (ht._hitTeams[idx].ships[i].dataKey === this.ship.dataKey && ht._hitTeams[idx].ships[i].personality == this.ship.entityPersonality) {
    			ht._hitTeams[idx].ships.splice(i, 1);
    			// if everyone is dead, remove the team entry
    			if (ht._hitTeams[idx].ships.length === 0) ht._hitTeams.splice(idx, 1);
    			break;
    		}
    	}
    }
    Scripts/galcopbb_investigations.js
    "use strict";
    this.name = "GalCopBB_Investigations";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Looks after all investigation missions (130-139)";
    this.license = "CC BY-NC-SA 4.0";
    
    this._setupTimer = null;
    this._monitorTimer = null;
    this._bomb = null;
    this._bombTimer = null;
    this._bombCounter = 0;
    this._pod = null;
    this._thargoidTimer = null;
    this._addedBeacon = false;
    this._addedEntities = false;
    this._missionID = 0;
    this._debug = false;
    
    /*
        investigate distant signal, which can be one of the following
        *130 - a stash of cargo pods with precious metals/gems
        *131 - a ship graveyard
        *132 - a damaged escape pod
        *133 - a pirate base
        *134 - a thargoid trap
        *135 - a proximity mine that causes a fuel leak
        *136 - a spooky noise/comms
        *137 - a pirate trap
        *138 - an asteroid field with blackbox
        139 - (currently unusued)
    	*140 - destroy pirate base
    	*141 - deliver goodbye messages
    
    	todo:
    		cargo pod with documents which, when scooped, will inform the player that they will need to dock to unpack the contents
    			on docking, player will get a parcel mission in most circumstances
    			however, other missions (eg beacon, collect cargo) could be launched through the same mechanism
    		beacon waiting for a password to unlock content
    			
    
    */
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
    	var gcm = worldScripts.GalCopBB_Missions;
    	// add these mission types into the main control
    	var list = [130, 131, 132, 133, 134, 135, 136, 137, 138, 140];
    	gcm._availableMissionTypes = gcm._availableMissionTypes.concat(list);
    	// note: 141 is not spawned via the main mission system - only after a 132 mission
    	this._debug = gcm._debug;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.systemWillPopulate = function () {
    	this.$checkForActiveMissions();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillLaunchFromStation = function (station) {}
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_shipDied = function $gcm_shipDied(whom, why) {
    	this.$stopTimer();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillDockWithStation = function (station) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var list = gcm.$getListOfMissions(true, 141);
    	if (list.length > 0) {
    		var bb = worldScripts.BulletinBoardSystem;
    		for (var i = 0; i < list.length; i++) {
    			var item = list[i];
    			if (item.destination === system.ID && station.isMainStation && item.data.quantity === 0) {
    				item.data.quantity = 1;
    				bb.$updateBBMissionPercentage(item.ID, 1);
    
    				gcm.$logMissionData(item.ID);
    				//player.consoleMessage(expandDescription("[goal_updated]"));
    			}
    		}
    	}
    
    	// stop the fuel leaking after we dock
    	if (player.ship.fuelLeakRate != 0) player.ship.fuelLeakRate = 0;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_shipWillEnterWitchspace = function $gcm_shipWillEnterWitchspace() {
    	this.$stopTimer();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$stopTimer = function $stopTimer() {
    	delete this.shipDied;
    	delete this.shipWillEnterWitchspace;
    	if (this._monitorTimer && this._monitorTimer.isRunning) this._monitorTimer.stop();
    	this._monitorTimer = null;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$checkForActiveMissions = function $checkForActiveMissions() {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var list = gcm.$getListOfMissions(true, [130, 131, 132, 133, 134, 135, 136, 137, 138, 139]);
    	if (list.length > 0) {
    		// loop through all active missions and see if any need to be set up for this system
    		for (var i = 0; i < list.length; i++) {
    			if (list[i].destination === system.ID &&
    				list[i].data.quantity === 0 &&
    				list[i].data.destroyedQuantity === 0 &&
    				(list[i].expiry === -1 || list[i].expiry > clock.adjustedSeconds)) {
    
    				var posData = this.$getRandomPosition(list[i].data.locationType, 0.5, list[i].ID);
    				this._monitorPos = posData.position;
    				// start the monitoring timer to check the players position
    				this._monitorTimer = new Timer(this, this.$checkPlayerPosition, 5, 5);
    				this._monitorDist = posData.checkDist / 2;
    				this.shipWillEnterWitchSpace = this.$gcm_shipWillEnterWitchspace;
    				this.shipDied = this.$gcm_shipDied;
    				this._missionID = list[i].ID;
    
    				switch (list[i].data.missionType) {
    					case 130: // cargo pods with gold/platinum/gemstones
    						var pd = gcm._preferredCargoPods[Math.floor(Math.random() * gcm._preferredCargoPods.length)];
    						var pods = system.addShips("[" + pd + "]", Math.floor(Math.random() * 10 + 2), posData.position, 1000);
    						// if we couldn't create them with our preferred pod type, use the default
    						if (!pods) {
    							pods = system.addShips("[barrel]", Math.floor(Math.random() * 10 + 2), posData.position, 1000);
    						}
    						if (pods) {
    							var opts = ["computers", "luxuries", "furs", "alien_items", "liquor_wines", "firearms", "narcotics", "gold", "platinum", "gem_stones"];
    							var amts = [1, 1, 1, 1, 1, 1, 1, 30, 30, 60];
    							for (var j = 0; j < pods.length; j++) {
    								var choice = Math.floor(Math.random() * opts.length);
    								var amt = amts[choice];
    								if (amt === 1) {
    									pods[j].setCargo(opts[choice], amt);
    								} else {
    									pods[j].setCargo(opts[choice], Math.floor(Math.random() * amt + amt));
    								}
    							}
    						} else {
    							log("galcopBB_investigations", "!!ERROR: cargo pods not spawned");
    						}
    						break;
    					case 131: // ship graveyard
    						system.addShips("[alloy]", Math.floor(Math.random() * 20 + 10), posData.position, player.ship.scannerRange);
    						var graveTypes = ["adder", "anaconda", "asp", "boa", "boa-mk2", "cobra3-trader", "cobramk1", "ferdelance", "gecko", "krait", "mamba", "moray", "morayMED", "python", "sidewinder"];
    						var num = Math.floor(Math.random() * 20 + 5);
    						for (var j = 0; j < num; j++) {
    							var typ = graveTypes[Math.floor(Math.random() * graveTypes.length)];
    							var checkShips = system.addShips("[" + typ + "]", 1, posData.position, player.ship.scannerRange * 1.5);
    							if (checkShips) var s = checkShips[0];
    							if (s) {
    								s.bounty = 0;
    								// set script to default, to avoid a special script for the trader doing stuff. (like setting a new AI)
    								s.setScript("oolite-default-ship-script.js");
    								s.switchAI("oolite-nullAI.js");
    
    								// remove any escorts that came with the ship
    								if (s.escorts) {
    									for (var k = s.escorts.length - 1; k >= 0; k--) s.escorts[k].remove(true);
    								}
    
    								s.script.shipLaunchedEscapePod = this.$gcm_derelict_shipLaunchedEscapePod; // function to remove the escapepod after launch.
    								if (s.equipmentStatus("EQ_ESCAPE_POD") === "EQUIPMENT_UNAVAILABLE") s.awardEquipment("EQ_ESCAPE_POD");
    								s.abandonShip(); // make sure no pilot is left behind and this command turns the ship into cargo.
    								s.primaryRole = "gcm_derelict"; // to avoid pirate attacks
    								s.displayName = s.displayName + " (derelict)";
    								s.maxEnergy = 32;
    								s.lightsActive = false;
    							}
    						}
    						break;
    					case 132: // escape pod
    						// no passenger (dead) but includes delayed comm message ("If you're receiving this...")
    						// todo: comms message requests delivery of info to main station on system X (pilot's home system)
    						// essentially like a parcel, but with no expiry (because the guy is already dead), and no payment amount
    						// todo: will need customised completion message, rather than the standard. pay player as separate routine at end as reward
    						var checkShips = null;
    						for (var j = 0; j < 5; j++) {
    							checkShips = system.addShips("gcm_stricken_pod_dead", 1, posData.position, 0);
    							if (checkShips) this._pod = checkShips[0];
    							if (this._pod) break;
    						}
    						// set up a broadcast message when the player is within 15k
    						break;
    					case 133: // pirate base
    						var pb = worldScripts.GalCopBB_PirateBases;
    						if (pb.$systemHasPirateBase() === null) {
    							pb.$addPirateBaseDirect(system.ID, posData.x, posData.y, posData.z, posData.coordSystem, posData.locationType);
    							// make sure our beacon position matches the actual location of the base
    							this._monitorPos = pb.$forceSpawn();
    						}
    						var b = pb.$getSystemPirateBase();
    						break;
    					case 134: // thargoid trap
    						// spawn thargoids at position, but only when player gets there
    						break;
    					case 135: // proximity mine + fuel leak
    						this._bomb = system.addShips("gcm_remote_mine", 1, posData.position, 0)[0];
    						break;
    					case 136: // spooky noise
    						break;
    					case 137: // pirate trap
    						// todo: spawn pirates at position and give them lurk position
    						var gns = worldScripts.GalCopBB_GoonSquads;
    						gns.$createGoonSquad(Math.floor(Math.random() * 5 + 2), posData.position, player.ship.scannerRange * 0.5);
    						break;
    					case 138: // asteroid field with black box
    						// todo: spawn asteroids and blackbox
    						// scooping blackbox might create new mission
    						system.addShips("asteroid", 60, posData.position, 40E3);
    						// maybe spawn a blackbox
    						if (Math.random() > 0.6 && worldScripts["Rescue Stations"]) {
    							system.addShips("rescue_blackbox_generic", 1, posData.position, 5000);
    						}
    						break;
    					case 139: // unused
    						break;
    				}
    				if (this._debug) gcm.$setWaypoint(posData.position, [0, 0, 0, 0], "D", "Debug position (" + list[i].data.missionType + ")", list[i].data.missionType);
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_derelict_shipLaunchedEscapePod = function $gcm_derelict_shipLaunchedEscapePod(pod, passengers) {
    	pod.remove(true); // we don't want floating escapepods around but need them initially to create the derelict.
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$checkPlayerPosition = function $checkPlayerPosition() {
    	if (player.ship.isValid == false || !player.ship.position) {
    		this.$stopTimer();
    		return;
    	}
    	if (this._monitorDist >= 0) {
    		var checkDist = player.ship.position.distanceTo(this._monitorPos);
    		if (checkDist < player.ship.scannerRange * 0.9) {
    			// mark mission as complete and turn off beacon
    			this.$stopTimer();
    			var gcm = worldScripts.GalCopBB_Missions;
    			var bb = worldScripts.BulletinBoardSystem;
    			var item = bb.$getItem(this._missionID);
    			if (item) {
    				if (item.data.destroyedQuantity === 0 && item.data.quantity === 0) {
    					item.data.quantity = 1;
    					bb.$updateBBMissionPercentage(item.ID, 1);
    
    					gcm.$logMissionData(item.ID);
    					player.consoleMessage(expandDescription("[goal_updated]"));
    
    					if (item.data.missionType === 132) {
    						// add an accepted mission and tell the player
    						this.$createSecondaryPodMission(this._missionID);
    					}
    					if (item.data.missionType === 133) {
    						// create a destroy the base mission (140)
    						// we're going to do this manually here, rather than using the linked mission system
    						// the reason being that the pirate base is hard, and the player might not want to do it.
    						// it might be a bit much to force a hard mission on them like that
    						// however, once a pirate base is established, it will be present until destroyed, and 
    						// the mission will be recreated every time the system is populated, so the player can
    						// always come back and try it later.
    						gcm.$addLocalMissions([140], "", system.ID, "")
    					}
    					if (item.data.missionType === 134) {
    						this._thargoidTimer = new Timer(this, this.$createThargoids, 2, 0);
    					}
    					if (item.data.missionType === 135) {
    						// start the proximity mine countdown
    						this._bombTimer = new Timer(this, this.$proximityMine, 1, 1);
    					}
    					if (item.data.missionType === 136) {
    						var s_type = Math.floor(Math.random() * 10) + 1;
    						var mySound = new SoundSource;
    						mySound.sound = "gcm_spooky" + s_type + ".ogg";
    						mySound.loop = false;
    						mySound.play();
    						// comms message delay
    						var delay = parseInt(expandDescription("[gcm_spooky_length_" + s_type + "]")) - 3;
    						this._commsTimer = new Timer(this, this.$sendSpookyComms, delay, 0);
    					}
    				}
    			}
    		}
    		// if we're inside the max distance, add the beacon
    		if (checkDist < this._monitorDist && checkDist > player.ship.scannerRange * 0.9) {
    			if (this._debug) log(this.name, "adding beacon to system");
    			//if (checkDist > player.ship.scannerRange * 0.9) { // debug version - on from start
    			if (this._addedBeacon === false) {
    				if (player.ship.isInSpace === true) {
    					player.consoleMessage("Unrecognised navigational beacon detected", 3);
    				}
    				system.addVisualEffect("gcm_unidentified_signal2", this._monitorPos);
    				this._addedBeacon = true;
    			}
    		} else {
    			// otherwise, remove it, if present
    			if (this._addedBeacon === true) {
    				if (this._debug) log(this.name, "removing beacon from system");
    				var targets = system.allVisualEffects;
    				if (targets.length > 0) {
    					for (var i = targets.length - 1; i >= 0; i--) {
    						if (targets[i].dataKey === "gcm_unidentified_signal2") {
    							targets[i].remove();
    						}
    					}
    				}
    				this._addedBeacon = false;
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$proximityMine = function $proximityMine() {
    	if (!this._bomb || this._bomb.isValid === false) {
    		this._bombTimer.stop();
    		return;
    	}
    	if (this._bombCounter <= 5) {
    		if (this._bomb.position.distanceTo(player.ship) < player.ship.scannerRange) {
    			this._bomb.commsMessage((5 - this._bombCounter), player.ship);
    		}
    		this._bombCounter += 1;
    		if (this._bombCounter === 6) {
    			this._bomb.dealEnergyDamage(500, player.ship.scannerRange * 0.94);
    			if (this._bomb.position.distanceTo(player.ship) < player.ship.scannerRange) {
    				player.ship.fuelLeakRate = 0.5;
    			}
    			this._bomb.explode();
    			this._bombTimer.stop();
    			this._bomb = null;
    			this._bombTimer = null;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$sendSpookyComms = function $sendSpookyComms() {
    	var p = player.ship;
    	var checkShips = system.addShips("spookycomms", 1, p.position.add(p.vectorForward.multiply(5000)), 0);
    	var cr = null;
    	if (checkShips) cr = checkShips[0];
    	if (cr) {
    		cr.commsMessage(expandDescription("[gcm_spooky]"), p);
    		cr.remove();
    	}
    	
    	// 70% chance of a jump scare
    	if (Math.random() > 0.7) return;
    
    	var mySound = new SoundSource;
    	mySound.sound = "gcm_spooky_punch.ogg";
    	mySound.loop = false;
    	mySound.play();
    
    	p.script._thargoidVETime = 0;
    
    	var key = "gcm_std_effect_thargoid";
    	if (Ship.shipDataForKey("griff_thargoid-NPC") != null) key = "gcm_griff_effect_thargoid";
    	p.script._thargoidVE = system.addVisualEffect(key, p.position.add(p.vectorForward.multiply(500)));
    	p.script._thargoidVE.orientation = p.orientation;
    	p.script._thargoidVE.orientation = p.script._thargoidVE.orientation.rotateZ(Math.PI * 1.2);
    	p.script._thargoidVE.orientation = p.script._thargoidVE.orientation.rotateX(-0.5);
    
    	// set up the frame callback
    	this._fcbID = addFrameCallback(function (delta) {
    		var p = player.ship;
    		var ps = p.script;
    		if (ps._thargoidVETime > 1) {
    			ps._thargoidVE.remove();
    			removeFrameCallback(worldScripts.GalCopBB_Investigations._fcbID);
    			return;
    		}
    		ps._thargoidVETime += delta;
    		ps._thargoidVE.position = p.position.add(p.vectorForward.multiply(500));
    		ps._thargoidVE.orientation = p.orientation;
    		ps._thargoidVE.orientation = ps._thargoidVE.orientation.rotateZ(Math.PI * 1.2);
    		ps._thargoidVE.orientation = ps._thargoidVE.orientation.rotateX(-0.5);
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$createThargoids = function $createThargoids() {
    	system.addShips("thargoid", Math.floor(Math.random() * 4) + 1, player.ship.position, 20000);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$createSecondaryPodMission = function $createSecondaryPodMission(missID) {
    	// pick a destination
    	if (this._pod.isValid === false || this._pod.isInSpace === false) return;
    	var dest = null;
    	var tries = 0;
    	do {
    		//var sysList = system.info.systemsInRange(5);
    		//dest = sysList[Math.floor(Math.random() * sysList.length)];
    
    		dest = System.infoForSystem(galaxyNumber, Math.floor(Math.random() * 256))
    		if (!dest.routeToSystem(system.info)) {
    			dest = null;
    		}
    		tries += 1;
    	} while (dest === null && tries < 10);
    	if (dest === null) return; // no secondary mission - no dest system found
    
    	// create a client name
    	var cust = [];
    	var clientName = worldScripts.GalCopBB_Reputation.$getClientName(system.ID, "");
    	cust.push({
    		heading: "Client:",
    		value: clientName
    	});
    
    	var familyType = expandDescription("[gcm_141_target]");
    	var gcm = worldScripts.GalCopBB_Missions;
    	var bb = worldScripts.BulletinBoardSystem;
    	var id = bb.$addBBMission({
    		source: system.ID,
    		destination: dest.systemID,
    		stationKey: expandDescription("[missionType141_stationKeys]"),
    		description: expandDescription("[missionType141_description]"),
    		details: expandDescription("[missionType141_details]", {
    			destination: dest.name
    		}).replace(/family/g, familyType),
    		overlay: gcm.$getTexture(expandDescription("[missionType141_bbOverlay]")),
    		payment: 0,
    		deposit: 0,
    		allowTerminate: false,
    		completionType: expandDescription("[missionType141_completionType]"),
    		stopTimeAtComplete: true,
    		allowPartialComplete: expandDescription("[missionType141_partialComplete]"),
    		accepted: true,
    		expiry: -1,
    		percentComplete: 0,
    		disablePercentDisplay: expandDescription("[missionType141_disablePercent]"),
    		noEmails: true,
    		customDisplayItems: cust,
    		markerShape: "MARKER_X", // marker control will be handled by contracts system
    		completedCallback: "$completedMission",
    		confirmCompleteCallback: "$confirmCompleted",
    		manifestCallback: "$updateManifestEntry",
    		worldScript: this.name,
    		postStatusMessages: [{
    			status: "completed",
    			text: expandDescription("[missionType141_completedMessage]", {
    				name: clientName
    			}).replace(/family/g, familyType),
    			return: "list"
    		}],
    		data: {
    			source: "GalCopBB_Missions",
    			missionType: 141,
    			quantity: 0,
    			targetQuantity: 1,
    			destroyedQuantity: 0,
    			remaining: 0,
    			commodity: "",
    			unit: "",
    			destination: dest.name,
    			system: system.name,
    			expiry: "",
    			stage: "0"
    		}
    	});
    
    	var txt = expandDescription("[missionType141_miniBriefing_escapepod]", {
    		name: clientName,
    		destination: dest.name
    	}).replace(/family/g, familyType);
    
    	// communicate with player about the new mission
    	worldScripts.GalCopBB_Missions._pendingMissionID = id;
    	worldScripts.GalCopBB_Missions._pendingMissionOrigID = missID;
    	this._holdID = id;
    	worldScripts.GalCopBB_Missions._pendingMissionCallback = this.$secondaryPodMissionCallback.bind(this);
    	worldScripts.GalCopBB_Missions_MFD.$updateMFD(txt, true, this._pod, true);
    
    	this._pod = null;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$secondaryPodMissionCallback = function $secondaryPodMissionCallback() {
    	var bb = worldScripts.BulletinBoardSystem;
    	var rep = worldScripts.GalCopBB_Reputation;
    	var missID = worldScripts.GalCopBB_Investigations._holdID;
    	var item = bb.$getItem(missID);
    
    	// update the manifest
    	bb.$updateBBManifestText(
    		missID,
    		rep.$transformText(expandDescription("[missionType" + item.data.missionType + "_manifest]", {
    			destination: System.systemNameForID(item.destination),
    			expiry: ""
    		}), item.source, item.destination)
    	);
    	bb.$updateBBStatusText(
    		missID,
    		rep.$transformText(expandDescription("[missionType" + item.data.missionType + "_status]", {
    			destination: System.systemNameForID(item.destination)
    		}), item.source, item.destination)
    	);
    	worldScripts.GalCopBB_Missions._pendingMissionCallback = null;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptedMission = function $acceptedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if (this._debug) log(this.name, "accepted mission id = " + missID);
    
    	if (!item) {
    		log(this.name, "!!ERROR: BB returned null value from $getItem on mission ID " + missID);
    		return;
    	}
    	gcm.$updateLastMissionDate(item.source, item.data.missionType);
    	gcm.$updateGeneralSettings(item);
    
    	// do some population-type things
    	this.$checkForActiveMissions();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$completedMission = function $completedMission(missID) {
    	var p = player.ship;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// update mission history
    	gcm.$updateSuccessHistoryReputation(item);
    
    	if (item.data.missionType === 141) {
    		var creds = Math.floor(Math.random() * 300 + 300);
    		player.credits += creds;
    		player.consoleMessage("You have been gifted " + formatCredits(creds, false, true));
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$confirmCompleted = function $confirmCompleted(missID) {
    	var p = player.ship;
    	var result = "";
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (item) {
    		if (item.data.missionType === 141) {
    			if (item.destination != system.ID) result = "travel to destination system";
    		}
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$terminateMission = function $terminateMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	//var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// adjust reputation only when the terminatePenalty flag is set to true 
    	if (item.data.terminatePenalty === true) {
    		// update mission history
    		gcm.$updateFailedHistoryReputation(item);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$failedMission = function $failedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var eq = "";
    
    	if (!sbm) {
    		// if there's no blackmarket option, just remove the equipment
    		this.$terminateMission(missID);
    		return;
    	}
    
    	var item = bb.$getItem(missID);
    	// update mission history
    	gcm.$updateFailedHistoryReputation(item);
    
    	if (eq != "") gcm._equipmentFromFailedMissions.push({
    		missionType: item.data.missionType,
    		source: item.source,
    		equip: eq,
    		date: clock.adjustedSeconds
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateManifestEntry = function $updateManifestEntry(missID) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	gcm.$updateManifestEntry(missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
    	return worldScripts.GalCopBB_Missions.$missionAvailability(missID, missType, origSysID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 130
    this.$missionType130_Values = function $missionType130_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var mv = worldScripts.GalCopBB_CoreMissionValues;
    	var locTypes = [0, 3, 4, 5, 6, 7];
    	var result = {};
    	result["locationType"] = locTypes[Math.floor(Math.random() * locTypes.length)];
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 40) + 20) / 10) * 10 + (7 - destSysInfo.government) * 50 +
    		mv.$calcPlayerBonus(200); // plus a possible bonus price, based on player score 
    	result["expiry"] = -1; // no expiry
    	result["penalty"] = 0;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 140
    this.$missionType140_Values = function $missionType140_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var mv = worldScripts.GalCopBB_CoreMissionValues;
    	var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 1000) + 2000) / 10) * 10 + (7 - destSysInfo.government) * 50 +
    		mv.$calcPlayerBonus(2000); // plus a possible bonus price, based on player score 
    	result["expiry"] = -1; // no expiry
    	result["penalty"] = 0;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // gets a random position based on a locationType value. locationType can be 0, 3, 4, 5, or 6.
    this.$getRandomPosition = function $getRandomPosition(locationType, factor, missID) {
    	/*var result;
    	var coord = "";
    	var dist = 0;*/
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	switch (locationType) {
    		case 0:
    			return gcm.$getRandomPosition(locationType, factor, missID, 2, 3);
    			/*var x = 0 + (factor !== 0 ? Math.floor(system.scrambledPseudoRandomNumber(missID) * factor) * (system.scrambledPseudoRandomNumber(missID) > 0.5 ? 1 : -1) : 0);
    			var y = 0 + (factor !== 0 ? Math.floor(system.scrambledPseudoRandomNumber(missID + 1) * factor) * (system.scrambledPseudoRandomNumber(missID + 1) > 0.5 ? 1 : -1) : 0);
    			var z = (system.scrambledPseudoRandomNumber(missID + 2)) + 2; // number between 2 and 3
    			result = Vector3D(x, y, z).fromCoordinateSystem("wpu"); // on the other side of the planet from the witchpoint
    			coord = "wpu";
    			dist = system.mainPlanet.position.distanceTo([0, 0, 0]);*/
    			break;
    		case 3:
    			return gcm.$getRandomPosition(locationType, factor, missID, -2, -3);
    			/*var x = 0 + (factor !== 0 ? Math.floor(system.scrambledPseudoRandomNumber(missID) * factor) * (system.scrambledPseudoRandomNumber(missID) > 0.5 ? 1 : -1) : 0);
    			var y = 0 + (factor !== 0 ? Math.floor(system.scrambledPseudoRandomNumber(missID + 1) * factor) * (system.scrambledPseudoRandomNumber(missID + 1) > 0.5 ? 1 : -1) : 0);
    			var z = (system.scrambledPseudoRandomNumber(missID + 2)) + 2; // number between 2 and 3
    			result = Vector3D(x, y, z).fromCoordinateSystem("pwu"); // behind the witchpoint from the planet
    			coord = "pwu";
    			dist = system.mainPlanet.position.distanceTo([0, 0, 0]);*/
    			break;
    		case 4:
    			return gcm.$getRandomPosition(locationType, factor, missID, -2, -3);
    			/*var x = 0 + (factor !== 0 ? Math.floor(system.scrambledPseudoRandomNumber(missID) * factor) * (system.scrambledPseudoRandomNumber(missID) > 0.5 ? 1 : -1) : 0);
    			var y = 0 + (factor !== 0 ? Math.floor(system.scrambledPseudoRandomNumber(missID + 1) * factor) * (system.scrambledPseudoRandomNumber(missID + 1) > 0.5 ? 1 : -1) : 0);
    			var z = (system.scrambledPseudoRandomNumber(missID + 2)) + 2; // number between 2 and 3
    			coord = "swu";
    			result = Vector3D(x, y, z).fromCoordinateSystem("swu"); // behind the witchpoint from the sun
    			dist = system.sun.position.distanceTo([0, 0, 0]);*/
    			break;
    		case 5:
    			return gcm.$getRandomPosition(locationType, factor, missID, 2, 3);
    			/*var x = 0 + (factor !== 0 ? Math.floor(system.scrambledPseudoRandomNumber(missID) * factor) * (system.scrambledPseudoRandomNumber(missID) > 0.5 ? 1 : -1) : 0);
    			var y = 0 + (factor !== 0 ? Math.floor(system.scrambledPseudoRandomNumber(missID + 1) * factor) * (system.scrambledPseudoRandomNumber(missID + 1) > 0.5 ? 1 : -1) : 0);
    			var z = (system.scrambledPseudoRandomNumber(missID + 2)) + 2; // number between 2 and 3
    			result = Vector3D(x, y, z).fromCoordinateSystem("psu"); // on the other side of the sun from the planet
    			coord = "psu";
    			dist = system.mainPlanet.position.distanceTo(system.sun);*/
    			break;
    		case 6:
    			return gcm.$getRandomPosition(locationType, factor, missID, 2, 3);
    			/*var x = 0 + (factor !== 0 ? Math.floor(system.scrambledPseudoRandomNumber(missID) * factor) * (system.scrambledPseudoRandomNumber(missID) > 0.5 ? 1 : -1) : 0);
    			var y = 0 + (factor !== 0 ? Math.floor(system.scrambledPseudoRandomNumber(missID + 1) * factor) * (system.scrambledPseudoRandomNumber(missID + 1) > 0.5 ? 1 : -1) : 0);
    			var z = (system.scrambledPseudoRandomNumber(missID + 2)) + 2; // number between 2 and 3
    			result = Vector3D(x, y, z).fromCoordinateSystem("wsu"); // on the other side of the sun from the witchpoint
    			coord = "wsu";
    			dist = system.sun.position.distanceTo([0, 0, 0]);*/
    			break;
    		case 7:
    			return gcm.$getRandomPosition(locationType, factor, missID);
    			/*var x = 0 + (factor !== 0 ? Math.floor(system.scrambledPseudoRandomNumber(missID) * factor) * (system.scrambledPseudoRandomNumber(missID) > 0.5 ? 1 : -1) : 0);
    			var y = 0 + (factor !== 0 ? Math.floor(system.scrambledPseudoRandomNumber(missID + 1) * factor) * (system.scrambledPseudoRandomNumber(missID + 1) > 0.5 ? 1 : -1) : 0);
    			var z = (system.scrambledPseudoRandomNumber(missID + 2)) + 2; // number between 2 and 3
    			result = Vector3D(x, y, z).fromCoordinateSystem("spu"); // on the other side of the planet from the sun
    			coord = "spu";
    			dist = system.mainPlanet.position.distanceTo(system.sun);*/
    			break;
    	}
    	/*return {
    		position: result,
    		x: x,
    		y: y,
    		z: z,
    		coordSystem: coord,
    		locType: locationType,
    		checkDist: dist
    	};*/
    }
    Scripts/galcopbb_lifesupport.js
    "use strict";
    this.name = "GalCopBB_LifeSupport";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Controls handling of life support messages and functions";
    this.license = "CC BY-NC-SA 4.0";
    
    this._lifeSupportDefaultTime = 480; // default amount of time (in seconds) given to rescue entity before life support fails
    this._lifeSupportTimer = null; // timer to control the life support checking process
    this._lifeSupportRemaining = []; // array of dictionary objects that have details about each life support check in system
    this._distressMessageTimer = null; // timer to control the transmission of a distress signal from an escape pod
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function () {
    	this._debug = worldScripts.GalCopBB_Missions._debug;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillEnterWitchspace = function () {
    	// if we jump out with any pods or stricken ships waiting for us they will all die
    	if (this._lifeSupportTimer && this._lifeSupportTimer.isRunning) {
    		// end life support on any entities in-system at the moment
    		for (var i = 0; i < this._lifeSupportRemaining.length; i++) {
    			this._lifeSupportRemaining[i].remaining = 1;
    		}
    		// force the check to happen (because we're about to turn the timers off, and we want to make sure all data is updated correctly)
    		this.$lifeSupportCheck();
    	}
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = function (whom, why) {
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // stop all timers
    this.$stopTimers = function $stopTimers() {
    	if (this._distressMessageTimer && this._distressMessageTimer.isRunning) this._distressMessageTimer.stop();
    	delete this._distressMessageTimer;
    	if (this._lifeSupportTimer && this._lifeSupportTimer.isRunning) this._lifeSupportTimer.stop();
    	delete this._lifeSupportTimer;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$addShip = function $addShip(obj) {
    	if (obj.ent.script.hasOwnProperty("_missionID")) obj.ent.script._missionID_lifeSupport = obj.ent.script._missionID;
    	this._lifeSupportRemaining.push(obj);
    	if (this._distressMessageTimer == null || this._distressMessageTimer.isRunning === false) {
    		this._distressMessageTimer = new Timer(this, this.$sendDistressMessage, 5, 5);
    	}
    	if (this._lifeSupportTimer == null || this._lifeSupportTimer.isRunning === false) {
    		this._lifeSupportTimer = new Timer(this, this.$lifeSupportCheck, 5, 5);
    	}
    	this.$turnOffSpawningOXPs();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // finds any ships in desperate state and send the player a comms message once they're in range
    this.$sendDistressMessage = function $sendDistressMessage() {
    	function gcm_findDistressTargets(entity) {
    		return (entity.primaryRole === "gcm_stricken_ship" || entity.primaryRole === "gcm_stricken_pod" || entity.primaryRole === "gcm_runaway_pod");
    	}
    	var targets = system.filteredEntities(this, gcm_findDistressTargets);
    	if (targets.length > 0) {
    		for (var i = 0; i < targets.length; i++) {
    			targets[i].throwSpark();
    			// only send a message if the target has a crew
    			if (targets[i].crew.length > 0) {
    				if (player.ship.position.distanceTo(targets[i]) < 20000 & targets[i].script._sentDistress === false) {
    					targets[i].script._sentDistress = true;
    					targets[i].commsMessage(expandDescription("[distress_message_" + targets[i].primaryRole + "]"), player.ship);
    				}
    			}
    		}
    	} else {
    		this._distressMessageTimer.stop();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // checks the level of life support remaining in any stricken ships or escape pods
    // converts any entity that runs out of life support into a dead ship
    this.$lifeSupportCheck = function $lifeSupportCheck() {
    	var notify = [600, 540, 480, 420, 360, 300, 240, 180, 150, 120, 90, 60, 50, 40, 30, 20, 10, 5];
    
    	var found = false;
    	for (var i = 0; i < this._lifeSupportRemaining.length; i++) {
    		if (this._lifeSupportRemaining[i].ent.isInSpace && this._lifeSupportRemaining[i].remaining > 0) {
    			found = true;
    			if (this._lifeSupportRemaining[i].ent.primaryRole === "gcm_stricken_ship" || this._lifeSupportRemaining[i].ent.hasRole("gcm_special_pod")) {
    				this._lifeSupportRemaining[i].last = this._lifeSupportRemaining[i].remaining;
    				this._lifeSupportRemaining[i].remaining -= 5;
    				if (this._lifeSupportRemaining[i].remaining < 0) this._lifeSupportRemaining[i].remaining = 0;
    
    				// determine when to notify player
    				var transmit = "";
    				for (var j = 0; j < notify.length; j++) {
    					if (this._lifeSupportRemaining[i].remaining <= notify[j] && this._lifeSupportRemaining[i].last > notify[j]) transmit = expandDescription("[life_support_notify" + notify[j] + "]");
    				}
    				// determine when to send comms
    				if (this._lifeSupportRemaining[i].remaining <= 60 && this._lifeSupportRemaining[i].comms === false) {
    					this._lifeSupportRemaining[i].ent.commsMessage(expandDescription("[distress_hurry]"), player.ship);
    					this._lifeSupportRemaining[i].comms = true;
    				}
    
    				if (this._lifeSupportRemaining[i].remaining === 0) {
    					// dang it - out of time!
    					if (this._lifeSupportRemaining[i].ent.hasRole("gcm_special_pod")) {
    						// convert pod to derelict
    						var shp = this._lifeSupportRemaining[i].ent;
    
    						var retarget = false;
    						if (player.ship.target === shp) retarget = true;
    						var o = shp.orientation;
    						var ep = shp.entityPersonality;
    						var pos = shp.position;
    						var ds = shp.desiredSpeed;
    						var ms = shp.maxSpeed;
    						var pr = shp.primaryRole;
    						var mid = shp.script._missionID_lifeSupport;
    						var vel = shp.velocity;
    
    						shp.remove(true);
    
    						// we should never get an instance where the new ship isn't created, but just in case...
    						var checkShips = system.addShips("gcm_stricken_pod_dead", 1, pos, 0);
    						var newshp = null;
    						if (checkShips) newshp = checkShips[0];
    						if (newshp) {
    							newshp.orientation = o;
    							newshp.entityPersonality = ep;
    							newshp.maxSpeed = ms;
    							newshp.desiredSpeed = ds;
    							newshp.script._missionID_lifeSupport = mid;
    							newshp.script.shipDied = this.$gcm_entity_shipDied;
    							newshp.velocity = vel;
    
    							if (pr === "gcm_stricken_pod") newshp.reactToAIMessage("START_TUMBLING");
    
    							newshp.primaryRole = "gcm_derelict";
    							newshp.setCrew(null);
    
    							var item = worldScripts.BulletinBoardSystem.$getItem(newshp.script._missionID_lifeSupport);
    							item.data.targetQuantity = 0;
    							if (retarget) player.ship.target = newshp;
    						}
    					}
    					if (this._lifeSupportRemaining[i].ent.primaryRole === "gcm_stricken_ship") {
    						// convert ship to derelict
    						var shp = this._lifeSupportRemaining[i].ent;
    						// set script to default, to avoid a special script for the trader doing stuff. (like setting a new AI)
    						shp.setScript("oolite-default-ship-script.js");
    						if (shp.script.shipBeingAttacked) delete shp.script.shipBeingAttacked;
    
    						shp.switchAI("oolite-nullAI.js");
    
    						shp.script.shipLaunchedEscapePod = worldScripts.GalCopBB_Derelict.$gcm_derelict_shipLaunchedEscapePod; // function to remove the escapepod after launch.
    						if (shp.equipmentStatus("EQ_ESCAPE_POD") === "EQUIPMENT_UNAVAILABLE") shp.awardEquipment("EQ_ESCAPE_POD");
    						shp.abandonShip(); // make sure no pilot is left behind and this command turns the ship into cargo.
    						shp.primaryRole = "gcm_derelict";
    						var item = worldScripts.BulletinBoardSystem.$getItem(shp.script._missionID_lifeSupport);
    						item.data.targetQuantity = 0;
    						shp.displayName = shp.displayName + " (no life signs)";
    					}
    					this.$restartSpawningOXPs();
    				} else {
    					if (transmit !== "") {
    						player.consoleMessage("Estimated " + transmit + " of life support remaining for " + this._lifeSupportRemaining[i].type, 5);
    					}
    				}
    			}
    		}
    	}
    	// if there were no ships with life support still active, stop the timer.
    	if (found === false) {
    		this._lifeSupportTimer.stop();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_entity_shipDied = function $gcm_entity_shipDied(whom, why) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var bb = worldScripts.BulletinBoardSystem;
    
    	if (gcm._debug) log(this.name, "running shipDied for " + this.ship + ": reason " + why + ", " + whom);
    	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);
    
    	var item = bb.$getItem(this.ship.script._missionID_lifeSupport);
    	if (item) item.data.destroyedQuantity = 1;
    	if (gcm._distressMessageTimer && gcm._distressMessageTimer.isRunning) gcm._distressMessageTimer.stop();
    
    	var ls = worldScripts.GalCopBB_LifeSupport;
    	for (var i = 0; i < ls._lifeSupportRemaining.length; i++) {
    		if (ls[i].ent == this.ship) {
    			ls._lifeSupportRemaining[i].remaining = 0;
    			break;
    		}
    	}
    	ls.$restartSpawningOXPs();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // turn off OXP's that will spawn ships in your path, as they will likely interfere too much with any mission
    // where life support time is limited
    this.$turnOffSpawningOXPs = function $turnOffSpawningOXPs() {
    	if (worldScripts.spacecrowds) {
    		if (this._debug) log(this.name, "turning off space crowds");
    		worldScripts.spacecrowds.checkTimer.stop();
    	}
    	if (worldScripts.deep_space_pirates) {
    		if (this._debug) log(this.name, "turning off deep space pirates");
    		worldScripts.deep_space_pirates.checkTimer.stop();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$restartSpawningOXPs = function $restartSpawningOXPs() {
    	var found = false;
    	for (var i = 0; i < this._lifeSupportRemaining.length; i++) {
    		if (this._lifeSupportRemaining[i].remaining > 0) found = true;
    	}
    	if (found === true) return;
    	if (worldScripts.spacecrowds) {
    		if (this._debug) log(this.name, "restarting space crowds");
    		worldScripts.spacecrowds.checkTimer.start();
    	}
    	if (worldScripts.deep_space_pirates) {
    		if (this._debug) log(this.name, "restarting deep space pirates");
    		worldScripts.deep_space_pirates.checkTimer.start();
    	}
    }
    Scripts/galcopbb_meetship.js
    "use strict";
    this.name = "GalCopBB_MeetShip";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Controls ship interactions for missions 30/32";
    this.license = "CC BY-NC-SA 4.0";
    
    this._commsNPC = null; // reference to the ship that is sending a comms message to the player
    this._contactPoint = null; // holding object, containing the ship that is about to give the player a new mission
    this._contactPointTimer = null; // timer to control the transmission of the new mission
    this._deliveryPoint = null; // holding object, containing reference to ship to whom data is being transmitted
    this._deliveryPointTimer = null; // timer to control the transmission of data to the target
    this._checkPlayerNearbyTimer = null; // timer to check if the player is nearby
    this._setData = [];
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
    	var gcm = worldScripts.GalCopBB_Missions;
    	// add these mission types into the main control
    	var list = [30, 32];
    	gcm._availableMissionTypes = gcm._availableMissionTypes.concat(list);
    	this._debug = gcm._debug;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.systemWillPopulate = function () {
    	// reset the mission populator data array
    	this._setData.length = 0;
    	var list = worldScripts.GalCopBB_Missions.$getListOfMissions(true, [30, 32]);
    
    	if (list.length > 0) {
    		// loop through all active missions and see if any need to be set up for this system
    		for (var i = 0; i < list.length; i++) {
    			// *** type 30 - fly to waypoint, meet ship
    			if (list[i].data.missionType === 30 &&
    				list[i].destination === system.ID &&
    				list[i].data.quantity === 0 &&
    				list[i].data.targetQuantity > 0 &&
    				list[i].data.destroyedQuantity === 0 &&
    				list[i].expiry > clock.adjustedSeconds) {
    
    				this._setData.push({
    					missionType: 30,
    					missionID: list[i].ID,
    					source: list[i].source,
    					goons: 0,
    					quantity: 0,
    					target: list[i].data.targetQuantity,
    					name: list[i].data.name
    				});
    
    				// add ship with populator
    				system.setPopulator("gcm-meet-ship-" + list[i].ID, {
    					callback: function (pos) {
    						var ms = worldScripts.GalCopBB_MeetShip;
    						var missData = ms.$getMissionData(30);
    						var gcm = worldScripts.GalCopBB_Missions;
    						var gns = worldScripts.GalCopBB_GoonSquads;
    						// 3 possibilities:
    						// 1 - ship is there, ready to receive (types, 1,2,3,4,5,7,9)
    						// 2 - it's a trap! player is attacked by pirates (type 6)
    						// 3 - no ship at all - mission is a bust (types 8, 10)
    						var type = parseInt(Math.random() * 15) + 1;
    						switch (type) {
    							case 1:
    							case 2:
    							case 3:
    							case 4:
    							case 5:
    							case 7:
    							case 8:
    							case 9:
    							case 10:
    							case 11:
    							case 12:
    							case 13:
    							case 14:
    							case 15:
    								var shp = null;
    								var checkShips = null;
    								for (var j = 1; j <= 5; j++) {
    									checkShips = system.addShips("[" + gcm.$getRandomShipKey(parseInt(missData.missionID), 0) + "]", 1, pos, 3000);
    									if (checkShips) shp = checkShips[0];
    									if (shp) break;
    								}
    								if (shp) {
    									if (gcm._rsnInstalled) shp.shipUniqueName = gcm.$getRandomShipName(shp, "trader");
    									shp.bounty = 0;
    									// make sure they don't have fuel scoops so they can't accidentally scoop the cargo they drop
    									shp.removeEquipment("EQ_FUEL_SCOOPS");
    
    									// override the pilot name, so it's the same as the mission briefing.
    									// it might not ever be seen, but it's still worth making it consistent
    									shp.setCrew({
    										name: missData.name
    									});
    
    									// set script to default, to avoid a special script for the trader doing stuff. (like setting a new AI)
    									shp.setScript("oolite-default-ship-script.js");
    									shp.switchAI("oolite-nullAI.js");
    									shp.script._savedPrimaryRole = shp.primaryRole;
    									shp.script._passcode = expandDescription("[gcm_passcode]");
    									shp.desiredSpeed = 0;
    
    									// remove any escorts that came with the ship
    									if (shp.escorts) {
    										for (var j = shp.escorts.length - 1; j >= 0; j--) {
    											shp.escorts[j].remove(true);
    										}
    									}
    
    									shp.primaryRole = "gcm_contactpoint"; // to avoid pirate attacks
    
    									shp.script._missionID = missData.missionID;
    									// add our shipDied event to the shp
    									if (shp.script.shipDied) shp.script.$gcm_hold_shipDied = shp.script.shipDied;
    									shp.script.shipDied = gcm.$gcm_entity_shipDied;
    									// if the ship gets attacked by someone (player or otherwise) include a ship script to tell it to break out of its "silent" mode.
    									if (shp.script.shipBeingAttacked) shp.script.$gcm_hold_shipBeingAttacked = shp.script.shipBeingAttacked;
    									shp.script.shipBeingAttacked = ms.$gcm_contact_shipBeingAttacked;
    									if (shp.script.shipBeingAttackedByCloaked) shp.script.$gcm_hold_shipBeingAttackedByCloaked = shp.script.shipBeingAttackedByCloaked;
    									shp.script.shipBeingAttackedByCloaked = ms.$gcm_contact_shipBeingAttackedByCloaked;
    									if (shp.script.shipAttackedWithMissile) shp.script.$gcm_hold_shipAttackedWithMissile = shp.script.shipAttackedWithMissile;
    									shp.script.shipAttackedWithMissile = ms.$gcm_contact_shipAttackedWithMissile;
    
    									// set up broadcast comms interface
    									var bcc = worldScripts.BroadcastCommsMFD;
    									if (bcc.$checkMessageExists("gcm_transmit_passcode") === false) {
    										bcc.$createMessage({
    											messageName: "gcm_transmit_passcode",
    											callbackFunction: ms.$transmitPasscode.bind(ms),
    											displayText: "Transmit passcode",
    											messageText: expandDescription("The passcode is " + shp.script._passcode),
    											ship: shp,
    											transmissionType: "target",
    											deleteOnTransmit: true,
    											delayCallback: 5,
    											hideOnConditionRed: true
    										});
    									}
    
    								} else {
    									log("galcopBB_meetship", "!!ERROR: Meeting ship not spawned!");
    								}
    								break;
    
    							case 6:
    								if (this._debug) log(this.name, "contact point surprise");
    								gns.$createGoonSquad(parseInt((Math.random() * 4) + 3), pos, player.ship.scannerRange * 0.5);
    								/*var grp = system.addGroup("pirate", parseInt((Math.random() * 4) + 3), pos, 10000);
    								var gn = grp.ships;
    								if (gn && gn.length > 0) {
    									//var grp = new ShipGroup;
    									var pop = worldScripts["oolite-populator"];
    									for (var j = 0; j < gn.length; j++) {
    										// configure our pirates
    										gn[j].setBounty(20 + system.info.government + missData.goons + Math.floor(Math.random() * 8), "setup actions");
    										// make sure the pilot has a bounty
    										gn[j].setCrew({name:randomName() + " " + randomName(), bounty:gn[j].bounty, insurance:0});
    										if (gn[j].hasHyperspaceMotor) {
    											pop._setWeapons(gn[j], 1.75); // bigger ones sometimes well-armed
    										} else {
    											pop._setWeapons(gn[j], 1.3); // rarely well-armed
    										}
    										// in the safer systems, rarely highly skilled (the skilled ones go elsewhere)
    										pop._setSkill(gn[j], 4 - system.info.government);
    										if (Math.random() * 16 < system.info.government) {
    											pop._setMissiles(gn[j], -1);
    										}
    										// make sure the AI is switched
    										gn[j].switchAI("gcm-pirateAI.js"); 
    									}
    									gns._goonSquads.push({group:grp, position:pos});
    									break;
    								}*/
    								break;
    							default: // disabled, as it's too confusing to the player if there's no notification
    								if (this._debug) log("galcopBB_meetship", "contact point surprise 2 - nothing!");
    								break;
    						}
    
    						// we'll set the waypoint up regardless of the random roll above, so the player won't know what the result is
    						gcm.$setWaypoint(pos, [0, 0, 0, 0], "M", "Meeting point", "meeting_" + missData.missionID);
    
    					}.bind(this),
    					location: "INNER_SYSTEM",
    					locationSeed: list[i].ID
    				});
    				if (this._checkPlayerNearbyTimer == null || this._checkPlayerNearbyTimer.isRunning === false) {
    					this._checkPlayerNearbyTimer = new Timer(this, this.$isPlayerNearby, 5, 5);
    				}
    			}
    
    			// *** type 32 - fly to waypoint, meet ship deliver data package
    			if (list[i].data.missionType === 32 &&
    				list[i].destination === system.ID &&
    				list[i].data.quantity === 0 &&
    				list[i].data.targetQuantity > 0 &&
    				list[i].data.destroyedQuantity === 0 &&
    				list[i].expiry > clock.adjustedSeconds) {
    
    				this._setData.push({
    					missionType: 32,
    					missionID: list[i].ID,
    					source: list[i].source,
    					goons: 0,
    					quantity: 0,
    					target: list[i].data.targetQuantity
    				});
    
    				// add ship with populator
    				system.setPopulator("gcm-deliver-data-package-" + list[i].ID, {
    					callback: function (pos) {
    						var ms = worldScripts.GalCopBB_MeetShip;
    						var missData = ms.$getMissionData(32);
    						var gcm = worldScripts.GalCopBB_Missions;
    						var shp = null;
    						var checkShips = null;
    						for (var j = 1; j <= 5; j++) {
    							checkShips = system.addShips("[" + gcm.$getRandomShipKey(parseInt(missData.missionID), 0) + "]", 1, pos, 3000);
    							if (checkShips) shp = checkShips[0];
    							if (shp) break;
    						}
    						if (shp) {
    							if (gcm._rsnInstalled) shp.shipUniqueName = gcm.$getRandomShipName(shp, "trader");
    							shp.bounty = 0;
    
    							// set script to default, to avoid a special script for the trader doing stuff. (like setting a new AI)
    							shp.setScript("oolite-default-ship-script.js");
    							shp.switchAI("oolite-nullAI.js");
    							shp.script._savedPrimaryRole = shp.primaryRole;
    							shp.primaryRole = "gcm_deliverypoint"; // to avoid pirate attacks
    							shp.desiredSpeed = 0;
    
    							// remove any escorts that came with the ship
    							if (shp.escorts) {
    								for (var j = shp.escorts.length - 1; j >= 0; j--) {
    									shp.escorts[j].remove(true);
    								}
    							}
    
    							shp.script._missionID = missData.missionID;
    							// add our shipDied event to the shp
    							if (shp.script.shipDied) shp.script.$gcm_hold_shipDied = shp.script.shipDied;
    							shp.script.shipDied = gcm.$gcm_entity_shipDied;
    							if (shp.script.shipBeingAttacked) shp.script.$gcm_hold_shipBeingAttacked = shp.script.shipBeingAttacked;
    							shp.script.shipBeingAttacked = ms.$gcm_contact_shipBeingAttacked;
    							if (shp.script.shipBeingAttackedByCloaked) shp.script.$gcm_hold_shipBeingAttackedByCloaked = shp.script.shipBeingAttackedByCloaked;
    							shp.script.shipBeingAttackedByCloaked = ms.$gcm_contact_shipBeingAttackedByCloaked;
    							if (shp.script.shipAttackedWithMissile) shp.script.$gcm_hold_shipAttackedWithMissile = shp.script.shipAttackedWithMissile;
    							shp.script.shipAttackedWithMissile = ms.$gcm_contact_shipAttackedWithMissile;
    
    							// set up broadcast comms interface
    							var bcc = worldScripts.BroadcastCommsMFD;
    							if (bcc.$checkMessageExists("gcm_transmit_datapackage") === false) {
    								bcc.$createMessage({
    									messageName: "gcm_transmit_datapackage",
    									callbackFunction: ms.$transmitDataPackage.bind(ms),
    									displayText: "Transmit data package",
    									messageText: expandDescription("Standby for delivery of data package."),
    									ship: shp,
    									transmissionType: "target",
    									deleteOnTransmit: true,
    									delayCallback: 5,
    									hideOnConditionRed: true
    								});
    							}
    							gcm.$setWaypoint(pos, [0, 0, 0, 0], "M", "Meeting point", "meeting_" + missData.missionID);
    						} else {
    							log("galcopBB_meetship", "!!ERROR: Data package ship not spawned!");
    						}
    					}.bind(this),
    					location: "INNER_SYSTEM",
    					locationSeed: list[i].ID
    				});
    				if (this._checkPlayerNearbyTimer == null || this._checkPlayerNearbyTimer.isRunning === false) {
    					this._checkPlayerNearbyTimer = new Timer(this, this.$isPlayerNearby, 5, 5);
    				}
    			}
    
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // gets mission specific data for the populator routines
    // this works on a first in/first out basis - if there are multiple missions of the same type being populated, the mission specific data would
    // get pushed in to the setData array in order, and then this routine pulls that data out in the same order
    // that's the theory, anyway!
    this.$getMissionData = function (missionType) {
    	for (var i = 0; i < this._setData.length; i++) {
    		if (this._setData[i].missionType === missionType) {
    			var result = {
    				missionID: this._setData[i].missionID,
    				trueMissionType: (this._setData[i].trueMissionType ? this._setData[i].trueMissionType : missionType),
    				source: this._setData[i].source,
    				goons: this._setData[i].goons,
    				quantity: this._setData[i].quantity,
    				target: this._setData[i].target
    			};
    			this._setData.splice(i, 1);
    			return result;
    		}
    	}
    	return null;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillEnterWitchspace = function (cause, destination) {
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = function (whom, why) {
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$stopTimers = function $stopTimers() {
    	if (this._checkPlayerNearbyTimer && this._checkPlayerNearbyTimer.isRunning) this._checkPlayerNearbyTimer.stop();
    	delete this._checkPlayerNearbyTimer;
    	if (this._deliveryPointTimer && this._deliveryPointTimer.isRunning) this._deliveryPointTimer.stop();
    	delete this._deliveryPointTimer;
    	if (this._contactPointTimer && this._contactPointTimer.isRunning) this._contactPointTimer.stop();
    	delete this._contactPointTimer;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // attached to meeting ships, designed to handle what happens if the ship is attacked
    this.$gcm_contact_shipBeingAttacked = function $gcm_contact_shipBeingAttacked(whom) {
    	if (this.ship.script.$gcm_hold_shipBeingAttacked) this.ship.script.$gcm_hold_shipBeingAttacked(whom);
    
    	worldScripts.GalCopBB_MeetShip.$contactShipAttacked(this.ship, whom);
    
    	// remove this script so it doesn't keep running
    	delete this.ship.script.shipBeingAttacked;
    	// restore the original script (if there was one)
    	if (this.ship.script.$gcm_hold_shipBeingAttacked) {
    		this.ship.script.shipBeingAttacked = this.ship.script.$gcm_hold_shipBeingAttacked;
    		delete this.ship.script.$gcm_hold_shipBeingAttacked;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_contact_shipBeingAttackedByCloaked = function $gcm_contact_shipBeingAttackedByCloaked() {
    	if (this.ship.script.$gcm_hold_shipBeingAttackedByCloaked) this.ship.script.$gcm_hold_shipBeingAttackedByCloaked();
    
    	worldScripts.GalCopBB_MeetShip.$contactShipAttacked(this.ship);
    
    	// remove this script so it doesn't keep running
    	delete this.ship.script.shipBeingAttackedByCloaked;
    	// restore the original script (if there was one)
    	if (this.ship.script.$gcm_hold_shipBeingAttackedByCloaked) {
    		this.ship.script.shipBeingAttackedByCloaked = this.ship.script.$gcm_hold_shipBeingAttackedByCloaked;
    		delete this.ship.script.$gcm_hold_shipBeingAttackedByCloaked;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_contact_shipAttackedWithMissile = function $gcm_contact_shipAttackedWithMissile(missile, whom) {
    	if (this.ship.script.$gcm_hold_shipAttackedWithMissile) this.ship.script.$gcm_hold_shipAttackedWithMissile(missile, whom);
    
    	worldScripts.GalCopBB_MeetShip.$contactShipAttacked(this.ship, whom);
    
    	// remove this script so it doesn't keep running
    	delete this.ship.script.shipAttackedWithMissile;
    	// restore the original script (if there was one)
    	if (this.ship.script.$gcm_hold_shipAttackedWithMissile) {
    		this.ship.script.shipAttackedWithMissile = this.ship.script.$gcm_hold_shipAttackedWithMissile;
    		delete this.ship.script.$gcm_hold_shipAttackedWithMissile;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$contactShipAttacked = function $contactShipAttacked(ship, whom) {
    	ship.commsMessage(expandDescription("[gcm_contact_attacked]"), player.ship);
    
    	var bb = worldScripts.BulletinBoardSystem;
    	var missID = ship.script._missionID;
    	if (missID > 0) {
    		var item = bb.$getItem(missID); // this._dataCache.script._missionID
    		item.data.targetQuantity = 0;
    
    		// remove the broadcast comms message - this mission is now a bust
    		var w = worldScripts.BroadcastCommsMFD;
    		w.$removeMessage("gcm_transmit_passcode");
    
    		if (ship.AIScript.name === "Null AI") ship.switchAI("oolite-traderAI.js");
    		ship.target = whom;
    
    		// turn off any pending responses
    		var g = worldScripts.GalCopBB_Missions_MFD;
    		if (g._subMessageTimer && g._subMessageTimer.isRunning) g._subMessageTimer.stop();
    		if (g._offerTimer && g._offerTimer.isRunning) g.$declineNewMission();
    	}
    	// get the ships response
    	if (whom) {
    		ship.target = whom;
    		ship.performAttack();
    	}
    	this.ship.AIScript.oolite_priorityai.reconsiderNow();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // response after player has transmitted passcode to a contact vessel (via BroadcastCommsMFD)
    this.$transmitPasscode = function $transmitPasscode() {
    	var targets = system.shipsWithPrimaryRole("gcm_contactpoint");
    	for (var i = 0; i < targets.length; i++) {
    		if (player.ship.position.distanceTo(targets[i]) < player.ship.scannerRange) {
    			worldScripts.GalCopBB_Missions.$unsetWaypoint("meeting_" + targets[i].script._missionID)
    
    			var bcc = worldScripts.BroadcastCommsMFD;
    			if (bcc.$checkMessageExists("gcm_player_arrived_data") === true) bcc.$removeMessage("gcm_player_arrived_data");
    
    			targets[i].commsMessage(expandDescription("[gcm_transmit_passcode]"), player.ship);
    
    			// start a timer to control data transfer to player
    			this._contactPoint = targets[i];
    			this._contactPointTimer = new Timer(this, this.$contactPointComplete.bind(this), 10, 0);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // completes the meet ship mission, and possibly sets up secondary mission
    this.$contactPointComplete = function $contactPointComplete() {
    	if (this._contactPoint.isInSpace) {
    		if (player.ship.position.distanceTo(this._contactPoint) > player.ship.scannerRange) {
    			// set up broadcast comms interface again
    			var bcc = worldScripts.BroadcastCommsMFD;
    			if (bcc.$checkMessageExists("gcm_transmit_passcode") === false) {
    				bcc.$createMessage({
    					messageName: "gcm_transmit_passcode",
    					callbackFunction: this.$transmitPasscode.bind(this),
    					displayText: "Transmit passcode",
    					messageText: expandDescription("The passcode is " + this._contactPoint.script._passcode),
    					ship: this._contactPoint,
    					transmissionType: "target",
    					deleteOnTransmit: true,
    					delayCallback: 5,
    					hideOnConditionRed: true
    				});
    			}
    			return;
    		}
    
    		var bb = worldScripts.BulletinBoardSystem;
    		var gcm = worldScripts.GalCopBB_Missions;
    		this._contactPoint.commsMessage(expandDescription("[gcm_passcode_success]"), player.ship);
    		var missID = this._contactPoint.script._missionID;
    
    		var item = bb.$getItem(missID); // this._dataCache.script._missionID
    		item.data.quantity = 1;
    
    		gcm.$logMissionData(item.ID);
    		player.consoleMessage(expandDescription("[goal_updated]"));
    
    		player.setPlayerRole("trader-courier+");
    
    		this._commsNPC = this._contactPoint;
    		
    		var id = -1 
    		var counter = 0;
    		while (id == -1) {
    			id = gcm.$createSecondaryMission(missID, true);
    			counter += 1;
    			// try 10 times to get a secondary
    			if (counter == 10) break;
    		}
    		if (id !== -1) {
    			// we've got a new (pending) secondary mission
    			// wait for response from player
    			gcm._pendingMissionCallback = this.$contactPointFree.bind(this);
    			gcm._pendingMissionOrigID = missID;
    		} else {
    			// give the player some reason to have called them out this far
    			this._contactPoint.commsMessage(expandDescription("[gcm_no_mission]"), player.ship);
    			// no mission, so just free the ship to go on its way
    			this.$contactPointFree();
    		}
    		// the mission can now be completed at this point - no earlier
    		// because the completion type could be "IMMEDIATE", if we do this too early, the mission will disappear from the BB
    		// and then all the chaining info is gone.
    		bb.$updateBBMissionPercentage(missID, 1);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$contactPointFree = function $contactPointFree() {
    	// contact is free now - revert to stored values and let them go
    	this._contactPoint.primaryRole = this._contactPoint.script._savedPrimaryRole;
    
    	this._contactPoint.switchAI("oolite-traderAI.js");
    	// remove any connection to this mission (so if they're attacked by someone there won't be any lingering mission links)
    	this._contactPoint.script._missionID = 0;
    
    	this._commsNPC = null;
    	this._contactPoint = null;
    	worldScripts.GalCopBB_Missions._pendingMissionCallback = null;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // response after player has started to transmit data package to target ship (via BroadcastCommsMFD)
    this.$transmitDataPackage = function $transmitDataPackage() {
    	var targets = system.shipsWithPrimaryRole("gcm_deliverypoint");
    	for (var i = 0; i < targets.length; i++) {
    		if (player.ship.position.distanceTo(targets[i]) < player.ship.scannerRange) {
    			targets[i].commsMessage(expandDescription("[gcm_transmit_datapackage]"), player.ship);
    
    			// start a timer to control data transfer to ship
    			this._deliveryPoint = targets[i];
    			this._dataDelivery = 0;
    			this._deliveryPointTimer = new Timer(this, this.$deliveryPointTransfer.bind(this), 2, 2);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // data package transfer process
    this.$deliveryPointTransfer = function $deliveryPointTransfer() {
    	// make sure the delivery point ship is still valid and in space
    	if (this._deliveryPoint.isValid === false || this._deliveryPoint.isInSpace === false) {
    		this._deliveryPointTimer.stop()
    		return;
    	}
    
    	// make sure we can still see the ship
    	if (player.ship.position.distanceTo(this._deliveryPoint) > player.ship.scannerRange) {
    		// add the broadcast comms message back into the list
    		var w = worldScripts.BroadcastCommsMFD;
    		if (w.$checkMessageExists("gcm_transmit_datapackage") === false) {
    			w.$createMessage({
    				messageName: "gcm_transmit_datapackage",
    				callbackFunction: this.$transmitDataPackage.bind(this),
    				displayText: "Transmit data package",
    				messageText: "Standby for delivery of data package.",
    				ship: this._deliveryPoint,
    				transmissionType: "target",
    				deleteOnTransmit: true,
    				delayCallback: 5
    			});
    		}
    		return;
    	}
    
    	this._dataDelivery += 10;
    	player.consoleMessage(this._dataDelivery + "\% complete", 1);
    
    	if (this._dataDelivery >= 100) {
    		this._deliveryPointTimer.stop();
    		if (this._deliveryPoint.isInSpace) {
    			this._deliveryPoint.commsMessage(expandDescription("[gcm_delivery_success]"), player.ship);
    			var missID = this._deliveryPoint.script._missionID;
    
    			var bb = worldScripts.BulletinBoardSystem;
    			var gcm = worldScripts.GalCopBB_Missions;
    			var item = bb.$getItem(missID); // this._dataCache.script._missionID
    			item.data.quantity = 1;
    
    			// the mission can now be completed 
    			bb.$updateBBMissionPercentage(missID, 1);
    
    			gcm.$logMissionData(item.ID);
    			player.consoleMessage(expandDescription("[goal_updated]"));
    
    			player.setPlayerRole("trader-courier+");
    
    			this._commsNPC = this._deliveryPoint;
    			var id = gcm.$createSecondaryMission(missID, true);
    
    			// contact is free now - revert to stored values and let them go
    			this._deliveryPoint.primaryRole = this._deliveryPoint.script._savedPrimaryRole;
    			this._deliveryPoint.switchAI("oolite-traderAI.js");
    			// remove any connection to this mission (so if they're attacked by someone there won't be any lingering mission links)
    			this._deliveryPoint.script._missionID = 0;
    
    			this._commsNPC = null;
    			this._deliveryPoint = null;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$isPlayerNearby = function $isPlayerNearby() {
    	// don't run this check if the player is busy
    	if (player.alertCondition === 3 || player.alertHostiles === true) return;
    	// get all powered ships in range
    	var ships = player.ship.checkScanner(true);
    	var missTypes = [30, 32];
    	for (var i = 0; i < ships.length; i++) {
    		if (ships[i].script.hasOwnProperty("_missionID") === true && ships[i].hasRole("gcm_delivery_ship") === true) {
    			var item = worldScripts.BulletinBoardSystem.$getItem(ships[i].script._missionID);
    			if (item && missTypes.indexOf(item.data.missionType) >= 0) {
    				if (ships[i].script.hasOwnProperty("_greeted") === false) {
    					ships[i].script._greeted = true;
    					var bcc = worldScripts.BroadcastCommsMFD;
    					// transmit message
    					ships[i].commsMessage(expandDescription("[gcm_meeting_question]"), player.ship);
    					if (bcc.$checkMessageExists("gcm_player_arrived_data") === false) {
    						bcc.$createMessage({
    							messageName: "gcm_player_arrived_data",
    							callbackFunction: this.$transmitNoResponse.bind(this),
    							displayText: "Greet ship",
    							messageText: expandDescription("[gcm_player_arrived_data]"),
    							ship: ships[i],
    							transmissionType: "target",
    							deleteOnTransmit: true,
    							hideOnConditionRed: true
    						});
    					}
    					bcc.$addShipToArray(ships[i], bcc._greeted);
    					bcc.$buildMessageList();
    				}
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$transmitNoResponse = function $transmitNoResponse() {
    	// this function intentionally left blank
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptedMission = function $acceptedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if (this._debug) log(this.name, "accepted mission id = " + missID);
    
    	if (!item) {
    		log(this.name, "!!ERROR: BB returned null value from $getItem on mission ID " + missID);
    		return;
    	}
    	gcm.$updateLastMissionDate(item.source, item.data.missionType);
    
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$completedMission = function $completedMission(missID) {
    	var p = player.ship;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    
    	// update mission history
    	gcm.$updateSuccessHistoryReputation(item);
    
    	// remove any data-type black market sales
    	if (item.data.missionType === 32) {
    		if (sbm) sbm.$removeSaleItem("DTA_DATA_DELiVERY:" + missID);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$confirmCompleted = function $confirmCompleted(missID) {
    	var p = player.ship;
    	var result = "";
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (item) {
    
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$terminateMission = function $terminateMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// adjust reputation only when the terminatePenalty flag is set to true 
    	if (item.data.terminatePenalty === true) {
    		// update mission history
    		gcm.$updateFailedHistoryReputation(item);
    	}
    	// remove any data-type black market sales
    	if (item.data.missionType === 32) {
    		if (sbm) sbm.$removeSaleItem("DTA_DATA_DELIVERY:" + missID);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$failedMission = function $failedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var eq = "";
    
    	if (!sbm) {
    		// if there's no blackmarket option, just remove the equipment
    		this.$terminateMission(missID);
    		return;
    	}
    
    	var item = bb.$getItem(missID);
    	// update mission history
    	gcm.$updateFailedHistoryReputation(item);
    
    	if (item.data.missionType === 32 && item.data.quantity === 0 && item.data.targetQuantity > 0) eq = "DTA_DATA_DELIVERY";
    
    	if (eq != "") gcm._equipmentFromFailedMissions.push({
    		missionType: item.data.missionType,
    		source: item.source,
    		equip: eq,
    		date: clock.adjustedSeconds
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateManifestEntry = function $updateManifestEntry(missID) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	gcm.$updateManifestEntry(missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
    	return "";
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 30 - meet ship
    this.$missionType30_Values = function $missionType30_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 40) + 20) / 10) * 10 + (7 - destSysInfo.government) * 20 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(200) // plus a possible bonus price, based on player score 
    		+
    		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 20); // plus a distance bonus
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = 0;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 32 - deliver data to ship
    this.$missionType32_Values = function $missionType32_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 40) + 20) / 10) * 10 + (7 - destSysInfo.government) * 20 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(200) // plus a possible bonus price, based on player score 
    		+
    		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 20); // plus a distance bonus
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price / 2);
    	return result;
    }
    Scripts/galcopbb_missiondetails.js
    "use strict";
    this.name = "GalCopBB_MissionDetails";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Gets the PhraseGen mission details version for each mission";
    this.license = "CC BY-NC-SA 4.0";
    
    /*
        Notes: 
        It appears that phraseGen has a problem with apostrophes inside dice roll (Dnn=) functions.
    
        Remember to prefix all [ and ] characters with a double backslash "\\". This ensures they won't get 
        translated by PhraseGen. Also remember to have a trailing space after the closing bracket, even with "," and ".".
    
        Be careful of including actual numbers in the text (eg "50km"). These should be added via one of the replacement 
        elements, rather than in the sentence text.
    
        Generally descriptions should hint at system or galactic events, rather than being explicit. 
        For instance, talking about relations cooling or heating up between two systems is OK as there doesn't need to be 
        any additional game content for it to be "true" in the player's mind. Talking about outright war between two 
        systems is harder, because now the war has to be (or should be) simulated with ships, the length of the war needs 
        to be calculated, and the repercussions of who won/lost need to be added. By keeping it hinted at only, there is 
        no need for additional resources.
        The exception to this rule is when a particular system or galactic event is part of the mission spec (eg medical 
        missions)
    */
    
    this._gcmPG = {
        "abandon": ["abandon", "dump", "drop", "leave behind"],
        "ability": ["ability", "skills", "capability", "power", "wherewithal"],
        "accept": ["accept", "take on", "agree to", "sign up for"],
        "achievable": ["achievable", "attainable", "reachable", "feasible", "viable", "realistic"],
        "agents": ["agents", "security agents", "undercover agents", "deep-cover agents", "security personnel", "deep-cover security personnel", "operatives", "security operatives", "undercover operatives", "undercover security operatives", "deep-cover operatives", "deep-cover security operatives"],
        "approximately": ["approximately", "about", "around"],
        "are_expensive": ["are expensive", "are quite expensive", "require considerable financial resources", "aren't cheap", "require a significant financial outlay"],
        "are_willing_to": ["are willing to", "are able to", "can", "are prepared to", "are ready to"],
        "assistance": ["help", "assistance", "aid", "support", "backing"],
        "attacked": ["attacked", "assaulted", "molested", "hit", "struck", "battered", "waylaid", "ambushed"],
        "attention": ["It has come to our attention", "We have reason to believe", "We have it on good authority"],
        "backbone": ["who have the ", "who have the backbone and ", "who have the fortitude and ", "who have the courage and ", "who have the grit and determination, and just the ", "who have the strength of character and "],
        "battle": ["a battle", "a skirmish", "a fight", "a mêlée", "a scuffle", "a fracas", "an altercation"],
        "be_given": ["be given", "be issued with", "be supplied with", "receive", "get", "get issued with", "get supplied with", "be provided with"],
        "be_sufficient": ["suffice", "do", "be enough", "be adequate", "be sufficient"],
        "broadcasting": ["giving off", "emitting", "broadcasting"],
        "broken_down": ["broken down", "been suspended", "been put on ice", "been put on hold", "stalled"],
        "calling_for": ["calling for", "asking for", "seeking", "requesting", "looking for"],
        "care_for": ["help", "assist", "care for", "look after"],
        "collect": ["collect", "recover", "retrieve", "pick up"],
        "concerning": ["concerning", "regarding", "about", "in relation to", "with regard to", "in connection with"],
        "constant": ["constant", "continual", "continuous", "regular"],
        "containers": ["canisters", "containers", "pods"],
        "contract": ["contract", "mission", "assignment", "commission"],
        "crime": ["cultural insensitivity", "inappropriate text messages", "a poorly-timed and innuendo-riddled comedy sketch", "bad breath", "a political minefield involving small, furry animals", "a bad case of food poisoning", "disagreement over the price of our chief import and export commodities", "an unfortunate incident involving a paper-clip and a writing stylus", "a dispute over land-rights", "a dispute over legal representation", "a dispute over political machinations", "a dispute over seating arrangements at the negotiation table", "the content of an internally leaked memo", "alcoholic beverages", "some very bad background muzak", "a dispute over the qualities of some TV sitcoms", "a dispute over the quality of imported Trumble meat", "the perceived lack of quality donuts during trade meetings", "the death of the primary negotiator", "the death of the substitute negotiator", "the inconvenience caused by a bomb detonating in the primary meeting room"],
        "desires": ["desires", "is keen to have", "needs", "wants", "wishes for"],
        "destroy": ["annihilate$", "batter$", "burn$", "crush$", "destroy$", "eradicate$", "exterminate$", "pulverise$", "terminate$", "vaporise$"],
        "destroyed": ["annihilated", "battered", "burned", "crushed", "destroyed", "eradicated", "exterminated", "obliterated", "pulverised", "terminated", "vaporised"],
        "destroying": ["annihilating", "battering", "burning", "crushing", "destroying", "eradicating", "exterminating", "obliterating", "pulverising", "terminating", "vaporising"],
        "deteriorated": ["deteriorated", "worsened", "declined", "gone downhill", "weakened"],
        "donation": ["donation", "contribution", "gift", "bequest", "endowment"],
        "due_to": ["due to", "due in no small part to", "thanks to", "due largely to", "largely due to", "owing to", "largely owing to", "mostly due to", "mainly due to", "in no small part due to", "largely because of", "largely thanks to", "because of", "largely because of", "mostly because of", "mainly because of"],
        "enable_us": ["enable us", "allow us", "help us"],
        "forced": ["forced", "compelled", "duty-bound", "obliged"],
        "grown_exponentially": ["has grown exponentially", "is very clear and growing each day", "grows continually", "is growing exponentially", "is growing continually", "is building every day"],
        "happen": ["happen", "take place", "transpire", "occur"],
        "happened": ["happened", "taken place", "occurred", "transpired"],
        "happening": ["happening", "taking place", "transpiring", "occurring"],
        "hard": ["hard", "difficult", "complicated", "tricky"],
        "harmful": ["harmful", "damaging", "dangerous", "unsafe", "detrimental", "toxic"],
        "helpful_reason": ["external arbitration", "mood lighting in the various meeting rooms", "fragrant candles in meetings", "a popular rock band for lunch-time entertainment during trade talks", "a broad selection of donuts and pastries during negotiations", "nicely pitched ambient background music during trade talks"],
        "imperative": ["imperative", "vital", "crucial", "essential"],
        "important": ["important", "critical", "valuable", "crucial", "vital", "significant", "imperative"],
        "in_search_of": ["in search of", "searching for", "looking for", "trying to find", "trying to locate", "in need of", "seeking"],
        "inexperienced": ["poor", "inadequate", "badly trained", "inexperienced", "opinionated", "loud-mouthed", "idiotic"],
        "incursions": ["incursions", "invasions", "raids", "attacks", "forays", "sorties"],
        "information": ["information", "data", "evidence", "material", "intelligence"],
        "information_about": ["information about", "data concerning", "evidence of", "material relating to", "intelligence about"],
        "is_willing_to": ["is willing to", "is able to", "can", "is prepared to", "is ready to"],
        "large": ["high", "large", "considerable", "significant", "substantial", "sizeable"],
        "many_different": ["many different types", "a wide variety", "several types", "a large array", "a complex array", "a diverse array", "a wide array"],
        "need_for": ["demand for", "need for", "necessity of"],
        "on_route_to": ["on route to", "on their way to", "heading for"],
        "operating_mantra": ["operating mantra", "reason for being", "stated goal", "simple goal", "fundamental drive", "goal and purpose"],
        "pilot": ["pilot", "commander", "captain"],
        "pirates": ["pirates", "brigands", "bandits"],
        "plenty_of": ["plenty of", "many", "quite a number of", "a large number of", "any number of"],
        "received": ["received", "uncovered", "discovered", "unearthed", "obtained", "collected", "gotten hold of"],
        "repercussions": ["repercussions", "consequences", "ramifications", "impact"],
        "set_up": ["set up", "established", "created"],
        "sexy": ["sexy", "glamorous", "that much fun", "trendy", "exciting", "fashionable", "thrilling"],
        "short": ["short", "brief", "limited"],
        "steal": ["steal", "pirate", "pinch", "pilfer", "filch", "appropriate", "thieve", "pocket", "nick", "make off with"],
        "stealing": ["stealing", "pirating", "pinching", "pilfering", "filching", "appropriating", "thieving", "pocketing", "nicking", "making off with"],
        "struggling": ["struggling", "fighting", "battling", "striving"],
        "travel_there": ["head there", "go there", "fly there", "travel there", "head over there", "fly over there", "journey there"],
        "travel_to": ["travel to", "enter", "go to", "fly to", "head to", "venture to", "head over to", "journey to", "journey into"],
        "venture_into": ["venture into", "enter", "head into", "go into", "fly into"],
        "vile": ["vile", "abhorrent", "abominable", "despicable", "disgusting", "hideous", "revolting", "toxic"],
        "war": ["war", "struggle", "fight", "battle"],
        "we_believe": ["we believe", "we think", "we estimate", "in our estimation", "it's our opinion", "we are of the opinion", "it's our considered opinion"],
    };
    
    this._debug = true;
    this._setDef = null;
    this._phraseGen = null;
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
        this._phraseGen = worldScripts.GNN_PhraseGen;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType1 = function $missionType1() {
        var def = {
            fieldA: ["danger is lurking in our system", "a hidden danger (D50=lurks|lies in wait) in our system", "A serious threat exists on our spacelanes"],
            fieldB: ["are needed", "are being sought", "are being called(D50= on|)", "are required"],
            fieldC: ["to help", "to assist", "to lend a hand"],
            fieldD: ["in cleaning up the spacelanes", "in making our system safer", "in improving the safety of all ships entering out system"],
            fieldE: ["going out", "heading out", "flying out"],
            fieldF: [].concat(this._gcmPG.destroying),
            fieldG: ["in this system", "in our system space", "in our transit lanes", "between the witchpoint and the main planet, or between the planet and the sun"],
            fieldH: [],
            fieldI: ["task involves", "job involves", "mission involves", "undertaking involves", "operational parameters are"],
            fieldJ: ["(D50=[5]|)a recent asteroid storm", "(D50=[5]|)an unfortunate mining incident", "(D50=[5]|)an accident involving mining personnel", "(D50=[5]|)a recent series of accidents involving asteroids"],
            fieldK: ["a number of", "quite a few", "several"],
            fieldL: ["threaten the safety of ships entering our system", "are creating havoc for ships on our spacelanes", "are causing consternation for all pilots in our system", "are impeding the flow of traffic into and out of our system", "has prompted local authorities to take action", "are lying around in our system"],
            fieldM: ["need your (D50=assistance|help)", "are asking for (D50=assistance|help)", "are requesting your (D50=assistance|help)", "are asking for your (D50=assistance|help)", "are [8] [9]s who can help"],
            fieldN: ["A strike by local miners, combined with "],
            fieldO: [].concat(this._gcmPG.hard),
            fieldP: [].concat(this._gcmPG.sexy),
            fieldQ: [].concat(this._gcmPG.in_search_of),
            fieldR: [].concat(this._gcmPG.pilot),
            sentences: [
                ">{9 }2 3 4 by 5 and #6 \\[target\\] asteroids (D50=anywhere you can find them |)7.",
                "(D50=The Spacelane Safety Commission is|We are) {8 >9 }3 4. The 9 5 and 6 \\[target\\] asteroids (D50=anywhere you can find them |)7.",
                "1. {1 has left behind 2 (D50=stray |)rocks that 3. We(D50=, the Spacelane Safety Commission,|) 4 in }6 \\[target\\] asteroids (D50=anywhere you can find them|wherever they might be hiding).",
                "{9! Want to earn a few extra credits? Prepared to do whatever it takes? The Spacelane Safety Commission is 8 anyone who is capable of hunting down and }6 \\[target\\] asteroids. (D50=Yes|We know), it's not (D50=particularly |){6, or (D50=even |)7, but it pays. Do you have what it takes?"
            ],
        };
    
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType2 = function $missionType2() {
        var def = {
            fieldA: [].concat(this._gcmPG.incursions),
            fieldB: ["increasing", "on the increase", "escalating", "intensifying"],
            fieldC: [].concat(this._gcmPG.in_search_of),
            fieldD: [].concat(this._gcmPG.backbone),
            fieldE: [].concat(this._gcmPG.ability),
            fieldF: [].concat(this._gcmPG.travel_there),
            fieldG: [].concat(this._gcmPG.destroy),
            fieldH: ["to assist with making", "to help with making", "to help us make"],
            fieldI: ["solutions(D50= to the problem|)", "answers(D50= to the problem|)", "resolutions(D50= to the problem|)", "ways to fix the problem"],
            fieldJ: ["helping", "assisting", "coming to the aid of", "providing assistance to"],
            fieldK: [" for all space-farer's", " for everyone", " for all ships that come here"],
            fieldL: ["plagued", "inundated", "overwhelmed", "beset"],
            fieldM: [].concat(this._gcmPG.pirates),
            fieldN: [" recently", " in recent months", " of late", " lately"],
            fieldO: ["vermin", "pests", "parasites", "bottom-dwellers", "scum", "poachers"],
            fieldP: [].concat(this._gcmPG.contract),
            fieldQ: ["struggled", "found it increasingly difficult", "found it hard"],
            fieldR: [].concat(this._gcmPG.pilot),
            sentences: [
                "Pirates 1 from nearby \\[destination\\] have been 2{5. We are (D50=now |)}3 >{9 }45 to 6 and 7 \\[target\\] pirate ships 8 our system safer. (D50=By doing this you will receive, aside from the monetary benefits, the unending gratitude of all the inhabitants of \\[system\\].|)",
                "With pirates 1 from nearby \\[destination\\] 2, we are (D50=now |)3 9. The conclusion we reached was that we need >{9 }45 to head to \\[destination\\] (D50=and|to engage and) 7 \\[target\\] pirate ships. By doing so you will be {1 local authorities in making our system safer2.",
                "Our system has been {3 by 4 from \\[destination\\]5, and we have 8 to (D50=cope|deal) (D50=effectively |)with the onslaught. We are (D50=now |)}3 any >{9 }45 to enter \\[destination\\] space (D50=and|to engage and) 7 \\[target\\] of the {6. Succeeding in this 7 will bring safety and (D50=security|stability) to our spacelanes(D50=, and gratitude from all the inhabitants of \\[system\\]|).",
                "{4 from nearby \\[destination\\] have5 become bolder in their }1 on our spacelanes, and local authorities have {8 to (D50=cope|deal) (D50=effectively |)with the onslaught. So we are }3 >{9 }45 to 6 and take them on. We need \\[target\\] of the {6 ^}7. If you succeed in this {7 you will be bringing (D50=peace and safety|safety and security) to our spacelanes.",
                "A recent (D50=series|wave) of pirate attacks from nearby \\[destination\\], with the associated loss of life and property, has (D50=prompted|driven) local authorities to authorise }1 into their space. The (D50=goal|outcome) of this {7 is simple: to (D50=|engage and )}7 \\[target\\] pirate ships. Aside from (D50=any|the) monetary (D50=rewards|benefits), (D50=and the gratitude of all the inhabitants of \\[system\\], |)completing this {7 will help (D50=to |)bring (D50=safety|peace) and security to our spacelanes."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType3 = function $missionType3() {
        var def = {
            fieldA: [].concat(this._gcmPG.due_to),
            fieldB: [].concat(this._gcmPG.crime),
            fieldC: ["drastic", "severe", "extreme", "harsh"],
            fieldD: [].concat(this._gcmPG.in_search_of),
            fieldE: [].concat(this._gcmPG.backbone),
            fieldF: [].concat(this._gcmPG.ability),
            fieldG: [].concat(this._gcmPG.destroy),
            fieldH: [],
            fieldI: ["lengthy negotiations", "our best efforts"],
            fieldJ: [].concat(this._gcmPG.helpful_reason),
            fieldK: [].concat(this._gcmPG.broken_down),
            fieldL: [].concat(this._gcmPG.are_willing_to),
            fieldM: [].concat(this._gcmPG.travel_to),
            fieldN: [].concat(this._gcmPG.incursions),
            fieldO: ["regret", "sorrow", "grief", "deep sorrow"],
            fieldP: ["delegates", "agents", "envoys", "emissaries", "ambassadors"],
            fieldQ: [].concat(this._gcmPG.destroying),
            fieldR: [].concat(this._gcmPG.pilot),
            sentences: [
                "A (D50=recent |)breakdown in trade relations between \\[system\\] and \\[destination\\], 1 2, has led to the 3 action of attempting to put a limit on their trade. We are 4 >{9 }56, who will risk the wrath of local authorities, to 7 \\[target\\] trader (D50=ships|vessels).",
                "Despite 9, and the presence of {1, trade relations between \\[system\\] and \\[destination\\] have 2. While regrettable, we are pursuing every possible action to bring \\[destination\\] back to the negotiation table, and so we are looking to put a limit on their trade. We are }4 >{9 }56, who {3 4 \\[destination\\] and }7 \\[target\\] trade (D50=ships|vessels).",
                "Trade relations with \\[destination\\] have {2 }1 2. In order to bring them back to the negotiation table we want to disrupt the flow of goods into their system. We are 4 >{9 }56, who {3 4 \\[destination\\] and }7 \\[target\\] trade (D50=ships|vessels).",
                "With trade talks stalled, }1 2, our leaders has been left with no alternative but to authorise {5 into \\[destination\\] to disrupt their trade. We are }4 >{9 }56, who {3 risk GalCop penalties by flying to \\[destination\\] and 8 \\[target\\] trade (D50=ships|vessels).",
                "It is with {6 that we have come to this point, but 7 from \\[destination\\] have refused to budge on key trade policies, which have forced us into the (D50=unfortunate|unenviable) position of having to resort to a more forceful response. We are authorising 5 into their space with the (D50=goal|sole purpose) of 8 \\[target\\] of their trade (D50=ships|vessels)."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType4 = function $missionType4() {
        var def = {
            fieldA: [].concat(this._gcmPG.incursions),
            fieldB: ["make any sort of impact on", "get any sort of traction with", "have any impact on"],
            fieldC: ["sailing into and out of the system", "brazenly chugging along the spacelanes without fear", "entering the system without fear", "able to enter the system almost without fear"],
            fieldD: ["we haven't generated one news story or public announcement", "we don't even rate a mention on their nightly news broadcasts", "we've been relegated to an also-ran behind the weather and sport on their news tabloids", "we are considered something of a has-been by the people there"],
            fieldE: ["know the truth about", "have their eyes opened about", "see the reality of the atrocities performed by"],
            fieldF: ["we now need help", "we are in need of some help", "we need some help", "we are now looking for some help"],
            fieldG: [].concat(this._gcmPG.in_search_of),
            fieldH: [].concat(this._gcmPG.destroy),
            fieldI: [],
            fieldJ: ["as a way of drawing attention to our cause", "as a way of bringing our cause back into the spotlight", "to once again instil (D50=terror|fear) into the hearts of traders in that system"],
            fieldK: ["had a pretty big impact on our ability to", "really messed up our illegal operations, to the point where we are struggling to", "drained the profits out of a lot of our operations, so that it is becoming extremely difficult to"],
            fieldL: ["send a message to", "let", "broadcast a message to", "broadcast over every comm channel, to"],
            fieldM: ["risking damage to their legal status", "dealing with the potential legal backlash", "leaving their moral high-ground at the door"],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [].concat(this._gcmPG.pilot),
            sentences: [
                "Our 1 in \\[destination\\] have been struggling to 2 the government there. More traders than we care to admit are 3, and 4. We want the people of \\[destination\\] to 5 their government. But 6. We are 7 >{9 who can risk the wrath of GalCop Vipers and }8 \\[target\\] trader (D50=ships|vessels), {1.",
                "A recent push by GalCop and local government forces has {2 keep our various lines of business operating smoothly. We want to 3 GalCop and anyone else that we will not go quietly into the night. We will survive. But }6. We are 7 >{9 who can(D50=, for a short time at least,|) defy GalCop by joining (D50=with us|our ranks) and 4 by going to \\[destination\\] to }8 \\[target\\] trader (D50=ships|vessels)(D50= from anywhere in the system|).",
                "With GalCop and local government forces spreading their version of 'peace' in \\[destination\\], traders have been 3. We want them to 5 their government and to return fear to the minds of all traders. We are 7 >{9 who are OK with 4, flying into \\[destination\\] and ruthlessly }8 \\[target\\] trade (D50=ships|vessels)(D50= from anywhere in the system|)."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType5 = function $missionType5() {
        var def = {
            fieldA: [].concat(this._gcmPG.incursions),
            fieldB: [].concat(this._gcmPG.in_search_of),
            fieldC: [].concat(this._gcmPG.pilot),
            fieldD: [].concat(this._gcmPG.backbone),
            fieldE: [].concat(this._gcmPG.ability),
            fieldF: ["increasing", "on the increase", "escalating", "intensifying"],
            fieldG: [].concat(this._gcmPG.venture_into),
            fieldH: [].concat(this._gcmPG.destroy),
            fieldI: ["combating", "fighting", "addressing", "contending with", "opposing", "battling"],
            fieldJ: ["recently", "in recent months", "of late", "lately"],
            fieldK: ["an increased number", "an [5] in the number", "a [4][3] in the number"],
            fieldL: ["increase", "upsurge", "escalation", "rise", "growth"],
            fieldM: ["steady ", "sharp ", "distinct "],
            fieldN: ["increase", "upsurge", "escalation"],
            fieldO: ["led us to believe", "brought us to the conclusion", "allowed us to determine"],
            fieldP: [].concat(this._gcmPG.contract),
            fieldQ: [],
            fieldR: [],
            sentences: [
                "Ongoing 1 by Thargoid warships have necessitated the recruitment of (D50=non-military|civilian) forces in 9 the threat. Long range (D50=scans|telemetry data) of the Thargoid arrival vector has {6 that their (D50=staging area|point of origin) is somewhere between \\[system\\] and \\[destination\\]. We are }2 >3 45 to 7 interstellar space between \\[system\\] and \\[destination\\] (D50=and|to engage and) 8 \\[target\\] Thargoid warships.",
                "We have been seeing {2 of Thargoid invasion vessels entering our system 1. Long range (D50=scans|telemetry data) taken from their arrival vector has 6 the Thargoids are (D50=coming from|staging) somewhere between \\[destination\\] and \\[system\\]. To combat this threat we are }2 (D50=non-military|civilian) >3 45 to 7 interstellar space (D50=and|to engage and) 8 \\[target\\] Thargoid warships.",
                "Thargoid warships have been invading our space with a disturbing frequency {1. Long-range (D50=scans|telemetry data) taken from their arrival vector has 6 they are (D50=coming from|staging) somewhere between \\[destination\\] and \\[system\\]. Our plan in }9 this threat is simple: we are 2 (D50=non-military|civilian) >3 45 to 7 interstellar space (D50=and|to engage and) 8 \\[target\\] Thargoid warships.",
                "Thargoid 1 have been 6 {1. While their ultimate purpose in our space is still unknown, their on-going presence has impacted on our trade. Long-range (D50=scans|telemetry data) taken from their arrival vector has 6 they are (D50=coming from|staging) somewhere between \\[destination\\] and \\[system\\], so to combat this threat we are }2 (D50=non-military|civilian) >3 45 to 7 interstellar space (D50=and|to engage and) 8 \\[target\\] Thargoid warships.",
                "Our military (D50=leaders|commanders) are planning a major (D50=battle|assault) against the Thargoids, but (D50=prior to this|before this happens) they want some non-military >3 45 to 7 interstellar space between \\[system\\] and \\[destination\\] to (D50=perform some diversionary tactics|act as a diversion). The military has authorised this {7, for the destruction of \\[target\\] warships, on top of the usual bounty given for Thargoid warship destruction."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType6 = function $missionType6() {
        var def = {};
        if (Math.random() > 0.4) {
            def = {
                fieldA: [].concat(this._gcmPG.containers),
                fieldB: ["stash", "supply", "hoard", "stockpile"],
                fieldC: ["abandoned", "dumped", "dropped", "discarded", "ditched"],
                fieldD: [].concat(this._gcmPG.in_search_of),
                fieldE: ["have them recovered", "get them back", "collect them"],
                fieldF: [].concat(this._gcmPG.pilot),
                fieldG: [].concat(this._gcmPG.is_willing_to),
                fieldH: [].concat(this._gcmPG.travel_there),
                fieldI: ["recover them", "pick them up", "collect them", "scoop them", "gather them up"],
                fieldJ: ["return them", "bring them back"],
                fieldK: ["quite valuable", "very precious", "quite expensive", "quite costly", "valuable", "expensive", "costly"],
                fieldL: ["Due to (D50=a rather|an) unfortunate (D50=sequence|series) of events, one of our trade (D50=ships|vessels) was", "Because of (D50=a rather|an) unfortunate (D50=sequence|series) of events, one of our trade (D50=ships|vessels) was", "An unfortunate (D50=sequence|series) of events has resulted in one of our trade (D50=ships|vessels) being", "Largely due to (D50=a rather|an) unfortunate (D50=sequence|series) of events, one of our trade (D50=ships|vessels) was", "Largely because of (D50=a rather|an) unfortunate (D50=sequence|series) of events, one of our trade (D50=ships|vessels) was", "Mainly because of (D50=a rather|an) unfortunate (D50=sequence|series) of events, one of our trade (D50=ships|vessels) was"],
                fieldM: [],
                fieldN: [].concat(this._gcmPG.attacked),
                fieldO: ["was [5]", "came under fire", "was fired upon", "has come under fire"],
                fieldP: [].concat(this._gcmPG.destroyed),
                fieldQ: ["exploded", "detonated", "disintegrated", "blew up"],
                fieldR: [].concat(this._gcmPG.contract),
                sentences: [
                    "A 2 of \\[target\\] cargo 1 has been 3 in an asteroid field in the \\[destination\\] system, \\[position\\]. The contents of these canisters is {2, so we are (D50=obviously|naturally) (D50=keen to|eager to) }5. We are 4 a 6 who 7 8, find the \\[target\\] lost cargo 1, 9 and {1, intact.",
                    "One of our trade (D50=ships|vessels) {6 in the \\[destination\\] system, \\[position\\], resulting in the }6 having to (D50=flee|eject) and the (D50=ship|vessel) {7. However, the }6 has notified us that the cargo was 3 in an asteroid field before the (D50=ship|vessel) {8, which gives us a chance to recover it. Your 9 is to }8, find the \\[target\\] missing cargo 1, 9 and {1 here.",
                    "{3 (D50=forced|compelled) to dump his 2 cargo in an asteroid field in \\[destination\\], \\[position\\]. We are (D50=keen|eager) to }5, so we are 4 a 6 who 7 8, find the \\[target\\] 3 cargo 1, 9 and {1(D50= here|).",
                    "A trade (D50=ship|vessel) (D50=carrying|hauling) valuable cargo {6 in an asteroid field in the \\[destination\\] system, \\[position\\], and was (D50=forced|compelled) to dump the cargo rather than risking it to being destroyed with his ship. (D50=Due to|Because of) the value of this cargo, we are (D50=understandably|most) (D50=keen|eager) to get it back. So we're }4 a 6 who 7 8, find the \\[target\\] 1, 9 and {1(D50= here|)."
                ],
            };
        } else {
            def = {
                fieldA: [].concat(this._gcmPG.happened),
                fieldB: ["an unusual", "a strange", "a weird", "a bizarre"],
                fieldC: [].concat(this._gcmPG.pilot),
                fieldD: [].concat(this._gcmPG.due_to),
                fieldE: ["past run-ins with GalCop", "a dislike for our cuisine", "the fallout from a broken relationship", "a falling out with a business partner", "having called a local politician [6]", "having called the local police chief [6]", "having called a local gangland leader [6]", "a bad case of indigestion"],
                fieldF: ["a[7] skunk", "a[7] trumble", "a[7] rat", "a[7] snake", "a[7] rodent", "a[7] gerbil", "a[7] micro-organism", "a[7] food-processor"],
                fieldG: ["n incontinent", " lop-sided", "n odorous", " miserable", " glutinous", "n overweight", "n oversized", " low-life", " dirty", " moronic"],
                fieldH: [].concat(this._gcmPG.travel_there),
                fieldI: ["recover them", "pick them up", "collect them", "scoop them", "gather them up"],
                fieldJ: ["abandoned", "dumped", "dropped", "discarded", "ditched"],
                fieldK: [].concat(this._gcmPG.containers),
                fieldL: ["has declined", "has refused", "was unwilling"],
                fieldM: ["quite valuable", "very precious", "quite expensive", "quite costly", "(D50=rather |)valuable", "(D50=rather |)expensive", "(D50=rather |)costly"],
                fieldN: [],
                fieldO: [],
                fieldP: [],
                fieldQ: [],
                fieldR: [],
                sentences: [
                    "2 set of circumstances has 1 with one of our shipments. The 3 {3 to (D50=actually |)enter our system }4 5. So instead, they have deposited the cargo in the \\[destination\\] system, in an asteroid field \\[position\\]. We (D50=need|require) a 3 to 8, find the \\[target\\] {1 cargo 2, }9 and return it here.",
                    "The 3 of one of our trade (D50=ships|vessels) {3 to (D50|actually |)enter our system }4 5, so instead they have {1 their 4 cargo in an asteroid field in the \\[destination\\] system, \\[position\\]. We (D50=need|require) a }3 to 8, find the \\[target\\] {1 2, }9 and bring them back here safely."
                ],
            };
        }
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType7 = function $missionType7() {
        var def = {
            fieldA: [].concat(this._gcmPG.in_search_of),
            fieldB: [].concat(this._gcmPG.pilot),
            fieldC: ["heading there", "going there", "flying there", "travelling there", "heading over there", "flying over there"],
            fieldD: [].concat(this._gcmPG.containers),
            fieldE: ["recovering them", "picking them up", "collecting them", "scooping them", "gathering them up"],
            fieldF: ["hauling", "carrying", "bringing"],
            fieldG: ["be a breeze", "be a walk in the park", "be simple enough", "be a piece of cake"],
            fieldH: ["were rewarded with", "scored big-time with", "had success with", "netted", "scored"],
            fieldI: ["Unfortunately they didn't have enough free cargo space", "But they were short on cargo space with which", "As luck would have it, they were already quite full from other raids and so were unable", "But as fate would have it, there wasn't enough cargo space on the ships in the convoy so they couldn't"],
            fieldJ: [].concat(this._gcmPG.attacked),
            fieldK: ["very juicy", "very satisfying", "most excellent", "impressive", "significant", "sizeable", "large, juicy", "very lucrative", "very sweet"],
            fieldL: ["loot", "cargo pods", "cargo canisters", "cargo", "cargo containers", "swag", "booty", "plunder", "spoils"],
            fieldM: ["scoop it all", "pick it all up", "collect it all", "recover it all"],
            fieldN: ["incursion into", "invasion into", "raid in", "attack in", "foray into", "sortie into"],
            fieldO: ["someone else finds it", "it falls into the wrong hands", "it is lost forever", "it disappears for good"],
            fieldP: ["collected", "scooped up", "recovered", "gathered up"],
            fieldQ: ["at commodity markets that aren't, shall we say, operated in complete compliance with the law", "among the many traders that pass through that system", "by extracting our cargo tax from passing ships"],
            fieldR: [", unfortunately, they didn't have enough free cargo space", " they were short on cargo space", ", as luck would have it, they were already quite full from other raids", ", as fate would have it, there wasn't enough cargo space on the ships in the convoy "],
            sentences: [
                "One of our teams {1 a convoy in the \\[destination\\], and }8 a {2 haul of 3. }9 to {4. We're }1 a 2 who can help us out by 3, finding the \\[target\\] cargo 4 in an asteroid field \\[position\\], 5 and 6 them back here. Slip in and out - should 7.",
                "During a recent {5 \\[destination\\], we }8 a {2 bounty, but some of it had to be left behind simply because9. This cargo, \\[target\\] }4 of it, located in an asteroid field \\[position\\], needs to be {7 and (D50=returned|brought back) here, before {6.",
                "Our raiding (D50=crews|teams) (D50=often|regularly) head to \\[destination\\], as there can be some {2 deals to be had {8. (D50=One trip recently|One recent trip) was (D50=particularly|especially) fruitful - so fruitful in fact that there wasn't enough cargo space to 4. So we are looking for a }2 to head over to \\[destination\\], find the \\[target\\] 4 in an asteroid field \\[position\\], and haul them all back here."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType8 = function $missionType8() {
        var def = {
            fieldA: [].concat(this._gcmPG.deteriorated),
            fieldB: [].concat(this._gcmPG.due_to),
            fieldC: [].concat(this._gcmPG.crime),
            fieldD: [].concat(this._gcmPG.in_search_of),
            fieldE: [].concat(this._gcmPG.pilot),
            fieldF: [].concat(this._gcmPG.backbone),
            fieldG: [].concat(this._gcmPG.ability),
            fieldH: [].concat(this._gcmPG.containers),
            fieldI: ["Our relations", "Our trade relations", "Trade relations"],
            fieldJ: [].concat(this._gcmPG.steal),
            fieldK: [].concat(this._gcmPG.helpful_reason),
            fieldL: [].concat(this._gcmPG.broken_down),
            fieldM: [].concat(this._gcmPG.stealing),
            fieldN: [].concat(this._gcmPG.be_sufficient),
            fieldO: [].concat(this._gcmPG.inexperienced),
            fieldP: [].concat(this._gcmPG.we_believe),
            fieldQ: ["inequitable", "unfair", "discriminatory", "unjust"],
            fieldR: ["take matters into our own hands", "deal with this issue directly", "do something about it"],
            sentences: [
                "9 with the \\[destination\\] system have 1, 2 3, and we are now actively pursuing all possible avenues for restitution. We are 4 >5 67 to find traders in the \\[destination\\] system and {1 their cargo. We (D50=need|require) \\[target\\] }8 of any type collected.",
                "Despite (D50=lengthy negotiations|our best efforts), and the presence of {2, our trade relations with \\[destination\\] have }1 to the point where they have {3, although many believe it was }2 3. To force them back to the negotiation table we are looking put a limit on their trade by sending willing >5 into their system and {1 \\[target\\] cargo }8. Any type of cargo will {5.",
                "2 some {6 negotiators and }3, trade relations with \\[destination\\] have 1 to the point where they have {3. But we are determined to bring them back to the negotiation table, so to force their hand we are looking to limit their trade by sending willing >}5 67 to go on a mission to {1 \\[target\\] cargo }8 from anywhere within their system. Any type of cargo will {5.",
                "In an embarrassing turn of events for our government, 2 3, we have been forced into a rather {8 trade deal with \\[destination\\]. But instead of just living with the economic (D50=limitations|implications) we are choosing to 9. We are authorising attacks on traders in \\[destination\\], with the goal of 4 \\[target\\] cargo }8 of any type. {7 this action will enable us to re-negotiate the trade agreement and bring about economic growth."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType9 = function $missionType9() {
        var def = {
            fieldA: [].concat(this._gcmPG.steal),
            fieldB: [].concat(this._gcmPG.containers),
            fieldC: [].concat(this._gcmPG.in_search_of),
            fieldD: [].concat(this._gcmPG.pilot),
            fieldE: ["Easy as pie", "Like stealing candy from a baby", "Money for jam", "Piece of cake", "As easy as 123", "Nice and simple", "No complications whatsoever"],
            fieldF: ["Roll up! Roll up!", "Come on in, commander!", "Welcome to paradise, commander!", "Welcome to the new world order!", "Come and see how the other half lives!"],
            fieldG: [],
            fieldH: [],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "We are pirates. We hunt traders for cargo. 5 - fly to \\[destination\\] and 1 \\[target\\] 2 of anything. It all sells.",
                "We want to increase the pressure we've been exerting on \\[destination\\], to make them feel the pain. One way we do that is to target their trade. We need anyone with a ship and a scoop to head over there and 1 \\[target\\] 2 of anything and everything.",
                "We are 3 >4 to join our team. The cost of entry is pretty low - just head to \\[destination\\] and 1 \\[target\\] 2 of anything. Even food has value, if you got it for nothing.",
                "6 This is your chance to live the life of a pirate. All you need is a ship and a scoop, and you're ready to roll. Head on over to \\[destination\\] and 1 \\[target\\] 2 of anything, and (D50=return|bring) it back here.",
                "This is as simple as it gets for pirates. Go to some location, in this case, \\[destination\\], and 1 \\[target\\] 2 of any commodity you can extract from traders in that system. Then (D50=return|bring) it back here. 5."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType10 = function $missionType10() {
        var def = {
            fieldA: [].concat(this._gcmPG.deteriorated),
            fieldB: [].concat(this._gcmPG.due_to),
            fieldC: [].concat(this._gcmPG.crime),
            fieldD: [].concat(this._gcmPG.in_search_of),
            fieldE: [].concat(this._gcmPG.pilot),
            fieldF: [].concat(this._gcmPG.backbone),
            fieldG: [].concat(this._gcmPG.ability),
            fieldH: [].concat(this._gcmPG.containers),
            fieldI: ["Our relations", "Our trade relations", "Trade relations"],
            fieldJ: [].concat(this._gcmPG.steal),
            fieldK: [].concat(this._gcmPG.helpful_reason),
            fieldL: [].concat(this._gcmPG.broken_down),
            fieldM: [].concat(this._gcmPG.stealing),
            fieldN: [],
            fieldO: [].concat(this._gcmPG.inexperienced),
            fieldP: [].concat(this._gcmPG.we_believe),
            fieldQ: ["inequitable", "unfair", "discriminatory", "unjust"],
            fieldR: ["take matters into our own hands", "deal with this issue directly", "do something about it"],
            sentences: [
                "9 with the \\[destination\\] system have 1, 2 3, and we are now actively pursuing all possible avenues for restitution. We are 4 >5 67 to find traders in the \\[destination\\] system and {1 their cargo. We (D50=need|require) \\[target\\] }8 of \\[commodity\\] collected. Remember, only \\[commodity\\].",
                "Despite (D50=lengthy negotiations|our best efforts), and the presence of {2, our trade relations with \\[destination\\] have }1 to the point there they have {3, although many believe it was }2 3. To force them back to the negotiation table we are looking put a limit on their trade by sending willing >5 into their system and {4 \\[target\\] cargo }8 of \\[commodity\\] only.",
                "2 some {6 negotiators and }3, trade relations with \\[destination\\] have 1 to the point where they have {3. But we are determined to bring them back to the negotiation table, so to force their hand we are looking to limit their trade by sending willing >}5 67 to go on a mission to {1 \\[target\\] }8 of \\[commodity\\] from anywhere within their system.",
                "In an embarrassing turn of events for our government, 2 3, we have been forced into a rather {8 trade deal with \\[destination\\]. But instead of just living with the economic (D50=limitations|implications) we are choosing to 9. We are authorising attacks on traders in \\[destination\\], with the goal of 4 \\[target\\] cargo }8 of \\[commodity\\]. {7 this action will enable us to re-negotiate the trade agreement and bring about economic growth."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType11 = function $missionType11() {
        var def = {
            fieldA: [].concat(this._gcmPG.in_search_of),
            fieldB: [].concat(this._gcmPG.steal),
            fieldC: [].concat(this._gcmPG.containers),
            fieldD: [].concat(this._gcmPG.pilot),
            fieldE: [],
            fieldF: [],
            fieldG: [],
            fieldH: [],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "We are pirates, and in order to maintain our position in this system, we raid neighbouring systems and 2 their cargo. At the moment, we have a need for \\[commodity\\], \\[target\\] 3 of it actually. We are 1 any 4 who can head to \\[destination\\] and do this job for us.",
                "Demand for \\[commodity\\] has gone up locally, but our raiding parties into \\[destination\\] haven't been able to meet that demand. We're looking for anyone with ship and a scoop to head over to \\[destination\\] and 2 \\[target\\] 3 of it.",
                "After a couple of disastrous runs into \\[destination\\], we're tight pressed to meet the demand for \\[commodity\\]. We need you to head over to that system, and 2 \\[target\\] 3 of it, and bring it back here."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType12 = function $missionType12() {
        var def = {
            fieldA: [].concat(this._gcmPG.attention),
            fieldB: [].concat(this._gcmPG.large),
            fieldC: ["shipped into", "transported into", "conveyed through", "distributed through"],
            fieldD: [].concat(this._gcmPG.vile),
            fieldE: [].concat(this._gcmPG.pilot),
            fieldF: [].concat(this._gcmPG.backbone),
            fieldG: [].concat(this._gcmPG.ability),
            fieldH: ["dump", "drop", "abandon", "ditch", "discard"],
            fieldI: [].concat(this._gcmPG.be_sufficient),
            fieldJ: ["GalCop security transcripts", "trade manifest data", "reports from both inside and outside the system"],
            fieldK: [].concat(this._gcmPG.in_search_of),
            fieldL: ["constant", "continual", "continuous", "regular"],
            fieldM: [].concat(this._gcmPG.we_believe),
            fieldN: [].concat(this._gcmPG.achievable),
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "1 that a 2 number of slaves are being 3 \\[destination\\], (D50=most|many) of whom are not willing participants. We are attempting to (D50=clamp|crack) down on this 4 trade by finding >5 67 to demand all traders in the \\[destination\\] system to 8 their cargo of slaves. A target of \\[target\\]t will 9. The slaves will be handed over to Amnesty Intergalactic for repatriation.",
                "According to {1, nearby \\[destination\\] has become a hub through which a }2 number of trader (D50=ships|vessels) are travelling while carrying an equivalently large number of slaves. This trafficking in life is a 4 practice, and we are attempting to stamp it out where we can. We are {2 >}5 67 to (D50=enter|visit) the \\[destination\\] system and demand all traders to 8 their cargo of slaves. We are looking for a target of \\[target\\] containers, and all slaves must be handed over to Amnesty Intergalactic for repatriation.",
                "A {3 flow of slave trafficking both into and through the \\[destination\\] system has prompted us to respond. We are 2 a }5 67 to enter \\[destination\\], find any ships carrying slaves, and demanding they be handed over. All slaves will then be repatriated through Amnesty Intergalactic. {4 a target of \\[target\\] containers of slaves will be 5.",
                "We have been monitoring the activities of slave traders in and around our system and have discovered that \\[destination\\] has become something of a hub for this 4 trade. We believe in freedom from slavery, so we are {2 a }5 67 to enter the \\[destination\\] system, find any ships carrying slaves, and demand they be handed over. All slaves will then be repatriated through Amnesty Intergalactic. A target of \\[target\\] containers should be {5."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType13 = function $missionType13() {
        var def = {
            fieldA: [].concat(this._gcmPG.plenty_of),
            fieldB: [].concat(this._gcmPG.in_search_of),
            fieldC: [].concat(this._gcmPG.pilot),
            fieldD: [].concat(this._gcmPG.venture_into),
            fieldE: [].concat(this._gcmPG.destroyed),
            fieldF: [].concat(this._gcmPG.destroy),
            fieldG: ["constantly", "continually", "continuously", "regularly"],
            fieldH: [],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "Our scientists are keen to study Thargoid technology, particularly in relation to their warships. There are 1 sources for their drone ships, but almost none for wreckage from larger Thargoid (D50=ships|vessels).\\n\\nWe are 2 a 3 who can 4 interstellar space and collect \\[target\\]t of alloys and wreckage from 5 Thargoid warships.",
                "With the war against the Thargoids continuing, we are conducting research into their technology in order to improve our ships and weapons against them. And while we have a steady supply of the Thargoid Robotic drone ships, we have far fewer samples of Thargoid warships to enable our technicians and scientists to adequately cover their tests and studies.\\n\\nSo we are 2 a 3 who can 4 interstellar space, 6 several Thargoid warships and collect \\[target\\]t of any alloys or wreckage left behind.",
                "War is (D50=escalating|intensifying) with the Thargoids, and we are 7 2 ways to improve our technology to better match theirs. In order to do that, our scientists (D50=need|require) access to Thargoid equipment and technology. We already have a steady supply of their drone ships - what we (D50=need|require) is wreckage from their warships.\\n\\nSo we are 2 a 3 who can 4 interstellar space and collect \\[target\\]t of alloys or wreckage from 5 Thargoid warships.",
                "While there is always a market for Thargoid drone ships, there is less of a one for the wreckage of Thargoid warships. But in order to study Thargoid technology and get the upper hand in the ongoing war, our scientists need \\[target\\]t of alloys or wreckage from 5 Thargoid warships. Studying this wreckage will give us the opportunity to study their technology (D50=intently|closely) and perhaps finding a way to eliminate the Thargoid menace."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType14 = function $missionType14() {
        var def = {
            fieldA: ["It has come to our attention", "We have reason to believe", "We have it on good authority"],
            fieldB: [].concat(this._gcmPG.in_search_of),
            fieldC: [].concat(this._gcmPG.pilot),
            fieldD: [].concat(this._gcmPG.travel_to),
            fieldE: [].concat(this._gcmPG.are_willing_to),
            fieldF: ["escalating", "mounting", "intensifying", "increasing"],
            fieldG: ["learned", "discovered", "ascertained", "found out"],
            fieldH: ["more than a little concerning", "extremely disquieting", "a cause for deep concern", "a reason to be seriously concerned"],
            fieldI: [].concat(this._gcmPG.agents),
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "1 that certain elements within the society on \\[destination\\] are importing high quality firearms from high-tech worlds. Given the level of antagonism that exists between both our worlds, we feel this development is 8. So we are 2 >3 who can 4 \\[destination\\] and collect \\[target\\]t of firearms from ships entering or leaving the system, and return them here. That way, we can restrict the flow of these weapons while at the same time making it unlikely the government on \\[destination\\] will hear about it.",
                "1 that the government of \\[destination\\] is stock-piling high-tech firearms. Given the relations between our two worlds has never been cordial, we are concerned these weapons may be used against us. We (D50=need|require) >3 to 4 their space and collect \\[target\\]t of firearms from ships entering or leaving the system, by whatever means necessary, and return them here.",
                "One of our 9, working in the \\[destination\\] system, has (D50=uncovered|discovered) large stockpiles of high-tech firearms, hidden away by the government. This constitutes a serious threat to the stability and safety of all local systems. To combat this, we are 2 >3 who 5 enter \\[destination\\] and collect \\[target\\]t of firearms from any (D50=ships|vessels) entering or leaving the system. Do not purchase firearms on the open market, as these transactions can be traced back to us. Once you have them, return them here.",
                "Tensions with a neighbouring system, \\[destination\\], are 6. We have 7 through unofficial channels that they are bringing in large quantities of firearms, which local military leaders have found 8. To combat this serious threat to our stability and safety we are authorising >3 to 4 \\[destination\\] and collect \\[target\\]t of firearms from any (D50=ships|vessels) entering or leaving the system, and return them here. Firearms purchases on the open market will not be accepted."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType15 = function $missionType15() {
        var def = {
            fieldA: [].concat(this._gcmPG.pilot),
            fieldB: [].concat(this._gcmPG.travel_to),
            fieldC: [].concat(this._gcmPG.steal),
            fieldD: [].concat(this._gcmPG.achievable),
            fieldE: [],
            fieldF: [],
            fieldG: [],
            fieldH: [],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "Local drug lords have reported that their trade has been interrupted by drug runners taking a larger than usual proportion of imported drugs to \\[destination\\]. Therefore they are commissioning >1 to 2 \\[destination\\] and 3 \\[target\\]t of narcotics from any and all ships in the system.",
                "In a move designed intentionally to disrupt the local illicit drug trade, dealers in the \\[destination\\] system have been bribing traders to bypass our system completely when delivering imported drugs. So to combat this development, the local dealers have combined to commission >1 to 2 \\[destination\\] and 3 \\[target\\]t of narcotics from any and all ships in the system.",
                "GalCop and local system authority (D50=ships|vessels) have been (D50=clamp|crack)ing down on our business, which means there's a shortage of drugs here and we are struggling to meet demands. We (D50=need|require) >1 to 2 \\[destination\\], 3 \\[target\\]t of narcotics from any and all ships in the system, and return them here.",
                "A series of bad crops and over-zealous police raids has led to a shortfall in our supply of recreational drugs. But we've heard that the \\[destination\\] system has plenty of narcotics being shipped in. We feel it's only fair for us to raid their trade ships and bring back some of these narcotics to help with local demand. A total of \\[target\\]t should be 4."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType16 = function $missionType16() {
        var def = {}
        def = {
            fieldA: ["hard at us", "hitting us hard", "attacking us hard"],
            fieldB: ["recently", "in recent days", "over the past few weeks"],
            fieldC: ["cutting in on our operations", "limiting our operations", "limiting our field of operations", "shutting down some of our operations", "restricting what we can do", "getting into our business interests"],
            fieldD: ["making life difficult", "making our lives unpleasant", "making life miserable", "making our lives here frustrating"],
            fieldE: ["flunkies", "hell-spawn", "hangers-on", "groupies", "mindless drones", "enforcers"],
            fieldF: ["But the time has come [7]", "We want [7]", "So we want [7]", "We're planning [7]", "So we're planning [7]", "We're going [7]", "So we're going [7]", "But all that is going to change", "But they are in for an unpleasant surprise", "But they are going to regret tangling with us"],
            fieldG: ["to fight back", "to give them a message they'll never forget", "to hit them where it hurts the most", "to hit them where they feel it most", "to attack them in the most fundamental way", "to take back what is ours", "to give them some payback"],
            fieldH: ["The mission we have for you", "The task we want you to perform", "The job we have in store for you", "The mission we need you to perform"],
            fieldI: [].concat(this._gcmPG.destroy),
            fieldJ: ["We know this is(D50= going to be|)", "We understand this is(D50= going to be|)", "We fully understand this is(D50= going to be|)"],
            fieldK: ["a dangerous mission", "a very dangerous mission", "a serious undertaking", "not something to be done on a whim", "not going to be easy"],
            fieldL: ["will mean gaining a fugitive status(D50= and make life harder for you [4]|)", "will result in a fugitive status(D50= for you|)(D5= and make life harder [4]|)", "will make life harder for you [4]", "will have serious ramifications for you [4]"],
            fieldM: ["in the future", "going forward", "in the short and long term"],
            fieldN: ["We trust", "We hope", "We think", "But we trust", "But we think", "But we hope"],
            fieldO: ["fee", "payment", "agreed price", "reward"],
            fieldP: ["will compensate [8]", "will go some way towards compensating you", "will make the task a little more agreeable [8]", "will enable you to perform this task with [9]"],
            fieldQ: ["in some way", "to some degree", "to a certain extent"],
            fieldR: ["enthusiasm", "vigour", "ferocity"],
            sentences: [
                "GalCop have been 1 2, 3 and generally 4. 6.\\n\\n8 is to 9 \\[target\\] police vessels in this system. {1 2, and 3. 5 that 6 7.",
                "GalCop have been 4 2, by 3. 6. {1 2, and 3. 5 that 6 7.\\n\\n}8 is to 9 \\[target\\] police ships in this system. (D50=Do you reckon \\[gcm_youre_fix\\]|Are you) (D50=ready|up for the challenge)?",
                "2 GalCop have been 4 by 1 and 3. 6.\\n\\n8 is to 9 \\[target\\] police craft in this system. {1 2, and 3. 5 that 6 7.",
                "2 GalCop Vipers and their 5 have been 1, 4 and 3. 6.\\n\\n8 is to 9 \\[target\\] police ships in this system. {1 2, and 3. 5 that 6 7."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType17 = function $missionType17() {
        var def = {
            fieldA: ["large", "significant", "considerable", "significant"],
            fieldB: ["waste", "rubbish", "garbage", "refuse", "trash", "junk"],
            fieldC: [].concat(this._gcmPG.destroyed),
            fieldD: [].concat(this._gcmPG.containers),
            fieldE: ["intense", "extreme", "concentrated"],
            fieldF: ["an oversupply", "a glut", "a surplus", "an excess"],
            fieldG: [].concat(this._gcmPG.due_to),
            fieldH: ["we know everyone loves", "who doesn't love", "you've gotta love"],
            fieldI: [].concat(this._gcmPG.destroying),
            fieldJ: [].concat(this._gcmPG.hard),
            fieldK: [].concat(this._gcmPG.sexy),
            fieldL: [].concat(this._gcmPG.pilot),
            fieldM: ["wonderful", "marvellous", "fantastic", "superb", "brilliant"],
            fieldN: [].concat(this._gcmPG.is_willing_to),
            fieldO: [].concat(this._gcmPG.calling_for),
            fieldP: [].concat(this._gcmPG.contract),
            fieldQ: [],
            fieldR: [],
            sentences: [
                "We have a 1 stockpile of radioactive 2 that we need 3. But we can't just blow up the waste in orbit, or in any area of local space. Instead, we need these cargo 4 dumped close to the sun, to be 3 by the 5 heat. At present, we have \\[target\\] containers of waste to dump.\\n\\nPlease note: the 4 must be destroyed by the heat of the sun, and in no other way, for this {7 to be completed successfully.",
                "Our planet-side waste storage (D50=facilities|centres) are (D50=currently|presently) experiencing 6 of radioactive 2. 7 the nature of this waste, it can't simply be 3 in orbit or in any area of local space. The only way to ensure this waste is completely 3 is to (D50=drop|dump) it in close proximity to the sun, and allow the 5 heat to destroy them. We have \\[target\\] containers waiting to be destroyed in this way.\\n\\nPlease note: the 4 must be destroyed by the heat of the sun, and in no other way, otherwise this {7 will be considered terminated.",
                ">{3! A 4 opportunity awaits to earn extra cash, and }8 extra cash. Our planet-side waste processing (D50=facilities|centres) are {6 any pilot who 5 fly \\[target\\] containers of radioactive }2 to the sun and dump them close by so that the 5 heat destroys them. So, no dropping them off at a nearby system, or 9 them in the station aegis. Yes, we know it's not {1, or even 2, but it pays. Do you have what it takes?",
                "With planet-side waste storage and processing at maximum capacity, we have been looking at other potential solutions for our radioactive 2. Our technicians have determined that this refuse can be effectively and safely 3 if it's dumped near the sun, after which the 5 heat will do the rest. But it can only be dumped hear the sun - destroying it any other way will render this {7 null and void. \\[target\\]t of waste has been transferred to the station, ready to be hauled away.",
                "Our power generation (D50=facilities|centres) are unable to process a large quantity of radioactive material on the planet, due to the high risk of radiation escaping the processing plant. So we regularly commission >{3 to take this waste and dump it near the sun, where the }5 heat can do all the work for us. We have \\[target\\] containers waiting for a willing {3 to step up and do this job for us.\\n\\nPlease note: the }4 must be destroyed by the heat of the sun, and in no other way, for this {7 to be payable."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType18 = function $missionType18() {
        var def = {
            fieldA: ["always been a little on the chilly side", "never been what you could call 'friendly'", "rarely been civil"],
            fieldB: ["we've been firing on their ships", "we've been stealing their cargo", "we've been undermining their illicit operations"],
            fieldC: ["[4]", "complete [4]", "a load of [4]", "complete and utter [4]", "utter [4]"],
            fieldD: ["rubbish", "bollox", "garbage", "nonsense", "drivel", "tosh", "hogwash", "twaddle", "bosh"],
            fieldE: ["we are now at war", "a rival gang war has begun", "we have declared war on them", "they have declared war on us", "things have escalated into all-out war"],
            fieldF: [].concat(this._gcmPG.in_search_of),
            fieldG: ["For the duration of this mission", "For the entirety of this mission", "While this mission is active", "During the timeframe of this mission"],
            fieldH: ["ensure", "make sure", "see to it that"],
            fieldI: [].concat(this._gcmPG.travel_to),
            fieldJ: [].concat(this._gcmPG.destroy),
            fieldK: [].concat(this._gcmPG.pilot),
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "Our relationship with the pirates from \\[destination\\] has 1, but now they have claimed that 2, which is 3. So 5. We are 6 a {2 to head over to \\[destination\\] and 1 \\[target\\] pirate (D50=ships|vessels). }7 we will 8 our own ships stay away, so there will be no chance of accidentally shooting the wrong pirate ship.",
                "Claims and counter-claims have been levelled at both sides, but regardless of the reasons, it has devolved into open war. We are 6 a {2 to head over to \\[destination\\] and {1 \\[target\\] pirate (D50=ships|vessels). }7 we will 8 our own ships stay away, so there will be no chance of accidentally shooting the wrong pirate ship.",
                "It probably comes as no surprise that our relationship with the pirates from \\[destination\\] has 1. But recent events have taken things to a whole new level. 5, and so we are 6 a {2 to help us take the war to them. We need you to }9 \\[destination\\] and {1 \\[target\\] of the blighters. We'll make sure none of our group will be in their space }7."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType20 = function $missionType20() {
        var def = {
            fieldA: [].concat(this._gcmPG.information_about),
            fieldB: [].concat(this._gcmPG.destroyed),
            fieldC: [].concat(this._gcmPG.pilot),
            fieldD: [].concat(this._gcmPG.in_search_of),
            fieldE: [].concat(this._gcmPG.travel_to),
            fieldF: ["notification of", "information [7]", "reports [7]"],
            fieldG: [].concat(this._gcmPG.concerning),
            fieldH: [].concat(this._gcmPG.broadcasting),
            fieldI: [].concat(this._gcmPG.pirates),
            fieldJ: [].concat(this._gcmPG.received),
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "Planetary authorities have {1 }1 a (D50=ship|vessel) that was 2 in \\[destination\\], \\[position\\]. And while the 3 managed to flee in an escape capsule, unfortunately the transponder was damaged and we have lost all contact with it. With a shortage of rescue (D50=ships|vessels) in the area, we are 4 a 3 to 5 \\[destination\\] and find this missing escape pod before the life support system fails.",
                "We have received 6 the destruction of one of our (D50=ships|vessels) while in transit in \\[destination\\], \\[position\\]. The 3 managed to get out in time in their escape capsule, but it was damaged in the explosion and the transponder is no longer 8 a tracking signal. Combined with a shortage of rescue (D50=ships|vessels) and a slowly failing life-support system in the escape pod, we are 4 a 3 to 5 \\[destination\\] and rescue it.",
                "An emergency situation has arisen in the \\[destination\\] system. One of our (D50=ships|vessels) was in transit when they were set upon by 9 and destroyed. The 3 managed to eject, but the capsule must have been damaged in the explosion and the transponder appears to be offline and worse, their life-support system is slowly failing. All we know is that their last known (D50=location|position) was \\[position\\]. Are you able to find this pilot for us?"
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType21 = function $missionType21() {
        var def = {
            fieldA: [].concat(this._gcmPG.information_about),
            fieldB: [].concat(this._gcmPG.destroyed),
            fieldC: [].concat(this._gcmPG.we_believe),
            fieldD: [].concat(this._gcmPG.pilot),
            fieldE: [].concat(this._gcmPG.in_search_of),
            fieldF: [].concat(this._gcmPG.venture_into),
            fieldG: [].concat(this._gcmPG.is_willing_to),
            fieldH: [].concat(this._gcmPG.forced),
            fieldI: [].concat(this._gcmPG.received),
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "Planetary authorities have 9 1 a (D50=ship|vessel) that was 2 in interstellar space, somewhere between \\[system\\] and \\[destination\\], and 3 the 4 managed to flee in an escape capsule. Unfortunately, the transponder for this escape capsule was damaged and we have lost all contact with it. With a shortage of rescue (D50=ships|vessels) in the area, we are 5 a 4 to 6 interstellar space between \\[system\\] and \\[destination\\] and find this missing escape pod before the life support system fails.",
                "A 4 was 8 to abandon his (D50=ship|vessel) in interstellar space, somewhere between \\[system\\] and \\[destination\\]. We are receiving intermittent transponder signals that make us suspect the escape capsule was damaged in the destruction of the (D50=ship|vessel). We are 5 a 4 who 7 6 interstellar space and rescue the escape pod before the life support systems fails or they are discovered and destroyed by Thargoids.",
                "We have received word of the destruction of one of our (D50=ships|vessels) in interstellar space, somewhere between \\[system\\] and \\[destination\\]. Some intermittent transponder signals have come through that lead us to believe the pilot managed to eject in their escape capsule, but there are strong indications the capsule was damaged in the explosion. We (D50=need|require) a 4 to head into interstellar space and rescue our pilot before their life-support fails or they are discovered and destroyed by Thargoids."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType22 = function $missionType22() {
        var def = {
            fieldA: [].concat(this._gcmPG.information_about),
            fieldB: [].concat(this._gcmPG.pilot),
            fieldC: [].concat(this._gcmPG.forced),
            fieldD: [].concat(this._gcmPG.imperative),
            fieldE: [].concat(this._gcmPG.we_believe),
            fieldF: [].concat(this._gcmPG.broadcasting),
            fieldG: [].concat(this._gcmPG.in_search_of),
            fieldH: [].concat(this._gcmPG.travel_to),
            fieldI: [].concat(this._gcmPG.pirates),
            fieldJ: [].concat(this._gcmPG.received),
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "We have {1 }1 a (D50=ship|vessel) that was abandoned in \\[destination\\] after the 2 was 3 to flee in an escape capsule. We (D50=need|have) to recover the black box from the (D50=ship|vessel) as information of 4 strategic importance is contained in it. 5 the (D50=ship|vessel) is 6 an intermittent signal that will show up on your space compass. We are 7 a 2 to 8 \\[destination\\], find this derelict (D50=ship|vessel) and recover the black box so we can act on the information.",
                "The 2 of a trade (D50=ship|vessel) from our fleet was 3 to abandon his (D50=ship|vessel) in \\[destination\\], and fortunately they were rescued shortly thereafter. However, 5 the (D50=ship|vessel)'s black box recorded some sensitive and time-critical information. We are 7 a 2 to 8 \\[destination\\], find the derelict (D50=ship|vessel), and recover the black box intact. 5 the derelict (D50=ship|vessel) is transmitting a signal intermittently, which will enable you to find it with your space compass.",
                "One of our >2 was allegedly attacked by 9 in the \\[destination\\] system and managed to eject. However, data from the escape pod has caused the insurers to question the story of the pilot. In order to fully process their claim, we (D50=need|require) a 2 to 8 \\[destination\\], find the derelict ship and recover the black box from within. The ship is giving off an irregular navigational signal, which should help you find it with your space compass. Retrieving this unit will enable the insurance claim to be processed quickly and get our 2 back in the black."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType23 = function $missionType23() {
        var def = {
            fieldA: [].concat(this._gcmPG.information_about),
            fieldB: [].concat(this._gcmPG.pilot),
            fieldC: [].concat(this._gcmPG.forced),
            fieldD: [].concat(this._gcmPG.imperative),
            fieldE: [].concat(this._gcmPG.in_search_of),
            fieldF: [].concat(this._gcmPG.venture_into),
            fieldG: [].concat(this._gcmPG.we_believe),
            fieldH: [].concat(this._gcmPG.received),
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "We have 8 1 a (D50=ship|vessel) that was abandoned in interstellar space, somewhere between \\[system\\] and \\[destination\\], after the 2 was 3 to flee in an escape capsule. We (D50=have|need) to recover the black box from the (D50=ship|vessel) as information of 4 strategic importance is contained in it. We are 5 a 2 to 6 interstellar space between \\[system\\] and \\[destination\\], find this derelict (D50=ship|vessel) and recover the black box so we can act on the information.",
                "The 2 of a trade (D50=ship|vessel) from our fleet was 3 to abandon his (D50=ship|vessel) in interstellar space, somewhere between \\[system\\] and \\[destination\\], and fortunately they were rescued shortly thereafter. However, 7 the (D50=ship|vessel)'s black box recorded some sensitive and time-critical information. We are 5 a 2 to 6 interstellar space between \\[system\\] and \\[destination\\], find the derelict (D50=ship|vessel), and recover the black box intact.",
                "One of our >2 was allegedly attacked in interstellar space, somewhere between \\[system\\] and \\[destination\\], but managed to eject. However, data from the escape pod has caused the insurers to question the story of the pilot. In order to fully process their claim, we (D50=need|require) a 2 to enter interstellar space between \\[system\\] and \\[destination\\], find the derelict ship and recover the black box from within. This will enable the insurance claim to be processed quickly and get our 2 back in the black."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType24 = function $missionType24() {
        var def = {
            fieldA: ["abandon", "eject from", "escape from", "leave"],
            fieldB: [].concat(this._gcmPG.forced),
            fieldC: [].concat(this._gcmPG.in_search_of),
            fieldD: [].concat(this._gcmPG.pilot),
            fieldE: [].concat(this._gcmPG.travel_to),
            fieldF: [].concat(this._gcmPG.destroyed),
            fieldG: [].concat(this._gcmPG.pirates),
            fieldH: [].concat(this._gcmPG.we_believe),
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "One of our couriers was 2 to 1 their (D50=ship|vessel) in the \\[destination\\] system, \\[position\\]. We have reports, however, that the ship wasn't completely 6 and that the special cargo on board may still be intact. We are 3 a 4 to 5 \\[destination\\], find the derelict (D50=ship|vessel), destroy it and return any cargo that is freed from the hulk.",
                "Our of our couriers was in transit in the \\[destination\\] system and on their way back here when they were set upon by 7 and 2 to 1 their (D50=ship|vessel) \\[position\\]. However, we don't believe the ship was destroyed, meaning there is a chance to recover the special cargo on board. We are 3 a 4 to 5 \\[destination\\], find the derelict (D50=ship|vessel), destroy it and return any cargo that is freed from the hulk.",
                "We take pride in the ability of our courier pilots to deliver on time, every time. Unfortunately, one was attacked by 7 in the \\[destination\\] system, and while the 4 is safe, having ejected from his ship, 8 the special cargo stored on his ship is still intact. This gives us the opportunity to send a willing 4 into \\[destination\\], with the job of finding the derelict (D50=ship|vessel), which is \\[position\\], destroying it, and returning any cargo that floats free."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType25 = function $missionType25() {
        var def = {
            fieldA: [].concat(this._gcmPG.information_about),
            fieldB: [].concat(this._gcmPG.destroyed),
            fieldC: [].concat(this._gcmPG.pilot),
            fieldD: [].concat(this._gcmPG.in_search_of),
            fieldE: [].concat(this._gcmPG.travel_to),
            fieldF: ["notification of", "information [7]", "reports [7]"],
            fieldG: [].concat(this._gcmPG.concerning),
            fieldH: [].concat(this._gcmPG.attention),
            fieldI: [].concat(this._gcmPG.happened),
            fieldJ: ["battle", "skirmish", "fight", "mêlée", "scuffle", "fracas", "altercation"],
            fieldK: [].concat(this._gcmPG.received),
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "Planetary authorities have {2 }1 a (D50=ship|vessel) that was 2 in \\[destination\\]. And while the 3 managed to flee in an escape capsule, the ejection system malfunctioned, sending the pod away from the crash site at tremendous speed. The pod is now in danger of leaving the system completely. With a shortage of rescue (D50=ships|vessels) in the area, we are 4 a 3 to 5 \\[destination\\] and find this missing escape pod before the life support system fails. GalCop tracking satellites will provide a regular waypoint update of the pod's last known (D50=location|position).",
                "We have received 6 the destruction of one of our (D50=ships|vessels) while in transit in \\[destination\\]. The 3 managed to escape in their escape capsule, but because of an engine malfunction, the pod was ejected from the ship at tremendous speed. The pod is now in danger of leaving the system entirely. Combined with a shortage of rescue (D50=ships|vessels) and a slowly failing life-support system in the escape pod, we are 4 a 3 to 5 \\[destination\\] and catch it. GalCop tracking satellites will provide a regular waypoint update of the pod's last known (D50=location|position).",
                "8 that one of our (D50=ships|vessels) was 2 while in transit in the \\[destination\\] system. Fortunately, the pilot managed to eject in an escape capsule, but we are concerned that something must have 9 as we have odd telemetry coming from the capsule's transponder. It's possible the capsule was damaged during the {1. With a shortage of rescue (D50=ships|vessels) in the area, we are }4 a 3 to 5 the system, and track down the escape capsule. GalCop tracking satellites will provide a regular waypoint update of the pod's last known (D50=location|position)."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType30 = function $missionType26() {
        var def = {
            fieldA: [].concat(this._gcmPG.accept),
            fieldB: [].concat(this._gcmPG.contract),
            fieldC: [].concat(this._gcmPG.be_given),
            fieldD: [].concat(this._gcmPG.pilot),
            fieldE: [],
            fieldF: [],
            fieldG: [],
            fieldH: [],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "\\[name\\] would like to meet with you in \\[destination\\], at a (D50=location|position) marked with a waypoint, to discuss another opportunity. Because these are dangerous times, when you 1 this 2 you will 3 a special passcode you will (D50=have|need) to transmit when you are in range of the ship at the destination to prove you are who you say you are. While you are free to accept or decline the additional opportunity, he is confident you will find the offer compelling.",
                "A certain 4, going by the name of \\[name\\], wants to meet up with you at a waypoint in \\[destination\\]. If you 1 this 2 you will 3 a security passcode you will (D50=have|need) to transmit to the ship at the destination. \\[name\\] believes the opportunity to present to you will be compelling, but you will be free to accept or decline as you see fit.",
                "A request has come through from a 4 by the name of \\[name\\]. They would like to meet with you in the \\[destination\\] system, at a waypoint you'll 3 if you 1 this 2. You'll also 3 a security passcode that you will (D50=have|need) to transmit to the ship at the destination in order to prove your identity. \\[name\\] has an opportunity to present to you, but you are free to accept it or decline it, based on your judgement."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType31 = function $missionType31() {
        var def = {
            fieldA: ["uncovered", "discovered", "unearthed", "found", "stumbled upon"],
            fieldB: [].concat(this._gcmPG.important),
            fieldC: [].concat(this._gcmPG.information),
            fieldD: ["urgency", "critical nature", "importance"],
            fieldE: [].concat(this._gcmPG.in_search_of),
            fieldF: [].concat(this._gcmPG.pilot),
            fieldG: [].concat(this._gcmPG.travel_to),
            fieldH: [].concat(this._gcmPG.short),
            fieldI: [].concat(this._gcmPG.imperative),
            fieldJ: [].concat(this._gcmPG.accept),
            fieldK: [].concat(this._gcmPG.contract),
            fieldL: [].concat(this._gcmPG.be_given),
            fieldM: ["pass on", "hand over", "provide"],
            fieldN: [].concat(this._gcmPG.set_up),
            fieldO: ["happening", "taking place", "transpiring", "occurring"],
            fieldP: [].concat(this._gcmPG.collect),
            fieldQ: ["\\n\\nIf the communication relay beacon signal is lost, you can turn it on for another 30 seconds or so by activating the 'Comms Relay Beacon Switch' primable equipment item (issued as part of the mission). The device can only be used 3 times before the transceiver inside it degrades, rendering it inoperable. Additionally, activating the device will incur a 5 percent penalty on the payment amount for the mission."],
            fieldR: [].concat(this._gcmPG.agents),
            sentences: [
                "One of our {9, (D50=operating|working) in the \\[destination\\] system, has }1 some 2 3, but they have been unable to transmit this data without compromising their cover. Instead, they have prepared an information drop point in the system, marked with a 'D' on your space compass. Because of the 4 of this matter, and to avoid sending official GalCop (D50=ships|vessels) into the area, we are 5 a 6 to 7 this spot, {7 the information cache and return. The relay can only (D50=broadcast|transmit) its (D50=location|position) for a }8 (D50=amount|period) of time, so it is 9 that the cache is collected and acted upon quickly.{8\\n\\nIf you {1 this 2, you will 3 security codes that will enable the data package to be delivered to your (D50=ship|vessel).",
                "One of our {9, who is (D50=operating|working) in the \\[destination\\] system, has made contact with us in order to 4 }2 3. Unfortunately, they were unable to transmit the entire data cache, but they have {5 a communications relay at drop-point in the \\[destination\\] system, marked with 'D' on your space compass. Because of the }4 of this matter, and to avoid sending official GalCop [gcm_ships] into the area, we are 5 a 6 to 7 this spot, {7 the information cache and return. The beacon on the relay can only (D50=broadcast|transmit) its (D50=location|position) for a }8 (D50=amount|period) of time, so it is 9 that the cache is collected quickly.{8\\n\\nIf you {1 this 2, you will 3 security codes that will enable the data package to be delivered to your (D50=ship|vessel).",
                "In these days of trouble, we need to be extra vigilant. We keep an eye on what is {6 in nearby systems in order to stop potential threats to the safety of our citizens. One of our 9, working in the \\[destination\\] system, has }2 3 to give us, but wasn't able to transmit it to us directly without exposing himself. So they have prepared an information drop, which, should you {1 this 2, will be marked as 'D' on your space compass. You will also be provided with a security code to unlock the data cache.\\n\\nThe relay can only (D50=broadcast|transmit) its (D50=location|position) for a }8 (D50=amount|period) of time, so it is 9 that the cache is collected quickly.{8"
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType32 = function $missionType32() {
        var def = {
            fieldA: ["sensitivity", "classified nature", "top secret nature", "sensitive nature"],
            fieldB: [].concat(this._gcmPG.information),
            fieldC: [].concat(this._gcmPG.accept),
            fieldD: [].concat(this._gcmPG.contract),
            fieldE: [].concat(this._gcmPG.be_given),
            fieldF: [].concat(this._gcmPG.pilot),
            fieldG: [].concat(this._gcmPG.due_to),
            fieldH: [].concat(this._gcmPG.in_search_of),
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "An encrypted data package needs to be (D50=delivered|transported) to a (D50=ship|vessel), (D50=currently in the \\[destination\\] system|in the \\[destination\\] system at the moment). The 1 of this 2 necessitates the use of a pilot and ship, rather than over standard transmission channels. If you 3 this 4, you will 5 the waypoint co-ordinates for the (D50=ship|vessel). You'll (D50=have|need) to fly to the waypoint and give the data package to the 6 waiting there.",
                "A (D50=ship|vessel), (D50=currently in the \\[destination\\] system|in the \\[destination\\] system at the moment), is expecting delivery of an encrypted data package. 7 the 1 of this data, it cannot be transmitted over standard communication channels. If you 3 this 4 you will be required to fly to a waypoint in that system and transmit the data package to the 6 waiting there.",
                "We have an encrypted data package to deliver, but due to the 1 of the information we cannot (D50=broadcast|transmit) it over standard communication channels. Instead, we are 8 a 6 who can take the data package and delivery it to a (D50=ship|vessel) waiting in the \\[destination\\] system, at a waypoint you'll 5 upon acceptance of this 4."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType33 = function $missionType33() {
        var def = {
            fieldA: [].concat(this._gcmPG.pilot),
            fieldB: [].concat(this._gcmPG.in_search_of),
            fieldC: ["one of our [4] facilities", "a [4] facility"],
            fieldD: ["secure", "government", "secret", "high security", "military"],
            fieldE: ["The thief, however,", "But the thief"],
            fieldF: ["follow", "track", "trace"],
            fieldG: ["We're confident", "We believe", "We're fairly confident", "Our information suggests"],
            fieldH: ["a window of opportunity", "a chance to intercept", "the opportunity to intercept the agent", "the possibility of recovering the items"],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "Some \\[missionstolenItemType\\] have been stolen from 3. 5 has left a trail we've been able to 6. 7 the ship is on its way to \\[destination\\] via the shortest route possible, which gives us 8. We (D50=need|require) a 1 who has a (D50=ship|vessel) capable of reaching the destination system ahead of the thief, who is flying a customised \\[missiontargetShipType\\], and recovering the items. Extreme force should only be used as a last resort.",
                "A foreign agent has infiltrated 3 and stolen some \\[missionstolenItemType\\]. However, when they departed our system we were able to track their ship, and we discovered they are headed to \\[destination\\] via the shortest route possible. This gives us 8. We are 2 a 1 who has a (D50=ship|vessel) capable of reaching the destination system ahead of the thief and recovering the items. The agent is flying a customised \\[missiontargetShipType\\]. Extreme force should only be used as a last resort.",
                "Our security specialists have detected that a foreign agent has stolen some \\[missionstolenItemType\\] from 3. They weren't able to stop the thief from escaping, but they were able to 6 their ship, a customised \\[missiontargetShipType\\]. Tracking data suggests the agent is heading for \\[destination\\], but by the shortest route, which gives us 8. We are 2 a 1 who can fly to \\[destination\\] by the quickest route, intercept the agent, and recover the \\[missionstolenItemType\\]. Extreme force should only be used as a last resort."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType40 = function $missionType40() {
        var def = {
            fieldA: [].concat(this._gcmPG.desires),
            fieldB: [].concat(this._gcmPG.be_given),
            fieldC: [],
            fieldD: [].concat(this._gcmPG.contract),
            fieldE: [],
            fieldF: [],
            fieldG: [],
            fieldH: [],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "Our client 1 a cargo of \\[target\\]t of \\[commodity\\] (D50=delivered|transported) to \\[destination\\]. However, this client does not want this transaction to appear in the standard GalCop shipping logs. Instead of (D50=delivering|transporting) the cargo to the main station, you will 2 a waypoint in \\[destination\\] to which you must travel and dump the cargo for the waiting ship.\\n\\nAlong with the payment listed on this bulletin board entry, you also will be paid with precious metals or gems once the cargo is scooped.",
                "In order to avoid the attention of GalCop, our client 1 a cargo of \\[target\\]t of \\[commodity\\] (D50=delivered|transported) to \\[destination\\], but not to the main station or any other facility. Instead, you will 2 a waypoint marker to which you must fly and then dump the cargo for the waiting (D50=ship|vessel).\\n\\nAlong with the payment listed on this bulletin board entry, you also will be paid with precious metals or gems once the cargo is scooped.",
                "GalCop record all trade (D50=ships|vessels) arriving and departing from stations, along with all cargo manifests. Our client (D50=needs|wants) to avoid this attention by having \\[target\\]t of \\[commodity\\] (D50=delivered|transported) to a waypoint in the \\[destination\\], the co-ordinates of which you will 2 upon acceptance of this 4.\\n\\nAlong with the payment listed on this bulletin board entry, you also will be paid with precious metals or gems once the cargo is scooped."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType41 = function $missionType41() {
        var def = {
            fieldA: [].concat(this._gcmPG.desires),
            fieldB: [].concat(this._gcmPG.set_up),
            fieldC: [].concat(this._gcmPG.short),
            fieldD: [].concat(this._gcmPG.be_given),
            fieldE: ["\\n\\nIf the communication relay beacon signal is lost, you can turn it on for another 30 seconds or so by activating the 'Comms Relay Beacon Switch' primable equipment item (issued as part of the mission). The device can only be used 3 times before the transceiver inside it degrades, rendering it inoperable. Additionally, activating the device will incur a 5 percent penalty on the payment amount for the mission."],
            fieldF: [],
            fieldG: [],
            fieldH: [],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "Our client 1 a certain cargo (D50=delivered|transported) to their location. However, this particular client does not want this transaction, the type of cargo or their current (D50=location|position) to appear in the standard GalCop shipping logs. A communications relay has been 2 in \\[destination\\] to which you must travel - it will be marked as 'D' on your space compass. The relay will only (D50=broadcast|transmit) its (D50=location|position) for a 3 (D50=amount|period) of time, so you will (D50=have|need) to act quickly when you arrive.5\\n\\nTransmit the passcode to this unit and you will 4 information about the type and amount of cargo you (D50=have|need) to (D50=deliver|transport), and a waypoint in another system, which should then allow you to purchase the cargo from any station. The cost of the cargo will be offset to you using the main station's prices at the final destination.\\n\\nYou must then travel to the final destination location and dump the cargo for the waiting ship.\\n\\nAlong with the payment listed on this bulletin board entry, you also will be paid with precious metals or gems once the cargo is scooped.",
                "In order to avoid the attention of GalCop, our client wants a particular cargo delivered to a specific location. But the client does not want this transaction, the type of cargo or their current (D50=location|position) to appear in the standard GalCop shipping logs. Instead, a communications relay has been 2 in the \\[destination\\] system, and it will be marked as 'D' on your space compass. The relay will only (D50=broadcast|transmit) its (D50=location|position) for a 3 (D50=amount|period) of time, so you must act quickly to reach it.5\\n\\nA security passcode, which you will 4 as part of the mission data package, must be transmitted to the relay, at which point it will communicate the type and amount of cargo, and where you need to deliver it to. You will then need to purchase the cargo from any station, the cost of which will be offset to you using the main station's prices at the destination system. When you reach the destination a waypoint will activate to enable you to find the pickup point where you must dump the cargo.\\n\\nAlong with the payment listed on this bulletin board entry, you also will be paid with precious metals or gems once the cargo is scooped.",
                "GalCop record all trade (D50=ships|vessels) arriving and departing from stations, along with all cargo manifests. Our client is keen to avoid this unwanted attention completely, so has set up a communications relay in the \\[destination\\] system which will appear, for a 3 (D50=amount|period) of time, as a 'D' on your space compass, so you need to act quickly to reach it.5\\n\\nAfter transmitting the required security code to the unit, you will be given the amount and type of cargo, and details of a waypoint in another system.\\n\\nAlong with the payment listed on this bulletin board entry, you also will be paid with precious metals or gems once the cargo is scooped."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType42 = function $missionType42() {
        var def = {
            fieldA: [].concat(this._gcmPG.accept),
            fieldB: [].concat(this._gcmPG.contract),
            fieldC: ["difficult", "hard", "challenging", "demanding", "tricky", "tough", "grim"],
            fieldD: [],
            fieldE: [],
            fieldF: [],
            fieldG: [],
            fieldH: [],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "We (D50=have|need) to (D50=deliver|transport) some specialised computer equipment to one of our (D50=ships|vessels) that is (D50=currently in the \\[destination\\] system|in the \\[destination\\] system at the moment). However, it is vital that the computers be (D50=delivered|transported) without there being any possibility of someone learning of the delivery, which is why this isn't being offered through the usual cargo contract channels. If you 1 this 2, you cannot dock at any GalCop station with the computers on board until they are delivered. Doing so will result in the computers being discovered and the 2 will be marked as failed.\\n\\nThe (D50=ship|vessel) will be waiting \\[position\\].",
                "We (D50=have|need) to (D50=deliver|transport) some highly specialised computer surveillance equipment to one of our (D50=ships|vessels), which is (D50=currently in the \\[destination\\] system|in the \\[destination\\] system at the moment). However, (D50=due to|because of) the nature of these computers and their ultimate purpose, we do not want there to be any chance that the delivery be discovered. Docking at any GalCop station while transporting these items would result in some very 3 questions being asked at a diplomatic level. If you 1 this 2 you will find our contact waiting \\[position\\].",
                "One of our (D50=ships|vessels) is (D50=currently in the \\[destination\\] system|in the \\[destination\\] system at the moment), \\[position\\], waiting for some specialised computer equipment. The mission is to (D50=deliver|transport) the cargo to the ship, but it is imperative that no one know about the transfer. If you dock at a GalCop station with these computers on board they will be discovered and the 2 will be terminated.",
                "We have a (D50=ship|vessel) (D50=currently in the \\[destination\\] system|in the \\[destination\\] system at the moment, \\[position\\]), awaiting the delivery of some specialised computer equipment. However, (D50=due to|because of) the nature of these computers, and their intended purpose, you cannot dock at any GalCop-aligned station while you have them on board. If you do, the computers will be discovered and confiscated, and the 2 will be terminated."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType43 = function $missionType43() {
        var def = {
            fieldA: [].concat(this._gcmPG.in_search_of),
            fieldB: [].concat(this._gcmPG.pilot),
            fieldC: [].concat(this._gcmPG.we_believe),
            fieldD: [].concat(this._gcmPG.attention),
            fieldE: [].concat(this._gcmPG.attacked),
            fieldF: ["was [5]", "came under fire", "was fired upon", "has come under fire"],
            fieldG: ["imperative", "vital", "crucial", "essential"],
            fieldH: [].concat(this._gcmPG.battle),
            fieldI: [].concat(this._gcmPG.travel_to),
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "One of our traders, (D50=operating|working) in the \\[destination\\] system, has become stranded. Their main engines are offline, and they lack critical parts to get it (D50=operational|functional) again. To make matters worse, their life support systems are beginning to fail, and they aren't fitted with an escape pod. We are 1 a 2 to take the required equipment to the (D50=ship|vessel) and dump it nearby to enable them to scoop it and effect repairs. 3 the (D50=ship|vessel) is stranded \\[position\\].",
                "4 that one of our trade (D50=ships|vessels), (D50=operating|working) in the \\[destination\\] system, 6 and their main engine has been damaged. The crew lack certain critical parts to enable them to restart their (D50=ship|vessel), and with their life support systems beginning to fail, and no escape pod on board, it is 7 we (D50=deliver|transport) the required equipment to them. We are fairly sure the (D50=ship|vessel) is stranded \\[position\\].",
                "We have received word that one of our trade (D50=ships|vessels), (D50=operating|working) in the \\[destination\\] system, was involved in 8 that has left the ship with no power to engines, and limited power to the life support system. The 2 was also, in hindsight quite foolishly, operating without an escape pod. We understand that a critical part is needed so the 2 can repair their ship, so we are 1 a 2 who can 9 \\[destination\\], find the (D50=ship|vessel) \\[position\\], and (D50=deliver|transport) the required equipment.",
                "We have received an emergency communication from one of our trade (D50=ships|vessels). They were involved in 8 in \\[destination\\] that has rendered their main engine inoperable. With no way to repair the engine, and with limited life-support remaining, we (D50=need|require) a 2 to take some equipment, 9 \\[destination\\], locate the stranded ship \\[position\\], and dump the equipment nearby. This will allow the stranded pilot to repair their ship and continue their journey."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType44 = function $missionType44() {
        var def = {
            fieldA: [].concat(this._gcmPG.on_route_to),
            fieldB: [].concat(this._gcmPG.we_believe),
            fieldC: [].concat(this._gcmPG.in_search_of),
            fieldD: [].concat(this._gcmPG.pilot),
            fieldE: [].concat(this._gcmPG.attention),
            fieldF: [].concat(this._gcmPG.imperative),
            fieldG: [].concat(this._gcmPG.venture_into),
            fieldH: [],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "One of our traders, 1 the \\[destination\\] system, has become stranded in interstellar space. 2 their main engines are offline, and they lack critical parts to get it (D50=operational|functional) again. To make matters worse, their life support systems are beginning to fail, and they aren't fitted with an escape pod. We are 3 a 4 to take the required equipment to the (D50=ship|vessel) in interstellar space and dump it nearby to enable them to scoop it and effect repairs.",
                "5 that one of our trade (D50=ships|vessels), 1 the \\[destination\\] system, has become stranded in interstellar space when their main engine was damaged. The crew lack certain critical parts to enable them to restart their (D50=ship|vessel), and with their life support systems beginning to fail, and no escape pod on board, it is 6 we (D50=deliver|transport) the required equipment to them.",
                "We have received word that one of our trade (D50=ships|vessels), 1 the \\[destination\\] system, has become stranded in interstellar space with no power to engines, and limited power to the life support system. The 4 was also, in hindsight quite foolishly, operating without an escape pod. We understand that a critical part is needed so the 4 can repair their ship, so we are 3 a 4 who can 7 interstellar space, find the (D50=ship|vessel), and (D50=deliver|transport) the required equipment.",
                "We have received an emergency communication from one of our trade (D50=ships|vessels). A mis-jump landed them in interstellar space between here and \\[destination\\], and they came under fire from a Thargoid warship. Unfortunately, during the battle their main engine was rendered inoperable. With no way to repair the engine, and with limited life-support remaining, we (D50=need|require) a 4 to take some equipment, 7 interstellar space, locate the stranded ship, and dump the equipment nearby. This will allow the stranded pilot to repair their ship and continue their journey."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType46 = function $missionType46() {
        var def = {
            fieldA: [].concat(this._gcmPG.desires),
            fieldB: [].concat(this._gcmPG.set_up),
            fieldC: [].concat(this._gcmPG.short),
            fieldD: [].concat(this._gcmPG.be_given),
            fieldE: ["\\n\\nIn order to dump the cargo so it remains within 20km the waypoint, you will need to use a device like the Cargo Stopper, the Ejection Damper, or a Cargo Shepherd, to bring ejected cargo to a standstill."],
            fieldF: [].concat(this._gcmPG.travel_to),
            fieldG: [],
            fieldH: [],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "Our client 1 a certain cargo (D50=delivered|transported) to their location. However, this particular client does not want this transaction, the type of cargo or their current (D50=location|position) to appear in the standard GalCop shipping logs. Instead, they have arranged for \\[target\\]t of \\[commodity\\] to be at a waypoint in the \\[destination\\] system. You need to 6 this (D50=location|position) and scoop all the cargo.\\n\\nYou will then need to 6 the \\[destinationA\\] system and deposit the cargo at another waypoint.5",
                "In order to avoid the attention of GalCop, our client wants a particular cargo delivered to a specific location. But the client does not want this transaction, the type of cargo or their current (D50=location|position) to appear in the standard GalCop shipping logs. Instead, a cargo drop of \\[target\\]t of \\[commodity\\] has been left at a waypoint in the \\[destination\\] system. You will need to collect this cargo, then 6 the \\[destinationA\\] system, where another waypoint has been set. Drop the cargo at this waypoint to complete the mission.5",
                "GalCop record all trade (D50=ships|vessels) arriving and departing from stations, along with all cargo manifests. Our client is keen to avoid this unwanted attention completely, so has set up a cargo drop of \\[target\\]t of \\[commodity\\] at a waypoint in the \\[destination\\] system.\\n\\nAfter collecting this cargo, you will need to 6 \\[destinationA\\], and deposit the cargo at another waypoint in that system.5"
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType50 = function $missionType50() {
        var bountyText = "";
        if (player.ship.bounty > 0) bountyText = "\\nCompleting this mission will reduce any outstanding bounty you may have.";
        var def = {
            fieldA: [].concat(this._gcmPG.short),
            fieldB: [].concat(this._gcmPG.contract),
            fieldC: [].concat(this._gcmPG.be_given),
            fieldD: [].concat(this._gcmPG.pilot),
            fieldE: [bountyText],
            fieldF: [],
            fieldG: [],
            fieldH: [],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "A recent outbreak of disease on \\[destination\\] has proved to be resistant to previous forms of antibiotic. The Galactic Centre for Disease Control have been calling for off-world medical supplies in an attempt to bring a halt to the spread of the disease. Delivering these medical supplies is time-critical - each shipment comes with a containment field that holds the antibiotics in stasis for a 1 (D50=amount|period) of time. If the shipment takes too long to arrive, the containment field will rupture, rendering the contents useless.\\n\\nUpon acceptance of this 2, you will 3 security codes that will (D50=need|have) to be transmitted to the destination station to allow your ship to dock.5",
                "In order to contain the spread of disease on \\[destination\\], the Galactic Centre for Disease Control have been calling on >4 to (D50=deliver|transport) off-world medical supplies, as the locally produced medicines are proving to be insufficient. Each delivery is time-critical, because the antibiotics are being held in stasis inside a containment field, which can only operate for a 1 time. If the containment field fails, all the medicine within is rendered useless.\\n\\nUpon acceptance of this 2, you will 3 security codes that will (D50=have|need) to be transmitted to the destination station to allow your ship to dock.5",
                "The Galactic Centre for Disease Control has issued a plea for >4 to transport antibiotics from high-tech systems to \\[destination\\] to help combat the recent outbreak of disease the planet is experiencing. Each delivery is time-critical, because the antibiotics are being held in stasis inside a containment field, which can only operate for a 1 time. If the containment field fails, all the medicine within is rendered useless.\\n\\nUpon acceptance of this 2, you will 3 security codes that will (D50=have|need) to be transmitted to the destination station to allow your ship to dock.5"
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType51 = function $missionType51() {
        var bountyText = "";
        if (player.ship.bounty > 0) bountyText = "\\nCompleting this mission will reduce any outstanding bounty you may have.";
        var def = {
            fieldA: [].concat(this._gcmPG.in_search_of),
            fieldB: [].concat(this._gcmPG.pilot),
            fieldC: [].concat(this._gcmPG.accept),
            fieldD: [].concat(this._gcmPG.contract),
            fieldE: [].concat(this._gcmPG.imperative),
            fieldF: ["5t"],
            fieldG: [bountyText],
            fieldH: [],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "There are a number of patients who have fallen seriously ill due to the current disease outbreak, and their condition is critical. The local medical facilities are inadequate for treating patients in this condition, so we are 1 >2 who can take on the job of transferring these patients to \\[destination\\], which is the closest planet with the required techlevel, for emergency medical procedures that will hopefully save their lives.\\n\\nIf you 3 this 4, a special patient transport unit will be installed into your ship, which takes up 6 of cargo space. The unit will be removed automatically when you reach \\[destination\\].7",
                "As a result of the recent outbreak of disease planet-side, and a lack of suitable medical equipment and antibiotics, the condition of many patients has deteriorated to the point where local medical facilities are inadequate for treating their condition. It is 5 these patients are transferred to \\[destination\\], the nearest system with the required techlevel, for emergency medical treatment that will hopefully save their lives.\\n\\nIf you 3 this 4, a special patient transport unit will be installed into your ship, which takes up 6 of cargo space. The unit will be automatically removed when you reach \\[destination\\].7",
                "The ongoing fight against disease on the planet has impacted thousands of citizens. Some are in an extremely critical condition and local medical services are unable to provide sufficient care for these patients. We are 1 >2 who can take some of these critically-ill patients to waiting medical teams on \\[destination\\], the nearest system that has the required techlevel.\\n\\nIf you 3 this 4, a special patient transport unit will be installed into your ship, which takes up 6 of cargo space. The unit will be automatically removed when you reach \\[destination\\].7"
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType52 = function $missionType52() {
        var bountyText = "";
        if (player.ship.bounty > 0) bountyText = "\\nCompleting this mission will reduce any outstanding bounty you may have.";
        var def = {
            fieldA: [].concat(this._gcmPG.plenty_of),
            fieldB: [].concat(this._gcmPG.due_to),
            fieldC: ["conduct$", "perform$", "carry$ out"],
            fieldD: [].concat(this._gcmPG.short),
            fieldE: [].concat(this._gcmPG.pilot),
            fieldF: [].concat(this._gcmPG.in_search_of),
            fieldG: [].concat(this._gcmPG.war),
            fieldH: [bountyText],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "The recent outbreak of disease has pushed local medical resources to breaking point, but in 1 cases they are inadequate 2 the mutating nature of the disease. Research into the various strains of the disease needs to be _3, and scientists on \\[destination\\] have agreed to 3 this service for us. We (D50=need|require) a batch of virus specimens (D50=delivered|transported) to the \\[destination\\] system so we can better understand and battle this outbreak.\\n\\nHowever, the containment unit around the specimens can only operate for a 4 time, and should that time expire the unit will automatically sterilise the contents to prevent any accidental contamination.8",
                "Local medical resources are limited, as is their ability to 3 sufficient testing on the different strains of the disease. However, scientists on \\[destination\\] have come forward and offered their services in this area, so we are 6 a 5 who can (D50=deliver|transport) a batch of virus specimens to this system for analysis.\\n\\nThe transportation device has a containment seal that prevents any accidental contamination, but there is a limited life in this device. If the transportation time exceeds the operational time limit, or the unit is damaged on route, the unit will automatically sterilise the contents.8",
                "The ongoing 7 against the outbreak of disease has a number of fronts, one of them being research. The more we understand the virus causing the disease, the better we are able to fight against it. Local medical facilities are swamped with the influx of patients, but scientists in the \\[destination\\] system have made their services available, if we can just get the specimens to them intact. We are 6 a 5 to (D50=deliver|transport) these virus specimens in a specialised containment device.\\n\\nBecause of the virulence of the disease, we can take no chances that the virus escapes. The containment unit has its own power supply, and should the power fail, or the unit is damaged, the contents will be automatically sterilised.8"
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType60 = function $missionType60() {
        var def = {
            fieldA: ["some time", "quite some time", "a while", "quite a while"],
            fieldB: [].concat(this._gcmPG.in_search_of),
            fieldC: [].concat(this._gcmPG.pilot),
            fieldD: [].concat(this._gcmPG.contract),
            fieldE: [].concat(this._gcmPG.be_given),
            fieldF: [].concat(this._gcmPG.constant),
            fieldG: [].concat(this._gcmPG.pirates),
            fieldH: ["recently", "in recent months", "of late", "lately"],
            fieldI: [].concat(this._gcmPG.we_believe),
            fieldJ: ["(WBSA)"],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "For 1 we have been concerned that GalCop are not doing enough to stem the tide of pirate (D50=ships|vessels) entering our system. They insist that their analysis of transit data shows that we are no different from any other system. However, GalCop will not release the transit data to us so we can perform our own analysis.\\n\\nTherefore, we are 2 a 3 who can take a customised Witchpoint Beacon Security Access {1 device and upload a small data package to the witchpoint marker at \\[destination\\]. This package will send us a regular data stream of transit data in and out of that system, so we can better communicate with GalCop.\\n\\nPlease be aware that using the WBSA device within range of any police (D50=ships|vessels) will get you marked as a criminal and will render this contract void, with associated penalties.\\n\\nWhen the }4 is accepted you will 5 the equipment, which must be primed and then activated while the witchpoint marker is targeted.",
                "We have been under siege by a 6 flow of 7 8, although GalCop insists this is a chart-wide issue, not (D50=limited|restricted) to our system. But without the transit data from our neighbouring systems we are not able to make GalCop aware of our difficulties.\\n\\nSo, we are 2 a 3 who can take a customised Witchpoint Beacon Security Access {1 device and upload a small software package to the witchpoint marker at \\[destination\\]. This will send us a regular data stream of transit data in and out of that system, so we can be better armed in negotiations with GalCop.\\n\\nPlease be aware that using the WBSA device within range of any police (D50=ships|vessels) will mean you will be marked as a criminal, and will also render this }4 void, with the associated penalties.\\n\\nWhen the 4 is accepted you will 5 the equipment, which must be primed and then activated while the witchpoint marker is targeted.",
                "We are in a political battle with GalCop, attempting to get accurate real-time data on the identity of all ships entering our system from \\[destination\\]. 9 some undesirable groups from that system have been using our system as a staging point, but because GalCop only provide the transit data on a semi-regular basis, it's impossible to do anything about it.\\n\\nHowever, we have come into possession of a customised Witchpoint Beacon Security Access {1 device, which gives us the opportunity to bypass GalCop and access the data stream directly. We are }2 a 3 who can take the device to \\[destination\\], get within range of the witchpoint beacon, and upload a small patch to the software.\\n\\nBe aware that using the WBSA device within range of any police (D50=ships|vessels) will result in you being marked as a criminal, and will also render this 4 void.\\n\\nUpon acceptance of this mission, you will 5 the WBSA device, which must be primed and then activated while the witchpoint market is targeted."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType61 = function $missionType61() {
        var def = {
            fieldA: [].concat(this._gcmPG.on_route_to),
            fieldB: [].concat(this._gcmPG.in_search_of),
            fieldC: [].concat(this._gcmPG.pilot),
            fieldD: [].concat(this._gcmPG.be_given),
            fieldE: [].concat(this._gcmPG.contract),
            fieldF: [].concat(this._gcmPG.happening),
            fieldG: [].concat(this._gcmPG.we_believe),
            fieldH: ["(WBSA)"],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "It has come to the attention of GalCop security personnel that the witchpoint marker in \\[destination\\] has been tampered with, the end result being that a high number of (D50=ships|vessels) have gone missing 1 this system, or report of experiencing a mis-jump when attempting to reach the system.\\n\\nTherefore, we are 2 a 3 who can take a customised Witchpoint Beacon Security Access 8 device to the \\[destination\\] system and upload a software patch that will restore the beacon into its original state.\\n\\nWhen the 5 is accepted you will 4 the equipment, which must be primed and then activated while the witchpoint marker is targeted.",
                "GalCop security personnel have been alerted to a software fault on the witchpoint marker in \\[destination\\] that is resulting in a large number of (D50=ships|vessels) disappearing 1 the system, or arriving with reports of mis-jumps. The cause of this software fault is unknown.\\n\\nTherefore, we are 2 a 3 who can take a customised Witchpoint Beacon Security Access 8 device to \\[destination\\] and upload a software patch that will restore the beacon into its original state.\\n\\nWhen the 5 is accepted you will 4 the equipment, which must be primed and then activated while the witchpoint marker is targeted.",
                "GalCop closely monitors system traffic, 2 trends and hot spots. It has become clear that there are a high number of mis-jumps 6 for (D50=ships|vessels) attempting to enter the \\[destination\\] system. 7 some unscrupulous individuals have managed to hack the witchpoint beacon in \\[destination\\], so that it occasionally gives off an invalid transponder signal and sends ships into interstellar space.\\n\\nTherefore, we are 2 a 3 to take a Witchpoint Beacon Security Access 8 device to the \\[destination\\] system, target and get in range of the beacon, then prime and activate the WBSA device. This will remove any invalid code from its memory core."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType62 = function $missionType62() {
        var def = {
            fieldA: [].concat(this._gcmPG.destroying),
            fieldB: [].concat(this._gcmPG.pilot),
            fieldC: [].concat(this._gcmPG.contract),
            fieldD: ["(that is, you are attacked by any pirate ships)"],
            fieldE: ["creating [6]", "proving to be a [7] nuisance", "wreaking havoc", "causing [6]"],
            fieldF: ["havoc", "chaos", "mayhem", "bedlam", "troubles", "difficulties", "headaches"],
            fieldG: ["considerable", "serious", "major", "significant"],
            fieldH: ["50kms"],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "GalCop has become concerned about the increase in pirate activity coming from \\[destination\\]. Of particular interest is a pirate Rock Hermit located in that system, as it appears to have become the hub for an entire pirate network operating in that area. However, rather than simply 1 the Hermit base, we want to monitor all traffic coming in and out of the Rock Hermit to gain a better understanding of the way pirates move around various systems.\\n\\nSo we need a security software package installed at the Rock Hermit. You will (D50=have|need) to approach the Hermit without being detected by any pirate vessels. If you are detected within 8 of the station 4 the 3 will not be able to be completed. Once docked, you must install a security package into the Rock Hermit's computer systems.",
                "A notorious gang of pirates from \\[destination\\] have been 5 in our spacelanes recently. Our Viper squadrons do their best to maintain safety for all >2, but there is only so much they can do in our system. So we have decided to take the war to the pirates. Of particular interest is a pirate Rock Hermit in \\[destination\\]. Evidence suggests this gang has either set up base there, or is using it as a hub for their operations. In any case, we need more information about this Hermitage and the ships that dock there.\\n\\nWe have a security package that we would like installed at the Rock Hermit. You'll (D50=have|need) to approach the Hermitage without being detected and attacked by any pirate vessels within 8 range of it. If you are then the mission is scrubbed. Once docked, you (D50=have|need) to install the security package onto the computer systems."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType63 = function $missionType63() {
        var def = {
            fieldA: ["difficult", "hard", "challenging", "demanding", "arduous", "tricky", "tough", "grim"],
            fieldB: [].concat(this._gcmPG.in_search_of),
            fieldC: [].concat(this._gcmPG.pilot),
            fieldD: [].concat(this._gcmPG.travel_there),
            fieldE: ["1"],
            fieldF: ["30"],
            fieldG: ["48"],
            fieldH: [],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "GalCop and elements within the system government have been making life 1 for those of us on the fringes of their economy. We want to change that. We have gained access to a software package that, when installed at the main station, will make all their commodity prices 5cr. We are 2 a 3 who can fly to the main station in this system and install the software package. There's two parts to this: first, you must initiate a data stream to the main station, but only when you have docking clearance. Once the data stream completed, you can dock and install the software. Don't dock before it completes. There are risks, however. While the data stream is being sent, you have to stay well clear of any police. You'll only have about 6 seconds after the data stream completes to install the software package. And you'll (D50=have|need) to launch within a few of seconds of installing the software and not return to the main station in less than 7 hours, otherwise their security detection systems will be able to track the security breach to your (D50=ship|vessel).",
                "Life in this system has become increasingly 1 for those of us on the fringes of society. GalCop have been colluding with the local government, cracking down on anything that doesn't add to their bottom lines. We want to send them a message that says we won't stand for this. We've recently come into possession of a piece of software that, when installed at the main station, will force all commodities to cost just 5cr. But we (D50=need|require) a 3 to 4 and install it. There are two steps: first, you have to transmit a data stream to the main station, but only while you have docking clearance. Don't let any police (D50=ships|vessels) get close while transmitting. When the data stream is completed, and only then, you can dock and install the software package. You'll have about 6 seconds after the data stream completes to do this. Once installed you'll (D50=have|need) to clear out of the place fast and not return for at least 7 hours, otherwise GalCop will be able to trace the security breach back to your (D50=ship|vessel)."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType64 = function $missionType64() {
        var def = {
            fieldA: [].concat(this._gcmPG.pilot),
            fieldB: ["48"],
            fieldC: ["30"],
            fieldD: [],
            fieldE: [],
            fieldF: [],
            fieldG: [],
            fieldH: [],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "GalCop has sided with the system government and is trying to limit our ability to maintain our own ships at the main station. We're after some payback, and as luck would have it, we have acquired a software package that can be installed at the main station which will disrupt all the ship maintenance computers and make it nigh on impossible for anyone to get anything done to their ships. We (D50=need|require) a 1 to dock at the main station and install the package. There are two parts to this: first, you have to transmit a special data stream to the main station while you have docking clearance. Keep clear of any police (D50=ships|vessels) while you're transmitting. After this, you (D50=have|need) to dock within 3 seconds and install the software package. You'll (D50=have|need) to stay sharp, though - you'll only have a few seconds from installing the package before GalCop discovers the breach and slaps you with a large fine. And best not dock there for at least 2 hours, for the same reason.",
                "It has become clear that GalCop and the local planetary government are in collusion, with the aim of restricting traffic not headed for their stations, but we're not taking this lying down. We have 'acquired' a software package that can be installed at the main station which will disrupt all ship maintenance computers and prevent pretty much every type of outfit, from simple refuelling operations to equipment installs and overhauls. All you (D50=have|need) to do is transmit a special data stream to the main station while you have docking clearance, and then dock at the main station inside 3 seconds and install the package. Simple. But don't let police ships get close while transmitting that data stream. And be ready to launch after installing the software package - you'll only have a few seconds to get clear of the station before GalCop discover the breach and fine you, big time. You should probably stay away from the main station for a good 2 hours afterwards, as well."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType65 = function $missionType65() {
        var def = {
            fieldA: ["officials", "representatives"],
            fieldB: ["difficult", "hard", "challenging", "demanding", "arduous", "tricky", "tough", "grim"],
            fieldC: [].concat(this._gcmPG.pilot),
            fieldD: ["stringent", "strict", "rigorous"],
            fieldE: [].concat(this._gcmPG.in_search_of),
            fieldF: ["50,000"],
            fieldG: ["7"],
            fieldH: ["canister", "container", "pod"],
            fieldI: [].concat(this._gcmPG.collect),
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "GalCop and local government 1 have been making life 2 for all of us outside their sanitised worldview. But we think we've found a way to make life 2 for them. We are 5 a 3 who can (D50=assist|help) us acquire some highly restricted security software from under their noses. The first step is to 9 a 'customised' escape pod from any non-GalCop station in \\[destination\\], bring it back to this system and activate it at more than 6 kms from the main station. We've learnt that security protocols aren't as 4 when dealing with escape pods, so the customised version can inject some specific commands into the computer system at the main station. After being rescued, you can launch, but the first police (D50=ship|vessel) that launches after you will self-destruct sometime in the following 7 minutes. When it does, a security software package will be ejected in a cargo 8. Scoop that cargo, and bring it back here. Easy right?",
                "One of our technical boffins has uncovered a rather unorthodox method of obtaining some highly restricted security software from GalCop. The method involves collecting a 'customised' escape capsule from any non-GalCop station in \\[destination\\], bringing it back here and activating it at more than 6 kms from the main station. Apparently security protocols aren't as 4 with escape pods, which enables this customised version to inject some specific commands into the computers back at the main station. When you launch after being rescued, the next police (D50=ship|vessel) to launch will have catastrophic failure in all systems and self-destruct, sometime in the following 7 minutes. When it does, a security software package will be ejected in a cargo 8. Scoop the cargo, and return it here. This will (D50=assist|help) us level the playing field with GalCop and the local government, who have been making life 2 for us out here on the fringes of society."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType70 = function $missionType70() {
        var def = {
            fieldA: [].concat(this._gcmPG.imperative),
            fieldB: [].concat(this._gcmPG.constant),
            fieldC: [].concat(this._gcmPG.many_different),
            fieldD: [].concat(this._gcmPG.repercussions),
            fieldE: [].concat(this._gcmPG.harmful),
            fieldF: [].concat(this._gcmPG.happen),
            fieldG: [].concat(this._gcmPG.in_search_of),
            fieldH: [].concat(this._gcmPG.pilot),
            fieldI: [].concat(this._gcmPG.approximately),
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "It is 1 that we maintain 2 watch on the solar activity of our sun, with 3 of scanners, in order to limit the 4 planet-side when 5 levels of activity 6.\\n\\nWith this in mind, we are 7 >8 with a suitably equipped ship who can take some specialised scanning equipment to within fuel-scooping range of the sun and perform a scan. Once installed, the scanner is started by priming it, and then activating it with your ships standard interface. The scan will take 9 one minute to complete, and will drain a significant amount of energy while operating.\\n\\nThe scanner is also quite fragile, so you will (D50=have|need) to be careful with it.",
                "With the planet-side 4 of solar activity a 2 concern for us, it is 1 that we maintain watch on our sun, with 3 of scanners, watching for when 5 levels of activity 6.\\n\\nWith this in mind, we have some specialised scanning equipment that needs to be activated inside fuel-scoop range of our sun. We're 7 >8 with a suitably equipped ship who can do this for us. Once installed, the scanner is started by priming it, and then activating it with your ships standard interface. It will take 9 one minute to complete, and will drain a significant amount of energy while operating.\n\nThe scanner is also quite fragile, so you will (D50=have|need) to be careful with it."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType71 = function $missionType71() {
        var def = {
            fieldA: [].concat(this._gcmPG.imperative),
            fieldB: [].concat(this._gcmPG.constant),
            fieldC: [].concat(this._gcmPG.many_different),
            fieldD: [].concat(this._gcmPG.harmful),
            fieldE: [].concat(this._gcmPG.happen),
            fieldF: [].concat(this._gcmPG.in_search_of),
            fieldG: [].concat(this._gcmPG.pilot),
            fieldH: [].concat(this._gcmPG.repercussions),
            fieldI: [].concat(this._gcmPG.collect),
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "It is 1 that we 2ly monitor the solar activity of our sun, with 3 of scanners, so that we can more accurately predict when a 4 event is about to 5.\\n\\nOne such scanning system involves two solar monitors in positions near the sun, one on the near side and one of the far side. Data from these monitors must be collected from within close proximity as there is too much interference from the sun for reliable long-distance data transfer. Our regular data gatherer (D50=ships|vessels) are grounded for maintenance and we are overdue on our data collection schedule. We are 6 a 7 to visit each of the solar monitors (marked with 'M' on your space compass), transmit a security code and 9 the data.",
                "With the planet-side 8 of solar activity a 2 concern for us, it is 1 that we maintain watch on our sun, with 3 of scanners, watching for when 4 levels of activity 5.\\n\\nOnce such scanning system involves two solar monitors in positions near the sub, one on the near side and one on the far side. Because of the solar interference we must manually 9 data from these monitors by flying a ship to within close range of them. But our regular data gatherer (D50=ships|vessels) have been grounded for maintenance, and we are overdue on our data collection schedule. We (D50=need|require) a 7 to visit both of the solar monitors (marked with 'M' on your space compare), transmit a security code and 9 the data."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType73 = function $missionType73() {
        var def = {
            fieldA: [].concat(this._gcmPG.imperative),
            fieldB: [].concat(this._gcmPG.constant),
            fieldC: [].concat(this._gcmPG.many_different),
            fieldD: [].concat(this._gcmPG.repercussions),
            fieldE: [].concat(this._gcmPG.harmful),
            fieldF: [].concat(this._gcmPG.happen),
            fieldG: [].concat(this._gcmPG.in_search_of),
            fieldH: [].concat(this._gcmPG.pilot),
            fieldI: [].concat(this._gcmPG.approximately),
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "It is 1 that we maintain 2 watch on the solar activity of our sun, with 3 of scanners, in order to limit the 4 planet-side when 5 levels of activity 6.\\n\\nWith this in mind, we are 7 >8 with a suitably equipped ship who can pick up some new, specialised scanning equipment from \\[destination\\], bring is back and take it to within fuel-scooping range of the sun and perform a scan. Once installed, the scanner is started by priming it, and then activating it with your ships standard interface. The scan will take 9 one minute to complete, and will drain a significant amount of energy while operating.\\n\\nThe scanner is also quite fragile, so you will (D50=have|need) to be careful with it.",
                "With the planet-side 4 of solar activity a 2 concern for us, it is 1 that we maintain watch on our sun, with 3 of scanners, watching for when 5 levels of activity 6.\\n\\nWith this in mind, we have ordered some new, specialised scanning equipment that needs to be collected from \\[destination\\], brought back here and activated inside fuel-scoop range of our sun. Once installed, the scanner is started by priming it, and then starting it with your ships standard interface. It will take 9 one minute to complete, and will drain a significant amount of energy while operating.\\n\\nThe scanner is also quite fragile, so you will (D50=have|need) to be careful with it."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType74 = function $missionType74() {
        var def = {
            fieldA: [],
            fieldB: [].concat(this._gcmPG.repercussions),
            fieldC: ["planet-side", "down on the planet", "on our citizens", "on our people"],
            fieldD: ["[9] be done elsewhere", "has been outsourced to other worlds", "[9] be sent off-world"],
            fieldE: ["is beyond our capabilities to process and ", "is just too complicated to be processed by our limited facilities and ", "can't be achieved with the technology we have available locally and ", ],
            fieldF: ["analysis", "data analysis", "processing", "data processing"],
            fieldG: ["ultra-hi-tech", "big", "large-scale"],
            fieldH: [", due to the amount of information, ", ", due to the sheer scale of the dataset, ", ", because of the size of the dataset, ", " "],
            fieldI: ["needs to", "must", "has to"],
            fieldJ: ["We need a set of solar scans delivered", "We have a set of solar scans that needs to be delivered", "We require a set of solar scans be delivered", "We've got a set of solar scans that needs to be delivered"],
            fieldK: ["so this analysis can be performed", "in order to get this deep analysis performed", "to allow this data processing to take place", "to enable the processing to be completed", "so this processing can be carried out"],
            fieldL: ["We [7] [5] [6] our sun", "As part of our public safety (D50=initiatives|programmes), we [7] [5] [6] our sun", "With a sun like ours, it is [4] we [7] [5] [6] it", "By nature of the volatility of our sun, we [7] [5] [6] it"],
            fieldM: [].concat(this._gcmPG.imperative),
            fieldN: ["a lot", "masses", "huge amounts", "a substantial amount", "a considerable amount", "quite a bit"],
            fieldO: ["of data about", "of information about"],
            fieldP: ["collect", "gather", "accumulate"],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "{3, trying to minimise the }2 of solar disturbances 3. (D50=But|However)8some of the 6 54, making use of 7 data processing facilities available at hi-tech worlds. {1 to \\[destination\\] 2."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType80 = function $missionType80() {
        var bountyText = "";
        if (player.ship.bounty > 0) bountyText = "\\nCompleting this mission will reduce any outstanding bounty you may have.";
        var def = {
            fieldA: [""],
            fieldB: [].concat(this._gcmPG.calling_for),
            fieldC: [].concat(this._gcmPG.war),
            fieldD: [].concat(this._gcmPG.donation),
            fieldE: ["repatriation programs", "programs of returning victims to their home system", "rehabilitation programs", "repatriation and rehabilitation programs"],
            fieldF: [].concat(this._gcmPG.are_expensive),
            fieldG: [],
            fieldH: [bountyText],
            fieldI: ["that don't want us around", "that aren't on board with us", "that aren't on the same page", "that don't see eye to eye with us", "that stand to lose out if the slave trade is stopped", "that turn a blind eye to slave trading"],
            fieldJ: ["need", "want", "desire", "require"],
            fieldK: [].concat(this._gcmPG.assistance),
            fieldL: ["long", "hard", "difficult", "never-ending", "long running"],
            fieldM: ["win", "succeed", "triumph"],
            fieldN: ["aspect", "factor", "component"],
            fieldO: ["money", "cash", "credits"],
            fieldP: ["to run", "to operate", "to maintain", "to manage", "to organise", "to carry out"],
            fieldQ: ["extensive", "wide-ranging", "complex", "intricate"],
            fieldR: ["to continue their work", "to fund the development of additional programs", "to help resettle even more victims", "to pay for further expansions of these programs", "to pay for the legal and transfer costs of victims"],
            sentences: [
                "Amnesty Intergalactic is 2 {2 in the on-going }3 against the slave trade. Their 5 6 {7, (D50=especially|particularly) in (D50=systems|places) }9. So they {1 your 2. They are }2 a 4 of \\[target\\] cr {9.}8",
                "The 3 against the slave trade is {3, but Amnesty Intergalactic is determined to {4. One 5 of this }3 is financial. Amnesty Intergalactic run {8 }5, and these 6 {7, (D50=especially|particularly) in (D50=systems|places) }9. So they are 2 a 4 of \\[target\\] cr {9.}8",
                "Amnesty Intergalactic, in its 3 against the slave trade, run a number of 5, but they 6 {7, (D50=especially|particularly) in (D50=systems|places) }9. Amnesty Intergalactic are 2 {2, by the }4 of {6, \\[target\\] cr specifically, {9.}8"
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType81 = function $missionType81() {
        var bountyText = "";
        if (player.ship.bounty > 0) bountyText = "\\nCompleting this mission will reduce any outstanding bounty you may have.";
        var def = {
            fieldA: [].concat(this._gcmPG.care_for),
            fieldB: ["don't come home", "are missing in action", "have been killed somewhere in the vastness of space", "have fallen victim to pirates or hunters", "fail to come home"],
            fieldC: ["monetary", "cash", "financial"],
            fieldD: ["pirates and interstellar disasters", "tragedy and heartbreak", "devastating mishaps"],
            fieldE: ["coming in daily", "arriving almost minute by minute", "announced daily"],
            fieldF: [].concat(this._gcmPG.need_for),
            fieldG: [bountyText],
            fieldH: [].concat(this._gcmPG.grown_exponentially),
            fieldI: [].concat(this._gcmPG.are_expensive),
            fieldJ: [].concat(this._gcmPG.calling_for),
            fieldK: [].concat(this._gcmPG.assistance),
            fieldL: ["news", "details", "information", "reports"],
            fieldM: [].concat(this._gcmPG.donation),
            fieldN: ["go a long way towards", "help us enormously in", "make a big difference in", "help us in"],
            fieldO: ["keeping our services running", "meeting our financial commitments", "meeting the needs of our clients", "keeping our services cost effective", "keeping us in the black", "keeping us solvent", "paying our creditors"],
            fieldP: [].concat(this._gcmPG.operating_mantra),
            fieldQ: [],
            fieldR: ["work the spacelanes", "make their living among the stars", "must fly there regularly"],
            sentences: [
                "The ~gcmTWOSTitle$G~ is an organisation whose {7 is to }1 those left behind when traders 2. With {3 of }4 5, the 6 our services(D50= and facilities|) 8. But those services 9, so we are {1 2 in the form of }3 >{4. \\[target\\] cr will 5 6.}7",
                "Space is a sometimes dangerous place, (D50=especially|particularly) for those who {9. And sometimes traders }2. {3 of }4 are 5. The ~gcmTWOSTitle$G~ exists solely to <1 those left behind, and the 6 our services(D50= and facilities|) 8. But those services 9, so we are {1 2. A }3 {4 of \\[target\\] cr will 5 6.}7",
                "{3 of }4 are 5, which usually means pilots 2. When }4 strike, the ~gcmTWOSTitle$G~ is there to 1 those left behind. As you can imagine, the 6 our services(D50= and facilities|) 8, and these services 9. We are {1 2. A 4 of \\[target\\] cr will 5 6.}7"
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType82 = function $missionType82() {
        var bountyText = "";
        if (player.ship.bounty > 0) bountyText = "\\nCompleting this mission will reduce any outstanding bounty you may have.";
        var def = {
            fieldA: ["aim", "seek", "attempt"],
            fieldB: [].concat(this._gcmPG.care_for),
            fieldC: ["those in the repair and services sector", "all mechanics and technicians", "those working in the repair workshops of stations across the Eight"],
            fieldD: ["get a solid education", "receive educational opportunities", "have educational experiences", "get quality educational opportunities"],
            fieldE: ["they otherwise wouldn't receive", "they might not be able to receive normally", "beyond what they would normally receive"],
            fieldF: [].concat(this._gcmPG.need_for),
            fieldG: [bountyText],
            fieldH: [].concat(this._gcmPG.grown_exponentially),
            fieldI: ["expense", "cost", "price tag"],
            fieldJ: [].concat(this._gcmPG.struggling),
            fieldK: ["to make ends meet", "to pay all our expenses", "to cover all our costs", "to meet the requirements of our clients", "to provide quality services", "to maintain the quality of our services"],
            fieldL: [].concat(this._gcmPG.calling_for),
            fieldM: [].concat(this._gcmPG.assistance),
            fieldN: [].concat(this._gcmPG.donation),
            fieldO: ["go a long way towards", "help enormously in", "make a big difference in", "help us in"],
            fieldP: ["keeping our services running", "meeting our financial commitments", "meeting the needs of our clients", "keeping our services cost effective", "keeping us in the black", "keeping us solvent", "paying our creditors"],
            fieldQ: ["into the future", "going forward", "in the coming days", "in the short term", "for the time being"],
            fieldR: ["cash", "credits", "money", "a financial sort"],
            sentences: [
                "The ~gcmMCEPTitle$G~ >1 to 2 the children of 3, enabling them to 4 5. The 6 our services(D50= and facilities|) 8, but so too has the 9. We are {1 2. To this end, we are 3 4 of a financial sort, a 5 of \\[target\\] cr, which will 6 7 8.}7",
                "The children of 3 can sometimes find themselves {1 to }4. The ~gcmMCEPTitle$G~ exists to give these children something 5. With the 6 our services(D50= and facilities|) growing, so too has the 9, leaving us {1 2. With this in mind, we are 3 >5 of 9, \\[target\\] cr, which will 6 7 8.}7",
                "The ~gcmMCEPTitle$G~ is all about the kids. We 1 to 2 the children of 3 to aim higher and enable them to 4 5. As 6 our services(D50= and facilities|) 8, so too has the 9, to the point where we are {1 2. We are now 3 a financial type of 4, a 5 of \\[target\\] cr, which will 6 7 8.}7"
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType83 = function $missionType83() {
        var bountyText = "";
        if (player.ship.bounty > 0) bountyText = "\\nCompleting this mission will reduce any outstanding bounty you may have.";
        var def = {
            fieldA: [].concat(this._gcmPG.operating_mantra),
            fieldB: ["provide quality mental health services for", "look after the mental health needs of", "provide compassionate, effective mental health services to"],
            fieldC: ["of every make and model", "of every kind, from traders to bounty hunters", "of every sort, from the mostly harmless to Elite"],
            fieldD: ["regardless of their history with GalCop", "regardless of their current legal standing", "regardless of their criminal associations, past or present"],
            fieldE: ["costs are rising", "costs are climbing", "our operating costs are climbing", "the cost of operating our services is escalating"],
            fieldF: [bountyText],
            fieldG: ["law and order breaks down", "GalCop struggles to maintain safety on the spacelanes", "the dangers from pirate raids increase", "the threat of Thargoid invasion looms large"],
            fieldH: ["cost effective", "cheap", "affordable", "attainable", "maintainable"],
            fieldI: ["the full spread of available mental illness treatments", "the latest advances in mental health treatment"],
            fieldJ: ["hard", "difficult", "almost impossible", "more and more difficult"],
            fieldK: [].concat(this._gcmPG.calling_for),
            fieldL: [].concat(this._gcmPG.pilot),
            fieldM: ["support us", "partner with us", "work with us"],
            fieldN: [].concat(this._gcmPG.donation),
            fieldO: [].concat(this._gcmPG.enable_us),
            fieldP: ["expand and develop our services and facilities", "provide the best medical treatments and solutions", "ensure we can continue to look after the needs of our patients"],
            fieldQ: ["help all of our patients", "assist all of our patients", "enable all our patients to"],
            fieldR: ["have a positive, hopeful future", "feel confident about their future", "look forward to their future with hope", "face their future without fear"],
            sentences: [
                "The 1 of the ~gcmPMHSTitle$G~ is to 2 pilots 3. 5 though, (D50=especially|particularly) as 7. Providing commanders with 8 treatments, or giving them access to 9 is proving to be {1. We are 2 >3 who can 4 in the form of a 5 of \\[target\\] cr, that will 6 to 7 and 8 9.}6",
                "Contributing to ~gcmPMHSTitle$G~ isn't like making >{5 to other charities. The }1 of the fund is to 2 pilots 3, in a 8 way. We provide this service to pilots 4. But 5, (D50=especially|particularly) as 7. It is becoming {1 to give our patients access to }9. By making a {5 of \\[target\\] cr, you will 6 to 7 and 8 9.}6"
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType84 = function $missionType84() {
        var bountyText = "";
        if (player.ship.bounty > 0) bountyText = "\\nCompleting this mission will reduce any outstanding bounty you may have.";
        var def = {
            fieldA: ["wants to", "is seeking to", "is trying to", "is determined to", "is actively trying to", "is involved in a long-term mission, to"],
            fieldB: ["free", "release", "set free", "repatriate and rehabilitate"],
            fieldC: ["the road is long", "there is a long way to go", "it's going to take a long time"],
            fieldD: ["there are limited resources", "there are many hurdles to jump", "there are many obstacles to overcome", "there are many who are actively trying to stop us", "there are many complications and pitfalls"],
            fieldE: ["on this station", "on this very station", "right here", "right now"],
            fieldF: ["lives being shattered", "lives being treated as little more than cargo", "lives being torn apart", "lives being destroyed"],
            fieldG: [].concat(this._gcmPG.vile),
            fieldH: [].concat(this._gcmPG.calling_for),
            fieldI: [].concat(this._gcmPG.assistance), 
            fieldJ: [].concat(this._gcmPG.war),
            fieldK: ["currently on the commodities market", "available right now on the commodities market"],
            fieldL: ["bringing them here", "bringing them to us", "giving them to us"],
            fieldM: ["for rehabilitation and repatriation", "so we can help them recover and return them to their homeland", "so we can start the process of rebuilding their lives"],
            fieldN: ["purchase", "buy", "acquire"],
            fieldO: ["there is hope", "there is something you can do", "there are ways you can help", "there is a way for you to make a difference", "there is still hope"],
            fieldP: ["sentient beings", "people", "unfortunate individuals"],
            fieldQ: ["respond", "reply", "participate", "help them out", "help them make a difference"],
            fieldR: [bountyText],
            sentences: [
                "Amnesty Intergalactic 1 2 every slave and end the slave-trade itself. But 3, and 4. Even 5 there are 6 because of this 7 trade. We are 8 your 9 in this on-going {1 by purchasing the \\[target\\] t of slaves 2, and 3 4.{9",
                "{2, }5, there are 6, as the 7 slave trade is allowed to continue. Amnesty Intergalactic 1 2 every single slave, although 4. 3 in this {1, but with your }9, we can make a difference. We are 8 for you to {5 all slaves available on this station, all \\[target\\] t, then 3 4.{9",
                "Seeking to 2 every slave should be everyone's concern, and is of prime concern to Amnesty Intergalactic. 4, so 3. Even now, 5, there are 6 as {7 are forced to participate in the }7 slave trade. But {6, if you can 5 the \\[target\\] t of slaves 2. Then 3 4. Amnesty Intergalactic are }8 your help. Will you {8?9"
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType85 = function $missionType85() {
        var bountyText = "";
        if (player.ship.bounty > 0) bountyText = "\\nCompleting this mission will reduce any outstanding bounty you may have.";
        var def = {
            fieldA: [].concat(this._gcmPG.care_for),
            fieldB: ["don't come home", "are missing in action", "have been killed somewhere in the vastness of space", "have fallen victim to pirates or hunters", "fail to come home"],
            fieldC: [],
            fieldD: ["pirates and interstellar disasters", "tragedy and heartbreak", "devastating mishaps"],
            fieldE: ["coming in daily", "arriving almost minute by minute", "announced daily"],
            fieldF: [].concat(this._gcmPG.need_for),
            fieldG: [bountyText],
            fieldH: [].concat(this._gcmPG.grown_exponentially),
            fieldI: [].concat(this._gcmPG.are_expensive),
            fieldJ: [].concat(this._gcmPG.calling_for),
            fieldK: [].concat(this._gcmPG.assistance),
            fieldL: ["news", "details", "information", "reports"],
            fieldM: [].concat(this._gcmPG.donation),
            fieldN: ["go a long way towards", "help enormously in", "make a big difference in", "help us in"],
            fieldO: ["keeping our services running", "meeting our financial commitments", "meeting the needs of our clients", "keeping our services cost effective", "keeping us in the black", "keeping us solvent", "paying our creditors"],
            fieldP: [].concat(this._gcmPG.operating_mantra),
            fieldQ: ["cargo is just as useful as credits", "cargo is just as valid a [4] as credits", "cargo is a valid [4]"],
            fieldR: ["work the spacelanes", "make their living among the stars", "must fly there regularly"],
            sentences: [
                "The ~gcmTWOSTitle$G~ is an organisation whose {7 is to }1 those left behind when traders 2. With {3 of }4 5, the 6 our services(D50= and facilities|) 8. But those services 9, so we are {1 2 in the form of cargo >4. \\[target\\] \\[unit\\] of \\[commodity\\] will 5 6.}7",
                "Space is a sometimes dangerous place, (D50=especially|particularly) for those who {9. And sometimes traders }2. {3 of }4 are 5. The ~gcmTWOSTitle$G~ exists solely to <1 those left behind, and the 6 our services(D50= and facilities|) 8. But those services 9, so we are {1 2. 8, so a {4 of \\[target\\] \\[unit\\] of \\[commodity\\] will 5 6.}7",
                "{3 of }4 are 5, which usually means pilots 2. When }4 strike, the ~gcmTWOSTitle$G~ is there to 1 those left behind. As you can imagine, the 6 our services(D50= and facilities|) 8, and these services 9. We are {1 2. 8 to us, so a 4 of \\[target\\] \\[unit\\] of \\[commodity\\] will 5 6.}7"
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType86 = function $missionType86() {
        var bountyText = "";
        if (player.ship.bounty > 0) bountyText = "\\nCompleting this mission will reduce any outstanding bounty you may have.";
        var def = {
            fieldA: ["aim", "seek", "attempt"],
            fieldB: [].concat(this._gcmPG.care_for),
            fieldC: ["those in the repair and services sector", "all mechanics and technicians", "those working in the repair workshops of stations across the Eight"],
            fieldD: ["get a solid education", "receive educational opportunities", "have educational experiences", "get quality educational opportunities"],
            fieldE: ["they otherwise wouldn't receive", "they might not be able to receive normally", "beyond what they would normally receive"],
            fieldF: [].concat(this._gcmPG.need_for),
            fieldG: [bountyText],
            fieldH: [].concat(this._gcmPG.grown_exponentially),
            fieldI: ["expense", "cost", "price tag"],
            fieldJ: [].concat(this._gcmPG.struggling),
            fieldK: ["to make ends meet", "to pay all our expenses", "to cover all our costs", "to meet the requirements of our clients", "to meet the expectations of our clients", "to provide quality services", "to maintain the quality of our services"],
            fieldL: [].concat(this._gcmPG.calling_for),
            fieldM: [].concat(this._gcmPG.assistance),
            fieldN: [].concat(this._gcmPG.donation),
            fieldO: ["go a long way towards", "help enormously in", "make a big difference in", "help us in"],
            fieldP: ["keeping our services running", "meeting our financial commitments", "meeting the needs of our clients", "keeping our services cost effective", "keeping us in the black", "keeping us solvent", "paying our creditors"],
            fieldQ: ["into the future", "going forward", "in the coming days", "in the short term", "for the time being"],
            fieldR: ["cargo is just as useful as credits", "cargo is just as valid a [5] as credits", "cargo is a valid [5]"],
            sentences: [
                "The ~gcmMCEPTitle$G~ >1 to 2 the children of 3, enabling them to 4 5. The 6 our services(D50= and facilities|) 8, but so too has the 9. We are {1 2. To this end, we are 3 4 in the form of a 5. 9, so a 5 of \\[target\\] \\[unit\\] of \\[commodity\\] will 6 7 8.}7",
                "The children of 3 can sometimes find themselves {1 to }4. The ~gcmMCEPTitle$G~ exists to give these children something 5. With the 6 services(D50= and facilities|) growing, so too has the 9, leaving us {1 2. With this in mind, we are 3 >5 of cargo, \\[target\\] \\[unit\\] of \\[commodity\\] specifically, which will 6 7 8.}7",
                "The ~gcmMCEPTitle$G~ is all about the kids. We 1 to 2 the children of 3, helping them to aim higher and enabling them to 4 5. As 6 services(D50= and facilities|) 8, so too has the 9, to the point where we are {1 2. We are now 3 4 by way of a 5 of \\[target\\] \\[unit\\] of \\[commodity\\], which will 6 7 8.}7"
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType87 = function $missionType87() {
        var bountyText = "";
        if (player.ship.bounty > 0) bountyText = "\\nCompleting this mission will reduce any outstanding bounty you may have.";
        var def = {
            fieldA: [].concat(this._gcmPG.operating_mantra),
            fieldB: ["provide quality mental health services for", "look after the mental health needs of", "provide compassionate, effective mental health services to"],
            fieldC: ["of every make and model", "of every kind, from traders to bounty hunters", "of every sort, from the mostly harmless to Elite"],
            fieldD: ["regardless of their history with GalCop", "regardless of their current legal standing", "regardless of their criminal associations, past or present"],
            fieldE: ["costs are rising", "costs are climbing", "our operating costs are climbing", "the cost of operating our services is escalating"],
            fieldF: [bountyText],
            fieldG: ["law and order breaks down", "GalCop struggles to maintain safety on the spacelanes", "the dangers from pirate raids increase", "the threat of Thargoid invasion looms large"],
            fieldH: ["cost effective", "cheap", "affordable", "attainable", "maintainable"],
            fieldI: ["the full spread of available mental illness treatments", "the latest advances in mental health treatment"],
            fieldJ: ["hard", "difficult", "almost impossible", "more and more difficult"],
            fieldK: [].concat(this._gcmPG.calling_for),
            fieldL: ["pilots", "commanders", "anyone", "those"],
            fieldM: ["support us", "partner with us", "work with us"],
            fieldN: [].concat(this._gcmPG.donation),
            fieldO: [].concat(this._gcmPG.enable_us),
            fieldP: ["expand and develop our services and facilities", "provide the best medical treatments and solutions", "ensure we can continue to look after the needs of our patients"],
            fieldQ: ["help all of our patients", "assist all of our patients", "enable all our patients to"],
            fieldR: ["have a positive, hopeful future", "feel confident about their future", "look forward to their future with hope", "face their future without fear"],
            sentences: [
                "The 1 of the ~gcmPMHSTitle$G~ is to 2 pilots 3. 5 though, (D50=especially|particularly) as 7. Providing commanders with 8 treatments, or giving them access to 9 is proving to be {1. We are 2 3 who can 4 in the form of a cargo 5, \\[target\\] \\[unit\\] of \\[commodity\\], that will 6 to 7 and 8 9.}6",
                "Contributing to ~gcmPMHSTitle$G~ isn't like making >{5 to other charities. The }1 of the fund is to 2 pilots 3, in a 8 way. We provide this service to pilots 4. But 5, (D50=especially|particularly) as 7. It is becoming {1 to give our patients access to }9. By making a cargo {5 of \\[target\\] \\[unit\\] of \\[commodity\\], you will 6 to 7 and 8 9.}6"
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType90 = function $missionType90() {
        var bountyText = "";
        if (player.ship.bounty > 0) bountyText = "\\nCompleting this mission will reduce any outstanding bounty you may have.";
        var def = {
            fieldA: [].concat(this._gcmPG.in_search_of),
            fieldB: [].concat(this._gcmPG.pilot),
            fieldC: [].concat(this._gcmPG.accept),
            fieldD: [].concat(this._gcmPG.contract),
            fieldE: [].concat(this._gcmPG.imperative),
            fieldF: ["10t"],
            fieldG: [bountyText],
            fieldH: [],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "The ongoing civil war planet-side has left many homeless and destitute. Refugee camps are filled to capacity and more come every day. Many of these refugees have elected to be taken off-planet, to \\[destination\\] where a safe-haven has been established by the authorities. There are a high number of refugees who need to be transported, so we are 1 >2 who can take on the job of transferring them to \\[destination\\].\\n\\nIf you 3 this 4, a special passenger transport unit will be installed into your ship, which takes up 6 of cargo space, and accommodates fifty passengers. The unit will be removed automatically when you reach \\[destination\\].7",
                "As a result of the recent outbreak of civil war planet-side, many citizens have been displaced from their homes and are now filling refugee camps. Unfortunately, due to the limited space available in these camps, we are at risk of a breakdown of law and order due to overcrowding and insufficient supplies. To alleviate the problem, we have been in contact with authorities on \\[destination\\] and have established a safe-haven for some of these refugees. We are 1 >2 who can transfer some of these refugees to \\[destination\\] safely.\\n\\nIf you 3 this 4, a special passenger transport unit will be installed into your ship, which takes up 6 of cargo space, and can accommodate fifty refugees. The unit will be automatically removed when you reach \\[destination\\].7",
                "The ongoing hostilities on the planet have impacted thousands of citizens. Many of them have had their homes destroyed and are now filling up refugee camps in various places around the globe. But these camps are now at capacity, and with more refugees coming in daily we need a different solution.\\n\\nWe have been in contact with authorities on \\[destination\\] and have set up a safe-haven for some of these refugees, and we are now in need of pilots and ships who can take on the task of transporting the refugees there.\\n\\nIf you 3 this 4, a special passenger transport unit will be installed into your ship, which takes up 6 of cargo space. The unit will be automatically removed when you reach \\[destination\\].7"
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType91 = function $missionType91() {
        var bountyText = "";
        if (player.ship.bounty > 0) bountyText = "\\nCompleting this mission will reduce any outstanding bounty you may have.";
        var def = {
            fieldA: [].concat(this._gcmPG.in_search_of),
            fieldB: [].concat(this._gcmPG.pilot),
            fieldC: [].concat(this._gcmPG.accept),
            fieldD: [].concat(this._gcmPG.contract),
            fieldE: [].concat(this._gcmPG.imperative),
            fieldF: ["5t"],
            fieldG: [bountyText],
            fieldH: [],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "There are a number of patients who have been seriously injured in a recent outbreak of civil war, and their condition is critical. The local medical facilities are unable to cope with the volume of patients needing care, so we are 1 >2 who can take on the job of transferring these patients to \\[destination\\], which is the closest planet with the required techlevel, for the medical procedures that will hopefully save their lives.\\n\\nIf you 3 this 4, a special patient transport unit will be installed into your ship, which takes up 6 of cargo space. The unit will be removed automatically when you reach \\[destination\\].7",
                "As a result of the recent outbreak of civil war planet-side, and a lack of sufficient medical facilities, the condition of many injured patients has become critical. It is 5 these patients are transferred to \\[destination\\], the nearest system with the required techlevel, for emergency medical treatment that will hopefully save their lives.\\n\\nIf you 3 this 4, a special patient transport unit will be installed into your ship, which takes up 6 of cargo space. The unit will be automatically removed when you reach \\[destination\\].7",
                "The ongoing hostilities on the planet has impacted thousands of citizens. Some of the injured are in an extremely critical condition and local medical services are unable to provide sufficient care for these patients. We are 1 >2 who can take some of these critically-ill patients to waiting medical teams on \\[destination\\], the nearest system that has the required techlevel.\\n\\nIf you 3 this 4, a special patient transport unit will be installed into your ship, which takes up 6 of cargo space. The unit will be automatically removed when you reach \\[destination\\].7"
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType100 = function $missionType100() {
        var def = {
            fieldA: ["operate", "run", "maintain"],
            fieldB: ["in order to", "to help us", "so we can"],
            fieldC: [].concat(this._gcmPG.many_different),
            fieldD: ["our regular scanning (D50=vessel|ship)", "the (D50=vessel|ship) that normally performs these scans"],
            fieldE: [].concat(this._gcmPG.pilot),
            fieldF: [].concat(this._gcmPG.in_search_of),
            fieldG: ["fly to", "get", "approach to"],
            fieldH: ["1, 2, 3 and 4"],
            fieldI: ["approximately", "about", "around"],
            fieldJ: ["1km"],
            fieldK: ["With the threat of earthquakes a very present reality for our system, it's", "Earthquakes (D50=are|may be) a fact of life for our citizens, but that doesn't mean we're unprepared. It's", "The threat from earthquake disasters in our system is constant, which means"],
            fieldL: [].concat(this._gcmPG.constant),
            fieldM: ["watch", "monitor", "examine", "check"],
            fieldN: ["and be on the lookout for any possible disasters", "and minimise the possible loss of life these disasters bring", "and limit the damage these catastrophes cause"],
            fieldO: [].concat(this._gcmPG.imperative),
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "We 1 3 of scanning equipment on our planet 2 get as much advance notice of any possible earthquakes. (D50=However,|But) 4 is offline, so we are 6 a 5 to take a seismic resonance scanner to four positions in low orbit, marked as scan points 8 on your space compass, 7 within {1 of each point, and perform the scans for us.\\n\\nThe scanner is started by priming it, and then activating it with your ships standard interface. The scanner will take }9 one minute to complete, and will drain a significant amount of energy while operating.\\n\\nThe scan is also quite fragile, so you will (D50=have|need) to be careful with it.",
                "{2 6 we 3ly 4 all seismic activity 5. One method we employ is using a seismic resonance scanner from low orbit. (D50=However,|But) }4 is offline, so we are 6 a 5 to take one of these scanners to four different positions, marked as scan points 8 on your space compass, 7 within {1 of each point, and perform the scans for us.\\n\\nThe scanner is started by priming it, and then activating it with your ships standard interface. The scanner will take }9 one minute to complete, and will drain a significant amount of energy while operating.\\n\\nThe scan is also quite fragile, so you will (D50=have|need) to be careful with it."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType101 = function $missionType101() {
        var bountyText = "";
        if (player.ship.bounty > 0) bountyText = "\\nCompleting this mission will reduce any outstanding bounty you may have.";
        var def = {
            fieldA: ["A spate of recent", "Recent", "A recent spate of"],
            fieldB: ["devastated", "wrecked", "ravaged", "severely damaged", "wreaked havoc on"],
            fieldC: ["several", "a number of", "a few of"],
            fieldD: ["But", "Combined with this,", "In addition,"],
            fieldE: [].concat(this._gcmPG.large),
            fieldF: [].concat(this._gcmPG.calling_for),
            fieldG: ["someone", "a [8]"],
            fieldH: [].concat(this._gcmPG.pilot),
            fieldI: [].concat(this._gcmPG.accept),
            fieldJ: [bountyText],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "1 earthquakes have 2 3 our larger cities, and the death toll has been high. 4 we also have a 5 number of critically injured patients who need urgent medical care which can best be found on hi-tech worlds. We are 6 7 to 9 the job of (D50=transporting|transferring) these patients to \\[destination\\], which is the (D50=nearest|closest) planet with the (D50=necessary|requisite) technology level where their injuries can be treated.\\n\\nIf you take this job, a special patient transport unit will be installed into your ship, which takes up five tons of cargo space. The unit will be removed automatically when you reach \\[destination\\].{1"
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType102 = function $missionType102() {
        var def = {
            fieldA: ["several", "many", "a number of", "a large number of"],
            fieldB: ["imposing", "sizeable", "substantial", "significant"],
            fieldC: ["worrying", "disconcerting", "troubling"],
            fieldD: [].concat(this._gcmPG.pilot),
            fieldE: ["find", "track down", "locate", "pinpoint"],
            fieldF: [].concat(this._gcmPG.destroy),
            fieldG: ["A recent asteroid storm", "A mining transportation accident", "An asteroid storm, followed by a Miners' Guild industrial dispute,"],
            fieldH: [],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "7 has left 1 2 asteroids hanging in low orbit. For any other system this (D50=may|might) not be a problem, but for us, the gravitational impact of those asteroids is having 3 effects on the stability of the tectonic plates on the planet surface. We need a 4 to circle the planet, 5 the \\[target\\] asteroids and 6 them."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType104 = function $missionType104() {
        var def = {
            fieldA: ["collect a lot of data about", "do a lot of deep-level seismic scans of", "need to be ever-vigilant in our observations of"],
            fieldB: ["trying to", "in an attempt to", "so as to", "doing our best to"],
            fieldC: ["impact of", "effects from", "disastrous consequences of", "fallout from"],
            fieldD: ["cities and population", "people", "services and infrastructure"],
            fieldE: ["the analysis", "the number-crunching", "the heavy-lifting"],
            fieldF: ["a set of seismic scans", "some particularly complex seismic scan data", "a complex set of seismic scan data"],
            fieldG: ["so this analysis can be performed", "so we can learn everything we can from the data", "so we can get the maximum possible value from the data"],
            fieldH: [],
            fieldI: [],
            fieldJ: [],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "We 1 our planet, 2 minimise the 3 earthquakes on our 4. But some of 5 needs to be done elsewhere, making use of off-planet ultra-hi-tech data processing facilities. We need 6 delivered to \\[destination\\] 7. Some of the analysis is time critical, so the data must be delivered without delay.",
            ],
        }
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType110 = function $missionType110() {
        var def = {
            fieldA: ["One of our [9] has informed us that", "It has come to our attention that", "We have reason to believe", "We have it on good authority that", "Top-secret dispatches from [9] have revealed", "We believe"],
            fieldB: ["the \\[destination\\] system", "\\[destination\\]"],
            fieldC: ["However, we can't [4] as", "It would be unwise to [4] as", "While it would be tempting to simply [4] prudence dictates this option is unacceptable as"],
            fieldD: ["send one of our official ships to get the data,", "have one of our official vessels extract the data,"],
            fieldE: ["the exposure risk is too high", "the potential exposure risks are too high", "the political fallout and inflammation of security tensions should we be discovered are too great to ignore"],
            fieldF: ["We also can't", "Neither can we", "And it would be inappropriate to"],
            fieldG: [].concat(this._gcmPG.in_search_of),
            fieldH: [].concat(this._gcmPG.travel_to),
            fieldI: [].concat(this._gcmPG.agents),
            fieldJ: ["vital to our security", "of a critical nature", "of some significance", "which has a direct bearing on our security", "of strategic importance", "which threatens the peace and safety of our world", "undermines the stability of our region"],
            fieldK: [].concat(this._gcmPG.pilot),
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: ["15"],
            sentences: [
                "1 information {1 has been collected on surveillance satellites in }2. 3 5. 6 request the authorities in \\[destination\\] to simply hand over potentially sensitive and volatile security information.\\n\\nSo we are 7 an independent {2 who can }8 \\[destination\\] and extract all data captured on any surveillance satellites that can be found there. All surveillance satellites in the system will need to be accessed.\\n\\nTo access the satellite you will be given a one-time passcode you will need to transmit to the unit. This passcode will activate when you jump into \\[destination\\], and will remain active for {9 minutes. You will not be able to access the satellites after this time."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType111 = function $missionType111() {
        var def = {
            fieldA: ["Sensitive communications between some high-level security personnel", "A number of extremely volatile communications, between some highly placed military figures,", "Our of our [9] has been forced to transmit some highly sensitive data packets that"],
            fieldB: ["the \\[destination\\] system", "\\[destination\\]"],
            fieldC: ["However, we can't [4] as", "It would be unwise to [4] as", "While it would be tempting to simply [4] prudence dictates this option is unacceptable as"],
            fieldD: ["send one of our official ships to get the data,", "have one of our official vessels seen extracting the data,"],
            fieldE: ["the exposure risk is too high", "the potential exposure risks are too high", "the political fallout and inflammation of security tensions should we be discovered are too great to ignore"],
            fieldF: ["We also can't", "Neither can we", "And it would be inappropriate to"],
            fieldG: [].concat(this._gcmPG.in_search_of),
            fieldH: [].concat(this._gcmPG.travel_to),
            fieldI: [].concat(this._gcmPG.agents),
            fieldJ: [].concat(this._gcmPG.pilot),
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: ["15"],
            sentences: [
                "1 have been captured and logged on COM satellites in 2, and we believe these messages have a direct bearing on our security here. 3 5. 6 request the authorities in \\[destination\\] to simply hand over potentially sensitive and volatile communication logs.\n\\nSo we are 7 an independent 9 who can 8 \\[destination\\] and extract communication log data recorded on any communication satellites found there. All communication satellites (both COM and COMLR) will need to be accessed.\\n\\nTo access the satellite you will be given a one-time passcode you will need to transmit to the unit. This passcode will activate when you jump into \\[destination\\], and will remain active for {9 minutes. You will not be able to access the satellites after this time."
            ],
            custom: [0x0],
            splitRules: []
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType122 = function $missionType122() {
        var def = {
            fieldA: ["attempt to", "trying to", "attempting to", "going through a complicated process to"],
            fieldB: ["delete", "excise", "remove", "wipe out"],
            fieldC: ["relevant records", "offending material", "problematic data"],
            fieldD: ["easier", "more expedient", "faster and easier", "faster", "quicker"],
            fieldE: [].concat(this._gcmPG.in_search_of),
            fieldF: [].concat(this._gcmPG.pilot),
            fieldG: [].concat(this._gcmPG.travel_to),
            fieldH: [],
            fieldI: ["incur", "receive", "get", "be slapped with"],
            fieldJ: ["A series of errors by security personnel has resulted in confidential and sensitive information being", "The actions of some of our military personnel have unfortunately been", "The lives of some of our [9] hang in the balance, because some operational material has been"],
            fieldK: [],
            fieldL: [],
            fieldM: [],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [].concat(this._gcmPG.agents),
            sentences: [
                "{1 captured by surveillance and communication satellites in \\[destination\\]. Rather than }1 2 the 3 from the satellites, it is 4 to simply destroy them.\\n\\nWe are 5 a 6 who can 7 \\[destination\\] and destroy all surveillance and communication satellites that can be found there. Satellite destruction can potentially result in a legal penalty in \\[destination\\], but we hope the payment will cover the inconvenience of carrying whatever bounty you might 9."
            ],
        };
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType150 = function $missionType150() {
        var def = {
            fieldA: [].concat(this._gcmPG.information),
            fieldB: ["suggests", "indicates", "leads us to believe"],
            fieldC: ["are short of", "have minimal", "have a limited number of", "have a shortage of"],
            fieldD: [].concat(this._gcmPG.in_search_of),
            fieldE: [].concat(this._gcmPG.pilot),
            fieldF: [].concat(this._gcmPG.travel_to),
            fieldG: ["prevent", "stop"],
            fieldH: ["available", "operational"],
            fieldI: ["at the moment", "at the present time", "right now", "at present", "just now"],
            fieldJ: ["most likely", "in all likelihood", "with very high probability of", "and we expect they will be", "and we suspect they will be"],
            fieldK: ["pirates", "a band of pirates", "a group of pirates", "a pirate group", "a pirate gang", "mercenaries", "a group of mercenaries", "a band of mercenaries", "a mercenary group", "a mercenary gang"],
            fieldL: ["200"],
            fieldM: [].concat(this._gcmPG.received),
            fieldN: ["in transit", "on route", "on it's way"],
            fieldO: ["several", "a number of", "a few"],
            fieldP: ["[6] important government dignitaries", "[6] important government officials", "an important trade delegation", "a party of foreign diplomats"],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "We have {4 }1 that 2 {2 are planning an attack in the \\[destination\\] system, 1 targeting an Anaconda transport carrying 7 that is already 5 and will be in system by the time you arrive. We }3 8 system defence (D50=ships|vessels) 9, so we are 4 >5 who can 6 \\[destination\\] and 7 the group from destroying this ship, and potentially destabilising the already volatile political environment.\n\nThe transport ship is called '\\[missiontargetShipName\\]'.\n\nA bonus of {3cr will be awarded for each attacking vessel you destroy.",
                "+{2 in the \\[destination\\] system have been increasing their activity, and we have 4 }1 that 2 they will be attempting to destroy an Anaconda transport carrying {7 that is already 5 and will be in system by the time you arrive. Unfortunately, we }3 8 system defence (D50=ships|vessels) 9, which creates a problem for us, that we are hoping to address by finding >5 who can 6 \\[destination\\] and 7 this group from achieving their goals.\n\nThe transport ship is called '\\[missiontargetShipName\\]'.\n\nA bonus of {3cr will be awarded for each attacking vessel you destroy."
            ],
        }
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType152 = function $missionType152() {
        var bountyText = "";
        if (player.ship.bounty > 0) bountyText = "\\nCompleting this mission will reduce any outstanding bounty you may have.";
        var def = {
            fieldA: [].concat(this._gcmPG.information),
            fieldB: ["suggests", "indicates", "leads us to believe"],
            fieldC: ["are short of", "have minimal", "have a limited number of", "have a shortage of"],
            fieldD: [].concat(this._gcmPG.in_search_of),
            fieldE: [].concat(this._gcmPG.pilot),
            fieldF: [].concat(this._gcmPG.travel_to),
            fieldG: ["prevent", "stop"],
            fieldH: ["available", "operational"],
            fieldI: ["at the moment", "at the present time", "right now", "at present", "just now"],
            fieldJ: ["most likely", "in all likelihood", "with very high probability of", "and we expect they will be", "and we suspect they will be"],
            fieldK: ["pirates", "a band of pirates", "a group of pirates", "a pirate group", "a pirate gang", "mercenaries", "a group of mercenaries", "a band of mercenaries", "a mercenary group", "a mercenary gang"],
            fieldL: ["200"],
            fieldM: [].concat(this._gcmPG.received),
            fieldN: ["500"],
            fieldO: ["general disruption","mayhem and destruction","widespread mayhem and general disruption"],
            fieldP: [bountyText],
            fieldQ: [],
            fieldR: [],
            sentences: [
                "We have {4 }1 that 2 {2 are planning an attack on the \\[destination\\] system, 1 targeting the main station. While 6 is likely their goal, our intelligence operatives have provided evidence to suggest these pirates have come into possession of some very high-powered missiles, dubbed 'Station Killers', which they may attempt to fire at the station.\n\nWe }3 8 system defence (D50=ships|vessels) 9, so we are 4 >5 who can 6 \\[destination\\] and 7 this group from destroying or damaging the station.\n\nA bonus of {3cr will be awarded for each attacking vessel you destroy, plus 5cr for each Station Killer missile you shoot down.{7",
                "+{2 in the \\[destination\\] system have been increasing their activity, and we have 4 }1 that 2 they will be targeting the main station in that system, to cause as much {6 as possible. Our intelligence operatives have also uncovered information that indicates these brigands have taken possession of some high-energy missiles, called 'Station Killers', which they might attempt to shoot at the station.\n\nUnfortunately, we }3 8 system defence (D50=ships|vessels) 9, which creates a problem for us, which we are hoping to address by finding >5 who can 6 \\[destination\\] and 7 this group from achieving their goals.\n\nA bonus of {3cr will be awarded for each attacking vessel you destroy, plus 5cr for each Station Killer missile you shoot down.{7"
            ],
        }
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType154 = function $missionType154() {
        var bountyText = "";
        if (player.ship.bounty > 0) bountyText = "\\nCompleting this mission will reduce any outstanding bounty you may have.";
        var def = {
            fieldA: [].concat(this._gcmPG.information),
            fieldB: ["suggests", "indicates", "leads us to believe"],
            fieldC: ["are short of", "have minimal", "have a limited number of", "have a shortage of"],
            fieldD: [].concat(this._gcmPG.in_search_of),
            fieldE: [].concat(this._gcmPG.pilot),
            fieldF: [].concat(this._gcmPG.travel_to),
            fieldG: ["prevent", "stop"],
            fieldH: ["available", "operational"],
            fieldI: ["at the moment", "at the present time", "right now", "at present", "just now"],
            fieldJ: ["most likely", "in all likelihood", "with very high probability of", "and we expect they will be", "and we suspect they will be", "and our smartest minds are saying they will be"],
            fieldK: ["500"],
            fieldL: [].concat(this._gcmPG._received),
            fieldM: [bountyText],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: ["We have received 1 that 2 the Thargoids are planning an attack on the \\[destination\\] system, {1 targeting the main station. We }3 8 system defence (D50=ships|vessels) 9, so we are 4 >5 who can 6 \\[destination\\] and 7 the Thargoids from destroying or damaging the station.\n\nA bonus of {2cr will be awarded for each Thargoid warship you destroy.{4"],
        }
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionType156 = function $missionType156() {
        var bountyText = "";
        if (player.ship.bounty > 0) bountyText = "\\nCompleting this mission will reduce any outstanding bounty you may have.";
        var def = {
            fieldA: [].concat(this._gcmPG.information),
            fieldB: ["suggests", "indicates", "leads us to believe"],
            fieldC: ["are short of", "have minimal", "have a limited number of", "have a shortage of"],
            fieldD: [].concat(this._gcmPG.in_search_of),
            fieldE: [].concat(this._gcmPG.pilot),
            fieldF: [].concat(this._gcmPG.travel_to),
            fieldG: ["prevent", "stop"],
            fieldH: ["available", "operational"],
            fieldI: ["at the moment", "at the present time", "right now", "at present", "just now"],
            fieldJ: ["most likely", "in all likelihood", "with very high probability of", "and we expect they will be", "and we suspect they will be", "and our smartest minds are saying they will be"],
            fieldK: ["500"],
            fieldL: [].concat(this._gcmPG.received),
            fieldM: [bountyText],
            fieldN: [],
            fieldO: [],
            fieldP: [],
            fieldQ: [],
            fieldR: [],
            sentences: ["We have {3 }1 that 2 the Thargoids are planning an attack on the \\[destination\\] system, {1 targeting the witchpoint beacon. We }3 8 system defence (D50=ships|vessels) 9, so we are 4 >5 who can 6 \\[destination\\] and 7 the Thargoids from destroying the beacon.\n\nA bonus of {2cr will be awarded for each Thargoid warship you destroy.{4"],
        }
        if (this._debug) this._setDef = def;
        var text = this._phraseGen._makePhrase(def);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$shipName = function $shipName() {
        var text = this._phraseGen._makePhrase(this._phraseGen.$pool.GNN_NamesShips);
        return this.$cleanUp(text);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$cleanUp = function $cleanUp(text) {
        // remove any of the \ chars that prefix any [ and ] chars
        text = text.replace(/\\\[/g, "[");
        text = text.replace(/\\\]/g, "]");
        // look for any \n operatives, and replace them with an actual new line
        text = text.replace(/\\n/g, String.fromCharCode(13) + String.fromCharCode(10));
        // look for any "missionVariable" lookups and put the correct prefix in
        text = text.replace(/\[mission/g, "[mission_");
        return text;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // this function takes the phraseGen definition of a particular mission type and dumps all the options and text
    // to the log file to assist in data entry/debugging on the phraseGen html page
    this.$outputDef = function $outputDef(missType) {
        var ws = worldScripts.GalCopBB_MissionDetails;
        var fn = "$missionType" + missType;
        ws[fn]();
        if (ws._setDef != null) {
            var list = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R"];
            for (var i = 0; i <= 1; i++) {
                for (var j = 0; j <= 8; j++) {
                    var idx = (i * 9) + j;
                    var fld = "field" + list[idx];
                    var ary = ws._setDef[fld];
                    var out = fld + " (" + (j + 1) + ")";
                    if (ary.length > 0) {
                        for (var k = 0; k < ary.length; k++) {
                            out += "\n" + ary[k];
                        }
                        log(this.name, out);
                    } else {
                        log(this.name, out + "\n{empty}");
                    }
                }
            }
            var st = ws._setDef.sentences;
            var out = "Sentences";
            for (var i = 0; i < st.length; i++) {
                out += "\n" + st[i].replace(/\\/g, "\\\\");
            }
            log(this.name, out);
        }
        ws._setDef = null;
    }
    
    this.$outputAllSamples = function() {
        for (var i = 1; i <= 156; i++) {
            var fn = "$missionType" + i.toString();
            if (this[fn]) {
                log(this.name, i.toString() + " -- " + this[fn]());
            }
        }
    }
    Scripts/galcopbb_missions.js
    "use strict";
    this.name = "GalCopBB_Missions";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Adds some local missions to the GalCop Bulletin Board";
    this.license = "CC BY-NC-SA 4.0";
    
    /*
    	This OXP adds a number of player-selectable missions to most stations in the Ooniverse. 
    	The missions are generally local (<= 15 LY travel to destination) and for the most part are
    	using whatever ships are installed, rather than needing any customised ships.
    
    	The missions are designed to be achievable by any pilot, although common-sense dictates 
    	you won't accept a seek and destroy mission to a dangerous system if 
    	your ship isn't combat ready. The missions are all optional - the player has complete control 
    	over which missions to accept, and when they will accept them.
    
    	Also included in this mission pack is the concept of mission chains. So, you take one mission, 
    	and when you complete it another mission is launched which relates to it.
    
    	It is planned to increase the number of different mission types over time.
    
    	Thanks to Eric Walsh for his derelict ship creation code
    	And thanks to cim for allowing his "Black Box" ship model to be included in this OXP.
    
    	To force a particular mission type onto the current mission board (for testing), use the following code:
    		worldScripts.GalCopBB_Missions.$testMissionType(136); // change the number to the mission type you want to test
    	
    	REQUIRED BEFORE BETA:
    	- complete missions 36, 37, 45, 47
    	- Go over all mission text and improve randomisation
    	- Work out the different reputation series, how long each should take to earn highest rank, and what the ranks are.
    	- Test all missions, particularly chaining
    	- Test disease outbreak event creation and resolution
    
    	TODO
    		analyse cargo missions in relation to smugglers relabelled cargo
    		overarching mission:
    			there are 8 pieces of an ancient artefact, one in each sector, that the player can collect in a number of ways:
    				- added as a scoopable item with a type 6/7 mission
    				- given to the player from a type 30 mission
    				- pulled from the hulk of a ship in a type 22/23/24 mission
    				- given to player from a type 40/41/42 mission
    				- triggering the entry of an artefact will be based on the number of missions performed in each sector. 
    					- 1st artefact will trigger after 10 missions in the sector
    					- 2nd artefact will trigger after 20 missions in the sector
    					- 3rd artefact will trigger after 30 missions in the sector
    					- etc
    				- news items to appear after every 10 missions, therefore 55 news items req
    			if smugglers is installed
    				artefacts can be sold on the black market for big dollars, the more pieces you have the more $
    				if player sells items, they can be possibly bought back through the black market for high dollars
    					- player will be lead to black market location via items left on the Black market noticeboard
    			if smugglers is not installed
    				artefacts can be sold through ??
    				artefacts can be re-bought through ??
    				player lead to location via emails ?? message when docking
    			there will be several news items that talk about the artefact in some way to give the player some idea the mission exists in the first place
    
    			once all pieces are found, will lead player to a planet not on the charts. orbiting the planet will be a dockable entity
    				there will be thargoids in this part of space, but they will all spontaneously blow up when within 15km of player
    				dockable entity will be devoid of F3 and F8 screens. 
    				f4 screen will have an "explore station" item
    					- turn off hud, show creepy space station backgrounds
    					- have small number of locations (5-6 at most), with easy directions accessed via menu
    					- goal is a room where the artefact can be installed after completing a small minigame
    						- player has to arrange the pieces of the artefact in the correct order by swapping pieces
    							- menu will be "Swap piece X...", followed by "...with piece Y"
    							- will need an image that can be broken up into 8 pieces
    							- will need to limit the number of combinations
    						- once solved a short message will be displayed saying target locked
    						- when player launches, will receive console messages saying unusual reading coming from planet
    						- after a few of these, planet will explode
    						- player can then choose to destroy the station
    						- station will have defensive turrets (a few of them) that will inhibit the player from doing this
    						- possibly there could be drones that launch as well
    						- if player doesn't destroy the station... ?
    
    		with goon squads, arrange it so there are some on a direct path between the wp and the destination
    		
    		Fix issue where ejected policeman shouldn't give player a mission - should be fixed, but test
    		Fix issue where ejected police is scooped as a slave - should be fixed, but test
    		Have self-destruction police give an exclamation before it explodes.
    
    		Make sure escape pod occupant's name is applied to the client name of any missions they ask you to perform.
    
    		With hit team assassins, they need to declare when they attack why it is they're attacking ("You shouldn't have sold that security device on the BM Commander")
    		type 40/41 Check what happens if a cargo container is destroyed after being ejected and before being scooped
    
    	ISSUE: Allowing a mission to fail without having gone to the black market to sell an item, and then afterwards going there to 
    		sell the item, would currently mean that no hit teams are sent after the player.
    				Q: What to do with items left with the player when a mission expires. 
    					Remove them? << currently doing this.
    					Allow the player to sell them? 
    					Ask the player if they want to dispose of the item?
    					>> theoretically this scenario should never happen if the mission is flagged to stop countdown at the point of 
    					getting the item.
    				* Need interface for disposal of equip items from failed missions so they don't fill up space.
    				
    	Other mission ideas:
    		collection missions
    			based on the OXP stations in the game, arrange a multi-point station trip to pickup "macguffins" (or perhaps stranded passengers)
    			Possible stations:
    				Tionisla Chronicle Array
    				Tionisla Orbital Graveyard
    				ErehwonStation (from Tionisla Reporter, after mission is complete)
    				Taxi station
    				Hunting Lodge (Feudal states)
    				Astrofactory (Dictators)
    				FTZ
    				Astromine
    				Collective ZGF
    			Will need a calculation which will predict which systems will have some of these stations, as they aren't 
    			spawned in all systems
    
    		defence missions: defend a target (station, relay, witchpoint beacon, satellite) from waves of attackers
    		Multi-part missions (ie, not linked, but actually having multiple parts, involving multiple jumps and different tasks at each jump)
    		Opposing missions (for version 2)
    			- if there's a black box mission at the GalCop station, there is an equivalent Pirate mission to grab the black box, but only if the initial mission is accepted
    			- need to work out what missions could have an opposite position.
    				- black box missions (same, but deliver to pirates)
    				- data cache missions (same, but deliver to pirates)
    				- special cargo recovery (same, but deliver to pirates)
    				- collect cargo for sys auth (same, but deliver to pirates)
    				- collect cargo for pirate auth (same, but deliver to galcop)
    				- meet ship to deliver data (contract kill)
    				- recover stolen item (same, but deliver article to pirates)
    				- meeting request (opposing mission would be a contract kill)
    					? could opposing mission be offered to the player at some intermediate point (email? comms relay near witchpoint?)
    
    		Civil War missions
    			Civil war has escalated into space - check out factions and how it sets up fight
    		report on pirate strength
    			GalCop wants to monitor traffic going in and out of a pirate Rock Hermit in system X. You must fly there, find the hermit, approach and dock while under cloak. Once docked, install special tracking software into station systems from F4 interface. You must not be seen by any pirate ships while you are within 10km of the station. 
    
    		hide and seek
    			a ship is hiding in an asteroid field, using a cloaking device. You have to have find them and destroy them
    
    			
    
    	mission types:
    	*1 - "Cleanup the spaceways": destroy x number of asteroids in the current system
    			any system, max of 1 mission of this type
    	*2 - "Making the system safer": destroy x number of pirates in system y.
    			any safe system within 7 LY of a feudal or anarchy
    			record when mission is accepted and only offer another one to the same system after 10 days
    	*3 - "Disrupting trade (System authority)": destroy x number of traders in system y.
    			any multi-gov or worse, near a safe system
    			record when mission is accepted and only offer another one to the same system after 10 days
    	*4 - "Disrupting trade (Pirate authority)": destroy x number of traders in system y.
    			any multi-gov or worse, near a safe system
    			record when mission is accepted and only offer another one to the same system after 10 days
    	*5 - "Thargoid hunt" - intentionally misjump between system X and Y and destroy N number of thargoid motherships (not tharglets)
    
    	*6 - "Collect the deposit" (System authority): find dumped cargo in system X, deliver to system Y.
    		- cargo will be guarded by x number of high quality ships
    	*7 - "collect the deposit" (Pirate authority)
    
    	*8 - "Pirates for hire (System authority)": attack trader ships in system X to get their cargo (n tons required), 
    		return to system Y
    	*9 - "Pirates for hire (Pirate authority)": attack trader ships in system X to get their cargo (n tons required), return to system Y
    	*10 - "Pirate specific commodity (System authority)": find an amount of a particular commodity but only when in a 
    		particular system and only from piracy (not purchased from stations)
    	*11 - "Pirate specific commodity (Pirate authority)": find an amount of a particular commodity but only when in a particular system 
    		and only from piracy (not purchased from stations)
    		
    	*12 - "Free slaves": (if IGT is installed) target all ships entering Communist or higher system, and if carrying slaves, 
    		demand cargo.
    			(Recommend the "Manifest Scanner" be installed)
    			Use BroadcastCommsMFD to "Demand slaves"
    			Ship will then have the ability to respond: either "OK" (and slaves are dumped), or "I'm not carrying slaves", or "Get lost"
    	*13 - "Collect Thargoid wreckage" destroy thargoids and collect wreckage in interstellar space
    			add scenario where alloys are destroyed during flight
    			>> prevent thargoid alloys from being relabelled by Smugglers
    			>> prevent thargoid alloys from being put into storage by SC
    	*14 - Gun runner: attack ships in target looking for firearms
    	*15 - Drug dealer: attack ships in target looking for narcotics
    	*16 - Civil unrest (pirate authority): destroy as many police ships in current system as you can
    	*17 - Garbage scow: dump radioactive waste containers near sun so they are destroyed by heat
    	*18 - Rival gang war (ie pirate hunt) (pirate authority)
    
    	*20 - Find escape pod in particular system and rescue occupant
    			- when in range, send out a distress signal to the player
    
    	*21 - interstellar escape pod rescue
    	*22 - Find black box in particular system and collect it 
    			- make this different from Rescue Stations by making the black box be inside the hulk of a destroyed ship
    			- need some way of locating ship hulk - maybe an intermittent comms signal?
    				- have console message appear saying "Weak navigation beacon signal ("U") being picked up."
    				- player switches ASC to find "U", gets a heading, then signal disappears. The further away the player is, the more inaccurate the ASC heading is.
    				- repeat until the player finds the hulk (ie is comes within scanner range)
    			- give player the option of selling black box on the black market
    		
    	*23 - interstellar black box recovery
    	*24 - Destroy derelict ship, scoop data cache (special cargo pod) and return it to station
    	*25 - catch runaway escape pod - escape pod ended up with a massive ejection speed and is on it's way to interstellar space. 
    		a regular waypoint will be set showing the pods last position
    		
    	*30 - Find and meet a particular ship in a particular system, will lead to secondary mission
    		- via BB as a mission
    		- via email when created as a primary mission
    		- via a random trader along space lane or at witchpoint
    
    	*31 - go to waypoint in system y, receive message from comms relay
    	*32 - Meet ship in system X, give them info package
    	*33 - intercept ship carrying stolen data cache,  (use Broadcast comms to demand data pack), which must then be scooped
    	34 - track ship from witchpoint to station, then return
    			- if player gets with 25 km of target and appears to be following them (eg heading is roughly the same and the player is behind them) they will detect player and flee (mission failed)
    			- player can get closer if they are cloaked
    			this needs to be more interesting that just following them to the station. 
    	36 - Find ship in system X by sending a pass phrase to any/all ships - one of which will reply with a message with details of 
    		where to find special cargo to be returned to system Y
    	37 - Find ship in system X by sending a pass phrase to any/all ships - one of which will reply with a message with details of 
    		where to find data point to be returned to system Y
    
    	delivery-type missions
    	*40 - Special delivery - player given unique cargo (poss using Smugglers illegal goods) or equipment, 
    			must deliver to remote point in destination, paid in precious metals/gems given to the player at the drop point
    	*41 - Special delivery - player given cargo or equipment, must go to system X to find a data point, 
    			to get location of drop point in system Y, paid in previous metals/gems given to the player at the drop point
    	*42 - special delivery: transport special computer equipment to ship, without docking at a station (docking will render the 
    		mission void)
    	*43	- transport vital equipment to stricken ship to enable it to continue flying
    			- have option of giving player secondary mission here
    	*44	- transport vital equipment to stricken ship in interstellar space to enable it to continue flying and escape
    	45 - Special delivery (only used for a secondary mission. similar to 40 but without giving cargo) - 
    			player told to get Xt of commodity, then deliver to a remote point in destination, paid is precious metals/gems
    	46 - pickup cargo from waypoint, delivery to another waypoint
    		will need a cargo shepherd, cargo stopper for ejection damper
    		arrival at one or other of waypoints could result in an ambush
    	47 - (only used for a secondary mission. similar to 41 but without giving cargo) - player told to get Xt of commodity, 
    		go to X to find data point to get location of drop point in Y, paid in precious metals/gems
    	
    	medical missions
    	*50 - deliver critical medical supplies to systems with "disease" as part of their description
    			- each day do a check for
    				(a) is a disease outbreak starting on a system?
    					only have, at most 2-3 outbreaks at any one time
    					when starting the outbreak, send a news item to snoopers, and define the end point for the outbreak (2-3 months ahead), define propagation chance
    						if a player transports medical supplies, take a day off the end point, reduce the propagation chance
    				(b) is a disease outbreak stopping on a system?
    					send an item to snoopers
    				(c) is disease spreading to a neighbouring system
    					there is a chance disease will spread to a neighbouring system - add it to the outbreak list, with a news item and end point
    			- medical supply missions offered only from hi-tech worlds (TL 12 and above)
    				expiry time is based on the volatility of the antibiotics being carried.
    
    	*51 - Transport critically ill patients from disease system to high tech level destination
    	*52 - Transport virus specimens
    	*53 - collect 3 components for an experimental antidote from different parts of the galaxy
    	
    	*60 - Hacking witchpoints
    		System authorities in system X are concerned about the flow of pirate traffic through their system. They want to hack into the witchpoint beacons of all systems within a 7LY range, that will enable them to monitor traffic flows into their system, outside of GalCop's purview.
    			- will require getting within 200m of witchpoint marker, targeting the marker and initiating the special equipment (prime, activate)
    
    	*61 - Restoring witchpoints
    		GalCop has become aware of an illegal data stream emanating from witchpoint markers around system X. You need to go to each of the systems listed, and clean the illegal code from the units. 
    			- will require getting within 200m of witchpoint marker, targeting the marker and initiating the special equipment (prime, activate)
    			- will only be available once mission 60 has been performed at a system
    
    	*62 - deliver security software payload to pirate rock hermit that track all ships coming and going from the station
    		- need to use cloaking device to dock with hermit. going red alert within 50k of station will result in mission failure.
    		>> TEST
    			
    	*63 - (from non galcop stations) deliver software payload to main station that will disrupt the trade computers (allowing all 
    		commodities to be purchased for 1 cr). (possible) for a period of 2 weeks
    			- when installing the software you will be asked what amount to set commodity prices to. You must enter the number 
    				specified in the mission description (either 1 or 99).
    			- you have to launch within 2 minutes of installing the software (and not redock again for 24 hours) otherwise GalCop 
    				will be able to trace the upload and you'll get hit with a massive fine (10000cr, 200 bounty)
    		
    	*64 - (from non galcop stations) deliver software payload to main station that will disrupt the equipment page, preventing 
    		any access for a period of 2 weeks.
    			- you have to launch within 2 minutes of installing the software (and not redock again for 24 hours) otherwise GalCop 
    				will be able to trace the upload and you'll get hit with a massive fine (10000cr, 200 bounty)
    
    	*65 - get galcop system software 
    		Player needs to do a multi-step process in order to get the software:
    		1. go to destination (a multi-gov of lower system with TL > 7) and pick up customised escape pod from any non-GalCop station (mission screen will inform player of installation)
    		2. return to system and use customised escape pod at more than 100000k from main station
    		3. after redocking, launch and wait for the next police ship to launch
    		4. follow police ship - it will self-destruct after 5-10 minutes, spewing an escape pod and a cargo item. 
    		5. Scoop the cargo item and return it to the source station.
    
    	66 - Perform scan of rock hermits, possibly while cloaked, return collected data
    		- need to create scanning equipment item
    		- harvest phase scanner code from Smugglers for general operating params
    
    	67 - Perform scan of main station, possibly while cloaked, return collected data
    			
    	70-79 Solar activity system missions
    	*70 - Use scan equipment to take close up measurements of sun
    			get to fuel scoop dist = ((system.sun.collisionRadius * system.sun.collisionRadius) / system.sun.position.squaredDistanceTo(player.ship.position)) > 0.75;
    			prime scanning device, activate
    			await completion
    			If player retreats before scan is complete, scan will stop, but will restart automatically when in range again (ie it won't start from the beginning)
    	*71 - Recover data cache in low orbit over sun
    			find data cache
    			transmit security code
    			await data package
    	72 - Launch monitoring devices (special cargo or missiles)
    			must deploy 2 special cargo items from within fuel scoop range, one on the planet side, the other on the far side of the sun.
    
    	*73 - Variation on 70: Go to another system to pick up new equipment, then bring it back and scan the sun
    	*74 - Take solar data to hi-tech system for analysis
    	75 - return analysed solar data to original system?
    	76 - Recover derelict ship near the sun (derelict will be given a high heat shield rating to prevent destruction)
    			- Use DeepSpaceDredger for equipment/process of ship recovery?
    
    	Charity missions
    	*80/81/82/83 - Give credits to entity
    	*84/85/86/87 - purchase and give commodity to entity (slaves for AI, any for the others)
    	88 - purchase any commodity (not slaves, firearms or narcs) and give to AI
    	89 - donation to nationalist entity of system
    		- reward of better commodity prices?
    			maybe just better prices on illegals?
    		- reward of better equipment prices?
    		- reward of better ship prices?
    	? - donation to religious entity
    
    	90-99 - war zone missions
    	*90 - evacuate refugees
    	*91 - transfer injured
    	92 - import weapons
    
    	100-109 Earthquake system missions
    	*100 - Use scan equipment (seismic resonance scanner) to scan planet in 3-4 locations
    		player must get in low enough orbit to perform scan successfully
    		have some way of informing the player where the scan points are - waypoints, must be within 2km of waypoints
    	*101 - transport injured people to hi-techlevel system for treatment 
    	*102 - destroy a number of asteroids around the planet. The gravitational effect of the asteroids means instability on the surface
    	103 - collect and deliver special equipment
    	*104 - take seismic data to hi-techlevel system for data analysis 
    
    	110-129 Satellite missions (when Satellites OXP is installed)
    	110 - Extract data
    	111 - Install new firmware
    	112 - Destroy satellite
    	113 - Hack satellite
    	114 - insert data
    
    	130-139 - Investigate unknown signal missions
    
    	150-159 - defend missions
    		*150 - defend anaconda from pirates
    		151 - defend liner from pirates (need liners OXP installed)
    		*152 - defend main station from pirates
    		153 - defend other galcop station from pirates (OXP required)
    		*154 - defend main station from thargoids
    		155 - defend other station from thargoids (OXP's required)
    		*156 - defend witchpoint beacon from thargoids
    
    	160 Scan planet from low orbit
    		requires to maintain a low orbit for a certain amount of time, with some possibility of attack
    */
    
    this._debug = false; // flag to control whether debugging log messages will appear
    this._rsnInstalled = false; // indicates whether randomshipnames OXP is installed
    this._igtInstalled = false; // indicates whether Illegal Goods Tweak is installed
    this._emailInstalled = false; // indicates whether the email system is installed
    this._equipmentFromFailedMissions = []; // array of equipment/data items from failed missions. 
    this._lastMission = []; // records last time a mission was accepted in a particular system
    this._setData = []; // hold value for populator routine
    this._missionHistory = []; // history of what missions have been completed at which systems
    this._fromSystem = -1; // system player is leaving
    this._toSystem = -1; // system player is heading to
    this._initialTime = 0; // initial time when adding new missions after a witchspace jump (to factor in the player's transit time to the station);
    this._transitTime = 1900; // transit time in seconds used to calculated expiry times
    this._workTime = 3900; // amount of time in seconds to allocate to doing the job in a system
    this._storedClientName = ""; // stored name, used to override client name when creating secondary missions
    this._waypointList = []; // list of waypoints added to the system
    this._positions = ["[gcm_position_0]", "[gcm_position_1]", "[gcm_position_2]", "[gcm_position_3]", "[gcm_position_4]", "[gcm_position_5]", "[gcm_position_6]", "[gcm_position_7]"];
    this._positionReferences = ["[gcm_position_0_reference]", "", "", "[gcm_position_3_reference]", "[gcm_position_4_reference]", "[gcm_position_5_reference]", "[gcm_position_6_reference]", "[gcm_position_7_reference]"];
    this._newMissionDelayTimer = null; // timer to give the new mission announcement a realistic time delay
    this._requestSpecialCargo = ""; // text of commodity, indicating that the player is due to receive 1t of commodity upon docking at GalCop station
    this._pendingMissionID = -1; // id of the current pending mission
    this._pendingMissionCallback = null; // callback routine for the pending mission
    this._pendingMissionOrigID = -1; // id of the original mission that generated the pending one
    this._postScoopMissionID = -1; // holds mission ID of the original mission of an escape pod/blackbox
    this._forceCargo = [];
    this._slaveRescue = false; // flag indicating that a slave rescue mission is in play in the current system
    this._slaveDemandHistory = []; // list of ships to whom the player has transmitted a demand to drop all slaves
    this._availableMissionTypes = []; // array of all mission type ID's that are currently available for use
    this._interstellarMissionTypes = []; // array of mission type ID's that will send a player into interstellar space
    this._cargoMissionTypes = []; // array of mission type ID's that relate to cargo transfer
    this._multiStageMissionTypes = []; // array of mission type ID's that have a multi-stage process
    this._preferredTargetShips = []; // list of trader ships with max speed > 150 to be used when creating target ships
    this._maxCargoOfShips = 0; // highest possible amount of cargo in list of preferred ship list
    this._initTimer = null; // timer used to create missions after startup
    this._thargoidAlloys = false; // flag indicating that a thargoid alloy collection mission is in play
    this._thargoidPosition = []; // array of positions where thargoid warships died
    this._generateMissionTimer = null; // timer to run when docked to add/remove missions
    this._singleMissionOnly = false; // flag to indicate just one mission should be generated
    this._generateMissionFrequency = 900; // number of seconds between cycles of adding new and removing old missions
    this._preferredCargoPods = []; // array of cargo containers that don't have scripted cargo (ie that can be used safely)
    this._emailMissionAdded = false; // flag to indicate that an email-type mission has already been created
    this._commodityUnit = {};
    this._trueValues = ["yes", "1", 1, "true", true];
    this._simulator = false;
    this._loopPoint1 = 0;
    this._createTimer = null;
    this._createDelay = 0.5;
    this._forceCreate = [];
    this._removeTimers = [];
    
    // there is only one equipment item translation here, but in case we need to add some extras later
    this._equipmentProvidingLookup = {
    	"EQ_CARGO_SCOOPS": "EQ_FUEL_SCOOPS",
    };
    
    // if the player gets special cargo, the related task can be set up here
    this._specialCargoTask = {};
    
    // configuration settings for use in Lib_Config
    this._gcmConfig = {
    	Name: this.name,
    	Alias: "GalCop Missions",
    	Display: "Debug Options",
    	Alive: "_gcmConfig",
    	Bool: {
    		B0: {
    			Name: "_debug",
    			Def: false,
    			Desc: "Enable debug messages"
    		},
    		Info: "Turns on debug messages and helpers."
    	},
    };
    
    //=============================================================================================================
    // event handlers
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
    	// load any stored data
    	if (missionVariables.GalCopBBMissions_EquipmentFailedMissions) {
    		this._equipmentFromFailedMissions = JSON.parse(missionVariables.GalCopBBMissions_EquipmentFailedMissions);
    		delete missionVariables.GalCopBBMissions_EquipmentFailedMissions;
    	}
    	if (missionVariables.GalCopBBMissions_Last) {
    		this._lastMission = JSON.parse(missionVariables.GalCopBBMissions_Last);
    		delete missionVariables.GalCopBBMissions_Last;
    	}
    	if (missionVariables.GalCopBBMissions_History) {
    		this._missionHistory = JSON.parse(missionVariables.GalCopBBMissions_History);
    		delete missionVariables.GalCopBBMissions_History;
    	}
    
    	worldScripts["oolite-libPriorityAI"].PriorityAIController.prototype.pirateHasLurkPosition = function () {
    		if (this.getParameter("oolite_pirateLurk")) {
    			return true;
    		} else {
    			return false;
    		}
    	}
    
    	// make sure the BB data is loaded.
    	if (missionVariables.BBData) {
    		worldScripts.BulletinBoardSystem.startUp();
    	}
    	if (worldScripts.BulletinBoardSystem._data && worldScripts.BulletinBoardSystem._data.length > 0) {
    		this.$dataFix();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function () {
    	// set a flag if random ship names is installed
    	if (worldScripts["randomshipnames"]) this._rsnInstalled = true;
    	// set a flag if the illegal goods tweak is installed
    	if (worldScripts["illegal_goods_tweak"]) this._igtInstalled = true;
    	// set a flag if the email system is installed
    	if (worldScripts["EmailSystem"]) this._emailInstalled = true;
    
    	// register our settings, if Lib_Config is present
    	if (worldScripts.Lib_Config) worldScripts.Lib_Config._registerSet(this._gcmConfig);
    
    	if (worldScripts.AutoPrimeEquipment) {
    		// sync up autoprime equip so selecting our MFD automatically primes broadcast comms
    		worldScripts.AutoPrimeEquipment.$addConfig("GalCopBB_Missions_MFD", "EQ_BROADCASTCOMMSMFD");
    	}
    
    	this._lastSource = system.ID;
    
    	// grab a copy of the unit (t, kg, g) for each commodity, for easy retrieval later
    	var unittypes = ["t", "kg", "g"];
    	for (var c in system.mainStation.market) {
    		this._commodityUnit[c] = unittypes[parseInt(system.mainStation.market[c].quantity_unit)];
    	}
    	//this.$removeAllMissions(); // so we can regenerate everything (testing only)
    
    	// set up a timer to create new missions after startup
    	// this is because ShipConfiguration could adjust the cargo space available after doing the setup, which would create some very strange results
    	// when a mission will allocate more cargo than the player actually has
    	this._initTimer = new Timer(this, this.$initMissions, 1, 0);
    
    	this.$checkForSlaveMission(system.ID);
    	if (this._slaveRescue === true) this.$addSlaveCommsMessage();
    
    	// add out custom AI scripts into SDC's exceptions list
    	var sdc = worldScripts.StationDockControl;
    	if (sdc && sdc._AIScriptExceptions) {
    		sdc._AIScriptExceptions["GCM Assassins AI"] = "gcm-assassinAI.js";
    		sdc._AIScriptExceptions["GCM Pirate AI"] = "gcm-pirateAI.js";
    		sdc._AIScriptExceptions["GCM Scavenger AI"] = "gcm-scavengerAI.js";
    		sdc._AIScriptExceptions["GCM Guard AI"] = "gcm-guardAI.js";
    	}
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$removeAllMissions = function() {
    	var bb = worldScripts.BulletinBoardSystem;
    	var list = this.$getListOfMissions(false);
    	for (var i = 0; i < list.length; i++) {
    		bb.$removeBBMission(list[i].ID);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$dataFix = function() {
    	var bb = worldScripts.BulletinBoardSystem;
    	for (var i = 0; i < bb._data.length; i++) {
    		var item = bb._data[i];
    		if (item.data && item.data.hasOwnProperty("position")) {
    			item.data["locationType"] = item.data.position;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillSaveGame = function () {
    	// clean up any dregs that might have been left from secondary mission creation
    	if (missionVariables.stolenItemType) delete missionVariables.stolenItemType;
    	if (missionVariables.targetShipKey) delete missionVariables.targetShipKey;
    	if (missionVariables.targetShipName) delete missionVariables.targetShipName;
    
    	// save any data we currently have
    	if (this._equipmentFromFailedMissions.length > 0) missionVariables.GalCopBBMissions_EquipmentFailedMissions = JSON.stringify(this._equipmentFromFailedMissions);
    	if (this._lastMission.length > 0) missionVariables.GalCopBBMissions_Last = JSON.stringify(this._lastMission);
    	if (this._missionHistory.length > 0) missionVariables.GalCopBBMissions_History = JSON.stringify(this._missionHistory);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillEnterWitchspace = function (cause, destination) {
    	this._thargoidPosition.length = 0;
    	if (cause !== "galactic jump") {
    		this._fromSystem = system.ID;
    		if (destination === -1) {
    			this._toSystem = this.$playerTargetSystem();
    		} else {
    			this._toSystem = destination;
    		}
    	}
    	// terminate all active missions
    	if (cause === "galactic jump") {
    		var list = this.$getListOfMissions(true);
    		for (var i = list.length - 1; i >= 0; i--) {
    			this.$terminateMission(list[i].ID);
    		}
    	}
    	this.$stopTimers();
    	this._waypointList.length = 0;
    	this._slaveDemandHistory.length = 0;
    
    	// check for slave rescue mission
    	if (cause !== "galactic jump") {
    		this.$checkForSlaveMission(destination);
    		if (this._slaveRescue === true) {
    			this.$addSlaveCommsMessage();
    		} else {
    			this.$removeSlaveCommsMessage();
    		}
    	} else {
    		this._slaveRescue = false;
    		this.$removeSlaveCommsMessage();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillExitWitchspace = function () {
    	if (system.ID !== -1) this._lastSource = system.ID;
    	this._initialTime = 1800;
    	this._loopPoint1 = -1;
    	// shuffle order so it isn't always the same variant being checked first
    	this._availableMissionTypes.sort(function (a, b) {
    		return Math.random() - 0.5;
    	}); 
    	this._createTimer = new Timer(this, this.$addLocalMissions, this._createDelay, 0);
    	this._initialTime = 0;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerEnteredNewGalaxy = function (galaxyNumber) {
    	this._lastMission.length = 0;
    	// reset the source ID for any equipment hanging around from failed missionScreenEnded
    	// this will mean the player can sell it with impunity!
    	for (var i = 0; i < this._equipmentFromFailedMissions.length; i++) {
    		this._equipmentFromFailedMissions[i].source = -1;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.systemWillPopulate = function () {
    	/*
    		Programming note: Some data values need to be passed into the populator routines. If those values are 
    		static for this system, we can just use "this._my_variable". 
    		However, if some values are different for each mission that will be generated in this system, we need 
    		a way of passing data into the populator callback that will be called at some point in the future 
    		(close in the future ie < 1 sec, but not *immediately*).
    		So, we are are using the "this._setData" array, coupled with the "this.$getMissionData" function.
    		The way it works is, as mission objects are set up with their own populator callbacks, data is pushed 
    		onto the array.
    		Then, when the populator actually runs, it calls the function to get the top-most entry from the array 
    		for that mission type and removes it.
    		That data should be the correct data for the current mission type populator. Because the data is also 
    		removed, it means the next mission, if it happens to be of the same type, will again get the topmost 
    		entry which should be its data.
    	*/
    	// force the asteroids population script to run before this one, if it hasn't already
    	var ast = worldScripts.GalCopBB_AsteroidFields;
    	if (ast._populateComplete === false) {
    		ast.systemWillPopulate();
    	}
    
    	// make sure we haven't orphaned any active mission records
    	this.$validateActiveData();
    
    	// reset the mission populator data array
    	this._setData.length = 0;
    	this._forceCargo.length = 0;
    	this.$getPreferredShipList();
    	this.$getPreferredCargoPodList();
    	this._thargoidAlloys = false;
    	this._thargoidAlloysMissionID = 0;
    	var ignoreCargo = [6, 7, 13, 17]; // mission types we don't want to force cargo for
    
    	var list = this.$getListOfMissions(true);
    
    	if (list.length > 0) {
    		// loop through all active missions and see if any need to be set up for this system
    		for (var i = 0; i < list.length; i++) {
    			// if we're looking for any specific commodity in this system, then make sure some ships will have some
    			if (list[i].data.commodity !== "" &&
    				ignoreCargo.indexOf(list[i].data.missionType) === -1 && // not applicable to Thargoid alloy collections
    				list[i].destination === system.ID &&
    				list[i].expiry > clock.adjustedSeconds) {
    				this._forceCargo.push(list[i].data.commodity);
    			}
    
    			// check if any assassins need to be created
    			if (Number(list[i].data.assassinChance) > 0 &&
    				Math.random() < Number(list[i].data.assassinChance) &&
    				list[i].expiry > clock.adjustedSeconds &&
    				worldScripts.GalCopBB_DiseaseOutbreak.$systemHasDiseaseOutbreak(system.ID) === false) {
    				// create a groups of assassins at the witchpoint
    				// how many assassin groups?
    				var agc = 1;
    				if (Number(list[i].data.assassinChance) >= 0.4 && Number(list[i].data.assassinChance) <= 0.7) {
    					if (Math.random() < list[i].data.assassinChance) agc += 1;
    				}
    				if (Number(list[i].data.assassinChance) > 0.7) {
    					if (Math.random() < Number(list[i].data.assassinChance)) agc += 1;
    					if (Math.random() < Number(list[i].data.assassinChance)) agc += 1;
    				}
    
    				if (this._debug) log(this.name, "adding " + agc + " gcm assassin group(s)");
    
    				system.setPopulator("gcm-assassins-" + list[i].ID, {
    					priority: 90,
    					location: "WITCHPOINT",
    					groupCount: agc,
    					callback: this.$addGCMAssassin.bind(this)
    				});
    			}
    
    			// *** type 6/7 - recover cargo cache
    			if ((list[i].data.missionType === 6 || list[i].data.missionType === 7) &&
    				list[i].destination === system.ID &&
    				list[i].data.quantity < (list[i].data.targetQuantity - list[i].data.destroyedQuantity) &&
    				list[i].expiry > clock.adjustedSeconds) {
    
    				//var asterField = ast.$getSystemAsteroidField();
    				var position = ast.$getSystemAsteroidField(list[i].data.locationType);
    				//var position = this.$getRandomPosition(list[i].data.position, 0.1, list[i].ID).position;
    
    				this._setData.push({
    					missionType: 607,
    					missionID: list[i].ID,
    					source: list[i].source,
    					goons: 0,
    					quantity: (list[i].data.targetQuantity - list[i].data.destroyedQuantity) - list[i].data.quantity,
    					target: list[i].data.targetQuantity
    				});
    
    				// add the cargo canisters
    				system.setPopulator("gcm-cargo-" + list[i].ID, {
    					callback: function (pos) {
    						var missData = worldScripts.GalCopBB_Missions.$getMissionData(607);
    
    						// add an asteroid field
    						//var as = system.addShips("asteroid", 60, pos, 40E3);
    
    						// only create the ones that haven't been collected
    						var pd = this._preferredCargoPods[Math.floor(Math.random() * this._preferredCargoPods.length)];
    						var cg = system.addShips("[" + pd + "]", missData.quantity, pos, 10000);
    						// if we couldn't create them with our preferred pod type, use the default
    						if (!cg) {
    							cg = system.addShips("[barrel]", missData.quantity, pos, 10000);
    						}
    						if (cg && cg.length === missData.quantity) {
    							var bb = worldScripts.BulletinBoardSystem;
    							var item = bb.$getItem(missData.missionID);
    							for (var j = 0; j < cg.length; j++) {
    								cg[j].switchAI("oolite-nullAI.js"); // dumbAI.plist
    								cg[j].setScript("oolite-default-ship-script.js");
    
    								var cmdty = item.data.commodity;
    								// make a note of how much is put into each cargo pod, so we can validate this at completion
    								if (system.mainStation.market[item.data.commodity].quantity_unit == "0") {
    									var qty = 1;
    								} else {
    									var qty = Math.floor(system.scrambledPseudoRandomNumber(missData.missionID * j) * 10) + 1;
    								}
    								item.data.expected += qty;
    
    								cg[j].setCargo(cmdty, qty);
    								cg[j].script._missionID = missData.missionID;
    								cg[j].script._gcmSpecial = true;
    
    								// monkey patch if necessary
    								// add our shipDied event to the cargo
    								if (cg[j].script.shipDied) cg[j].script.$gcm_hold_shipDied = cg[j].script.shipDied;
    								cg[j].script.shipDied = this.$gcm_cargo_shipDied;
    
    								cg[j].primaryRole = "special_cargo";
    								cg[j].name = "Cargo container";
    							}
    							// add our cargo pods to the cargo monitor
    							worldScripts.GalCopBB_CargoMonitor.$addMonitor(missData.missionID, item.data.commodity, system.ID, false, cg);
    						} else {
    							log(this.name, "!!ERROR: Cargo not spawned!");
    						}
    
    						// spawn some alloy wreckage as well
    						system.addShips("scarred-alloy", (Math.floor(Math.random() * 5) + 2), pos, 5000);
    
    					}.bind(this),
    					location: "COORDINATES",
    					coordinates: position
    				});
    
    				if (this._debug) this.$setWaypoint(position, [0, 0, 0, 0], "D", "Debug position (6)", "6");
    			}
    
    			// *** type 13 - alloys/wreckage from thargoid ships
    			if (list[i].data.missionType === 13 &&
    				list[i].data.quantity < list[i].data.targetQuantity) {
    				//log(this.name, "turning on thargoid alloy monitoring flag");
    				// tell the shipSpawned event to start monitoring for thargoids
    				this._thargoidAlloys = true;
    				this._thargoidAlloysMissionID = list[i].ID;
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.interstellarSpaceWillPopulate = function () {
    	this._thargoidAlloys = false;
    	this._thargoidAlloysMissionID = 0;
    	var list = this.$getListOfMissions(true, 13);
    	if (list.length > 0) {
    		// loop through all active missions and see if any need to be set up for this system
    		for (var i = 0; i < list.length; i++) {
    			// *** type 13 - alloys/wreckage from thargoid ships
    			if (list[i].data.missionType === 13 &&
    				list[i].data.quantity < list[i].data.targetQuantity) {
    				//log(this.name, "turning on thargoid alloy monitoring flag");
    				// tell the shipSpawned event to start monitoring for thargoids
    				this._thargoidAlloys = true;
    				this._thargoidAlloysMissionID = list[i].ID;
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    /*this.systemWillRepopulate = function() {
    	function gcm_findstricken(entity) {
    		return (entity.primaryRole === "gcm_stricken_ship");
    	}
    }*/
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipKilledOther = function (whom, damageType) {
    
    	var bb = worldScripts.BulletinBoardSystem;
    	// mission type 1 - asteroid hunt
    	if (whom.hasRole("asteroid")) {
    		var list = this.$getListOfMissions(true, 1);
    		for (var i = 0; i < list.length; i++) {
    			if (system.ID === list[i].destination &&
    				list[i].expiry > clock.adjustedSeconds &&
    				list[i].data.quantity < list[i].data.targetQuantity) {
    
    				list[i].data.quantity += 1;
    				bb.$updateBBMissionPercentage(list[i].ID, (list[i].data.quantity / list[i].data.targetQuantity));
    
    				this.$logMissionData(list[i].ID);
    				player.consoleMessage(expandDescription("[goal_updated]"));
    				//break;
    			}
    		}
    	}
    
    	// mission type 2/18 - pirate hunt
    	if (whom.bounty >= 0 && (Ship.roleIsInCategory(whom.primaryRole, "oolite-pirate") || (whom.AI && whom.AI.toLowerCase().indexOf("pirate") >= 0) || (whom.AIScript && whom.AIScript.name.toLowerCase().indexOf("pirate") >= 0))) {
    		var list = this.$getListOfMissions(true, [2, 18]);
    		for (var i = 0; i < list.length; i++) {
    			if (system.ID === list[i].destination &&
    				list[i].expiry > clock.adjustedSeconds &&
    				list[i].data.quantity < list[i].data.targetQuantity) {
    
    				list[i].data.quantity += 1;
    				bb.$updateBBMissionPercentage(list[i].ID, (list[i].data.quantity / list[i].data.targetQuantity));
    
    				this.$logMissionData(list[i].ID);
    				player.consoleMessage(expandDescription("[goal_updated]"));
    				//break;
    			}
    		}
    	}
    
    	// mission type 3/4 - trader hunt
    	if (whom.primaryRole && whom.primaryRole.indexOf("trader") >= 0) {
    		var list = this.$getListOfMissions(true, [3, 4]);
    		for (var i = 0; i < list.length; i++) {
    			if (system.ID === list[i].destination &&
    				list[i].expiry > clock.adjustedSeconds &&
    				list[i].data.quantity < list[i].data.targetQuantity) {
    
    				list[i].data.quantity += 1;
    				bb.$updateBBMissionPercentage(list[i].ID, (list[i].data.quantity / list[i].data.targetQuantity));
    
    				this.$logMissionData(list[i].ID);
    				player.consoleMessage(expandDescription("[goal_updated]"));
    				//break; removed to allow player to have a type 3 and a type 4 active and updated at the same time.
    			}
    		}
    	}
    
    	// mission type 5 - thargoid hunt
    	if (whom.isThargoid === true && whom.hasRole("tharglet") === false && whom.hasRole("thargon") === false && whom.hasRole("EQ_THARGON") === false) {
    		var list = this.$getListOfMissions(true, 5);
    		for (var i = 0; i < list.length; i++) {
    			if (list[i].expiry > clock.adjustedSeconds &&
    				system.ID === -1 &&
    				((this._fromSystem === list[i].destination || this._fromSystem === list[i].source) &&
    					(this._toSystem === list[i].destination || this._toSystem === list[i].source)) &&
    				list[i].data.quantity < list[i].data.targetQuantity) {
    
    				list[i].data.quantity += 1;
    				bb.$updateBBMissionPercentage(list[i].ID, (list[i].data.quantity / list[i].data.targetQuantity));
    
    				this.$logMissionData(list[i].ID);
    				player.consoleMessage(expandDescription("[goal_updated]"));
    				//break;
    			}
    		}
    	}
    
    	// mission type 16 - police hunt/civil unrest
    	if (whom.hasRole("police")) {
    		var list = this.$getListOfMissions(true, 16);
    		for (var i = 0; i < list.length; i++) {
    			if (list[i].expiry > clock.adjustedSeconds &&
    				list[i].destination === system.ID &&
    				list[i].data.quantity < list[i].data.targetQuantity) {
    
    				list[i].data.quantity += 1;
    				bb.$updateBBMissionPercentage(list[i].ID, (list[i].data.quantity / list[i].data.targetQuantity));
    
    				this.$logMissionData(list[i].ID);
    				player.consoleMessage(expandDescription("[goal_updated]"));
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipScoopedOther = function (whom) {
    	if (whom && whom.isCargo && whom.primaryRole !== "gcm_blackbox" && whom.primaryRole !== "escape-capsule" && whom.hasRole("gcm_special_pod") === false) {
    		if (whom.script) {
    			if (whom.script._specialisedComputers && whom.script._specialisedComputers === 1) {
    				delete whom.script._specialisedComputers;
    				delete whom.script._missionID;
    				return;
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDumpedCargo = function(cargo) {
    	cargo.script._fromPlayer = true;
    
    	if (cargo.commodity === "radioactives") {
    		// check to see if a garbage scow mission is active
    		var list = this.$getListOfMissions(true, 17);
    		for (var i = 0; i < list.length; i++) {
    			if (list[i].data.quantity < (list[i].data.targetQuantity - list[i].data.destroyedQuantity)) {
    				// attach our custom scripts
    				if (cargo.script.shipDied && !cargo.script.$gcm_hold_shipDied) cargo.script.$gcm_hold_shipDied = cargo.script.shipDied;
    				cargo.script.shipDied = this.$gcm_garbage_shipDied;
    				cargo.script._missionID = list[i].ID;
    				break;
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipSpawned = function (ship) {
    	/*var p = player.ship;
    	// did the player just dump cargo?
    	if (p && ship && ship.isCargo && ship.position && p.position && p.position.distanceTo(ship) < 300) {
    		//gotcha - flag this canister so we can check it later if the player scoops it
    		ship.script._fromPlayer = true;
    	}*/
    
    	// check for a thargoid alloy mission
    	if (this._thargoidAlloys === true) {
    		/*if (ship.hasRole("alloy") && ship.name === "Metal fragment") {
    			log(this.name, "alloy spawned");
    			log(this.name, "missionID = " + ship.script._missionID);
    			log(this.name, "nearDeadThargoid = " + this.$shipNearDeadThargoid(ship));
    		}*/
    		if (ship.hasRole("alloy") && ship.name.toLowerCase() === "metal fragment" && !ship.script._missionID && this._thargoidPosition.length > 0 && this.$shipNearDeadThargoid(ship) === true) {
    			//var list = this.$getListOfMissions(true, 13);
    			//for (var i = 0; i < list.length; i++) {
    			//	if (list[i].data.quantity < list[i].data.targetQuantity) {
    					ship.displayName = "Thargoid " + ship.displayName;
    					ship.script._fromThargoid = true;
    					ship.script._missionID = this._thargoidAlloysMissionID;
    			//	}
    			//}
    		}
    		if (ship.isThargoid === true && ship.hasRole("tharglet") === false && ship.hasRole("thargon") === false && ship.hasRole("EQ_THARGON") === false) {
    			//var list = this.$getListOfMissions(true, 13);
    			//for (var i = 0; i < list.length; i++) {
    				if (ship.script.shipDied) ship.script.$gcm_hold_shipDied = ship.script.shipDied;
    				ship.script.shipDied = this.$gcm_monitorThargoid_shipDied;
    				ship.script._missionID = this._thargoidAlloysMissionID;
    			//}
    		}
    	}
    
    	// check for a slave rescue mission, and make sure there are some ships with slaves to rescue
    	// check for any cargo that needs to be present for the player to pirate/liberate
    	if (!system.isInterstellarSpace && this._forceCargo.length > 0) {
    		var fc = this._forceCargo;
    		for (var i = 0; i < fc.length; i++) {
    			var checkShip = false;
    			if (system.mainStation.market[fc[i]].legality_import > 0 || system.mainStation.market[fc[i]].legality_export > 0) {
    				// for illegal goods we'll add cargo to pirate group leaders
    				// possibly smugglers or even normal traders
    				if (Ship.roleIsInCategory(ship.primaryRole, "oolite-pirate-leader") || (Math.random() > 0.4 && ship.primaryRole === "trader-smuggler") || (Math.random() > 0.8 && Ship.roleIsInCategory(ship.primaryRole, "oolite-trader")))
    					checkShip = true;
    			} else {
    				// for other cargo types, we'll just add it to traders
    				if (Ship.roleIsInCategory(ship.primaryRole, "oolite-trader") && Math.random() > 0.5) checkShip = true;
    			}
    			if (checkShip === true) {
    				// does this ship have any cargo space left
    				if (ship.cargoSpaceAvailable > 0) {
    					// does this ship have any of this cargo on board already?
    					var haveCargo = false;
    					for (var j = 0; j < ship.cargoList.length; j++) {
    						var itm = ship.cargoList[j];
    						if (itm.commodity === fc[i] && itm.quantity > 0) {
    							haveCargo = true;
    							break;
    						}
    					}
    					// add some cargo
    					if (haveCargo === false && Math.random() > 0.7) {
    						var max = 3;
    						if (ship.cargoSpaceAvailable < 3) max = ship.cargoSpaceAvailable;
    						var adjamt = parseInt(Math.floor(Math.random() * max) + 1);
    						if (this._debug) log(this.name, "adding " + adjamt + "t " + fc[i] + " to " + ship);
    						ship.adjustCargo(fc[i], adjamt);
    					}
    				}
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = function (whom, why) {
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillDockWithStation = function (station) {
    	if (this._simulator === true) return;
    	// if we dock at a station with any pods or stricken ships waiting for us, the quickest time to undock again is 10 minutes, so they will all die
    	if (this._lifeSupportTimer && this._lifeSupportTimer.isRunning) {
    		// end life support on any entities in-system at the moment
    		for (var i = 0; i < this._lifeSupportRemaining.length; i++) {
    			this._lifeSupportRemaining[i].remaining = 1;
    		}
    	}
    	// give the player the special machinery if they've docked
    	if (this._requestSpecialCargo !== "" && station.allegiance === "galcop") {
    		if (player.ship.cargoSpaceAvailable === 0 && player.ship.cargoSpaceCapacity >= 1) this.$freeCargoSpace(1, this._requestSpecialCargo);
    		player.ship.manifest[this._requestSpecialCargo] += 1;
    		var text = expandDescription("[gcm_give_player_special_cargo]", {
    			commodity: displayNameForCommodity(this._requestSpecialCargo).toLowerCase(),
    			task: this._specialCargoTask[this._requestSpecialCargo]
    		});
    		player.addMessageToArrivalReport(text);
    		this._requestSpecialCargo = "";
    	}
    	this._generateMissionTimer = new Timer(this, this.$generateMissions, this._generateMissionFrequency, this._generateMissionFrequency);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDockedWithStation = function (station) {
    	if (this._simulator === true) this._simulator = false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillLaunchFromStation = function (station) {
    	this._emailMissionAdded = false;
    	if (system.ID != -1) this._lastSource = system.ID;
    	if (this._generateMissionTimer && this._generateMissionTimer.isRunning) this._generateMissionTimer.stop();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipLaunchedFromStation = function (station) {
    	if (this.$simulatorRunning() === true) this._simulator = true;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerSoldCargo = function (commodity, units, price) {
    	// check if the player just sold the special computers
    	if (commodity === "computers") this.$checkSpecialDeliveryRemoved();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipTakingDamage = function (dmg_amount, whom, type) {
    	// check if the player just lost special computers through damage
    	if (dmg_amount > 0) {
    		this.$checkSpecialDeliveryRemoved();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerBoughtNewShip = function (ship, price) {
    	// check if the player just lost the special computers
    	this.$checkSpecialDeliveryRemoved();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$initMissions = function $initMissions() {
    	// if we don't have any available missions, try to add some now.
    	var list = this.$getListOfMissions();
    	if (list.length === 0) {
    		this._loopPoint1 = -1;
    		this._availableMissionTypes.sort(function (a, b) {
    			return Math.random() - 0.5;
    		}); // shuffle order so it isn't always the same variant being checked first
    		this._createTimer = new Timer(this, this.$addLocalMissions, this._createDelay, 0);
    	}
    	delete this._initTimer;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // gets mission specific data for the populator routines
    // this works on a first in/first out basis - if there are multiple missions of the same type being populated, the mission specific data would
    // get pushed in to the setData array in order, and then this routine pulls that data out in the same order
    // that's the theory, anyway!
    this.$getMissionData = function $getMissionData(missionType) {
    	for (var i = 0; i < this._setData.length; i++) {
    		if (this._setData[i].missionType === missionType) {
    			var result = {
    				missionID: this._setData[i].missionID,
    				trueMissionType: (this._setData[i].trueMissionType ? this._setData[i].trueMissionType : missionType),
    				source: this._setData[i].source,
    				goons: this._setData[i].goons,
    				quantity: this._setData[i].quantity,
    				target: this._setData[i].target
    			};
    			this._setData.splice(i, 1);
    			return result;
    		}
    	}
    	return null;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // stop all timers
    this.$stopTimers = function $stopTimers() {
    	if (this._newMissionDelayTimer && this._newMissionDelayTimer.isRunning) this._newMissionDelayTimer.stop();
    	delete this._newMissionDelayTimer;
    	if (this._generateMissionTimer && this._generateMissionTimer.isRunning) this._generateMissionTimer.stop();
    	delete this._generateMissionTimer;
    	if (this._createTimer && this._createTimer.isRunning) this._createTimer.stop();
    	delete this._createTimer;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$setWaypoint = function $setWaypoint(position, orientation, code, lbl, debugCode) {
    	system.setWaypoint(
    		this.name + (debugCode !== "" ? "_" + debugCode : ""), position, orientation, {
    			size: 50,
    			beaconCode: code,
    			beaconLabel: lbl
    		}
    	);
    	this._waypointList.push(this.name + (debugCode !== "" ? "_" + debugCode : ""));
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$unsetWaypoint = function $unsetWaypoint(debugCode) {
    	system.setWaypoint(
    		this.name + (debugCode !== "" ? "_" + debugCode : "")
    	);
    	var idx = this._waypointList.indexOf(this.name + (debugCode !== "" ? "_" + debugCode : ""));
    	this._waypointList.splice(idx, 1);
    }
    
    //=============================================================================================================
    // BB interface handlers
    //-------------------------------------------------------------------------------------------------------------
    
    //-------------------------------------------------------------------------------------------------------------
    // moves the mission onto the active list, performs any mission specific controls
    this.$acceptedMission = function $acceptedMission(missID) {
    	function compare(a, b) {
    		return ((a.price > b.price) ? 1 : -1);
    	}
    
    	if (this._debug) log(this.name, "accepted mission id = " + missID);
    
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (!item) {
    		log(this.name, "!!ERROR: BB returned null value from $getItem on mission ID " + missID);
    		return;
    	}
    	this.$updateLastMissionDate(item.source, item.data.missionType);
    	this.$updateGeneralSettings(item);
    
    	if (item.data.missionType === 6 | item.data.missionType === 7) {
    		var ast = worldScripts.GalCopBB_AsteroidFields;
    		// check to see if we've previously added an asteroid field in the destination
    		if (!ast.$systemHasAsteroidField(item.destination, item.data.locationType)) {
    			// if not, tell the asteroid field generator to create one
    			ast.$addAsteroidField(item.destination, 0.1, item.ID, item.data.locationType);
    		}
    	}
    
    	if (this._interstellarMissionTypes.indexOf(item.data.missionType) >= 0 && item.destination >= 0) {
    		if (!missionVariables.GalCopBB_Interstellar) {
    			missionVariables.GalCopBB_Interstellar = "yes"
    			var email = worldScripts.EmailSystem;
    			email.$createEmail({
    				sender: "GalCop Flight School",
    				subject: "Interstellar Tips",
    				date: global.clock.adjustedSeconds + 10, 
    				sentFrom: system.ID,
    				message: expandDescription("[interstellar_tips]"),
    			});
    		}
    	}
    
    	// turn on flag to monitor thargoid ship destruction
    	if (item.data.missionType === 13) {
    		this._thargoidAlloys = true;
    		this._thargoidAlloysMissionID = item.ID;
    	}
    
    	if ([8, 9, 10, 11, 12, 13, 14, 15].indexOf(item.data.missionType) >= 0) {
    		var cm = worldScripts.GalCopBB_CargoMonitor;
    		// for types 8/9, because they can be any cargo, we'll add monitoring to all commodity types
    		if (item.data.missionType === 8 || item.data.missionType === 9) {
    			for (var i = 0; i < cm._commodityIDList.length; i++) {
    				cm.$addMonitor(item.ID, cm._commodityIDList[i], item.destination, false)
    			}
    		} else {
    			// even though the mission details for type 13's is for interstellar space, we actually don't
    			// care where the wreckage is found - system space or interstellar. so long as it comes from a thargoid
    			cm.$addMonitor(item.ID, item.data.commodity, (item.data.missionType === 13 ? 256 : item.destination), (item.data.missionType === 13 ? true : false));
    		}
    	}
    
    	// give player cargo for special delivery mission, but only if they're not in space
    	// if they get here from a secondary mission given in space, they'll need to find their own cargo
    	if (item.data.missionType === 17) {
    		if (player.ship.isInSpace === false) {
    			for (var j = 0; j < item.data.targetQuantity; j++) {
    				// free up space if required
    				if (player.ship.cargoSpaceAvailable === 0 && player.ship.cargoSpaceCapacity >= item.data.targetQuantity) this.$freeCargoSpace(1, item.data.commodity);
    				// add the commodity to the player's ship
    				player.ship.manifest[item.data.commodity] += 1;
    				// reduce the quantity in the station market, if possible
    				if (player.ship.dockedStation.market[item.data.commodity].quantity > 0) {
    					player.ship.dockedStation.setMarketQuantity(item.data.commodity, player.ship.dockedStation.market[item.data.commodity].quantity - 1);
    				}
    			}
    		}
    	}
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // processes the acceptance of a mission via an email
    this.$acceptMissionFromOther = function $acceptMissionFromOther(details) {
    	var cust = details.custom;
    	var extra = "";
    	switch (details.originator) {
    		case "email":
    			extra = expandDescription("[received_via_email]");
    			break;
    		case "ship":
    			extra = expandDescription("[received_via_ship]");
    			break;
    	}
    
    	// process any post status messages and prep the object 
    	var postMsg = [];
    	var rep = worldScripts.GalCopBB_Reputation;
    	for (var m = 1; m <= 3; m++) {
    		var extract = ""
    		switch (m) {
    			case 1:
    				extract = rep.$transformText(expandDescription("[missionType" + details.missionType + "_initiatedMessage]"), details.sourceID, details.destinationID);
    				break;
    			case 2:
    				extract = rep.$transformText(expandDescription("[missionType" + details.missionType + "_completedMessage]"), details.sourceID, details.destinationID);
    				break;
    			case 3:
    				extract = rep.$transformText(expandDescription("[missionType" + details.missionType + "_terminatedMessage]"), details.sourceID, details.destinationID);
    				break;
    		}
    		if (extract !== "") {
    			var brk = extract.split("|");
    			var txt = brk[0];
    			var rt = "item";
    			if (m > 1) rt = "list";
    			var bk = "";
    			var ov = "";
    			var md = "";
    			var mdp = 0;
    			var mds = true;
    			for (var k = 0; k < brk.length; k++) {
    				if (brk[k].indexOf("return:") >= 0) rt = brk[k].split(":")[1];
    				if (brk[k].indexOf("background:") >= 0) bk = brk[k].split(":")[1];
    				if (brk[k].indexOf("overlay:") >= 0) ov = brk[k].split(":")[1];
    				if (brk[k].indexOf("model:") >= 0) md = brk[k].split(":")[1];
    				if (brk[k].indexOf("modelPersonality:") >= 0) mdp = parseInt(brk[k].split(":")[1]);
    				if (brk[k].indexOf("spinModel:") >= 0) mds = brk[k].split(":")[1];
    			}
    			var pmItem = {};
    			pmItem["status"] = (m === 1 ? "initiated" : (m === 2 ? "completed" : "terminated"));
    			pmItem["return"] = rt;
    			pmItem["text"] = txt;
    			if (bk != null && bk !== "") pmItem["background"] = this.$getTexture(bk);
    			if (ov != null && ov !== "") pmItem["overlay"] = this.$getTexture(ov);
    			if (md !== "") pmItem["model"] = md;
    			if (mdp !== 0) pmItem["modelPersonality"] = mdp;
    			if (mds === false) pmItem["spinModel"] = mds;
    			postMsg.push(pmItem);
    		}
    	}
    
    	// ok, time to add the mission to the BB with the accepted flag set to true straight away
    	var dtls = "";
    	var cmdty_unit = this.$getCommodityUnit(details.commodity);
    	if (expandDescription("[missionType" + details.missionType + "_phraseGen]") === "1") {
    		var fn = expandDescription("[missionType" + details.missionType + "_details]");
    		dtls = rep.$transformText(expandDescription(worldScripts.GalCopBB_MissionDetails[fn](), {
    			amount: details.amount,
    			commodity: displayNameForCommodity(details.commodity).toLowerCase(),
    			unit: cmdty_unit,
    			target: details.target,
    			system: details.system,
    			destination: details.destination,
    			expiry: details.expiry,
    			position: details.location,
    			name: details.name
    		}), details.sourceID, details.destinationID);
    	} else {
    		dtls = rep.$transformText(expandDescription("[missionType" + details.missionType + "_details]", {
    			amount: details.amount,
    			commodity: displayNameForCommodity(details.commodity).toLowerCase(),
    			unit: cmdty_unit,
    			target: details.target,
    			system: details.system,
    			destination: details.destination,
    			expiry: details.expiry,
    			position: details.location,
    			name: details.name
    		}), details.sourceID, details.destinationID);
    	}
    
    	var bb = worldScripts.BulletinBoardSystem;
    	var id = bb.$addBBMission({
    		source: details.sourceID,
    		destination: details.destinationID,
    		stationKey: expandDescription("[missionType" + details.missionType + "_stationKeys]"),
    		description: rep.$transformText(details.subject, details.sourceID, details.destinationID),
    		details: dtls + extra,
    		overlay: this.$getTexture(expandDescription("[missionType" + details.missionType + "_bbOverlay]")),
    		payment: details.payment,
    		penalty: details.penalty,
    		deposit: details.deposit,
    		allowPartialComplete: expandDescription("[missionType" + details.missionType + "_partialComplete]"),
    		completionType: details.completionType,
    		stopTimeAtComplete: details.stopTimeAtComplete,
    		accepted: true,
    		noEmails: true,
    		expiry: details.expire,
    		disablePercentDisplay: expandDescription("[missionType" + details.missionType + "_disablePercent]"),
    		customDisplayItems: (cust.length !== 0 ? cust : ""),
    		initiateCallback: "$acceptedMission",
    		completedCallback: "$completedMission",
    		confirmCompleteCallback: "$confirmCompleted",
    		terminateCallback: "$terminateMission",
    		failedCallback: "$failedMission",
    		manifestCallback: "$updateManifestEntry",
    		worldScript: expandDescription("[missionType" + details.missionType + "_mainWorldScript]"),
    		postStatusMessages: postMsg,
    		data: {
    			source: this.name,
    			missionType: details.missionType,
    			locationType: details.locationType,
    			targetQuantity: details.target,
    			quantity: 0,
    			destroyedQuantity: 0,
    			commodity: details.commodityKey,
    			missionChain: details.missionChain,
    			chained: (details.missionChain === "" ? false : true),
    			altManifest: false,
    			name: details.name,
    			assassinChance: details.assassinChance,
    			stolenItemType: details.stolenItemType,
    			targetShipKey: details.targetShipKey,
    			targetShipName: (details.targetShipName ? details.targetShipName : ""),
    			delivered: 0,
    			expected: 0,
    			origSystemID: details.sourceID,
    			satelliteTypes: [],
    			destinationA: details.destinationA
    		}
    	});
    
    	// add this to the active missions list
    	this.$acceptedMission(id);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$terminateMission = function $terminateMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    
    	worldScripts.GalCopBB_CargoMonitor.$removeMonitor(missID);
    
    	// adjust reputation only when the terminatePenalty flag is set to true 
    	if (item.data.terminatePenalty === true) {
    		// update mission history
    		this.$updateFailedHistoryReputation(item);
    	}
    
    	//var sbm = worldScripts.Smugglers_BlackMarket;
    
    	// remove any special equipment provided by the mission
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$failedMission = function $failedMission(missID) {
    	if (!worldScripts.Smugglers_BlackMarket) {
    		// if there's no black market option, just remove the equipment
    		this.$terminateMission(missID);
    		return;
    	}
    
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	// update mission history
    	this.$updateFailedHistoryReputation(item);
    	var eq = "";
    
    	worldScripts.GalCopBB_CargoMonitor.$removeMonitor(missID);
    
    	if (eq != "") this._equipmentFromFailedMissions.push({
    		missionType: item.data.missionType,
    		source: item.source,
    		equip: eq,
    		date: clock.adjustedSeconds
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$checkForFollowupMission = function $checkForFollowupMission(item) {
    	if (item.percentComplete === 1 && this._emailInstalled === true && this._emailMissionAdded === false) {
    		// work out if there might be a followup mission
    		var check = expandDescription("[missionType" + item.data.missionType + "_postMissionChance]");
    		if (check !== "") {
    			var chance = parseFloat(check);
    			check = expandDescription("[missionType" + item.data.missionType + "_postMissionTypes]");
    			if (Math.random() <= chance && check !== "") {
    				var chain = item.data.missionChain;
    				var chainData = chain.split("|");
    				var missionList = [];
    				// OK, time to work out the next mission for the player
    				var getList = check.split(",");
    				for (var i = 0; i < getList.length; i++) {
    					// don't add any mission types already in the chain
    					if (chainData.indexOf(getList[i]) === -1) missionList.push(parseInt(getList[i]));
    				}
    				if (this._debug) log(this.name, "post mission list = " + missionList);
    				if (missionList.length > 0) {
    					this._singleMissionOnly = true;
    					this._storedClientName = item.data.name;
    					var id = this.$addLocalMissions(missionList, item.data.missionType, item.source, chain, true);
    					this._storedClientName = "";
    					this._singleMissionOnly = false;
    					if (id === -2) this._emailMissionAdded = true;
    				}
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$completedMission = function $completedMission(missID) {
    	var p = player.ship;
    	var bb = worldScripts.BulletinBoardSystem;
    	var cm = worldScripts.GalCopBB_CargoMonitor;
    	var item = bb.$getItem(missID);
    	//var payPlayerForCargo = false;
    
    	this.$updateSuccessHistoryReputation(item);
    
    	//var sbm = worldScripts.Smugglers_BlackMarket;
    
    	// if the player is due a refund, pay it now.
    	if (item.data.hasOwnProperty("refund") && item.data.refund > 0) {
    		player.credits += item.data.refund;
    		player.consoleMessage("You have been given a refund of " + formatCredits(item.data.refund, false, true) + ".");
    	}
    
    	if ([6, 7, 8, 9, 10, 11, 12, 13, 14, 15].indexOf(item.data.missionType) >= 0) {
    		var amt = cm.$countCargoForMission(missID);
    		for (var i = 1; i <= amt; i++) {
    			var cargo = cm.$removeCargoForMission(missID, 1);
    			if (cargo.commodity != "") {
    				//log(this.name, "removing from player ship");
    				p.manifest[cargo.commodity] -= cargo.quantity;
    			}
    		}
    	}
    
    	// *** type 6/7 - recover cargo - remove cargo from hold and reward player 
    	/*if (item.data.missionType === 6 || item.data.missionType === 7) {
    		p.manifest[item.data.commodity] -= item.data.expected;
    		//if (payPlayerForCargo === true) player.credits += (p.dockedStation.market[item.data.commodity].price / 10) * item.data.expected;
    	}*/
    	// *** type 8/9 - steal any cargo - remove cargo from hold and reward player
    	// possible issue: if player scooped one commodity, but then sold it/lost it
    	// should still be ok, as routine will check if player has enough of that cargo to remove
    	/*if (item.data.missionType === 8 || item.data.missionType === 9) {
    		if (item.data.cargoRecord && item.data.cargoRecord.length > 0) {
    			for (var i = 0; i < item.data.cargoRecord.length; i++) {
    				var c = item.data.cargoRecord[i];
    				if (p.manifest[c.commodity] >= c.amount) {
    					p.manifest[c.commodity] -= c.amount;
    					if (payPlayerForCargo === true) player.credits += (p.dockedStation.market[c.commodity].price / 10) * c.amount;
    				}
    			}
    		}
    	}*/
    	// *** type 10 - steal specific cargo - remove cargo from hold and reward player
    	// *** type 11 - steal specific cargo - remove cargo from hold and reward player
    	// *** type 13 - collect thargoid alloys - remove cargo from hold and reward player
    	// *** type 14 - gun runner - remove cargo from hold and reward player
    	// *** type 15 - drug dealer - remove cargo from hold and reward player
    	/*if (item.data.missionType === 10 || item.data.missionType === 11 || item.data.missionType === 13 || item.data.missionType === 14 || item.data.missionType === 15) {
    		p.manifest[item.data.commodity] -= item.data.quantity;
    		//if (payPlayerForCargo === true) player.credits += (p.dockedStation.market[item.data.commodity].price / 10) * item.data.quantity;
    	}*/
    	// remove any special equipment provided by the mission
    
    	cm.$removeMonitor(missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // checks that a mission has been completed
    this.$confirmCompleted = function $confirmCompleted(missID) {
    	var p = player.ship;
    	var m = p.manifest;
    	var result = "";
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (item) {
    		// *** type 6/7 - recover cargo
    		if (item.data.missionType === 6 || item.data.missionType === 7) {
    			// check the player has the correct amount of the commodity
    			if (m[item.data.commodity] < item.data.expected) {
    				var unit = this.$getCommodityUnit(item.data.commodity);
    				result += (result === "" ? "" : "\n") + "Insufficient amount of " + displayNameForCommodity(item.data.commodity).toLowerCase() + " - expected " + item.data.expected + " " + unit;
    			}
    		}
    		// *** type 12 - free the slaves
    		if (item.data.missionType === 12) {
    			if (item.data.quantity < item.data.targetQuantity) {
    				result += (result === "" ? "" : "\n") + "Insufficient number of slaves released to Amnesty Intergalactic - short by " + (item.data.targetQuantity - item.data.quantity) + "t";
    			}
    		}
    	}
    	return result;
    }
    
    //--------------------------------------------------------------------------------------------------------------
    // endpoint for custom menu items
    // handles case when player wants to hand in a partial amount of mission (cargo) but keep mission open so they can keep going
    this.$partialComplete = function $partialComplete(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var cm = worldScripts.GalCopBB_CargoMonitor;
    	var amt = cm.$countCargoForMission(missID);
    	//log(this.name, "amt = " + amt);
    
    	if (amt > 0 && (item.data.targetQuantity - item.data.quantity) >= 0) {
    		if (amt > (item.data.targetQuantity - item.data.quantity)) amt = (item.data.targetQuantity - item.data.quantity);
    		item.data.quantity += amt;
    		for (var i = 1; i <= amt; i++) {
    			var cargo = cm.$removeCargoForMission(missID, 1);
    			if (cargo.commodity != "") {
    				//log(this.name, "removing from player ship");
    				player.ship.manifest[cargo.commodity] -= cargo.quantity;
    			}
    		}
    		bb.$updateBBMissionPercentage(item.ID, ((item.data.quantity < item.data.targetQuantity ? item.data.quantity : item.data.targetQuantity) / item.data.targetQuantity));
    	}
    	// add a new custom menu (same as the old one which will be autoremoved)
    	// but only if there is a possibility the player could do another partial complete
    	if ((item.data.targetQuantity - item.data.quantity) >= 1) {
    		var mnu = expandDescription("[missionType" + item.data.missionType + "_customMenu]");
    		var itms = mnu.split("|");
    		item.customMenuItems.push({
    			text: itms[0],
    			worldScript: itms[1],
    			callback: itms[2],
    			condition: itms[3],
    			autoRemove: (this._trueValues.indexOf(itms[4]) >= 0 ? true : false)
    		});
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // checks if the partial load and continue menu can be selected by the player
    this.$partialCondition = function $partialCondition(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var cm = worldScripts.GalCopBB_CargoMonitor;
    	var amt = cm.$countCargoForMission(missID);
    	var result = "";
    	if (amt === 0) result = "No " + (item.data.missionType === 13 ? "Thargoid wreckage" : (item.data.missionType === 8 || item.data.missionType === 9 ? "pirated cargo" : displayNameForCommodity(item.data.commodity).toLowerCase())) + " in cargo hold";
    	if (item.completionType == "AT_SOURCE" && item.source != system.ID) result = "Return to originating system for cargo return";
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateManifestEntry = function $updateManifestEntry(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (item) {
    		var exp_text = bb.$getTimeRemaining(item.expiry);
    		if (exp_text.toLowerCase().indexOf("expired") >= 0) exp_text = "*no time*";
    		var rep = worldScripts.GalCopBB_Reputation;
    		var cm = worldScripts.GalCopBB_CargoMonitor;
    
    		var unit = this.$getCommodityUnit(item.data.commodity);
    		// is this mission finished?
    		if (item.data.quantity === (item.data.targetQuantity - item.data.destroyedQuantity) && item.data.targetQuantity > 0) {
    			var altMan = "";
    			// are use using an alternate manifest text entry
    			if (item.data.altManifest === true) altMan = "_alt";
    
    			// update the manifest
    			bb.$updateBBManifestText(
    				missID,
    				rep.$transformText(expandDescription("[missionType" + item.data.missionType + altMan + "_finishedManifest]", {
    					system: System.systemNameForID(item.source),
    					expiry: exp_text
    				}), item.source, item.destination)
    			);
    			// update the status text as well
    			// if we're using the alternate manifest, check to see if there is alternate status
    			// if not, just switch back to the normal manifest
    			if (altMan === "_alt" && expandDescription("[missionType" + item.data.missionType + "_alt_finishedStatus]", {
    					system: ""
    				}) === "") altMan = "";
    			bb.$updateBBStatusText(
    				missID,
    				rep.$transformText(expandDescription("[missionType" + item.data.missionType + altMan + "_finishedStatus]", {
    					system: System.systemNameForID(item.source)
    				}), item.source, item.destination)
    			);
    		} else {
    			// so this is an incomplete mission
    			// update the manifest text
    			var text = rep.$transformText(expandDescription("[missionType" + item.data.missionType + "_manifest]", {
    				quantity: item.data.quantity,
    				target: item.data.targetQuantity - item.data.destroyedQuantity,
    				remaining: (item.data.targetQuantity - item.data.destroyedQuantity) - item.data.quantity,
    				hold: cm.$countCargoForMission(item.ID),
    				commodity: displayNameForCommodity(item.data.commodity).toLowerCase(),
    				unit: unit,
    				destination: bb.$systemNameForID(item.destination),
    				destinationA: (item.data.hasOwnProperty("destinationA") ? bb.$systemNameForID(item.data.destinationA) : ""),
    				position: (item.data.hasOwnProperty("locationType") ? expandDescription("[gcm_position_" + item.data.locationType + "_short]") : ""),
    				system: System.systemNameForID(item.source),
    				expiry: exp_text,
    				stage: (item.data.hasOwnProperty("stage") ? item.data.stage : "0")
    			}), item.source, item.destination);
    			// check to see if there's a multi-stage mission text to pick up
    			if (text.indexOf("_stage_") >= 0) {
    				text = rep.$transformText(expandDescription("[" + text + "]", {
    					quantity: item.data.quantity,
    					target: item.data.targetQuantity - item.data.destroyedQuantity,
    					remaining: (item.data.targetQuantity - item.data.destroyedQuantity) - item.data.quantity,
    					hold: cm.$countCargoForMission(item.ID),
    					commodity: displayNameForCommodity(item.data.commodity).toLowerCase(),
    					unit: unit,
    					destination: bb.$systemNameForID(item.destination),
    					destinationA: (item.data.hasOwnProperty("destinationA") ? bb.$systemNameForID(item.data.destinationA): ""),
    					position: (item.data.hasOwnProperty("locationType") ? expandDescription("[gcm_position_" + item.data.locationType + "_short]") : ""),
    					system: System.systemNameForID(item.source),
    					expiry: exp_text,
    					stage: item.data.stage
    				}), item.source, item.destination);
    			}
    			bb.$updateBBManifestText(item.ID, (missID === this._pendingMissionID ? "(Pending) " : "") + text);
    
    			// update the status text as well
    			text = rep.$transformText(expandDescription("[missionType" + item.data.missionType + "_status]", {
    				quantity: item.data.quantity,
    				target: item.data.targetQuantity - item.data.destroyedQuantity,
    				remaining: (item.data.targetQuantity - item.data.destroyedQuantity) - item.data.quantity,
    				hold: cm.$countCargoForMission(item.ID),
    				commodity: displayNameForCommodity(item.data.commodity).toLowerCase(),
    				unit: unit,
    				destination: bb.$systemNameForID(item.destination),
    				destinationA: (item.data.hasOwnProperty("destinationA") ? bb.$systemNameForID(item.data.destinationA) : ""),
    				system: System.systemNameForID(item.source),
    				stage: (item.data.hasOwnProperty("stage") ? item.data.stage : "0")
    			}), item.source, item.destination);
    			// check to see if there's a multi-stage mission text to pick up
    			if (text.indexOf("_stage_") >= 0) {
    				text = rep.$transformText(expandDescription("[" + text + "]", {
    					quantity: item.data.quantity,
    					target: item.data.targetQuantity - item.data.destroyedQuantity,
    					remaining: (item.data.targetQuantity - item.data.destroyedQuantity) - item.data.quantity,
    					hold: cm.$countCargoForMission(item.ID),
    					commodity: displayNameForCommodity(item.data.commodity).toLowerCase(),
    					destination: bb.$systemNameForID(item.destination),
    					destinationA: (item.data.destinationA ? bb.$systemNameForID(item.data.destinationA) : ""),
    					system: System.systemNameForID(item.source),
    					stage: item.data.stage
    				}), item.source, item.destination);
    			}
    			bb.$updateBBStatusText(item.ID, (missID === this._pendingMissionID ? "(Pending) " : "") + text);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // accept the currently pending mission
    this.$acceptPendingMission = function $acceptPendingMission() {
    	// convert the "pending" to "active"
    	var missID = this._pendingMissionID;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (item.data.missionType === 42) {
    		worldScripts.GalCopBB_Delivery.$acceptPendingMission(item);
    	}
    
    	// flag the old mission as having been chained (if there is an old mission)
    	var oldItem = bb.$getItem(this._pendingMissionOrigID);
    	if (oldItem != null) oldItem.data.chained = true;
    
    	// reset the pending mission variables
    	this._pendingMissionID = -1;
    	this.$updateManifestEntry(missID);
    	// run the callback function, if it's been set
    	if (this._pendingMissionCallback != null) this._pendingMissionCallback();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // declines the currently pending mission
    this.$declinePendingMission = function $declinePendingMission() {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(this._pendingMissionOrigID);
    
    	if (item) {
    		// if we didn't get a secondary mission, update the original mission to be "stopTimeAtComplete" = false
    		// but only if the original mission didn't have a "stopTimeAtComplete" flag set already
    		var missType = item.data.missionType;
    		var checkType = expandDescription("[missionType" + missType + "_completionType]");
    		if (item.stopTimeAtComplete === true && checkType.indexOf("|1") === -1) {
    			item.stopTimeAtComplete = false;
    
    			// also, set a new expiry time to be the distance to the source system + 10 minutes
    			var info = System.infoForSystem(galaxyNumber, item.source);
    			var route = system.info.routeToSystem(info);
    			item.expiry = clock.adjustedSeconds + ((route.time * 3600) + 600);
    			item.expiry = clock.adjustedSeconds + ((route.time * 3600) + 600);
    
    			// switch to the alternate manifest entry
    			item.data.altManifest = true;
    		}
    	}
    
    	var pendItem = bb.$getItem(this._pendingMissionID);
    	// remove email evidence of the pended acceptance
    	if (pendItem && pendItem.lastEmailID) {
    		var msgID = pendItem.lastEmailID;
    		worldScripts.EmailSystem.$deleteSelectedItem(msgID);
    	}
    	
    	// delete the mission from the BB and from local data
    	bb.$removeBBMission(this._pendingMissionID);
    	bb.$refreshManifest();
    	this._requestSpecialCargo = "";
    	this._pendingMissionID = -1;
    	// run the callback function, if it's been set
    	if (this._pendingMissionCallback != null) this._pendingMissionCallback();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // adds local missions to be bulletin board
    // this is the main function for generating missions
    // also controls adding secondary missions if the overrideMissionType, originalMissionType, original source are passed
    this.$addLocalMissions = function $addLocalMissions(overrideMissionTypes, originalMissionType, originalSource, missChain, postMission) {
    	function compareDist(a, b) {
    		var rt1 = system.info.routeToSystem(a);
    		var rt2 = system.info.routeToSystem(b);
    		if (rt1 && rt2) {
    			if (rt1.distance < rt2.distance) {
    				return -1;
    			} else {
    				return 1;
    			}
    		} else {
    			if (rt1) return 1;
    			if (rt2) return -1;
    		}
    	}
    
    	var sliceAmt = 15;
    	// don't add missions in interstellar space
    	if (system.ID === -1 && overrideMissionTypes == null) return -1;
    	// don't add missions in nova systems
    	if (system.sun.hasGoneNova === true) return -1;
    
    	var src = system.ID;
    	if (originalSource && originalSource >= 0 && originalSource <= 255) src = originalSource;
    	var srcSystem = System.infoForSystem(galaxyNumber, src);
    
    	// make sure the player hasn't done something to warrant suppressing missions in this system
    	if (originalMissionType === "" && worldScripts.GalCopBB_HitTeams.$areMissionsSuppressed(src) === true) return -1;
    
    	var missTypes = this._availableMissionTypes;
    	if (overrideMissionTypes && overrideMissionTypes.length > 0) {
    		missTypes = overrideMissionTypes;
    		// shuffle order so it isn't always the same variant being checked first
    		missTypes.sort(function (a, b) {
    			return Math.random() - 0.5;
    		}); 
    	}
    
    	if (originalMissionType == null) originalMissionType = "";
    
    	if (missChain == null || missChain === "") missChain = "";
    
    	var endPoint = missTypes.length;
    	if (!overrideMissionTypes) {
    		if (this._loopPoint1 === -1) {
    			this._loopPoint1 = 0;
    		} else {
    			this._loopPoint1 += sliceAmt;
    		}
    		endPoint = this._loopPoint1 + sliceAmt;
    		if (endPoint > missTypes.length) endPoint = missTypes.length;
    	} else {
    		this._loopPoint1 = 0;
    	}
    	var bb = worldScripts.BulletinBoardSystem;
    
    	// add missions to list
    	for (var mt = this._loopPoint1; mt < endPoint; mt++) {
    		var selectedMissionType = missTypes[mt];
    		// if we hit a "0" mission type, just abort straight away - no mission to create
    		if (selectedMissionType === 0) return -1;
    
    		var mainWS = expandDescription("[missionType" + selectedMissionType + "_mainWorldScript]");
    		var conditions = expandDescription("[missionType" + selectedMissionType + "_conditions]");
    		// check the conditions for this mission
    		var testResult = this.$testMissionConditions(conditions, src, selectedMissionType, -1, (originalMissionType === "" ? false : true));
    		if (testResult === true && originalMissionType !== "") {
    			// force a check of mission availability items (which are normally just checked by the BB itself)
    			// however, we don't want to offer the player a secondary mission which they can't actually complete
    			var checkAvailability = worldScripts[mainWS].$missionAvailability(-1, selectedMissionType, src);
    			if (checkAvailability !== "") testResult = false;
    		}
    		if (testResult === true) {
    			// get interstellar range for this mission
    			var rng = parseInt(expandDescription("[missionType" + selectedMissionType + "_destRange]"));
    			//log(this.name, "check destination range: " + rng);
    			if (rng === 0) {
    				// a range value of 0 means it's just the local system
    				var sys = [];
    				sys.push(srcSystem);
    			} else if (rng > 0 && rng < 256) {
    				var sys = system.info.systemsInRange(rng);
    				for (var i = sys.length - 1; i >= 0; i--) {
    					//log(this.name, "checking against " + sys[i].systemID + " - " + sys[i].name + ": " + system.info.distanceToSystem(sys[i]));
    					var chkRt = system.info.routeToSystem(sys[i]);
    					// remove any unreachable systems or systems whose actual route distance is greater than the requirement
    					if (chkRt == null || chkRt.distance > rng)
    						sys.splice(i, 1);
    					// remove any systems that don't meet conditions
    					else if (this.$testMissionConditions(expandDescription("[missionType" + selectedMissionType + "_destConditions]"), sys[i].systemID, selectedMissionType, src, (originalMissionType === "" ? false : true)) === false) {
    						sys.splice(i, 1);
    					} else if (sys[i].systemID === src) {
    						// remove the current system if it's there
    						sys.splice(i, 1);
    					}
    				}
    
    				// if we didn't find any possible destinations, continue
    				if (sys == null || sys.length === 0) {
    					if (this._debug) log(this.name, "No systems found to generate mission type " + selectedMissionType);
    					continue;
    				}
    
    				// get a random sequence of index values, so we don't end up getting the same planets coming up first
    				sys.sort(function (a, b) {
    					return Math.random() - 0.5;
    				});
    			} else if (rng === -1) {
    				var sys = [];
    				sys.push({
    					systemID: -1,
    					name: "Interstellar space"
    				});
    			} else {
    				var sys = [];
    				sys.push({
    					systemID: 256,
    					name: "No fixed destination"
    				});
    			}
    
    			var sysSelectType = expandDescription("[missionType" + selectedMissionType + "_systemSelectProcess]");
    			switch (sysSelectType) {
    				case "random":
    					var l_start = 0;
    					var l_end = 1;
    					var choice = Math.floor(Math.random() * sys.length);
    					break;
    				case "multipath": // trip to destination has multiple paths of different lengths
    					var l_start = 0;
    					var l_end = -1;
    					var choice = -1;
    					for (var i = 1; i < sys.length; i++) {
    						var route1 = system.info.routeToSystem(sys[i], "OPTIMIZED_BY_JUMPS");
    						var route2 = system.info.routeToSystem(sys[i], "OPTIMIZED_BY_TIME");
    						if ((route1.time - route2.time) > 2) {
    							choice = i;
    							l_start = 0;
    							l_end = 1;
    							break;
    						}
    					}
    					break;
    				case "sequential":
    					var l_start = 0;
    					var l_end = sys.length;
    					var choice = 0; // we'll pick it up in the loop
    					break;
    				case "source":
    					var l_start = 0;
    					var l_end = 1;
    					var choice = 0; // only one to select from
    					break;
    				case "closest":
    					sys.sort(compareDist);
    					var l_start = 0;
    					var l_end = sys.length;
    					var choice = 0;
    					break;
    			}
    
    			for (var i = l_start; i < l_end; i++) {
    				if (sysSelectType === "sequential") choice = i;
    
    				// get a systemInfo object of the destination system
    				var dest = sys[choice];
    				if (dest == null) continue; // in case this ever happens...
    
    				// get the route information from the current system to the destination system
    				if (dest.systemID >= 0 && dest.systemID <= 255) {
    					var route = system.info.routeToSystem(dest, "OPTIMIZED_BY_JUMPS");
    					if (route == null) continue; // if there's no route to the system
    				}
    
    				// ship, email or bb?
    				// ship means mission was initiated from a NPC ship, but not as a secondary mission
    				// email means mission was initiated from an email sent to the player
    				// bb means mission was initiated from the BB
    				var method = expandDescription("[missionType" + selectedMissionType + "_primaryMethod]");
    				if (postMission && postMission === true) {
    					method = "email";
    				}
    				if (this._debug) log(this.name, "missType selected = " + selectedMissionType + ", originalMissionType = " + originalMissionType + ", method = " + method);
    
    				// any secondary mission can only be created as if it was from the BB
    				if ((originalMissionType != "" && (!postMission || postMission === false)) && method != "bb") method = "bb";
    
    				// are multiple methods defined?
    				if (method.indexOf(",") >= 0) {
    					// pick a random one from the list
    					var methodItems = method.split(",");
    					method = methodItems[Math.floor(Math.random() * methodItems.length)];
    					//} else {
    					//	var methodItems = [];
    					//	methodItems.push(method);
    				}
    
    				var cust = [];
    				var rtime = 0;
    				var rdist = 0;
    				var s_type = "0";
    				var completeType = expandDescription("[missionType" + selectedMissionType + "_completionType]");
    				if (this._debug) log(this.name, "missType selected = " + selectedMissionType + ", completeType = " + completeType);
    				// there should be the same number of items in completeType as for method - select the matching item
    				if (completeType.indexOf(",") >= 0) {
    					var compTypeItems = completeType.split(",");
    					completeType = compTypeItems[methodItems.indexOf(method)];
    				}
    
    				if (completeType.indexOf("|") >= 0) {
    					// check the completion type for a change to the stopTime flag
    					s_type = completeType.split("|")[1];
    					completeType = completeType.split("|")[0];
    				}
    
    				if (completeType === "AT_SOURCE" || completeType === "WHEN_DOCKED_SOURCE") {
    					cust.push({
    						heading: "To complete:",
    						value: "Return to station in originating system"
    					});
    				}
    				if (completeType === "AT_STATIONKEY" || completeType === "WHEN_DOCKED_STATIONKEY") {
    					cust.push({
    						heading: "To complete:",
    						value: "Return to any equivalent station"
    					});
    				}
    
    				// calculate our time and distance to destination and back to source (if required)
    				if (route) {
    					if (s_type === "0" && (completeType === "AT_SOURCE" || completeType === "WHEN_DOCKED_SOURCE")) {
    						// time includes round trip
    						rtime = route.time * 3600 * 2;
    						// plus 30 minutes transit time in each system
    						rtime += (route.route.length * this._transitTime) * 2;
    						rdist = route.distance * 2;
    						if (src !== system.ID) {
    							// trip out and back might not be the same
    							// get system info object of original source system
    							var sys2 = srcSystem;
    							// get the route from the original source system to the new destination system
    							var route2 = sys2.routeToSystem(dest, "OPTIMIZED_BY_JUMPS");
    							rtime = (route.time + route2.time) * 3600;
    							rtime += (route.route.length * this._transitTime) + (route2.route.length * this._transitTime);
    							rdist = route.distance + route2.distance;
    						}
    					} else {
    						// time is one way only
    						rtime = route.time * 3600;
    						// plus 30 minutes transit time in each system
    						rtime += route.route.length * this._transitTime;
    						rdist = route.distance;
    					}
    				} else {
    					rtime = 24 * 3600 + this._transitTime;
    					rdist = -1;
    				}
    				// if we're adding missions after a witchspace jump, add the extra time now
    				rtime += this._initialTime;
    
    				var stnKey = expandDescription("[missionType" + selectedMissionType + "_stationKeys]");
    				// use first reputation entity for controlling client names
    				var repEntity = expandDescription("[missionType" + selectedMissionType + "_reputationEntities]");
    				var repSel = repEntity;
    				if (repEntity.indexOf(",") !== -1) repSel = repEntity.split(",")[0];
    
    				var locType = 0;
    				var selectedCmdty = "";
    				var pen = 0;
    				var qty = 0;
    				var amt = 0;
    				var expire = 0;
    				var dpst = 0;
    				var destA = -1;
    				var satType = 0;
    				var values = {};
    				var clientName = worldScripts.GalCopBB_Reputation.$getClientName(src, repSel);
    				var ingr = [];
    
    				// check for an interstellar type of mission
    				var interstellarWarning = "";
    				if (this._interstellarMissionTypes.indexOf(selectedMissionType) >= 0 && dest.systemID >= 0) {
    					cust.push({
    						heading: "Interstellar space target:",
    						value: "**Yes**"
    					});
    					interstellarWarning = expandDescription("[interstellar_warning]", {
    						system: srcSystem.name,
    						destination: dest.name
    					});
    				}
    
    				// mission specific config
    				var ws = worldScripts[expandDescription("[missionType" + selectedMissionType + "_valuesCallbackWS]")];
    				if (ws) {
    					values = ws[expandDescription("[missionType" + selectedMissionType + "_valuesCallbackFN]")](this._workTime, rtime, rdist, dest);
    					// if we didn't get a result back, there must be an error, so just continue with the next mission type
    					if (values == null) continue;
    					qty = values.quantity;
    					if (values.hasOwnProperty("price") === true) amt = values.price;
    					expire = values.expiry;
    					if (values.hasOwnProperty("penalty") === true) pen = values.penalty;
    					if (values.hasOwnProperty("locationType") === true) locType = values.locationType;
    					if (values.hasOwnProperty("commodity") === true) selectedCmdty = values.commodity;
    					if (values.hasOwnProperty("deposit") === true) dpst = values.deposit;
    					if (values.hasOwnProperty("clientName") === true) clientName = values.clientName;
    					if (values.hasOwnProperty("donation") === true) cust.push({
    						heading: "Donation amount:",
    						value: formatCredits(values.donation, false, true)
    					});
    					if (values.hasOwnProperty("ingredients") === true) ingr = values.ingredients;
    					if (values.hasOwnProperty("satelliteTypes") === true) satType = values.satelliteTypes;
    					if (values.hasOwnProperty("destinationA") === true) destA = values.destinationA;
    				} else {
    					log(this.name, "!!ERROR: No value callback found for mission type " + selectedMissionType);
    					continue;
    				}
    
    				// pull up our stored client name if set
    				if (this._storedClientName !== "") clientName = this._storedClientName;
    
    				if (this._debug) log(this.name, "stationkeys = " + stnKey + ", locType = " + locType);
    
    				// get the players reputation for this mission type in this system and chart-wide
    				var rep_factor = (this.$playerMissionReputation(src, selectedMissionType) + (this.$playerMissionReputation(-1, selectedMissionType) * (this.$playerRank() / 8)));
    				// turn off rep factor if the result would be less than any deposit amount
    				if (dpst != 0 && amt * rep_factor < dpst) rep_factor = 0;
    				if (rep_factor != 0) amt = Math.floor(amt * rep_factor);
    
    				// when in override mode, add new missions as accepted automatically
    				var autoAccept = false;
    				if (originalMissionType !== "" && (!postMission || postMission === false)) autoAccept = true;
    
    				// set the the possibility of assassins with this mission
    				var assassins = 0.0;
    				assassins = parseFloat(expandDescription("[missionType" + selectedMissionType + "_assassinChance]"));
    
    				var risk = "";
    				if (assassins > 0 && assassins <= 0.3) risk = "Low";
    				if (assassins > 0.3 && assassins <= 0.6) risk = "Medium";
    				if (assassins > 0.6) risk = "High";
    
    				cust.push({
    					heading: "Client name:",
    					value: clientName
    				});
    				if (risk !== "") cust.push({
    					heading: "Danger level:",
    					value: risk
    				});
    
    				// add equipment requirements
    				if (conditions.indexOf("include_equipment") >= 0) {
    					var condList = conditions.split(",");
    					var eqList = condList[this.$getConditionIndex(condList, "include_equipment")].split(":")[1].split("|");
    					if (eqList && eqList.length > 0) {
    						var eqText = "";
    						for (var j = 0; j < eqList.length; j++) {
    							var ref = eqList[j];
    							if (this._equipmentProvidingLookup[ref]) ref = this._equipmentProvidingLookup[ref];
    							var eqItem = EquipmentInfo.infoForKey(ref);
    							if (!eqItem) {
    								log(this.name, "!!ERROR: Eq item not found = " + ref);
    							} else {
    								if (eqText != "") eqText += "\n";
    								if (ref === "EQ_PASSENGER_BERTH") {
    									eqText += eqItem.name.substring(0, eqItem.name.indexOf(" - ")).trim();
    								} else {
    									eqText += eqItem.name;
    								}
    							}
    						}
    						cust.push({
    							heading: "Equipment required:",
    							value: eqText
    						});
    					}
    				}
    
    				// special cases
    				var type53Menu = null;
    				switch (selectedMissionType) {
    					case 33:
    						// we're going to put some values into mission variables here, so the expandDesc function will include the data
    						missionVariables.stolenItemType = expandDescription("[gcm_stolen_item_types]");
    						missionVariables.targetShipKey = this.$pickGetawayShip();
    						var shipspec = Ship.shipDataForKey(missionVariables.targetShipKey);
    						missionVariables.targetShipType = shipspec.name;
    						break;
    					case 53:
    						type53Menu = [];
    						for (var i = 0; i < ingr.length; i++) {
    							missionVariables["ingredient" + (i + 1)] = expandDescription("[gcm_ingredient_" + ingr[i] + "]");
    							type53Menu.push({
    								text: expandDescription("[gcm_ingredient_" + ingr[i] + "_menu]"),
    								worldScript: mainWS,
    								callback: "$type53_collect_" + ingr[i],
    								condition: "$type53_confirm_" + ingr[i],
    								autoRemove: true
    							});
    						}
    						break;
    					case 150:
    						missionVariables.targetShipName = worldScripts.GalCopBB_MissionDetails.$shipName();
    						break;
    				}
    
    				var rep = worldScripts.GalCopBB_Reputation;
    				switch (method) {
    					case "ship":
    						// because some mission conditions don't get checked on BB missions, we need to redo the checks to include them
    						var secondCheck = this.$testMissionConditions(conditions, src, selectedMissionType, dest.systemID, true);
    						if (secondCheck === false) continue;
    
    						var id = -1;
    						var ws = expandDescription("[missionType" + selectedMissionType + "_shipCallbackWS]");
    						var fn = expandDescription("[missionType" + selectedMissionType + "_shipCallbackFN]");
    
    						var w = worldScripts[ws];
    						if (w) {
    							// we're putting a lot of info in the payload, so the mission can be fully configured when it's transferred from another ship to the player
    							var payload = {
    								missionType: selectedMissionType,
    								//originalType:selectedMissionType,
    								originator: "ship",
    								originalMissionType: originalMissionType,
    								amount: formatCredits(amt, false, true),
    								payment: amt,
    								penalty: 0,
    								deposit: 0,
    								commodity: displayNameForCommodity(selectedCmdty).toLowerCase(),
    								commodityKey: selectedCmdty,
    								target: qty,
    								system: srcSystem.name,
    								sourceID: src,
    								destination: dest.name,
    								destinationID: dest.systemID,
    								destinationA: destA,
    								destinationAName: System.systemNameForID(destA),
    								expire: expire,
    								expiry: bb.$getTimeRemaining(expire),
    								completionType: completeType,
    								stopTimeAtComplete: (s_type === "1" ? true : false),
    								terminatePenalty: false, // missions added via ship-to-ship are auto-accepted, so no penalty on terminate
    								risk: risk.toLowerCase(),
    								assassinChance: assassins,
    								location: expandDescription(this._positions[locType]),
    								locationType: locType,
    								stolenItemType: missionVariables.stolenItemType,
    								targetShipKey: missionVariables.targetShipKey,
    								targetShipName: missionVariables.targetShipName,
    								custom: cust,
    								missionChain: missChain + (missChain === "" ? "" : "|") + originalMissionType,
    								subject: expandDescription("[missionType" + selectedMissionType + "_description]"),
    								name: clientName
    							};
    
    							// execute the function with the mission data payload
    							w[fn](payload);
    
    						}
    						break;
    					case "email":
    						// if the primary delivery mode is set to email, make sure it's installed, otherwise just skip this mission
    						if (this._emailInstalled === false) continue;
    						var email = worldScripts.EmailSystem;
    
    						// because some mission conditions don't get checked on BB missions, we need to redo the checks to include them
    						var secondCheck = this.$testMissionConditions(conditions, src, selectedMissionType, dest.systemID, true);
    						if (this._debug) log(this.name, "second check result = " + secondCheck);
    						if (secondCheck === false) continue;
    
    						var id = -2;
    
    						var accept = expandDescription("[missionType" + selectedMissionType + "_emailAccept]", {
    							name: clientName
    						});
    						if (accept === "") accept = expandDescription("[gcm_email_accept]", {
    							name: clientName
    						});
    						var reject = expandDescription("[missionType" + selectedMissionType + "_emailReject]", {
    							name: clientName
    						});
    						if (reject === "") reject = expandDescription("[gcm_email_reject]", {
    							name: clientName
    						});
    						var expiry = expandDescription("[missionType" + selectedMissionType + "_emailExpiry]");
    						if (expiry === "") expandDescription("[gcm_email_expiry]");
    						var emailSubject = expandDescription("[missionType" + selectedMissionType + "_emailSubject]");
    
    						var lastMissionTxt = "";
    						if (postMission && postMission === true) {
    							lastMissionTxt = expandDescription("[missionType" + originalMissionType + "_emailLastMissionText]");
    						}
    
    						// we're putting a lot of info in the payload, so the mission can be fully configured when it's accepted through the email system
    						var payload = {
    							missionType: selectedMissionType,
    							originalMissionType: originalMissionType,
    							originator: "email",
    							amount: formatCredits(amt, false, true),
    							payment: amt,
    							penalty: 0,
    							deposit: 0,
    							commodity: displayNameForCommodity(selectedCmdty).toLowerCase(),
    							commodityKey: selectedCmdty,
    							target: qty,
    							system: srcSystem.name,
    							sourceID: src,
    							destination: dest.name,
    							destinationID: dest.systemID,
    							destinationA: destA,
    							destinationAName: System.systemNameForID(destA),
    							expire: expire,
    							expiry: bb.$getTimeRemaining(expire),
    							completionType: completeType,
    							stopTimeAtComplete: (s_type === "1" ? true : false),
    							terminatePenalty: true, // emails have an accept/reject method, so its the same as for BB items
    							risk: risk.toLowerCase(),
    							assassinChance: assassins,
    							location: expandDescription(this._positions[locType]),
    							locationType: locType,
    							position: expandDescription(this._positions[locType]),
    							stolenItemType: missionVariables.stolenItemType,
    							targetShipKey: missionVariables.targetShipKey,
    							targetShipName: missionVariables.targetShipName,
    							custom: cust,
    							subject: emailSubject,
    							name: clientName,
    							missionChain: missChain + (missChain === "" ? "" : "|") + originalMissionType,
    							lastMissionText: lastMissionTxt,
    						};
    						// we aren't defining a rejection callback - the mission isn't registered at this point so there is nothing to reject.
    						email.$createEmail({
    							sender: clientName,
    							subject: emailSubject,
    							date: global.clock.adjustedSeconds + (postMission && postMission === true ? 10 : 0), //(3600 * parseInt(Math.Random() * 40) + 8) : 0), // put post mission emails in the future
    							sentFrom: (dest.systemID >= 0 && dest.systemID <= 255 ? dest.systemID : src),
    							message: expandDescription("[missionType" + selectedMissionType + "_email]", payload),
    							expiryDate: expire,
    							allowExpiryCancel: false,
    							expiryText: expiry,
    							option1: {
    								display: accept.split("|")[0],
    								reply: accept.split("|")[1],
    								script: "GalCopBB_Missions",
    								callback: "$acceptMissionFromOther",
    								parameter: payload
    							},
    							option2: {
    								display: reject.split("|")[0],
    								reply: reject.split("|")[1]
    							}
    						});
    
    						break;
    					case "bb":
    					default:
    						// process any post status messages and prep the object 
    						var postMsg = [];
    						for (var m = 1; m <= 3; m++) {
    							var extract = ""
    							switch (m) {
    								case 1:
    									extract = rep.$transformText(expandDescription("[missionType" + selectedMissionType + "_initiatedMessage]"), src, dest.systemID);
    									break;
    								case 2:
    									extract = rep.$transformText(expandDescription("[missionType" + selectedMissionType + "_completedMessage]"), src, dest.systemID);
    									break;
    								case 3:
    									extract = rep.$transformText(expandDescription("[missionType" + selectedMissionType + "_terminatedMessage]"), src, dest.systemID);
    									break;
    							}
    							if (extract !== "") {
    								var brk = extract.split("|");
    								var txt = brk[0];
    								var rt = "item";
    								if (m > 1) rt = "list";
    								var bk = "";
    								var ov = "";
    								var md = "";
    								var mdp = 0;
    								var mds = true;
    								for (var k = 0; k < brk.length; k++) {
    									if (brk[k].indexOf("return:") >= 0) rt = brk[k].split(":")[1];
    									if (brk[k].indexOf("background:") >= 0) bk = brk[k].split(":")[1];
    									if (brk[k].indexOf("overlay:") >= 0) ov = brk[k].split(":")[1];
    									if (brk[k].indexOf("model:") >= 0) md = brk[k].split(":")[1];
    									if (brk[k].indexOf("modelPersonality:") >= 0) mdp = parseInt(brk[k].split(":")[1]);
    									if (brk[k].indexOf("spinModel:") >= 0) mds = brk[k].split(":")[1];
    								}
    								var pmItem = {};
    								pmItem["status"] = (m === 1 ? "initiated" : (m === 2 ? "completed" : "terminated"));
    								pmItem["return"] = rt;
    								pmItem["text"] = txt;
    								if (bk !== "") pmItem["background"] = this.$getTexture(bk);
    								if (ov !== "") pmItem["overlay"] = this.$getTexture(ov);
    								if (md !== "") pmItem["model"] = md;
    								if (mdp !== 0) pmItem["modelPersonality"] = mdp;
    								if (mds === false) pmItem["spinModel"] = mds;
    								postMsg.push(pmItem);
    							}
    						}
    						var linkedtype = "";
    						if (originalMissionType !== "" && (!postMission || postMission === false)) {
    							linkedtype = "_" + expandDescription("[missionType" + originalMissionType + "_linkedMissionSource]");
    						}
    						var missText = "";
    						var cmdty_unit = this.$getCommodityUnit(selectedCmdty);
    						if (linkedtype === "" && expandDescription("[missionType" + selectedMissionType + "_phraseGen]") === "1") {
    							var fn = expandDescription("[missionType" + selectedMissionType + "_details]");
    							missText = rep.$transformText(expandDescription(worldScripts.GalCopBB_MissionDetails[fn](), {
    								amount: formatCredits(amt, false, true),
    								commodity: displayNameForCommodity(selectedCmdty).toLowerCase(),
    								unit: cmdty_unit,
    								target: qty,
    								system: srcSystem.name,
    								destination: dest.name,
    								destinationA: System.systemNameForID(destA),
    								expiry: bb.$getTimeRemaining(expire),
    								position: expandDescription(this._positions[locType]),
    								position_reference: expandDescription(this._positionReferences[locType]),
    								name: clientName
    							}), src, dest.systemID);
    						} else {
    							missText = rep.$transformText(expandDescription("[missionType" + selectedMissionType + "_details" + linkedtype + "]", {
    								amount: formatCredits(amt, false, true),
    								commodity: displayNameForCommodity(selectedCmdty).toLowerCase(),
    								unit: cmdty_unit,
    								target: qty,
    								system: srcSystem.name,
    								destination: dest.name,
    								destinationA: System.systemNameForID(destA),
    								expiry: bb.$getTimeRemaining(expire),
    								position: expandDescription(this._positions[locType]),
    								position_reference: expandDescription(this._positionReferences[locType]),
    								name: clientName
    							}), src, dest.systemID);
    						}
    						var custMenu = null;
    						var mnu = expandDescription("[missionType" + selectedMissionType + "_customMenu]");
    						if (mnu !== "") {
    							var itms = mnu.split("|");
    							custMenu = [];
    							custMenu.push({
    								text: itms[0],
    								worldScript: itms[1],
    								callback: itms[2],
    								condition: itms[3],
    								autoRemove: (this._trueValues.indexOf(itms[4]) >= 0 ? true : false)
    							});
    						} else {
    							custMenu = "";
    						}
    						if (type53Menu != null) custMenu = type53Menu;
    
    						// items added as a secondary item won't get emails.
    						var noEmail = false;
    						if (postMission && postMission === false) noEmail = true;
    
    						var addMarkers = [];
    						if (destA != -1) {
    							addMarkers.push({system:destA, markerShape:"MARKER_SQUARE", markerColor:"orangeColor"});
    						}
    
    						// ok, time to add the mission to the BB
    						var id = bb.$addBBMission({
    							source: src,
    							destination: dest.systemID,
    							stationKey: stnKey,
    							description: rep.$transformText(expandDescription("[missionType" + selectedMissionType + "_description]"), src, dest.systemID),
    							details: missText + interstellarWarning,
    							overlay: this.$getTexture(expandDescription("[missionType" + selectedMissionType + "_bbOverlay]")),
    							payment: amt,
    							penalty: pen,
    							deposit: dpst,
    							allowPartialComplete: expandDescription("[missionType" + selectedMissionType + "_partialComplete]"),
    							completionType: completeType,
    							stopTimeAtComplete: (s_type === "1" ? true : false),
    							accepted: autoAccept,
    							expiry: expire,
    							disablePercentDisplay: expandDescription("[missionType" + selectedMissionType + "_disablePercent]"),
    							customDisplayItems: (cust.length !== 0 ? cust : ""),
    							noEmails: noEmail,
    							initiateCallback: "$acceptedMission",
    							completedCallback: "$completedMission",
    							confirmCompleteCallback: "$confirmCompleted",
    							terminateCallback: "$terminateMission",
    							failedCallback: "$failedMission",
    							manifestCallback: "$updateManifestEntry",
    							availableCallback: "$missionAvailability",
    							worldScript: mainWS,
    							customMenuItems: custMenu,
    							postStatusMessages: postMsg,
    							additionalMarkers:addMarkers,
    							data: {
    								source: this.name,
    								missionType: selectedMissionType,
    								locationType: locType,
    								quantity: 0,
    								targetQuantity: qty,
    								destroyedQuantity: 0,
    								commodity: selectedCmdty,
    								missionChain: missChain + (missChain === "" ? "" : "|") + originalMissionType,
    								chained: (originalMissionType === "" ? false : true),
    								terminatePenalty: true, // all missions accepted via the bulletin board will have reputation penalties when terminated/failed
    								altManifest: false,
    								name: clientName,
    								assassinChance: assassins,
    								stolenItemType: missionVariables.stolenItemType,
    								targetShipKey: missionVariables.targetShipKey,
    								targetShipName: missionVariables.targetShipName,
    								ingredients: ingr,
    								delivered: 0,
    								expected: 0,
    								origSystemID: src,
    								satelliteTypes: satType,
    								destinationA: destA
    							}
    						});
    						if (this._debug) log(this.name, "BB ID added " + id);
    						if (this._debug) log(this.name, "Original " + selectedMissionType + ", new mission chain " + missChain + (missChain === "" ? "" : "|") + originalMissionType);
    						break;
    				}
    
    				// special cases
    				switch (selectedMissionType) {
    					case 33:
    						// clean up
    						delete missionVariables.stolenItemType;
    						delete missionVariables.targetShipKey;
    						delete missionVariables.targetShipType;
    						break;
    					case 53:
    						for (var i = 0; i < ingr.length; i++) {
    							delete missionVariables["ingredient" + (i + 1)];
    						}
    						break;
    					case 150:
    						delete missionVariables.targetShipName;
    						break;
    				}
    
    				// if we got here through the secondary mission process, we want to stop after adding one mission, and force it to accepted
    				if (originalMissionType !== "" && (!postMission || postMission === false)) {
    					// put this mission in as pending - the player will need to accept or decline
    					this._pendingMissionID = id;
    					this.$acceptedMission(id);
    					// make sure the manifest screen is updated as well
    					bb.$addManifestEntry(id);
    					bb.$initInterface(player.ship.dockedStation);
    					return id;
    				}
    				// if this is a single mission only, return now.
    				if (this._singleMissionOnly === true) {
    					bb.$initInterface(player.ship.dockedStation);
    					return id;
    				}
    				// retest the conditions to see if we can continue or not
    				if (this.$testMissionConditions(expandDescription("[missionType" + selectedMissionType + "_conditions]"), src, selectedMissionType, -1, (originalMissionType === "" ? false : true)) === false) break;
    			}
    		} else {
    			if (this._debug) log(this.name, "Conditions not met for " + selectedMissionType);
    		}
    	}
    
    	// are we going around again?
    	if (!overrideMissionTypes) {
    		bb.$initInterface(player.ship.dockedStation);
    		if (endPoint < missTypes.length) {
    			if (this._createTimer && this._createTimer.isRunning) this._createTimer.stop();
    			this._createTimer = new Timer(this, this.$addLocalMissions, this._createDelay, 0);
    		}
    	}
    	// mix up the list a bit, if we aren't in override mode
    	//if (originalMissionType === "") bb.$shuffleBBList();
    	return -1;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // timer target for creating a new mission for the player
    // passing a negative for the origMissID will force the routine to act as if a mission of type ABS(origMissID) has just been completed
    this.$createSecondaryMission = function $createSecondaryMission(origMissID, liveSource) {
    
    	var id = -1;
    	// don't create a secondary in interstellar space
    	if (system.ID === -1) return id;
    
    	var bb = worldScripts.BulletinBoardSystem;
    	if (origMissID > 0) {
    		var item = bb.$getItem(origMissID);
    		if (!item) log(this.name, "**ERROR: did not get mission details back for mission ID " + origMissID);
    	} else {
    		var item = null;
    	}
    	// don't give a player a new mission they need to make decisions about while they're under hostile red alert
    	if (player.ship.alertCondition !== 3 || player.alertHostiles === false) {
    		if (this._debug) {
    			log(this.name, "Creating second mission from original mission " + origMissID);
    			if (item) log(this.name, "Orig misstype " + item.data.missionType);
    		}
    		if (item) {
    			var chain = item.data.missionChain;
    			var origType = item.data.missionType;
    			var origSource = item.source;
    
    			if (chain == null) chain = "";
    		} else {
    			var chain = "";
    			var origType = Math.abs(origMissID);
    			var origSource = system.ID;
    		}
    		if (this._debug) log(this.name, "new miss type " + origType);
    		var linked = expandDescription("[missionType" + origType + "_linkedMissionTypes]");
    
    		var missionList = [];
    		var chainData = chain.split("|");
    		// OK, time to work out the next mission for the player
    		if (linked && linked !== "") {
    			var getList = linked.split(",");
    			for (var i = 0; i < getList.length; i++) {
    				// don't add any mission types already in the chain
    				if (chainData.indexOf(getList[i]) === -1) missionList.push(parseInt(getList[i]));
    			}
    		}
    
    		if (this._debug) log(this.name, "MissionList = " + missionList);
    		if (missionList.length > 0) {
    			missionList.sort(function (a, b) {
    				return Math.random() - 0.5;
    			});
    			this._singleMissionOnly = true;
    			if (liveSource && liveSource === true) {
    				id = this.$addLocalMissions(missionList, origType, origSource, chain, false);
    			} else {
    				id = this.$addLocalMissions(missionList, origType, origSource, chain);
    			}
    			this._singleMissionOnly = false;
    		}
    		if (this._debug) log(this.name, "New id " + id);
    	}
    
    	if (id < 0 && item) {
    		var missType = item.data.missionType;
    		var checkType = expandDescription("[missionType" + missType + "_completionType]");
    		// if we didn't get a secondary mission, update the original mission to be "stopTimeAtComplete" = false
    		if (item.stopTimeAtComplete === true && checkType.indexOf("|1") === -1) {
    			item.stopTimeAtComplete = false;
    
    			// also, set a new expiry time to be the distance to the source system + 10 minutes
    			var dest = System.infoForSystem(galaxyNumber, item.source);
    			var route = system.info.routeToSystem(dest);
    			item.expiry = clock.adjustedSeconds + ((route.time * 3600) + 600);
    
    			// switch to the alternate manifest entry
    			item.data.altManifest = true;
    		}
    	} else if (id > 0) {
    		// turn the current mission's process to stop time now (it would be a bit harsh to give the player a new mission and expecting the ETA not to change)
    		if (item) {
    			if (item.stopTimeAtComplete === false) {
    				item.stopTimeAtComplete = true;
    				// do we need to turn on the alt manifest now?
    				if (expandDescription("[missionType" + item.data.missionType + "_completionType]").indexOf("|1") === -1) {
    					item.data.altManifest = true;
    				}
    			}
    		}
    
    		// ok, we have a new pending mission - better tell the player about it I suppose...
    		var nItem = bb.$getItem(id);
    		if (origMissID < 0) {
    			// no termination penalty for secondary missions created spontaneously (ie by scooping a black box/escape pod)
    			nItem.penalty = 0;
    			nItem.data.terminatePenalty = false;
    		}
    
    		// special cases
    		switch (nItem.data.missionType) {
    			case 33:
    				// we're going to put some values into mission variables here, so the expandDesc function will include the data
    				missionVariables.stolenItemType = expandDescription("[gcm_stolen_item_types]");
    				missionVariables.targetShipKey = this.$pickGetawayShip();
    				var shipspec = Ship.shipDataForKey(missionVariables.targetShipKey);
    				missionVariables.targetShipType = shipspec.name;
    				break;
    		}
    
    		var linkedtype = expandDescription("[missionType" + origType + "_linkedMissionSource]");
    		var txt = expandDescription("[missionType" + nItem.data.missionType + "_miniBriefing_" + linkedtype + "]", {
    			target: nItem.data.targetQuantity,
    			destination: bb.$systemNameForID(nItem.destination),
    			system: System.systemNameForID(nItem.source),
    			position: expandDescription(this._positions[nItem.data.locationType]),
    			position_reference: expandDescription(this._positionReferences[nItem.data.locationType]),
    			expiry: bb.$getTimeRemaining(nItem.expiry),
    			commodity: displayNameForCommodity(nItem.data.commodity).toLowerCase(),
    			amount: formatCredits(nItem.payment, false, true)
    		});
    
    		this._pendingMissionOrigID = origMissID;
    
    		var typ = expandDescription("[missionType" + origType + "_linkedMissionCommsMethod]");
    		if (this._debug) log(this.name, "checking " + typ);
    		// communicate with player about the new mission
    		worldScripts.GalCopBB_Missions_MFD.$updateMFD(
    			txt,
    			(typ.indexOf("comms") >= 0 ? true : false),
    			(typ.indexOf("comms") >= 0 ? worldScripts.GalCopBB_MeetShip._commsNPC : null),
    			(typ.indexOf("|1") >= 0 ? true : false)
    		);
    	}
    	return id;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // removes an existing mission and adds a new mission
    this.$generateMissions = function $generateMissions() {
    	// remove one existing mission
    	var bb = worldScripts.BulletinBoardSystem;
    	for (var i = 0; i < bb._data.length; i++) {
    		if (bb._data[i].accepted === false && bb._data[i].data && bb._data[i].source == this.name && Math.random() > 0.6) {
    			bb._data.splice(i, 1);
    			break;
    		}
    	}
    	this._singleMissionOnly = true;
    	// shuffle order so it isn't always the same variant being checked first
    	this._availableMissionTypes.sort(function (a, b) {
    		return Math.random() - 0.5;
    	}); 
    	this.$addLocalMissions();
    	this._singleMissionOnly = false;
    }
    
    //=============================================================================================================
    // general functions
    //-------------------------------------------------------------------------------------------------------------
    // check the condition string and return either true (conditions passed) or false
    // note: conditions checked in this function will prevent a mission from being generated
    // the "$missionAvailability" function handles conditions that could be changed by the player just limit the ability of the player to accept the mission.
    this.$testMissionConditions = function $testMissionConditions(condString, systemID, missionType, origSystemID, isSecondary) {
    	var result = true;
    	// if there is no condition, it's true in all situations, so just return the result now.
    	if (condString === "") return result;
    	// if we're debugging one of the missions, always add it in.
    	if (condString.indexOf("debug") >= 0) return result;
    	// are we attempting to test a specific mission?
    	if (this._forceCreate.indexOf(missionType) >= 0) {
    		var fc = this._forceCreate.indexOf(missionType);
    		this._forceCreate.splice(fc, 1);
    		return result;
    	}
    
    	/*
    		Note about "stationKeys":
    		Normally the stationKeys element would restrict where a mission would be offered from. ie galcop stationKeys would mean galcop only stations.
    		However, for secondary missions the stationKey doesn't come into play as they aren't accepted from the BB itself but through some other means
    		ie email, comms, ship.
    		So that means that a player can start a mission from a pirate station, and still be given a galcop mission (eg get the cargo)
    		However, because the description of a pirate-type 6 and a galcop-type 6 should be quite different, this should not really be allowed to happen.
    		Note to self: take care when setting up secondary email-type missions.
    	*/
    
    	var dest = System.infoForSystem(galaxyNumber, systemID);
    	var condList = condString.split(",");
    	for (var i = 0; i < condList.length; i++) {
    		var condItem = condList[i];
    		if (condItem.indexOf(":") >= 0) {
    			var items = condItem.split(":");
    			var checkVal = this.$getConditionParameter(condItem, origSystemID);
    			switch (items[0]) {
    				case "chance": // random chance of offering this mission
    					if (Math.random() > (checkVal / 100)) result = false;
    					break;
    				case "mission_gap_days": // how long until another mission will be generated in this system
    					if (this.$lastMissionGapDays(systemID, missionType) < checkVal) result = false;
    					break;
    				case "max_mission_count": // how many of these missions can be available in order to include this one?
    					if (this.$countMissions(missionType) > checkVal) result = false;
    					break;
    				case "incompatible_with": // if any of these missions already exist in this system, don't include this type
    					// trying to avoid things like having trade negotiations breaking down and the result being (a) to destroy traders and (b) steal cargo
    					var incompatible = items[1].split("|");
    					for (var j = 0; j < incompatible.length; j++) {
    						if (this.$countMissions(parseInt(incompatible[j])) > 0) result = false;
    					}
    					break;
    				case "has_player_role":
    					if (this.$checkPlayerRoles(1, items[1]) === false) result = false;
    					break;
    				case "no_player_role":
    					if (this.$checkPlayerRoles(1, items[1]) === true) result = false;
    					break;
    				case "success_missions": // example data: 46|3  means: must have successfully completed at least 3 type 46 missions
    					var extra = items[1].split("|");
    					var missType = parseInt(extra[0]);
    					var num = parseInt(extra[1]);
    					if (this.$getSuccessMissions(systemID, missType) < num) result = false;
    					break;
    				case "fail_missions": // example data: 46|3  means: must have failed at least 3 type 46 missions
    					var extra = items[1].split("|");
    					var missType = parseInt(extra[0]);
    					var num = parseInt(extra[1]);
    					if (this.$getFailedMissions(systemID, missType) < num) result = false;
    					break;
    				case "is_hacked":
    					if (checkVal === 0 && worldScripts.GalCopBB_WBSA._wpHacks.indexOf(systemID) >= 0) result = false;
    					if (checkVal === 1 && worldScripts.GalCopBB_WBSA._wpHacks.indexOf(systemID) === -1) result = false;
    					break;
    				case "is_secondary":
    					if (checkVal === 0 && isSecondary === true) result = false;
    					if (checkVal === 1 && isSecondary === false) result = false;
    					break;
    				case "min_government":
    					if (items[1] === "source_system" && origSystemID >= 0) checkVal = System.infoForSystem(galaxyNumber, origSystemID).government;
    					if (dest.government < checkVal) result = false;
    					break;
    				case "max_government":
    					if (items[1] === "source_system" && origSystemID >= 0) checkVal = System.infoForSystem(galaxyNumber, origSystemID).government;
    					if (dest.government > checkVal) result = false;
    					break;
    				case "government":
    					if (items[1] === "source_system" && origSystemID >= 0) checkVal = System.infoForSystem(galaxyNumber, origSystemID).government;
    					if (items[1].indexOf("|") >= 0) {
    						var govList = items[1].split("|");
    						var tempresult = false;
    						for (var j = 0; j < govList.length; j++) {
    							if (dest.government === parseInt(govList[j])) tempresult = true;
    						}
    						if (tempresult === false) result = false;
    					} else {
    						if (dest.government !== checkVal) result = false;
    					}
    					break;
    				case "description_properties":
    					var props = items[1].split("|");
    					for (var j = 0; j < props.length; j++) {
    						// look for a "not" clause (indicated by a "!" symbol)
    						if (props[j].indexOf("!") === 0) {
    							if (dest.description.indexOf(props[j].substring(props[j].indexOf("!") + 1)) >= 0) result = false;
    						} else {
    							if (dest.description.indexOf(props[j]) === -1) result = false;
    						}
    					}
    					break;
    				case "min_economy":
    					if (items[1] === "source_system" && origSystemID >= 0) checkVal = System.infoForSystem(galaxyNumber, origSystemID).economy;
    					if (dest.economy < checkVal) result = false;
    					break;
    				case "max_economy":
    					if (items[1] === "source_system" && origSystemID >= 0) checkVal = System.infoForSystem(galaxyNumber, origSystemID).economy;
    					if (dest.economy > checkVal) result = false;
    					break;
    				case "economy":
    					if (items[1] === "source_system" && origSystemID >= 0) checkVal = System.infoForSystem(galaxyNumber, origSystemID).economy;
    					if (items[1].indexOf("|") >= 0) {
    						var ecoList = items[1].split("|");
    						var tempresult = false;
    						for (var j = 0; j < ecoList.length; j++) {
    							if (dest.economy === parseInt(ecoList[j])) tempresult = true;
    						}
    						if (tempresult === false) result = false;
    					} else {
    						if (dest.economy !== checkVal) result = false;
    					}
    					break;
    				case "min_techlevel":
    					if (items[1] === "source_system" && origSystemID >= 0) checkVal = System.infoForSystem(galaxyNumber, origSystemID).techlevel;
    					if (dest.techlevel < checkVal) result = false;
    					break;
    				case "max_techlevel":
    					if (items[1] === "source_system" && origSystemID >= 0) checkVal = System.infoForSystem(galaxyNumber, origSystemID).techlevel;
    					if (dest.techlevel > checkVal) result = false;
    					break;
    				case "techlevel":
    					if (items[1] === "source_system" && origSystemID >= 0) checkVal = System.infoForSystem(galaxyNumber, origSystemID).techlevel;
    					if (items[1].indexOf("|") >= 0) {
    						var tlList = items[1].split("|");
    						var tempresult = false;
    						for (var j = 0; j < tlList.length; j++) {
    							if (dest.government === parseInt(tlList[j])) tempresult = true;
    						}
    						if (tempresult === false) result = false;
    					} else {
    						if (dest.techlevel !== checkVal) result = false;
    					}
    					break;
    				case "min_dangerous_neighbours":
    					var dang = dest.systemsInRange(7);
    					var dangCount = 0;
    					for (var j = 0; j < dang.length; j++) {
    						if (dang[j].government < 3) dangCount += 1;
    					}
    					if (dangCount < checkVal) result = false;
    					break;
    				case "max_score":
    					if (player.score > checkVal) result = false;
    					break;
    				case "exclude_equipment": // player cannot have any of these equipment items installed
    					var eqList = items[1].split("|");
    					for (var j = 0; j < eqList.length; j++) {
    						if (eqList[j] !== "" && player.ship.hasEquipmentProviding(eqList[j]) === true) result = false;
    					}
    					break;
    				case "max_bounty":
    					if (player.bounty > checkVal) result = false;
    					break;
    				case "min_bounty":
    					if (player.bounty < checkVal) result = false;
    					break;
    				case "bounty":
    					if (player.bounty !== checkVal) result = false;
    					break;
    				case "max_parcel_reputation":
    					if (player.parcelReputation > checkVal) result = false;
    					break;
    				case "max_cargo_reputation":
    					if (player.contractReputation > checkVal) result = false;
    					break;
    				case "max_passenger_reputation":
    					if (player.passengerReputation > checkVal) result = false;
    					break;
    				case "max_mission_reputation":
    					if (this.$playerMissionReputation(systemID, missionType) > checkVal) result = false;
    					break;
    				case "has_outbreak":
    					if (checkVal === 0 && worldScripts.GalCopBB_DiseaseOutbreak.$systemHasDiseaseOutbreak(systemID) === true) result = false;
    					if (checkVal === 1 && worldScripts.GalCopBB_DiseaseOutbreak.$systemHasDiseaseOutbreak(systemID) === false) result = false;
    					break;
    				case "has_solar_activity":
    					if (checkVal === 0 && worldScripts.GalCopBB_SolarActivity._solarActivitySystems.indexOf(systemID) >= 0) result = false;
    					if (checkVal === 1 && worldScripts.GalCopBB_SolarActivity._solarActivitySystems.indexOf(systemID) === -1) result = false;
    					break;
    				case "has_earthquake":
    					if (checkVal === 0 && worldScripts.GalCopBB_Earthquake._earthquakeSystems.indexOf(systemID) >= 0) result = false;
    					if (checkVal === 1 && worldScripts.GalCopBB_Earthquake._earthquakeSystems.indexOf(systemID) === -1) result = false;
    					break;
    				case "has_pirate_hermit":
    					if (checkVal === 0 && worldScripts.GalCopBB_SoftwareInstall._pirateHermitInfo[systemID] >= 0) result = false;
    					if (checkVal === 1 && (!worldScripts.GalCopBB_SoftwareInstall._pirateHermitInfo[systemID] || worldScripts.GalCopBB_SoftwareInstall._pirateHermitInfo[systemID] == undefined)) result = false;
    					break;
    				case "market_disrupted":
    					if (checkVal === 0 && worldScripts.GalCopBB_StationDisrupt.$disruptCommodities(systemID) === true) result = false;
    					if (checkVal === 1 && worldScripts.GalCopBB_StationDisrupt.$disruptCommodities(systemID) === false) result = false;
    					break;
    				case "equipment_disrupted":
    					if (checkVal === 0 && worldScripts.GalCopBB_StationDisrupt.$disruptEquipment(systemID) === true) result = false;
    					if (checkVal === 1 && worldScripts.GalCopBB_StationDisrupt.$disruptEquipment(systemID) === false) result = false;
    					break;
    				case "security_software":
    					if (checkVal === 0 && worldScripts.GalCopBB_SoftwareInstall._pirateHermitInfo[systemID] && worldScripts.GalCopBB_SoftwareInstall._pirateHermitInfo[systemID] > 0) result = false;
    					if (checkVal === 1 && (!worldScripts.GalCopBB_SoftwareInstall._pirateHermitInfo[systemID] || worldScripts.GalCopBB_SoftwareInstall._pirateHermitInfo[systemID] == undefined || worldScripts.GalCopBB_SoftwareInstall._pirateHermitInfo[systemID] === 0)) result = false;
    					break;
    				case "igt_installed":
    					if (checkVal === 0 && this._igtInstalled === true) result = false;
    					if (checkVal === 1 && this._igtInstalled === false) result = false;
    					break;
    				case "ups_med_mission":
    					// checks to see if a UPS parcel medical mission is underway or not.
    					if (worldScripts.ups_parcel) {
    						var ups = ["MEDICIN_DELIVERY", "MEDICIN_DELIVERY_2"];
    						if (checkVal === 0 && ups.indexOf(worldScripts.ups_parcel.ups_parcel) >= 0) result = false;
    						if (checkVal === 1 && ups.indexOf(worldScripts.ups_parcel.ups_parcel) === -1) result = false;
    					} else {
    						if (checkVal === 1) result = false;
    					}
    					break;
    				case "no_satellite_type":
    					var sat = worldScripts.GalCopBB_Satellites;
    					var satsys = sat._satArray[systemID];
    					if (!satsys || Array.isArray(satsys) === false) {
    						result = false;
    					} else {
    						if (items[1].indexOf("|") >= 0) {
    							// can be | separated for an "or" list of types (ie. 2|3 means type 2 or type 3)
    							var stypes = items[1].split("|");
    						} else {
    							// or a single sat type only
    							var stypes = [items[1]];
    						}
    						var found = false;
    						for (var j = 0; j < stypes.length; j++) {
    							var stype = parseInt(stypes[j]);
    							if (satsys.length >= stype && satsys[stype - 1] === 0) found = true;
    						}
    						if (found === false) result = false;
    					}
    					break;
    				case "has_satellite_type":
    					var sat = worldScripts.GalCopBB_Satellites;
    					var satsys = sat._satArray[systemID];
    					if (!satsys || Array.isArray(satsys) === false) {
    						result = false;
    					} else {
    						if (items[1].indexOf("|") >= 0) {
    							// can be | separated for an "or" list of types (ie. 2|3 means type 2 or type 3)
    							var stypes = items[1].split("|");
    						} else {
    							// or a single sat type only
    							var stypes = [items[1]];
    						}
    						var found = false;
    						for (var j = 0; j < stypes.length; j++) {
    							var stype = parseInt(stypes[j]);
    							if (satsys.length >= stype && satsys[stype - 1] > 0) found = true;
    						}
    						if (found === false) result = false;
    					}
    					break;
    				case "has_pirate_base":
    					var b = worldScripts.GalCopBB_PirateBases._bases;
    					var found = false;
    					for (var j = 0; j < b.length; j++) {
    						if (b[j].systemID === systemID) {
    							found = true;
    							break;
    						}
    					}
    					if (found === false && checkVal === 1) result = false;
    					if (found === true && checkVal === 0) result = false;
    					break;
    				case "check_equipment": // required equipment, but not shown on mission detail page
    					// this is to provide options to limit mission availability to higher-speced ships, without making it obvious
    					var eqList = items[1].split("|");
    					for (var j = 0; j < eqList.length; j++) {
    						if (eqList[j] !== "") {
    							var check = true;
    							if (this._equipmentProvidingLookup[eqList[j]]) {
    								check = player.ship.hasEquipmentProviding(eqList[j]);
    							} else {
    								if (player.ship.equipmentStatus(eqList[j]) != "EQUIPMENT_OK") check = false;
    							}
    							if (check === false) result = check;
    						}
    					}
    					break;
    				case "min_cargo_space":
    				case "min_score":
    				case "min_missile_slots":
    				case "min_cargo_reputation":
    				case "min_passenger_reputation":
    				case "min_parcel_reputation":
    				case "include_equipment":
    					// only perform these condition tests when in secondary mission mode
    					if (isSecondary === true) {
    						var test = this.$missionAvailability(-1, missionType, origSystemID);
    						if (test !== "") result = false;
    					}
    					break;
    				case "commodity_legal":
    					if (this.$isCommodityIllegal(origSystemID, null, checkVal) === true) result = false;
    					break;
    				case "commodity_illegal":
    					if (this.$isCommodityIllegal(origSystemID, null, checkVal) === false) result = false;
    					break;
    				case "market_has":
    					if (system.mainStation && system.mainStation.market[items[1]].quantity === 0) result = false;
    					break;
    				case "min_oolite_ver":
    					if (oolite.compareVersion(items[1]) > 0) result = false;
    					break;
    				case "system_position":
    					// or combination list: x<4|x>200|y<10|y>200
    					var sub = items[1].split("|");
    					var check = false;
    					for (var j = 0; j < sub.length; j++) {
    						var checkCoord = 0;
    						var comp = "";
    						if (sub[j].indexOf("x") >= 0) checkCoord = dest.internalCoordinates.x;
    						if (sub[j].indexOf("y") >= 0) checkCoord = dest.internalCoordinates.y;
    						if (sub[j].indexOf("<") >= 0) {
    							checkVal = parseInt(sub[j].split("<")[1]);
    							comp = "<";
    						}
    						if (sub[j].indexOf(">") >= 0) {
    							checkVal = parseInt(sub[j].split(">")[1]);
    							comp = ">";
    						}
    						switch (comp) {
    							case "<":
    								if (checkCoord < checkVal) check = true;
    								break;
    							case ">":
    								if (checkCoord > checkVal) check = true;
    								break;
    						}
    					}
    					if (check === false) result = false;
    					break;
    				case "max_system_x_position":
    					if (dest.internalCoordinates.x > checkVal) result = false;
    					break;
    				case "min_system_x_position":
    					if (dest.internalCoordinates.x < checkVal) result = false;
    					break;
    				case "max_system_y_position":
    					if (dest.internalCoordinates.y > checkVal) result = false;
    					break;
    				case "min_system_y_position":
    					if (dest.internalCoordinates.y < checkVal) result = false;
    					break;
    			}
    		}
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns text to the BB when asked to confirm if a mission is currently available
    // blank means available, otherwise the text of why the mission is unavailable
    // will be called during secondary mission setup with missID = -1
    this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
    	var result = "";
    	// get the BB item this missID (if != -1)
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	// if we have an index, get the conditions from the mission itself
    	// otherwise we are working in pre-creation mode, so get the conditions from the missionText
    	var conditions = "";
    	var sysID = -1;
    	if (item) {
    		conditions = expandDescription("[missionType" + item.data.missionType + "_conditions]");
    		sysID = item.source;
    	} else if (missType && missType != "") {
    		conditions = expandDescription("[missionType" + missType + "_conditions]");
    		sysID = system.ID;
    	}
    	var oSysID = -1;
    	if (missID === -1 && origSysID && origSysID >= 0) {
    		oSysID = origSysID;
    	}
    	if (item) oSysID = item.data.origSystemID;
    
    	if (conditions != "") {
    		var condList = conditions.split(",");
    
    		// check for cargo space requirements
    		if (this.$getConditionIndex(condList, "min_cargo_space") >= 0) {
    			var idx = this.$getConditionIndex(condList, "min_cargo_space");
    			var data = condList[idx].split(":");
    			if (isNaN(data[1]) === false) {
    				if (player.ship.cargoSpaceAvailable < parseInt(data[1])) {
    					result = "Insufficient cargo space";
    				}
    			} else {
    				if (item && data[1] === "quantity") {
    					if (player.ship.cargoSpaceAvailable < item.data.targetQuantity) {
    						result = "Insufficient cargo space";
    					}
    				}
    			}
    		}
    		// check for min score
    		if (this.$getConditionIndex(condList, "min_score") >= 0) {
    			var idx = this.$getConditionIndex(condList, "min_score");
    			var checkVal = this.$getConditionParameter(condList[idx], oSysID);
    			if (player.score < checkVal) {
    				result = "Insufficient combat experience";
    			}
    		}
    		// check for missile slots
    		if (this.$getConditionIndex(condList, "min_missile_slots") >= 0) {
    			var idx = this.$getConditionIndex(condList, "min_missile_slots");
    			var checkVal = this.$getConditionParameter(condList[idx], oSysID);
    			var flag = false;
    			var miss = [];
    			try {
    				miss = player.ship.missiles;
    				flag = true;
    			} catch (err) {
    				if (this._debug) log(this.name, "!!ERROR: " + err);
    			}
    			var avail = 0;
    			if (player.ship.missileCapacity > 0) {
    				avail = player.ship.missileCapacity - miss.length;
    			}
    			if (avail < checkVal) result = "Insufficient available missile slots";
    		}
    		// check for equipment requirements
    		if (this.$getConditionIndex(condList, "include_equipment") >= 0) {
    			var idx = this.$getConditionIndex(condList, "include_equipment");
    			var eqList = condList[idx].split(":")[1].split("|");
    			for (var j = 0; j < eqList.length; j++) {
    				if (eqList[j] !== "") {
    					var check = true;
    					if (this._equipmentProvidingLookup[eqList[j]]) {
    						check = player.ship.hasEquipmentProviding(eqList[j]);
    					} else {
    						if (player.ship.equipmentStatus(eqList[j]) != "EQUIPMENT_OK") check = false;
    					}
    					if (check === false) result = "Required equipment not installed";
    				}
    			}
    		}
    		// check reputations
    		if (this.$getConditionIndex(condList, "min_parcel_reputation") >= 0) {
    			var idx = this.$getConditionIndex(condList, "min_parcel_reputation");
    			var checkVal = this.$getConditionParameter(condList[idx], oSysID);
    			if (player.parcelReputation < checkVal) result = "Insufficient experience in courier services";
    		}
    		if (this.$getConditionIndex(condList, "min_cargo_reputation") >= 0) {
    			var idx = this.$getConditionIndex(condList, "min_cargo_reputation");
    			var checkVal = this.$getConditionParameter(condList[idx], oSysID);
    			if (player.parcelReputation < checkVal) result = "Insufficient experience in trader services";
    		}
    		if (this.$getConditionIndex(condList, "min_passenger_reputation") >= 0) {
    			var idx = this.$getConditionIndex(condList, "min_passenger_reputation");
    			var checkVal = this.$getConditionParameter(condList[idx], oSysID);
    			if (player.parcelReputation < checkVal) result = "Insufficient experience in passenger services";
    		}
    		if (this.$getConditionIndex(condList, "min_mission_reputation") >= 0) {
    			var idx = this.$getConditionIndex(condList, "min_mission_reputation");
    			var checkVal = this.$getConditionParameter(condList[idx], oSysID);
    			if (this.$playerMissionReputation(sysID, missType) < checkVal) result = "Insufficient experience with mission";
    		}
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // processes the condition parameter (the bit after the ":") and returns the result
    this.$getConditionParameter = function $getConditionParameter(condition, origSystemID) {
    	var items = condition.split(":");
    	var checkVal = -1;
    	if (items[1].indexOf("rand") >= 0) {
    		var max = parseInt(items[1].split("_")[1]);
    		checkVal = parseInt(Math.random() * (max + 1));
    	}
    	if (this.$isNumeric(items[1]) === true) {
    		checkVal = parseInt(items[1]);
    	}
    	if (items[1] === "source_system" && origSystemID >= 0) checkVal = origSystemID;
    	return checkVal;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns the index of a particular condition in an array
    this.$getConditionIndex = function $getConditionIndex(condList, condition) {
    	for (var i = 0; i < condList.length; i++) {
    		if (condList[i].indexOf(condition) >= 0) return i;
    	}
    	return -1;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns true if player has a role similar to roletype, otherwise false
    // cycles controls the number of times the routine will search for the given role (higher number menas more chances to find the role)
    this.$checkPlayerRoles = function $checkPlayerRoles(cycles, roleType) {
    	var result = false;
    	var pws = player.roleWeights;
    
    	for (var i = 1; i <= cycles; i++) {
    		var checkrole = pws[parseInt(Math.random() * pws.length)];
    		if (checkrole.indexOf(roleType) >= 0) result = true;
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // free up cargo space by selling cargo
    // this routine shouldn't be called anymore, as we are controlling access to missions based on cargo space from the BB itself, using $missionAvailability
    this.$freeCargoSpace = function $freeCargoSpace(qtyRequired, noSell) {
    	var removed = 0;
    	// sell something to fit this in
    	var p = player.ship;
    	var cargo = p.manifest.list;
    	var mkt = player.ship.dockedStation.market;
    	if (mkt == null) mkt = system.mainStation.market;
    	// ignore slaves on the first pass
    	for (var i = 0; i < cargo.length; i++) {
    		if (cargo[i].quantity > 0 && cargo[i].unit === "t" && cargo[i].commodity !== "slaves" && (noSell === "" || cargo[i].commodity != noSell)) {
    			do {
    				p.manifest[cargo[i].commodity] -= 1;
    				removed += 1;
    				player.credits += (mkt[cargo[i].commodity].price / 10);
    			} while (removed < qtyRequired && p.manifest[cargo[i].commodity] > 0);
    			if (removed === qtyRequired) break;
    		}
    	}
    	if (removed !== qtyRequired) {
    		// try again, this time with slaves in the list
    		if (p.manifest["slaves"] >= (qtyRequired - removed)) {
    			p.manifest["slaves"] -= qtyRequired - removed;
    			player.credits += (mkt["slaves"].price / 10);
    		} else {
    			// argh! no room for this one
    			throw "!ERROR: Unable to free up cargo space in player ship!";
    		}
    
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns true if the selected commodity is illegal in the selected system, otherwise false
    this.$isCommodityIllegal = function $isCommodityIllegal(sysID, station, cmdty) {
    	if (station == null && player.ship.dockedStation) station = player.ship.dockedStation;
    	if (station == null) station = system.mainStation;
    	if (sysID === system.ID) {
    		if (station.market[cmdty].legality_import > 0) return true;
    	}
    	if (cmdty === "firearms" || cmdty === "narcotics" || cmdty === "slaves") {
    		if (this._igtInstalled === true) return true;
    	}
    	return false;
    }
    //-------------------------------------------------------------------------------------------------------------
    // keeps track of last time a given mission type in a given system was accepted
    this.$updateLastMissionDate = function $updateLastMissionDate(sysID, missType) {
    	var done = false;
    	if (this._lastMission.length > 0) {
    		for (var i = 0; i < this._lastMission.length; i++) {
    			if (this._lastMission[i].missionType === missType && this._lastMission[i].system === sysID) {
    				this._lastMission[i].date === clock.adjustedSeconds;
    				done = true;
    				break;
    			}
    		}
    	}
    	if (done === false) {
    		this._lastMission.push({
    			missionType: missType,
    			system: sysID,
    			date: clock.adjustedSeconds
    		});
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateGeneralSettings = function $updateGeneralSettings(item) {
    	if (item.data.assassinChance > 0) {
    		var chance = item.data.assassinChance;
    		var name = item.data.name;
    		// low <= 0.3
    		// add possibly once
    		if (chance <= 0.3) {
    			if ((chance / 0.3) > Math.random()) {
    				worldScripts["oolite-contracts-helpers"]._setClientName(name);
    			}
    		}
    		// medium > 0.3, <= 0.6
    		// add possibly twice
    		if (chance > 0.3 && chance <= 0.6) {
    			worldScripts["oolite-contracts-helpers"]._setClientName(name);
    			if ((chance - 0.3) / 0.3 > Math.random()) {
    				worldScripts["oolite-contracts-helpers"]._setClientName(name);
    			}
    		}
    		// high > 0.6
    		// add possibly three times
    		if (chance > 0.6) {
    			worldScripts["oolite-contracts-helpers"]._setClientName(name);
    			worldScripts["oolite-contracts-helpers"]._setClientName(name);
    			if ((chance - 0.6) / 0.4 > Math.random()) {
    				worldScripts["oolite-contracts-helpers"]._setClientName(name);
    			}
    		}
    	}
    
    	// setup stage 0 for multi staged missions
    	if (this._multiStageMissionTypes.indexOf(item.data.missionType) >= 0) {
    		item.data.stage = 0;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // gets a random position based on a locationType value. locationType can be 0, 1, 2, 3, 4, 5, 6 or 7.
    this.$getRandomPosition = function $getRandomPosition(locationType, factor, missID, zrangeMin, zrangeMax) {
    	var result;
    	var coord = "";
    	var dist = 0;
    	var z;
    	var x = (factor !== 0 ? Math.floor(system.scrambledPseudoRandomNumber(missID) * factor) * (system.scrambledPseudoRandomNumber(missID + 1) > 0.5 ? 1 : -1) : 0);
    	var y = (factor !== 0 ? Math.floor(system.scrambledPseudoRandomNumber(missID + 2) * factor) * (system.scrambledPseudoRandomNumber(missID + 3) > 0.5 ? 1 : -1) : 0);
    
    	switch (locationType) {
    		case 0: // on the other side of the planet from the witchpoint
    			var zmin = (zrangeMin && !isNaN(zrangeMin) ? zrangeMin : 1.3);
    			var zmax = (zrangeMax && !isNaN(zrangeMax) ? zrangeMax : 1.6);
    			z = (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin)) + zmin;
    			coord = "wpu";
    			dist = system.mainPlanet.position.distanceTo([0, 0, 0]);
    			break;
    		case 1: // towards the sun from the witchpoint
    			var zmin = (zrangeMin && !isNaN(zrangeMin) ? zrangeMin : 0.3);
    			var zmax = (zrangeMax && !isNaN(zrangeMax) ? zrangeMax : 0.6);
    			z = (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin)) + zmin;
    			coord = "wsu";
    		case 2: // towards the sun from the planet
    			var zmin = (zrangeMin && !isNaN(zrangeMin) ? zrangeMin : 0.4);
    			var zmax = (zrangeMax && !isNaN(zrangeMax) ? zrangeMax : 0.7);
    			z = (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin)) + zmin;
    			coord = "psu";
    			break;
    		case 3: // behind the witchpoint from the planet
    			var zmin = (zrangeMin && !isNaN(zrangeMin) ? zrangeMin : -0.3);
    			var zmax = (zrangeMax && !isNaN(zrangeMax) ? zrangeMax : -0.7);
    			z = (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin)) + zmin;
    			coord = "wpu";
    			dist = system.mainPlanet.position.distanceTo([0, 0, 0]);
    			break;
    		case 4: // behind the witchpoint from the sun
    			var zmin = (zrangeMin && !isNaN(zrangeMin) ? zrangeMin : -0.3);
    			var zmax = (zrangeMax && !isNaN(zrangeMax) ? zrangeMax : -0.7);
    			z = (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin)) + zmin;
    			coord = "wsu";
    			dist = system.sun.position.distanceTo([0, 0, 0]);
    			break;
    		case 5: // on the other side of the sun from the planet
    			var zmin = (zrangeMin && !isNaN(zrangeMin) ? zrangeMin : 5);
    			var zmax = (zrangeMax && !isNaN(zrangeMax) ? zrangeMax : 20);
    			if (zrangeMin) {
    				z = (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin)) + zmin;
    			} else {
    				var ps_dist = system.sun.position.distanceTo(system.mainPlanet);
    				var far_dist = system.sun.radius * (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin) + zmin); // between 5 and 20 sun radii beyond
    				z = (1 + far_dist / ps_dist);
    			}
    			coord = "psu";
    			dist = system.mainPlanet.position.distanceTo(system.sun);
    			break;
    		case 6: // on the other side of the sun from the witchpoint
    			var zmin = (zrangeMin && !isNaN(zrangeMin) ? zrangeMin : 5);
    			var zmax = (zrangeMax && !isNaN(zrangeMax) ? zrangeMax : 20);
    			if (zrangeMin) {
    				z = (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin)) + zmin;
    			} else {
    				var ws_dist = system.sun.position.distanceTo([0, 0, 0]);
    				var far_dist = system.sun.radius * (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin) + zmin); // between 5 and 20 sun radii beyond
    				z = (1 + far_dist / ws_dist);
    			}
    			coord = "wsu";
    			dist = system.sun.position.distanceTo([0, 0, 0]);
    			break;
    		case 7: // on the other side of the planet from the sun
    			var zmin = (zrangeMin && !isNaN(zrangeMin) ? zrangeMin : 2);
    			var zmax = (zrangeMax && !isNaN(zrangeMax) ? zrangeMax : 3);
    			z = (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin)) + zmin;
    			coord = "spu";
    			dist = system.mainPlanet.position.distanceTo(system.sun);
    			break;
    	}
    
    	result = Vector3D(x, y, z).fromCoordinateSystem(coord); 
    	return {
    		position: result,
    		x: x,
    		y: y,
    		z: z,
    		coordSystem: coord,
    		locType: locationType,
    		checkDist: dist
    	};
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // counts the number of instances (active or available in current system) of this particular mission type
    this.$countMissions = function $countMission(missionType) {
    	var count = 0;
    	var list = this.$getListOfMissions(false, missionType);
    	if (list.length > 0) {
    		for (var i = 0; i < list.length; i++) {
    			if (list[i].source === system.ID) count += 1;
    		}
    	}
    	// check for any active missions of this type
    	list = this.$getListOfMissions(true, missionType);
    	count += list.length;
    	return count;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // are all the available missions of this type close to expiry? returns true if so, otherwise false
    // note: not currently in use
    this.$closeExpiry = function $closeExpiry(missionType) {
    	var count = 0;
    	var expiry_count = 0;
    	var list = this.$getListOfMissions(false, missionType);
    	for (var i = 0; i < list.length; i++) {
    		if (list[i].source === system.ID) {
    			count += 1;
    			if ((list[i].expiry - global.clock.adjustedSeconds) < 7200) expiry_count += 1;
    		}
    	}
    	if (count === expiry_count) return true;
    	return false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns the number of days since the last time one of these missions was accepted in the given system
    this.$lastMissionGapDays = function $lastMissionGapDays(sysID, missType) {
    	if (this._lastMission.length > 0) {
    		for (var i = 0; i < this._lastMission.length; i++) {
    			if (this._lastMission[i].missionType === missType && this._lastMission[i].system === sysID) {
    				return parseInt((clock.adjustedSeconds - this._lastMission[i].date) / 86400);
    			}
    		}
    	}
    	return 999;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // adds a record of successful missions
    this.$addMissionHistory = function $addMissionHistory(sysID, missType, completed, failed) {
    	var found = false;
    	for (var i = 0; i < this._missionHistory.length; i++) {
    		if (this._missionHistory[i].galaxy == galaxyNumber &&
    			this._missionHistory[i].system === sysID &&
    			this._missionHistory[i].missionType === missType) {
    
    			this._missionHistory[i].completedCount += completed;
    			this._missionHistory[i].failedCount += failed;
    			found = true;
    		}
    	}
    	if (found === false) {
    		this._missionHistory.push({
    			galaxy: galaxyNumber,
    			system: sysID,
    			missionType: missType,
    			completedCount: completed,
    			failedCount: failed
    		});
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns a value between 0.05 and 2 indicating the players reputation for a given mission type in a given system
    // used when calculating mission payments
    this.$playerMissionReputation = function $playerMissionReputation(sysID, missType) {
    	// if the reputation system is online, use it for rep calculations
    	if (worldScripts.GalCopBB_Reputation._disabled === false) {
    		return worldScripts.GalCopBB_Reputation.$playerMissionReputation(sysID, missType);
    	}
    	// otherwise use the basic calculation here.
    	var result = 1;
    	var recCount = 0;
    	var subFactor = 0;
    	var complete = 0;
    	for (var i = 0; i < this._missionHistory.length; i++) {
    		if ((sysID < 0 || (this._missionHistory[i].galaxy === galaxyNumber && this._missionHistory[i].system === sysID)) && this._missionHistory[i].missionType === missType) {
    			var complete = this._missionHistory[i].completed;
    			subFactor = this._missionHistory[i].completed - (this._missionHistory[i].failed * 1.5)
    			recCount = this._missionHistory[i].completed + this._missionHistory[i].failed;
    		}
    	}
    	if (recCount > 0) result = (subFactor / recCount) + (1 - Math.sqrt(Math.pow((complete / recCount), 2)));
    	if (result < 0) result = Math.abs(result) / 10;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns the number of successfully completed missions of a particular type in a particular system
    this.$getSuccessMissions = function $getSuccessMissions(sysID, missType) {
    	var result = 0;
    	for (var i = 0; i < this._missionHistory.length; i++) {
    		if (this._missionHistory[i].galaxy === galaxyNumber && this._missionHistory[i].system === sysID && this._missionHistory[i].missionType === missType) {
    			result = this._missionHistory[i].completed;
    			break;
    		}
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns the number of failed missions of a particular type in a particular system
    this.$getFailedMissions = function $getFailedMissions(sysID, missType) {
    	var result = 0;
    	for (var i = 0; i < this._missionHistory.length; i++) {
    		if (this._missionHistory[i].galaxy === galaxyNumber && this._missionHistory[i].system === sysID && this._missionHistory[i].missionType === missType) {
    			result = this._missionHistory[i].failed;
    			break;
    		}
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // use randomshipnames OXP to generate ship names (if installed) - otherwise just return a blank string (no name)
    this.$getRandomShipName = function $getRandomShipName(newship, role) {
    	var randomShipName = "";
    	if (this._rsnInstalled) {
    		if (newship == null) newship = player.ship;
    		if (newship) {
    			try {
    				if (Ship.roleIsInCategory(role, "oolite-bounty-hunter") || Ship.roleIsInCategory(role, "oolite-assassin"))
    					randomShipName = worldScripts["randomshipnames"].$randomHunterName(newship);
    				else if (Ship.roleIsInCategory(role, "oolite-pirate"))
    					randomShipName = worldScripts["randomshipnames"].$randomPirateName(newship);
    				// catch everything else as a trader
    				if (randomShipName === "")
    					randomShipName = worldScripts["randomshipnames"].$randomTraderName(newship);
    			} catch (err) {
    				log(this.name, "!!ERROR: Unable to get ship name from RSN: " + err);
    			}
    		}
    	}
    	return randomShipName;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns the player's target system (1.80) or the next jump to their target system (1.82)
    this.$playerTargetSystem = function $playerTargetSystem() {
    	if (player.ship.hasOwnProperty("nextSystem")) return player.ship.nextSystem;
    
    	var p = player.ship;
    	var target = p.targetSystem;
    	if (oolite.compareVersion("1.81") < 0 && p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY") === true) {
    		// in 1.81 or greater, the target system could be more than 7 ly away. It becomes, essentially, the final destination.
    		// there could be multiple interim stop points between the current system and the target system.
    		// the only way to get this info is to recreate a route using the same logic as entered on the ANA, and pick item 1
    		// from the list. That should be the next destination in the list.
    		var myRoute = System.infoForSystem(galaxyNumber, this._lastSource).routeToSystem(System.infoForSystem(galaxyNumber, target), p.routeMode);
    		if (myRoute) {
    			target = myRoute.route[1];
    		}
    	}
    	return target;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns the index of the first active, non-expired mission with a particular type
    this.$getActiveMissionIDByType = function $getActiveMissionIDByType(missType) {
    	var list = this.$getListOfMissions(true, missType);
    	if (list.length > 0) return list[0].ID;
    	return -1;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // writes some mission details to the log
    this.$logMissionData = function $logMissionData(missID) {
    	if (this._debug) {
    		var bb = worldScripts.BulletinBoardSystem;
    		log(this.name, "missionID " + missID);
    		var item = bb.$getItem(missID);
    		log(this.name, "target " + item.data.targetQuantity);
    		log(this.name, "quantity " + item.data.quantity);
    		log(this.name, "expiry " + clock.clockStringForTime(item.expiry));
    	}
    }
    
    //=============================================================================================================
    // spawned ship event handlers
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_cargo_shipDied = function $gcm_cargo_shipDied(whom, why) {
    	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);
    
    	if (this._debug) log(this.name, "!!OUCH! Special cargo destroyed " + why + ", " + whom);
    
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(this.ship.script._missionID);
    	if (item) {
    		item.data.destroyedQuantity += 1;
    		if (whom.isPlayer) item.payment = parseInt((item.payment * 0.9) * 10) / 10; // 10% penalty for loss cause by the player
    		bb.$updateBBMissionPercentage(item.ID, (item.data.quantity / (item.data.targetQuantity - item.data.destroyedQuantity)));
    		player.consoleMessage(expandDescription("[goal_updated]"));
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_garbage_shipDied = function $gcm_garbage_shipDied(whom, why) {
    	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);
    	var gcm = worldScripts.GalCopBB_Missions;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(this.ship.script._missionID);
    	if (why === "heat damage") {
    		// only increase the mission quantity if we're below the target (minus any that have been destroyed in other ways)
    		if (item && item.data.quantity < (item.data.targetQuantity - item.data.destroyedQuantity)) {
    			item.data.quantity += 1;
    			bb.$updateBBMissionPercentage(item.ID, item.data.quantity / item.data.targetQuantity);
    			gcm.$logMissionData(item.ID);
    			player.consoleMessage(expandDescription("[goal_updated]"));
    		}
    	} else {
    		// a little surprise...
    		this.ship.becomeCascadeExplosion();
    		if (item) {
    			item.data.destroyedQuantity += 1;
    			bb.$updateBBMissionPercentage(item.ID, item.data.quantity / item.data.targetQuantity);
    			player.consoleMessage(expandDescription("[goal_updated]"));
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_entity_shipDied = function $gcm_entity_shipDied(whom, why) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var bb = worldScripts.BulletinBoardSystem;
    
    	if (gcm._debug) log(this.name, "running shipDied for " + this.ship + ": reason " + why + ", " + whom);
    	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);
    
    	var item = bb.$getItem(this.ship.script._missionID);
    	if (item) {
    		item.data.destroyedQuantity = 1;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_monitorThargoid_shipDied = function $gcm_monitorThargoid_shipDied(whom, why) {
    	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);
    	//log(this.name, "adding dead thargoid position to array - " + this.ship.position);
    	//log(this.name, "Adding to dead thargoids " + this.ship.dataKey);
    	var gm = worldScripts.GalCopBB_Missions;
    	gm._thargoidPosition.push(this.ship.position);
    
    	var t = new Timer(gm, gm.$removePositionFromArray.bind(gm, this.ship.position), 5, 0);
    	gm._removeTimers.push(t);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$removePositionFromArray = function $removePositionFromArray(pos) {
    	var i = this._thargoidPosition.length;
    	while (i--) {
    		if (this._thargoidPosition[i].distanceTo(pos) == 0) {
    			//log(this.name, "removing position " + pos);
    			this._thargoidPosition.splice(i, 1);
    			break;
    		}
    	}
    }
    
    //=============================================================================================================
    // mission-specific functions
    //-------------------------------------------------------------------------------------------------------------
    this.$postScoopMission = function $postScoopMission() {
    	var id = this.$createSecondaryMission(this._postScoopMissionID, true);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // checks to see if the special computers have been removed from the ship
    this.$checkSpecialDeliveryRemoved = function $checkSpecialDeliveryRemoved() {
    	var itemCount = 0;
    	var list = this.$getListOfMissions(true, 42);
    	for (var i = 0; i < list.length; i++) {
    		if (list[i].data.quantity === 0 && list[i].data.destroyedQuantity === 0) itemCount += 1;
    	}
    	if (itemCount > 0 && itemCount > player.ship.manifest["computers"]) {
    		var id = this.$getActiveMissionIDByType(42);
    		if (id >= 0) {
    			var bb = worldScripts.BulletinBoardSystem;
    			var item = bb.$getItem(id);
    			item.data.destroyedQuantity = 1;
    			item.data.quantity = 0;
    			bb.$updateBBMissionPercentage(item.ID, 0);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns the number of thargoid alloys that have been collected
    this.$countThargoidAlloys = function $countThargoidAlloys() {
    	var qty = 0;
    	var list = this.$getListOfMissions(true, 13);
    	for (var i = 0; i < list.length; i++) {
    		if (list[i].data.quantity > 0) qty += list[i].data.quantity;
    	}
    	return qty;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // remove waypoints of a particular type that have been added to the system
    this.$removeSpecialWaypoint = function $removeSpecialWaypoint(wp_type) {
    	var wp = this._waypointList;
    	var found = false;
    
    	if (wp.length > 0) {
    		for (var i = wp.length - 1; i >= 0; i--) {
    			if (wp[i].indexOf(this.name + wp_type) >= 0 && wp[i].indexOf(this.name + "_cache") === -1) {
    				this.$unsetWaypoint(wp[i].replace(this.name + "_", ""));
    				found = true;
    			}
    			if (wp[i] && wp[i].indexOf(this.name + "_cache") >= 0) {
    				this.$unsetWaypoint(wp[i].replace(this.name + "_", ""));
    			}
    		}
    	}
    
    	return found;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$transmitFreeSlaves = function $transmitFreeSlaves() {
    	if (player.ship.target && player.ship.target.isValid && player.ship.position.distanceTo(player.ship.target) < player.ship.scannerRange) {
    		this._slaveDemandHistory.push(player.ship.target);
    		// work out the response
    		var resptype = 0;
    		var qty = 0;
    		// option 2: they don't have any slaves (true)
    		// option 3: they don't have any slaves (false ie a lie)
    		if (resptype === 0) {
    			var haveslaves = false;
    			for (var i = 0; i < player.ship.target.cargoList.length; i++) {
    				var itm = player.ship.target.cargoList[i];
    				if (itm.commodity === "slaves") {
    					haveslaves = true;
    					qty = itm.quantity;
    				}
    			}
    			if (haveslaves === true) {
    				if (Ship.roleIsInCategory(player.ship.target.primaryRole, "oolite-pirate")) {
    					// they're going to lie
    					if (Math.random() > 0.7) resptype = 3;
    				}
    			} else {
    				// they don't have any
    				resptype = 2;
    			}
    		}
    		// option 1: they free the slaves (if the player is a bigger threat or they're fleeing)
    		if (resptype === 0 && (player.ship.target.threatAssessment() < player.ship.threatAssessment() || player.ship.target.isFleeing === true)) resptype = 1;
    		// option 4: they ignore the player or tell them to get lost
    		if (resptype === 0) resptype = 4 + (Math.random() > 0.6 ? 1 : 0);
    
    		// send the message
    		if (resptype > 0 && resptype < 5) {
    			var msg = expandDescription("[gcm_freeslaves_response_type_" + resptype + "]");
    			player.ship.target.commsMessage(msg, player.ship);
    		}
    
    		if (resptype === 1) {
    			// eject all slaves
    			player.ship.target.dumpCargo(qty, "slaves");
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$checkForSlaveMission = function $checkForSlaveMission(sysID) {
    	this._slaveRescue = false;
    	// check for a type 12 mission here - saves having to do these checks every time we target a ship
    	var list = this.$getListOfMissions(true, 12);
    	for (var i = 0; i < list.length; i++) {
    		if (list[i].destination === sysID &&
    			list[i].data.quantity < list[i].data.targetQuantity &&
    			list[i].expiry > clock.adjustedSeconds) {
    			this._slaveRescue = true;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$addSlaveCommsMessage = function $addSlaveCommsMessage() {
    	if (this._slaveRescue === true) {
    		// add special message to broadcast comms
    		var bcc = worldScripts.BroadcastCommsMFD;
    		if (bcc.$checkMessageExists("gcm_demand_slaves") === false) {
    			bcc.$createMessage({
    				messageName: "gcm_demand_slaves",
    				callbackFunction: this.$transmitFreeSlaves.bind(this),
    				displayText: "Demand all slaves be freed",
    				messageText: "Drop all the slaves you have on board or be fired upon.",
    				transmissionType: "target",
    				deleteOnTransmit: false,
    				delayCallback: 5,
    				hideOnConditionRed: false
    			});
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$removeSlaveCommsMessage = function $removeSlaveCommsMessage() {
    	if (this._slaveRescue === true) {
    		// remove special message to broadcast comms
    		var bcc = worldScripts.BroadcastCommsMFD;
    		if (bcc.$checkMessageExists("gcm_demand_slaves") === true) bcc.$removeMessage("gcm_demand_slaves");
    	}
    	this._slaveRescue = false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // checks if a ship is within spawning range of a dead thargoid
    this.$shipNearDeadThargoid = function $shipNearDeadThargoid(ship) {
    	if (this._thargoidPosition.length === 0) return false;
    	for (var i = 0; i < this._thargoidPosition.length; i++) {
    		//log(this.name, "distance to deadThargoid = " + ship.position.distanceTo(this._thargoidPosition[i]));
    		if (ship.position.distanceTo(this._thargoidPosition[i]) < 2000) return true;
    	}
    	return false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // populates an array with ship data keys for use by the populator routines
    this.$getPreferredShipList = function $getPreferredShipList() {
    	// we've had instances where occassionally a ship won't be spawned in the destination system
    	// I suspect a ship script has a condition that I'm not catering for.
    	// to work around the issue, the following list of ship types are known to work OK, so in future
    	// only these types will be selected
    	var safe = ["Arachnid Mark 1","Arafura","Boa","Boa Class Cruiser","Bug","Chameleon","Chopped Cobra","Cobra AC",
    		"Cobra Mark III","Cobra Mark III-XT","Cobra Mark IV","Cruzer","D.T.T. Atlas","D.T.T. Cyclops","D.T.T. Kraken",
    		"D.T.T. MK-1","D.T.T. Snake Charmer","D.T.T. War Lance","DTT Mark I","Ghavial","Griff Boa Prototype",
    		"Imperial Trader","Lira","Miner Cobra Mark III","Monitor","Monitor Mark II","Mussurana","Ophidian","Ophidian Yacht",
    		"Python","Python Class Cruiser","Python ET Special","Salamander","Vampire Mark II Purgatori","Yasen-N","Yasen-N 'Advanced'"];
    
    	this._preferredTargetShips.length = 0;
    	var shipkeys = Ship.keysForRole("trader");
    	for (var i = 0; i < shipkeys.length; i++) {
    		var shipspec = Ship.shipDataForKey(shipkeys[i]);
    		if (safe.indexOf(shipspec.name) >= 0 && shipspec.max_flight_speed > 150 && shipspec.max_cargo > 15 && (typeof shipspec["hyperspace_motor"] == "undefined" || this._trueValues.indexOf(shipspec.hyperspace_motor) >= 0)) {
    			this._preferredTargetShips.push({
    				key: shipkeys[i],
    				cargoSpace: shipspec.max_cargo
    			});
    			if (shipspec.max_cargo > this._maxCargoOfShips) this._maxCargoOfShips = shipspec.max_cargo;
    		}
    	}
    }
    
    //--------------------------------------------------------------------------------------------------------------
    // populates the list of possible cargopods that can be spawned for missions
    this.$getPreferredCargoPodList = function $getPreferredCargoPodList() {
    	this._preferredCargoPods.length = 0;
    	var shipkeys = Ship.keysForRole("cargopod");
    	for (var i = 0; i < shipkeys.length; i++) {
    		var shipspec = Ship.shipDataForKey(shipkeys[i]);
    		if (shipspec.cargo_type != "CARGO_SCRIPTED_ITEM") this._preferredCargoPods.push(shipkeys[i]);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns a random ship key from the list
    this.$getRandomShipKey = function $getRandomShipKey(missID, minCargo) {
    	var result = "";
    	var tries = 0;
    	do {
    		var dta = this._preferredTargetShips[parseInt(system.scrambledPseudoRandomNumber(missID) * this._preferredTargetShips.length)];
    		if (dta.cargoSpace > minCargo) result = dta.key;
    		tries += 1;
    	} while (result === "" && tries < 100);
    	// if we tried 100 times and still didn't get a result, use the player's ship dataKey 
    	if (result === "") result = player.ship.dataKey;
    	return result;
    }
    
    //--------------------------------------------------------------------------------------------------------------
    // converts player's elite ranking into a number
    this.$playerRank = function $playerRank() {
    	var p = player;
    	if (p.score < 8) return 0; // harmless
    	if (p.score >= 8 && p.score < 16) return 1; // mostly harmless
    	if (p.score >= 16 && p.score < 32) return 2; //poor
    	if (p.score >= 32 && p.score < 64) return 3; //average
    	if (p.score >= 64 && p.score < 128) return 4; // above average
    	if (p.score >= 128 && p.score < 512) return 5; // competant
    	if (p.score >= 512 && p.score < 2560) return 6; // dangerous
    	if (p.score >= 2560 && p.score < 6400) return 7; // deadly
    	if (p.score >= 6400) return 8; // elite
    }
    
    //--------------------------------------------------------------------------------------------------------------
    // returns true if the passed value is numeric, otherwise false
    this.$isNumeric = function $isNumeric(n) {
    	return !isNaN(parseFloat(n)) && isFinite(n);
    }
    
    //--------------------------------------------------------------------------------------------------------------
    // returns the data key of a reasonably high powered ship
    this.$pickGetawayShip = function $pickGetawayShip() {
    	var shipkeys = Ship.keysForRole("assassin-medium");
    
    	if (shipkeys.length === 0) return "cobra3-pirate";
    	return shipkeys[Math.floor(Math.random() * shipkeys.length)];
    }
    
    //--------------------------------------------------------------------------------------------------------------
    // function used by Ship Configuration to determine if any cargo should not be allowed to be transferred to storage
    this.$cargoRestricted = function $cargoRestricted(commodity) {
    	var qty = 0;
    	var list = this.$getListOfMissions(true, 13);
    	for (var i = 0; i < list.length; i++) {
    		// look for any thargoid alloys with a type 13 mission
    		if (commodity === "alloys" && list[i].data.quantity > 0) qty += list[i].data.quantity;
    	}
    	return qty;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // play sound effects
    this.$playSound = function $playSound(soundtype) {
    	var mySound = new SoundSource;
    
    	switch (soundtype) {
    		case "mode":
    			mySound.sound = "[@click]";
    			break;
    		case "activate":
    			mySound.sound = "[@beep]";
    			break;
    		case "stop":
    			mySound.sound = "[@boop]";
    			break;
    	}
    	mySound.loop = false;
    	mySound.play();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns a list of missions
    // accepted (boolean) indicating whether only accepted (true) or available (false) missions should be returned
    // missType can be an integer, or an array of integers, indicating what types of missions should be returned
    this.$getListOfMissions = function $getListOfMissions(accepted, missType) {
    	var list = [];
    	var bb = worldScripts.BulletinBoardSystem;
    	for (var i = 0; i < bb._data.length; i++) {
    		var item = bb._data[i];
    		if (item.data && item.data.hasOwnProperty("source") && item.data.source === this.name) {
    			if (!accepted || item.accepted === accepted) {
    				if (!missType) {
    					list.push(item);
    				} else {
    					if (Array.isArray(missType) === false) {
    						if (item.data.missionType === missType) list.push(item);
    					} else {
    						if (missType.indexOf(item.data.missionType) >= 0) list.push(item);
    					}
    				}
    			}
    		}
    	}
    	return list;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // cleans up any old records that were created before the "data" element was added to the BB. 
    this.$validateActiveData = function $validateActiveData() {
    	var bb = worldScripts.BulletinBoardSystem;
    	for (var i = bb._data.length - 1; i >= 0; i--) {
    		if (bb._data[i].worldScript === this.name &&
    			(bb._data[i].hasOwnProperty("data") === false || bb._data[i].data === "")) {
    			bb._data.splice(i, 1);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // function called by system populator to add special assassins to a system.
    // these assassins are only in search of the player and will only appear at the witchpoint.
    // although if they can't find the player they will revert to doing what assassins do.
    // this is largely a copy of the _addAssassin routine in the oolite populator.
    // the only real change is that we are forcing our new AI on these ships
    this.$addGCMAssassin = function $addGCMAssassin(pos) {
    	var op = worldScripts["oolite-populator"];
    
    	var role = "assassin-light";
    	var extra = 0;
    	var ws = 2;
    	var g = system.info.government + 2;
    	if (Math.random() > g / 10) {
    		role = "assassin-medium";
    		extra = 1;
    		ws = 2.5;
    		if (Math.random() > g / 5) {
    			role = "assassin-heavy";
    			ws = 2.8;
    		}
    	}
    	if (!op._roleExists(role)) {
    		log(this.name, "No ships with role " + role + " defined - skipping addition");
    		return;
    	}
    	var main = op._addShips(role, 1, pos, 0)[0];
    	if (main.autoWeapons) {
    		main.awardEquipment("EQ_FUEL_INJECTION");
    		main.awardEquipment("EQ_ECM");
    		if (2 + Math.random() < ws) {
    			main.awardEquipment("EQ_SHIELD_BOOSTER");
    		}
    		// assassins don't respect escape pods and won't expect anyone else to either.
    		main.removeEquipment("EQ_ESCAPE_POD");
    		main.fuel = 7;
    		op._setWeapons(main, ws);
    		op._setSkill(main, extra);
    	}
    	// make sure the AI is switched
    	main.switchAI("gcm-assassinAI.js");
    
    	if (extra > 0) {
    		var g = new ShipGroup("assassin group", main);
    		main.group = g;
    		var numext = Math.floor(Math.random() * 3) + 1;
    		if (role === "assassin-heavy") {
    			var extras = op._addShips("assassin-medium", numext, pos, 3E3);
    		} else {
    			var extras = op._addShips("assassin-light", numext, pos, 3E3);
    		}
    		for (var i = 0; i < numext; i++) {
    			extras[i].group = g;
    			g.addShip(extras[i]);
    			if (extras[i].autoWeapons) {
    				extras[i].awardEquipment("EQ_FUEL_INJECTION");
    				extras[i].removeEquipment("EQ_ESCAPE_POD");
    				extras[i].fuel = 7;
    				op._setWeapons(extras[i], 1.8);
    			}
    			// make sure the AI is switched
    			extras[i].switchAI("gcm-assassinAI.js");
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$postScoopMissionCreation = function $postScoopMissionCreation(missID, delay) {
    	this._postScoopMissionID = missID;
    	this._newMissionDelayTimer = new Timer(this, this.$postScoopMission, delay, 0);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$getCommodityUnit = function $getCommodityUnit(commodity) {
    	if (commodity == "") return "";
    	return this._commodityUnit[commodity];
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // translates a text-based guiTextureIdentifier from descriptions.plist into a proper guiTextureIdentifier
    // format is:  filename.png~height=n~width=n
    this.$getTexture = function $getTexture(data) {
    	if (data === "") return "";
    	var items = data.split("~");
    	var height = "";
    	var width = "";
    	if (items.length > 1) {
    		if (items[1].indexOf("height") >= 0) height = items[1].split("=")[1];
    		if (items[1].indexOf("width") >= 0) width = items[1].split("=")[1];
    	}
    	if (items.length > 2) {
    		if (items[2].indexOf("height") >= 0) height = items[2].split("=")[1];
    		if (items[2].indexOf("width") >= 0) width = items[2].split("=")[1];
    	}
    	if (height === "" && width === "") {
    		return items[0];
    	}
    	if (height === "" && width !== "") {
    		return {
    			name: items[0],
    			width: width
    		};
    	}
    	if (height !== "" && width === "") {
    		return {
    			name: items[0],
    			height: height
    		};
    	}
    	if (height !== "" && width !== "") {
    		return {
    			name: items[0],
    			height: height,
    			width: width
    		};
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$modelIsAllowed = function $modelIsAllowed(shipkey) {
    	var shipdata = Ship.shipDataForKey(shipkey);
    	// are we allowed to include this data key in this system? check the conditions if there are some
    	var include = true;
    	if (shipdata.conditions) {
    		var cond = shipdata.conditions.toString().split(",");
    		//1,systemGovernment_number equal 4,systemGovernment_number,0,0,4,
    		//1,systemEconomy_number notequal 4,systemEconomy_number,1,0,4
    		//1,systemEconomy_number lessthan 4,systemEconomy_number,2,0,4
    		//1,systemEconomy_number greaterthan 4,systemEconomy_number,3,0,4
    		var offset = 0;
    		var finish = false;
    		var checking = -1;
    		do {
    			// get the value we're checking
    			checking = -1;
    			if (cond[offset + 2].substring(0, 6) === "mission") {
    				if (missionVariables[cond[offset + 2].replace("mission_", "")]) {
    					checking = missionVariables[cond[offset + 2].replace("mission_", "")];
    					log(this.name, "field = " + cond[offset + 2] + ", value = " + checking);
    				} else {
    					log(this.name, "!!NOTE: Condition value mission variable not set: " + cond[offset + 2]);
    				}
    			} else {
    				switch (cond[offset + 2]) {
    					case "systemGovernment_number":
    						checking = system.government;
    						break;
    					case "systemEconomy_number":
    						checking = system.economy;
    						break;
    					case "systemTechLevel_number":
    						checking = system.techLevel;
    						break;
    					case "score_number":
    						checking = player.score;
    						break;
    					case "galaxy_number":
    						checking = galaxyNumber;
    						break;
    					default:
    						log(this.name, "!!NOTE: Condition value not catered for: " + cond[offset + 2]);
    						break;
    				}
    			}
    			// in case a mission variable is a text value of some sort
    			if (isNaN(parseInt(checking)) && isNaN(parseFloat(checking))) {
    				switch (cond[offset + 3]) {
    					case "0": // equals
    						if (checking != cond[offset + 5]) include = false;
    						break;
    					case "1": // not equals
    						if (checking == cond[offset + 5]) include = false;
    						break;
    					default:
    						log(this.name, "!!NOTE: Condition comparison not catered for: " + cond[offset + 3]);
    						break;
    				}
    			} else {
    				if (checking >= 0) {
    					// work out the type of check, but in negative (or opposite)
    					switch (cond[offset + 3]) {
    						case "0": // equals
    							if (checking !== parseInt(cond[offset + 5])) include = false;
    							break;
    						case "1": // not equals
    							if (checking === parseInt(cond[offset + 5])) include = false;
    							break;
    						case "2": // lessthan
    							if (checking >= parseInt(cond[offset + 5])) include = false;
    							break;
    						case "3": // greaterthan
    							if (checking <= parseInt(cond[offset + 5])) include = false;
    							break;
    						default:
    							log(this.name, "!!NOTE: Condition comparison not catered for: " + cond[offset + 3]);
    							break;
    							// others?
    					}
    				}
    			}
    			offset += 6;
    			if (offset >= cond.length - 1) finish = true;
    		} while (finish === false);
    	} else if (shipdata.condition_script) {
    		// or the condition script
    		// create a dummy object to attach the script to so it can be executed
    		var temppos = system.sun.position.cross(system.mainPlanet.position).direction().multiply(4E9).subtract(system.mainPlanet.position);
    		var tempalloy = system.addShips("alloy", 1, temppos, 0);
    		if (tempalloy) {
    			tempalloy[0].setScript(shipdata.condition_script);
    			include = tempalloy[0].script.allowSpawnShip(key);
    			tempalloy[0].remove(true);
    		}
    	} else {
    		// otherwise we're free to play
    		include = true;
    	}
    	return include;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // routine to check the combat simulator worldscript, to see if it's running or not
    this.$simulatorRunning = function $simulatorRunning() {
    	var w = worldScripts["Combat Simulator"];
    	if (w && w.$checkFight && w.$checkFight.isRunning) return true;
    	return false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateSuccessHistoryReputation = function $updateSuccessHistoryReputation(item) {
    	// update mission history
    	this.$addMissionHistory(item.source, item.data.missionType, 1, 0);
    
    	this.$checkForFollowupMission(item);
    
    	// adjust reputation with entities
    	var rep = worldScripts.GalCopBB_Reputation;
    	if (rep._disabled === false) {
    		rep.$adjustReputationSuccess(item.data.missionType, item.source, item.destination, item.percentComplete);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateFailedHistoryReputation = function $updateFailedHistoryReputation(item) {
    	// update mission history
    	this.$addMissionHistory(item.source, item.data.missionType, 0, 1);
    	// adjust reputation with entities
    	// reputation only goes in one way (up or down)
    	// so even if the player completes 90% of a mission and then terminates it, their reputation will go down even if they received most of the payment credits
    	var rep = worldScripts.GalCopBB_Reputation;
    	if (rep._disabled === false) {
    		rep.$adjustReputationFailure(item.data.missionType, item.source, item.destination, item.percentComplete);
    	}
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // finds a spawn position just outside scanner range in a view other than the player's current view
    this.$findPosition = function $findPosition(altPos) {
    	var p = player.ship;
    	// limit is just outside scanner range
    	var limit = p.scannerRange + 1000;
    	var pos = p.position;
    	if (altPos) {
    		var pl = altPos;
    	} else {
    		var pl = system.mainPlanet.position;
    	}
    	if (p.viewDirection === "VIEW_CUSTOM" || p.viewDirection === "VIEW_GUI_DISPLAY") return null;
    	var done = false;
    	var redo = true;
    	do {
    		do {
    			var dir = Vector3D.randomDirection(limit);
    			var spwn = pos.add(dir);
    			var dev1 = 0;
    			var dev2 = 0;
    			switch (p.viewDirection) {
    				case "VIEW_FORWARD":
    					dev1 = p.vectorForward.angleTo(spwn.subtract(pos));
    					dev2 = p.vectorUp.angleTo(spwn.subtract(pos));
    					break;
    				case "VIEW_GUI_DISPLAY":
    				case "VIEW_AFT":
    					dev1 = p.vectorForward.angleTo(pos.subtract(spwn));
    					dev2 = p.vectorUp.angleTo(pos.subtract(spwn));
    					break;
    				case "VIEW_PORT":
    					dev1 = p.vectorRight.angleTo(pos.subtract(spwn));
    					dev2 = p.vectorUp.angleTo(pos.subtract(spwn));
    					break;
    				case "VIEW_STARBOARD":
    					dev1 = p.vectorRight.angleTo(spwn.subtract(pos));
    					dev2 = p.vectorUp.angleTo(pos.subtract(spwn));
    					break;
    			}
    			if (dev1 > 0.8 && dev2 > 1.0) done = true;
    		} while (done === false);
    		if (pl.distanceTo(spwn) > pl.distanceTo(pos)) redo = false;
    	} while (redo === true);
    	return spwn;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // creates a mission of a specific type, ignoring any mission conditions
    this.$testMissionType = function $testMissionType(missionType) {
    	this._forceCreate.push(missionType);
    	this.$addLocalMissions([missionType], "");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // outputs all data for a particular mission
    this.$logMissionDataEnhanced = function $logMissionDataEnhanced(missID, header) {
    	var bb = worldScripts.BulletinBoardSystem;
    	log(this.name, "==========================");
    	if (header && header != "") log(this.name, "=== " + header + "===");
    	var item = bb.$getItem(missID);
    	this.$startLoggingProcess(item, 0);
    	log(this.name, "==========================");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$startLoggingProcess = function $startLoggingProcess(item, indent) {
    	var keys = Object.keys(item);
    	for (var i = 0; i < keys.length; i++) {
    		var vl = item[keys[i]];
    		this.$logDetail(keys[i], vl, indent);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$logDetail = function $logDetail(key, vl, indent) {
    	var typ = typeof vl;
    	var pads = "......";
    	switch (typ) {
    		case "object":
    			if (Array.isArray(vl)) {
    				log(this.name, pads.substring(0, indent) + key + " (array): " + JSON.stringify(vl));
    			} else if (vl && vl.constructor == Object) {
    				log(this.name, pads.substring(0, indent) + key + " (dictionary):");
    				this.$startLoggingProcess(vl, indent + 1);
    			}
    			break;
    		case "string":
    			log(this.name, pads.substring(0, indent) + key + ": " + "\"" + vl + "\"");
    			break;
    		default:
    			log(this.name, pads.substring(0, indent) + key + ": " + vl);
    	}
    }
    Scripts/galcopbb_missions_mfd.js
    "use strict";
    this.name = "GalCopBB_Missions_MFD";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Controls the output the MFD";
    this.license = "CC BY-NC-SA 4.0";
    
    this._lineLength = 14;
    this._mfdID = -1;
    this._target = null; // holds a reference to the target with whom comms is happening
    this._offerTimeout = 90; // number of seconds to wait for response
    this._offerTimer = null; // timer for mission offer
    this._timeoutCounter = 0;
    this._currentMFDText = "";
    this._displayPoint = 0;
    this._holdData = {};
    this._subMessageTimer = null;
    this._originalMsg = "";
    this._final = false;
    this._autoAccept = false;
    this._declineMessageTimer = null;
    this._declineType = false;
    this._acceptMessageTimer = null;
    this._acceptType = false;
    this._disablReplyOptions = false;
    this._escapePod = null;
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillEnterWitchspace = function (cause, destination) {
    	if (this._offerTimer && this._offerTimer.isRunning) {
    		this.$stopTimers();
    		worldScripts.GalCopBB_Missions.$declinePendingMission();
    		this.$removeBCReplyOptions();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillDockWithStation = function (station) {
    	if (worldScripts.GalCopBB_Missions._simulator === true) return;
    	if (this._offerTimer && this._offerTimer.isRunning) {
    		// make sure we don't leave any half-accepted missions on the boards
    		this.$stopTimers();
    		worldScripts.GalCopBB_Missions.$declinePendingMission();
    		this.$removeBCReplyOptions();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = function (whom, why) {
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$stopTimers = function $stopTimers() {
    	if (this._offerTimer && this._offerTimer.isRunning) {
    		this._offerTimer.stop();
    		this._offerTimer = null;
    	}
    	if (this._subMessageTimer && this._subMessageTimer.isRunning) {
    		this._subMessageTimer.stop();
    		this._subMessageTimer = null;
    	}
    	if (this._declineMessageTimer && this._declineMessageTimer.isRunning) {
    		this._declineMessageTimer.stop();
    		this._declineMessageTimer = null;
    	}
    	this._declineType = false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // breaks text up into appropriate lengths for an MFD
    this.$processText = function $processText(msg) {
    	var line = "";
    	// the replace here should ensure all newline commands are at the end of a word, and not in the middle of one
    	var words = msg.replace(new RegExp("\n", 'g'), "\n ").split(" ");
    	var word = "";
    	var output = "";
    
    	for (var i = 0; i < words.length; i++) {
    		word = words[i].trim();
    		// make sure we have a word to add before trying to add it
    		if (word !== "") {
    			if (defaultFont.measureString(line + " " + word) <= this._lineLength) {
    				line += (line === "" ? "" : " ") + word;
    				// if the word ended in a newline command, add the line to the output and reset
    				if (word.indexOf("\n", word.length - 2) !== -1) {
    					output += line;
    					line = "";
    				}
    			} else {
    				output += line + "\n";
    				line = word;
    				// if the word ended in a newline command, add the line to the output and reset
    				if (word.indexOf("\n", word.length - 2) !== -1) {
    					output += line;
    					line = "";
    				}
    			}
    		}
    	}
    	output += line;
    
    	// update MFD so that new text goes on the bottom.
    	var lines = this._currentMFDText.split("\n");
    	if (this._currentMFDText === "") lines = [];
    	var newlines = output.split("\n");
    
    	for (var i = 0; i < newlines.length; i++) {
    		lines.push(newlines[i]);
    	}
    
    	var final = "";
    	if (lines.length <= 10) {
    		final = lines.join("\n");
    	} else {
    		for (var i = lines.length - 10; i < lines.length; i++) {
    			final += (final === "" ? "" : "\n") + lines[i];
    		}
    	}
    
    	this._currentMFDText = final;
    
    	return final;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // updates the text of the MFD
    this.$updateMFD = function $updateMFD(msg, useComms, src, autoAccept) {
    	var p = player.ship;
    
    	if (this._originalMsg === "") this._originalMsg = msg;
    	if (autoAccept && autoAccept === true) {
    		this._autoAccept = autoAccept;
    	} else {
    		this._autoAccept = false;
    	}
    
    	var msglist = msg.split("\n");
    	var currmsg = msglist[this._displayPoint];
    	// if this is the final message, just select the last one
    	if (this._final === true) {
    		currmsg = msglist[msglist.length - 1];
    	}
    
    	// if the hud is hidden don't try an update - it will get confusing as to which mfd slot is open or not.
    	if (p.hudHidden === false) {
    		// if useComms has been set to true, disable the MFD to force comms method instead
    		if (useComms === true) {
    			this._mfdID = -1;
    		} else {
    			// find the slot currently set for this MFD
    			this.$findMFDID();
    			// if we haven't got a set slot (this._mfdID === -1) or the set slot we had is now unavailable...
    			if (this._mfdID === -1 ||
    				(p.multiFunctionDisplayList[this._mfdID] && p.multiFunctionDisplayList[this._mfdID] !== "" && p.multiFunctionDisplayList[this._mfdID] !== this.name)) {
    				// find a free slot
    				// first, make sure we reset our slot id marker (in the case where the previous one is in use)
    				this._mfdID = -1;
    				// search for a free slot
    				for (var i = 0; i < p.multiFunctionDisplayList.length; i++) {
    					if (!p.multiFunctionDisplayList[i] || p.multiFunctionDisplayList[i] === "") {
    						this._mfdID = i;
    						break;
    					}
    				}
    			}
    		}
    
    		// we have a free slot, so force the mfd to display
    		if (this._mfdID !== -1) {
    			// set the text in the MFD
    			var output = this.$processText(currmsg);
    			p.setMultiFunctionText(this.name, output, false);
    			p.setMultiFunctionDisplay(this._mfdID, this.name);
    		} else {
    			if (useComms === true && src && src.isInSpace) {
    				if (player.ship.position.distanceTo(src) < player.ship.scannerRange) {
    					// yay! comms is working!
    					src.commsMessage(currmsg, p);
    				} else {
    					// out of range
    					// got back a step, and try again shortly
    					this._displayPoint -= 1;
    				}
    			} else {
    				// oh well, make it a console message
    				player.consoleMessage(currmsg, 20);
    			}
    		}
    	}
    
    	if (this._final === false) {
    		this._displayPoint += 1;
    		if (this._displayPoint < msglist.length) {
    			this._holdData.msg = msg;
    			this._holdData.useComms = useComms;
    			this._holdData.src = src;
    			this._subMessageTimer = new Timer(this, this.$processNextMessage, 5, 0);
    			// return at this point so the accept/reject items only appear when all the messages are displayed.
    			return;
    		}
    	}
    
    	this._holdData = {};
    	// get a local reference to the ship that is making the offer
    	if (src) this._target = src;
    	if (this._disablReplyOptions === false) this.$displayBCReplyOptions();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // re-run the update function so the next message in the list can be displayed
    this.$processNextMessage = function $processNextMessage() {
    	this.$updateMFD(this._holdData.msg, this._holdData.useComms, this._holdData.src, this._autoAccept);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // records the index of the MFD that currently holds the damage report mfd
    this.$findMFDID = function $findMFDID() {
    	var p = player.ship;
    	for (var i = 0; i < p.multiFunctionDisplayList.length; i++) {
    		if (p.multiFunctionDisplayList[i] === this.name) this._mfdID = i;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // hides all instances of the damage report MFD
    this.$autoHideMFD = function $autoHideMFD() {
    	var p = player.ship;
    	if (p && p.multiFunctionDisplayList) {
    		for (var i = 0; i < p.multiFunctionDisplayList.length; i++) {
    			if (p.multiFunctionDisplayList[i] === this.name) {
    				p.setMultiFunctionDisplay(i, "");
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // add the secondary mission accept/decline options to broadcast comms
    this.$displayBCReplyOptions = function $displayBCReplyOptions() {
    	if (this._autoAccept === true) {
    		worldScripts.GalCopBB_Missions.$acceptPendingMission();
    		this.$turnOffMFD();
    		return;
    	}
    	var bc = worldScripts.BroadcastCommsMFD;
    	if (bc.$checkMessageExists("gcm_accept_mission") === true || bc.$checkMessageExists("gcm_decline_mission") === true) return;
    	bc.$createMessage({
    		messageName: "gcm_accept_mission",
    		callbackFunction: this.$acceptNewMission.bind(this),
    		displayText: "(* Accept Mission *)",
    		messageText: "",
    		transmissionType: "broadcast",
    		deleteOnTransmit: true,
    		delayCallback: 1
    	});
    	bc.$createMessage({
    		messageName: "gcm_decline_mission",
    		callbackFunction: this.$declineNewMission.bind(this),
    		displayText: "(* Decline Mission *)",
    		messageText: "",
    		transmissionType: "broadcast",
    		deleteOnTransmit: true,
    		delayCallback: 1
    	});
    	// reset the timer counter
    	this._timeoutCounter = 0;
    	// start a timer to wait for player response
    	this._offerTimer = new Timer(this, this.$removeOffer, 1, 1);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptNewMission = function $acceptNewMission() {
    	if (this._target) {
    		player.ship.commsMessage(expandDescription("[gcm_accept_secondary]"));
    	} else {
    		player.consoleMessage("Mission accepted");
    		if (this._mfdID !== -1) {
    			//this._final = true;
    			this.$updateMFD(this._originalMsg + "\n>> " + expandDescription("[gcm_accept_secondary]"), false, null);
    		}
    	}
    	worldScripts.GalCopBB_Missions.$acceptPendingMission();
    	this.$removeBCReplyOptions();
    	this._acceptMessageTimer = new Timer(this, this.$sendAcceptComms, 5, 0);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$sendAcceptComms = function $sendAcceptComms() {
    	if (this._acceptType == false) {
    		if (this._target) {
    			this._target.commsMessage(expandDescription("[gcm_accept_response]"), player.ship);
    		} else {
    			if (this._mfdID !== -1) {
    				this._final = true;
    				this._disablReplyOptions = true;
    				this.$updateMFD(this._originalMsg + "\n" + expandDescription("[gcm_accept_response]"), false, null);
    			}
    		}
    	}
    	// turn off the MFD (if it was used)
    	this.$turnOffMFD();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$declineNewMission = function $declineNewMission(send_message) {
    	if (send_message == null) {
    		this._declineType = false;
    		if (this._target) {
    			player.ship.commsMessage(expandDescription("[gcm_decline_secondary]"));
    		} else {
    			player.consoleMessage("Mission declined");
    			if (this._mfdID !== -1) {
    				//this._final = true;
    				this.$updateMFD(this._originalMsg + "\n>> " + expandDescription("[gcm_decline_secondary]"), false, null);
    			}
    		}
    	} else {
    		this._declineType = true;
    	}
    	this.$removeBCReplyOptions();
    	this._declineMessageTimer = new Timer(this, this.$sendDeclineComms, 5, 0);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$sendDeclineComms = function $sendDeclineComms() {
    	if (this._declineType == false) {
    		if (this._target) {
    			this._target.commsMessage(expandDescription("[gcm_decline_response]"), player.ship);
    		} else {
    			if (this._mfdID !== -1) {
    				this._final = true;
    				this._disablReplyOptions = true;
    				this.$updateMFD(this._originalMsg + "\n" + expandDescription("[gcm_decline_response]"), false, null);
    			}
    		}
    	}
    	worldScripts.GalCopBB_Missions.$declinePendingMission();
    	// turn off the MFD (if it was used)
    	this.$turnOffMFD();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$turnOffMFD = function $turnOffMFD() {
    	// turn off the MFD (if it was used)
    	this._currentMFDText = "";
    	this._originalMsg = "";
    	this._final = false;
    	this._displayPoint = 0;
    	this._target = null;
    	if (this._mfdID !== -1) {
    		if (this._subMessageTimer && this._subMessageTimer.isRunning) this._subMessageTimer.stop();
    		this._subMessageTimer = new Timer(this, this.$autoHideMFD, 3, 0);
    	}
    	this._disablReplyOptions = false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$removeBCReplyOptions = function $removeBCReplyOptions() {
    	this._offerTimer.stop();
    	this._offerTimer = null;
    	var bc = worldScripts.BroadcastCommsMFD;
    	if (bc.$checkMessageExists("gcm_accept_mission") === true) bc.$removeMessage("gcm_accept_mission");
    	if (bc.$checkMessageExists("gcm_decline_mission") === true) bc.$removeMessage("gcm_decline_mission");
    
    	// clean up the escape pod entity, if present
    	if (this._escapePod && this._escapePod.script) {
    		delete this._escapePod.script.shipWasDumped;
    		if (this._escapePod.script.$gcmovr_shipWasDumped) {
    			this._escapePod.script.shipWasDumped = this._escapePod.script.$gcmovr_shipWasDumped;
    			delete this._escapePod.script.$gcmovr_shipWasDumped;
    		}
    	}
    	this._escapePod = null;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$removeOffer = function $removeOffer() {
    	this._timeoutCounter += 1;
    	// have we timed out
    	if (this._timeoutCounter >= this._offerTimeout) {
    		this.$declineNewMission();
    		return;
    	}
    	// if the offer came from another ship, is that ship still in range?
    	if (this._target && (this._target.isValid === false || player.ship.position.distanceTo(this._target) > player.ship.scannerRange)) {
    		this.$declineNewMission(true);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcmmfd_shipWasDumped = function $gcmmfd_shipWasDumped(dumper) {
    	if (this.ship.script.$gcmovr_shipWasDumped) this.ship.script.$gcmovr_shipWasDumped(dumper);
    
    	worldScripts.GalCopBB_Missions_MFD.$stopTimers();
    	worldScripts.GalCopBB_Missions.$declinePendingMission();
    	worldScripts.GalCopBB_Missions_MFD.$removeBCReplyOptions();
    }
    Scripts/galcopbb_missionvalues.js
    "use strict";
    this.name = "GalCopBB_CoreMissionValues";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "End points for most mission value settings";
    this.license = "CC BY-NC-SA 4.0";
    
    /*
    returned object must return:
    	quantity
    	price
    	expiry
    can optionally return (if required for the mission)
    	locationType
    	penalty
    	commodity
    	clientName
    */
    
    this._maxLocations = 0;
    this._commodityAverage = {
    	"food": 5.0,
    	"textiles": 7.2,
    	"radioactives": 23.2,
    	"slaves": 15.2,
    	"liquor_wines": 29.2,
    	"luxuries": 90.2,
    	"narcotics": 51.0,
    	"computers": 81.8,
    	"machinery": 56.6,
    	"alloys": 38.8,
    	"firearms": 69.2,
    	"furs": 70.5,
    	"minerals": 12.0,
    	"gold": 38.8,
    	"platinum": 71.8,
    	"gem_stones": 19.6,
    	"alien_items": 43.6
    };
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
    	var missionTypes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18];
    	var cargoMissions = [8, 9, 10, 11, 12, 14, 15, 17];
    
    	var gcm = worldScripts.GalCopBB_Missions;
    	// position 7 is not used in any of these mission types, so limit the possibilities 
    	this._maxLocations = gcm._positions.length - 1;
    
    	// add our mission types into the main control
    	gcm._availableMissionTypes = gcm._availableMissionTypes.concat(missionTypes);
    	gcm._cargoMissionTypes = gcm._cargoMissionTypes.concat(cargoMissions);
    	gcm._interstellarMissionTypes = gcm._interstellarMissionTypes.concat([5, 13]);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 1 - asteroid hunt
    this.$missionType1_Values = function $missionType1_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = parseInt((Math.random() * 10) + 15);
    	// make sure we're not asking for more asteroids that are currently in the system
    	var avail = system.countShipsWithRole("asteroid");
    	if (result > avail) result = parseInt(avail / 2);
    
    	// price is a straight (quantity * 5)
    	result["price"] = (result.quantity * 5) +
    		this.$calcPlayerBonus(200); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + workTime + (result.quantity * 600); // 1 hour + 10 minutes per asteroid to complete.
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 2 - pirate hunt
    this.$missionType2_Values = function $missionType2_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = parseInt((Math.random() * 8) + 6);
    	// base the reward rate on danger level in the target system
    	var amt = parseInt((result.quantity * (parseInt(Math.random() * ((4 - destSysInfo.government) * 50)) + 50)) / 10) * 10 +
    		this.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	// occasionally double the price
    	if (Math.random() > 0.95 && destSysInfo.government < 2) amt *= 2;
    	result["price"] = amt;
    	result["expiry"] = clock.adjustedSeconds + routeTime + (result.quantity * 1800) + workTime; // transit time + 30 mins per ship + 1 hour to complete
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 3 - trader hunt (local gov)
    this.$missionType3_Values = function $missionType3_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = parseInt((Math.random() * 10) + 7);
    	// base the reward rate on the danger level in the target system
    	// danger in this case is the likelyhood of being caught by police
    	result["price"] = parseInt((result.quantity * (parseInt(Math.random() * (7 - destSysInfo.government) * 50) + 50)) / 10) * 10 +
    		this.$calcPlayerBonus(400); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + (result.quantity * 600) + workTime; // transit time + 10 mins per ship + 1 hour to complete
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 4 - trader hunt (pirate)
    this.$missionType4_Values = function $missionType4_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = parseInt((Math.random() * 10) + 7);
    	// base the reward rate on the danger level in the target system
    	// danger in this case is the likelyhood of being caught by police
    	result["price"] = parseInt((result.quantity * (parseInt(Math.random() * (7 - destSysInfo.government) * 100) + 50)) / 10) * 10 +
    		this.$calcPlayerBonus(800); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + (result.quantity * 500) + workTime; // transit time + 8 mins per ship + 1 hour to complete
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 5 - thargoid hunt
    this.$missionType5_Values = function $missionType5_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = parseInt((Math.random() * 7) + 3);
    	result["price"] = parseInt((result.quantity * (parseInt(Math.random() * 100) + 150)) / 10) * 10 +
    		this.$calcPlayerBonus(1000); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + (result.quantity * 600) + workTime; // transit time + 10 mins per ship + 1 hour to complete
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 6 - recover cargo (local gov)
    this.$missionType6_Values = function $missionType6_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	// pick a location
    	var ast = worldScripts.GalCopBB_AsteroidFields.$systemHasAsteroidField(destSysInfo.ID);
    	if (ast == null) {
    		result["locationType"] = Math.floor(Math.random() * this._maxLocations);
    	} else {
    		result["locationType"] = ast.locationType;
    	}
    
    	// don't make missions greater than the player's cargo capacity
    	var max = (player.ship.cargoSpaceCapacity > 20 ? 20 : player.ship.cargoSpaceCapacity);
    
    	result["quantity"] = parseInt((Math.random() * max) + 1);
    	// don't add this mission if the quantity is 5 or less
    	if (result.quantity <= 5) return null;
    
    	// pick a valuable commodity (higher chances for gold/platinum/gems)
    	var p_metals = ["gold", "platinum", "gem_stones", "luxuries", "computers", "gold", "platinum", "gem_stones"];
    	result["commodity"] = p_metals[Math.floor(Math.random() * p_metals.length)];
    	var c_price = system.mainStation.market[result.commodity].price / 10;
    	var base_price = result.quantity * (c_price * 1.05);
    	//(result.quantity * (parseInt(Math.random() * 40) + 20))
    	// price is now based on the market value for the commodity
    	// so player won't be (well, shouldn't be) tempted to sell cargo for a better profit
    	result["price"] = parseInt(base_price / 10) * 10 + (7 - destSysInfo.government) * 50 +
    		this.$calcDistanceBonus(routeDistance, 10) // plus a distance bonus
    		+
    		this.$calcPlayerBonus(200); // plus a possible bonus price, based on player score 
    
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price / 5);
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 7 - recover cargo (pirate)
    this.$missionType7_Values = function $missionType7_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	// pick a location
    	result["locationType"] = Math.floor(Math.random() * this._maxLocations);
    	// don't make missions greater than the player's cargo capacity
    	var max = (player.ship.cargoSpaceCapacity > 20 ? 20 : player.ship.cargoSpaceCapacity);
    
    	result["quantity"] = parseInt((Math.random() * max) + 1);
    	// don't add this mission if the quantity is 5 or less
    	if (result.quantity <= 5) return null;
    
    	// pick a valuable commodity (higher chances for gold/platinum/gems)
    	var p_metals = ["gold", "platinum", "gem_stones", "luxuries", "computers", "gold", "platinum", "gem_stones"];
    	result["commodity"] = p_metals[Math.floor(Math.random() * p_metals.length)];
    	var c_price = system.mainStation.market[result.commodity].price / 10;
    	var base_price = result.quantity * (c_price * 1.2);
    	//(result.quantity * (parseInt(Math.random() * 80) + 20))
    	// price is now based on the market value for the commodity
    	// so player won't be (well, shouldn't be) tempted to sell cargo for a better profit
    	result["price"] = parseInt(base_price / 10) * 10 + (7 - destSysInfo.government) * 50 +
    		this.$calcDistanceBonus(routeDistance, 10) // plus a distance bonus
    		+
    		this.$calcPlayerBonus(400); // plus a possible bonus price, based on player score 
    
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price / 5);
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 8 - piracy for any cargo (local gov)
    this.$missionType8_Values = function $missionType8_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	var max = 30; //player.ship.cargoSpaceCapacity;
    	//if (max > 30) max = 30;
    	var min = 5;
    	if (min > player.ship.cargoSpaceCapacity) min = 1;
    	result["quantity"] = parseInt(Math.random() * (max - min) + min);
    	result["price"] = parseInt((result.quantity * (parseInt(Math.random() * 50) + 50)) / 10) * 10 + (7 - destSysInfo.government) * 60 +
    		this.$calcDistanceBonus(routeDistance, 10) // plus a distance bonus
    		+
    		this.$calcPlayerBonus(400); // plus a possible bonus price, based on player score 
    	result["expiry"] = -1; //clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 9 - piracy for any cargo (pirate)
    this.$missionType9_Values = function $missionType9_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	var max = 30; //player.ship.cargoSpaceCapacity;
    	//if (max > 30) max = 30;
    	var min = 5;
    	if (min > player.ship.cargoSpaceCapacity) min = 1;
    	result["quantity"] = parseInt(Math.random() * (max - min) + min);
    	result["price"] = parseInt((result.quantity * (parseInt(Math.random() * 80) + 50)) / 10) * 10 + (7 - destSysInfo.government) * 70 +
    		this.$calcDistanceBonus(routeDistance, 10) // plus a distance bonus
    		+
    		this.$calcPlayerBonus(800); // plus a possible bonus price, based on player score 
    	result["expiry"] = -1; //clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 10 - piracy for specific cargo (local gov)
    this.$missionType10_Values = function $missionType10_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	var max = 20; //player.ship.cargoSpaceCapacity;
    	//if (max > 8) max = 8;
    	var min = 3;
    	if (min > player.ship.cargoSpaceCapacity) min = 1;
    	result["quantity"] = parseInt(Math.random() * (max - min) + min);
    	var priceAdj = 1.15; // look for prices 15% higher than average
    	var c_types = ["luxuries", "computers", "furs", "liquor_wines", "alloys", "radioactives", "minerals", "alien_items", "machinery"];
    	var selcargo = "";
    	var tries = 0; // we'll try a few times to find a commodity that ships will want to be bringing in
    	do {
    		selcargo = c_types[Math.floor(Math.random() * c_types.length)];
    		tries += 1;
    	} while ((destSysInfo.samplePrice(selcargo) / 10) < (this._commodityAverage[selcargo] * priceAdj) && tries < 5);
    
    	if ((destSysInfo.samplePrice(selcargo) / 10) < (this._commodityAverage[selcargo] * priceAdj)) {
    		if (this._debug) log(this.name, "commodity selection for destination has lower than average prices (" + selcargo + ")");
    		return null;
    	}
    	result["commodity"] = selcargo;
    	result["price"] = parseInt((result.quantity * (parseInt(Math.random() * 50) + 50)) / 10) * 10 + (7 - destSysInfo.government) * 80 +
    		this.$calcDistanceBonus(routeDistance, 10) // plus a distance bonus
    		+
    		this.$calcPlayerBonus(400); // plus a possible bonus price, based on player score 
    	result["expiry"] = -1; //clock.adjustedSeconds + routeTime + (result.quantity * 1800) + 21600; // transit time + 30 mins per t + 6 hours to complete (cargo is sometimes hard to find...)
    	result["penalty"] = parseInt(result.price / 4);
    
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 11 - piracy for specific cargo (pirate)
    this.$missionType11_Values = function $missionType11_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	var max = 20; //player.ship.cargoSpaceCapacity;
    	//if (max > 8) max = 8;
    	var min = 3;
    	if (min > player.ship.cargoSpaceCapacity) min = 1;
    	result["quantity"] = parseInt(Math.random() * (max - min) + min);
    	var priceAdj = 1.15; // look for prices 15% higher than average
    	var c_types = ["luxuries", "computers", "furs", "liquor_wines", "alloys", "radioactives", "minerals", "alien_items", "machinery"];
    	var selcargo = "";
    	var tries = 0; // we'll try a few times to find a commodity that ships will want to be bringing in
    	do {
    		selcargo = c_types[Math.floor(Math.random() * c_types.length)];
    		tries += 1;
    	} while ((destSysInfo.samplePrice(selcargo) / 10) < (this._commodityAverage[selcargo] * priceAdj) && tries < 5);
    
    	if ((destSysInfo.samplePrice(selcargo) / 10) < (this._commodityAverage[selcargo] * priceAdj)) {
    		if (this._debug) log(this.name, "commodity selection for destination has lower than average prices (" + selcargo + ")");
    		return null;
    	}
    	result["commodity"] = selcargo;
    	result["price"] = parseInt((result.quantity * (parseInt(Math.random() * 80) + 50)) / 10) * 10 + (7 - destSysInfo.government) * 100 +
    		this.$calcDistanceBonus(routeDistance, 10) // plus a distance bonus
    		+
    		this.$calcPlayerBonus(800); // plus a possible bonus price, based on player score 
    	result["expiry"] = -1; //clock.adjustedSeconds + routeTime + (result.quantity * 1800) + 21600; // transit time + 30 mins per t + 6 hours to complete (cargo is sometimes hard to find...)
    	result["penalty"] = parseInt(result.price / 3);
    
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 12 - free the slaves
    this.$missionType12_Values = function $missionType12_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	var max = player.ship.cargoSpaceCapacity;
    	if (max > 8) max = 8;
    	var min = 3;
    	if (min > player.ship.cargoSpaceCapacity) min = 1;
    	result["quantity"] = parseInt(Math.random() * (max - min) + min);
    	var priceAdj = 1.3 // look for prices 15% higher than average
    	var selcargo = "slaves";
    	if ((destSysInfo.samplePrice(selcargo) / 10) < (this._commodityAverage[selcargo] * priceAdj)) {
    		if (this._debug) log(this.name, "commodity selection for destination has lower than average prices (" + selcargo + ")");
    		return null;
    	}
    	result["commodity"] = selcargo;
    	result["price"] = parseInt((result.quantity * (parseInt(Math.random() * 50) + 50)) / 10) * 10 + (7 - destSysInfo.government) * 80 +
    		this.$calcDistanceBonus(routeDistance, 10) // plus a distance bonus
    		+
    		this.$calcPlayerBonus(400); // plus a possible bonus price, based on player score 
    	result["expiry"] = -1; //clock.adjustedSeconds + routeTime + (result.quantity * 1800) + 21600; // transit time + 30 mins per t + 6 hours to complete (cargo is sometimes hard to find...)
    	result["penalty"] = parseInt(result.price / 4);
    
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 13 - collect thargoid wreckage
    this.$missionType13_Values = function $missionType13_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	// make sure we don't create missions where the quantity of cargo is more than the player ship
    	var max = 20;
    	player.ship.cargoSpaceCapacity;
    	//if (max > 20) max = 20;
    	var min = 5;
    	if (min > player.ship.cargoSpaceCapacity) min = 1;
    	result["commodity"] = "alloys";
    	result["quantity"] = parseInt(Math.random() * (max - min) + min);
    	result["price"] = parseInt((result.quantity * (parseInt(Math.random() * 100) + 150)) / 10) * 10 +
    		this.$calcPlayerBonus(1000); // plus a possible bonus price, based on player score 
    	// max time for longest jump is around 48 hours
    	// changed to "no expiry" as it's hard enough just getting into and out of interstellar space, without adding a time limit on it
    	result["expiry"] = -1; //clock.adjustedSeconds + (result.quantity * 1800) + 86400 + (workTime * 3); // 30 mins per wreckage item + 24 hrs transit time + some work time
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 14 - gun runner
    this.$missionType14_Values = function $missionType14_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	var max = 20; //player.ship.cargoSpaceCapacity;
    	//if (max > 8) max = 8;
    	var min = 3;
    	if (min > player.ship.cargoSpaceCapacity) min = 1;
    	result["quantity"] = parseInt(Math.random() * (max - min) + min);
    	var priceAdj = 1.3 // look for prices 15% higher than average
    	var selcargo = "firearms";
    
    	if ((destSysInfo.samplePrice(selcargo) / 10) < (this._commodityAverage[selcargo] * priceAdj)) {
    		if (this._debug) log(this.name, "commodity selection for destination has lower than average prices (" + selcargo + ")");
    		return null;
    	}
    	result["commodity"] = selcargo;
    	result["price"] = parseInt((result.quantity * (parseInt(Math.random() * 50) + 50)) / 10) * 10 + (7 - destSysInfo.government) * 80 +
    		this.$calcDistanceBonus(routeDistance, 10) // plus a distance bonus
    		+
    		this.$calcPlayerBonus(400); // plus a possible bonus price, based on player score 
    	result["expiry"] = -1; //clock.adjustedSeconds + routeTime + (result.quantity * 1800) + 21600; // transit time + 30 mins per t + 6 hours to complete (cargo is sometimes hard to find...)
    	result["penalty"] = parseInt(result.price / 4);
    
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 15 - drug runner
    this.$missionType15_Values = function $missionType15_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	var max = 20; //player.ship.cargoSpaceCapacity;
    	//if (max > 8) max = 8;
    	var min = 3;
    	if (min > player.ship.cargoSpaceCapacity) min = 1;
    	result["quantity"] = parseInt(Math.random() * (max - min) + min);
    	var priceAdj = 1.3 // look for prices 15% higher than average
    	var selcargo = "narcotics";
    
    	if ((destSysInfo.samplePrice(selcargo) / 10) < (this._commodityAverage[selcargo] * priceAdj)) {
    		if (this._debug) log(this.name, "commodity selection for destination has lower than average prices (" + selcargo + ")");
    		return null;
    	}
    	result["commodity"] = selcargo;
    	result["price"] = parseInt((result.quantity * (parseInt(Math.random() * 50) + 50)) / 10) * 10 + (7 - destSysInfo.government) * 80 +
    		this.$calcDistanceBonus(routeDistance, 10) // plus a distance bonus
    		+
    		this.$calcPlayerBonus(400); // plus a possible bonus price, based on player score 
    	result["expiry"] = -1; //clock.adjustedSeconds + routeTime + (result.quantity * 1800) + 21600; // transit time + 30 mins per t + 6 hours to complete (cargo is sometimes hard to find...)
    	result["penalty"] = parseInt(result.price / 4);
    
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 16 - civil unrest
    this.$missionType16_Values = function $missionType16_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = parseInt((Math.random() * 7) + 3);
    	result["price"] = parseInt((result.quantity * (parseInt(Math.random() * 500) + 550)) / 10) * 10 +
    		this.$calcPlayerBonus(2000); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + (result.quantity * 600) + workTime; // 10 mins per ship + 1 hour to complete
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 17 - garbage scow
    this.$missionType17_Values = function $missionType17_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	var max = player.ship.cargoSpaceCapacity;
    	if (max > 30) max = 30;
    	if (max > system.mainStation.market["radioactives"].quantity) max = system.mainStation.market["radioactives"].quantity;
    	var min = 5;
    	if (min > player.ship.cargoSpaceCapacity) min = 1;
    
    	result["quantity"] = parseInt(Math.random() * (max - min) + min);
    	result["commodity"] = "radioactives";
    	// price is related to main station's price for radioactives, plus a bit to make it worth the player's while (rather than just reselling the radioactives).
    	result["price"] = (result.quantity * (system.mainStation.market[result.commodity].price / 10 + 5)) +
    		this.$calcPlayerBonus(200); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + workTime + (result.quantity * 60); // 1 hour + 1 minutes per canister to complete.
    	result["deposit"] = result.quantity * (system.mainStation.market[result.commodity].price / 10);
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 18 - pirate hunt (pirate authority)
    this.$missionType18_Values = function $missionType18_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = parseInt((Math.random() * 10) + 8);
    	// base the reward rate on danger level in the target system
    	var amt = parseInt((result.quantity * (parseInt(Math.random() * ((4 - destSysInfo.government) * 75)) + 50)) / 10) * 10 +
    		this.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	// occasionally double the price
    	if (Math.random() > 0.95 && destSysInfo.government < 2) amt *= 2;
    	result["price"] = amt;
    	result["expiry"] = clock.adjustedSeconds + routeTime + (result.quantity * 600) + workTime; // transit time + 10 mins per ship + 1 hour to complete
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // calculates a bonus price based on the players score
    this.$calcPlayerBonus = function $calcPlayerBonus(maxVal) {
    	var amt = 0;
    	amt = ((player.score / 6000) > Math.random() ? parseInt((parseInt(Math.random() * (maxVal / 2)) + (maxVal / 2)) / 10) * 10 : 0);
    	if (amt > maxVal) amt = maxVal;
    	if (amt < 0) amt = 0;
    	var hs = worldScripts.HomeSystem;
    	if (hs) {
    		// is this a home system
    		if (hs.$isHomeSystem(system.ID) === true && hs.$checkLevel(system.ID, "missions") === true) {
    			//var rep = parseInt(hs._dockCounts[galaxyNumber][system.ID]);
    			if (amt === 0) amt = 100; // make sure we have some amount to work with;
    			var pct = 1.1;
    			if (hs.$checkLevel(system.ID, "missions_2") === true) pct = 1.2;
    			amt = amt * pct;
    		}
    	}
    	return Math.floor(amt);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // calculates a bonus price based on the distance to the destination
    // at the moment this is a very simple calc, but in future releases it may become more complex
    this.$calcDistanceBonus = function $calcDistanceBonus(distance, factor) {
    	var amt = parseInt(distance) * factor;
    	return Math.floor(amt);
    }
    Scripts/galcopbb_piratebases.js
    "use strict";
    this.name = "GalCopBB_PirateBases";
    this.author = "phkb";
    this.copyright = "2018 phkb";
    this.description = "Looks after the spawning, and subsequent re-creation, of pirate bases";
    this.license = "CC BY-NC-SA 4.0";
    
    // array of dictionaries, containing
    //      systemID        system ID where asteroid field is found
    //      x,y,z,coordSystem
    //      locationType
    this._bases = [];
    this._populateComplete = false;
    
    // todo: do something different with Qbombs? prevent them from exploding close to base?
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
        if (missionVariables.GalCopBBMissions_PirateBases) {
            this._bases = JSON.parse(missionVariables.GalCopBBMissions_PirateBases);
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillSaveGame = function () {
        missionVariables.GalCopBBMissions_PirateBases = JSON.stringify(this._bases);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillEnterWitchspace = function (cause, destination) {
        if (cause === "galactic jump") this._bases.length = 0;
        this._populateComplete = false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // scripts that will need pirate base should manually call this function from their own "systemWillPopulate" function.
    // this ensures any pirate bases will be created before they need to use them
    this.systemWillPopulate = function () {
        // make sure we don't run this twice
        if (this._populateComplete === true) return;
        this.$fixPirateBaseData();
        // theoretically there will only be 1 base per system, but just in case that changes, we'll store everything in an array
        var b = this._bases;
        for (var i = 0; i < b.length; i++) {
            if (b[i].systemID === system.ID) {
                var position = Vector3D(b[i].x, b[i].y, b[i].z).fromCoordinateSystem(b[i].coordSystem)
                // add a pirate base
                system.setPopulator("gcm-piratebase-" + i, {
                    callback: function (pos) {
                        // add some asteroids around the base
                        system.addShips("asteroid", 30, pos, 40E3);
                        // add the base itself
                        var key = "gcm-pirate-base-core";
                        if (Ship.shipDataForKey("griff_rockhermit") != null) key = "gcm-pirate-base-griff";
                        var checkShips = system.addShips("[" + key + "]", 1, pos, 0);
                        var base = null;
                        if (checkShips) base = checkShips[0];
                        if (base) {
                            base.script.shipDied = worldScripts.GalCopBB_PirateBases.$pb_shipDied;
                            worldScripts.GalCopBB_PirateBases.$addMissionIDToBase(base);
                        }
                        // create some lurking goons
                        worldScripts.GalCopBB_GoonSquads.$createGoonSquad(parseInt((Math.random() * 4) + 3), pos, player.ship.scannerRange * 0.9);
    
                    }.bind(this),
                    location: "COORDINATES",
                    coordinates: position
                });
            }
        }
        this._populateComplete = true;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // pirate bases are added with factor and seed values
    // once they are actually spawned, though, they will instead have x,y,z values to keep the field in roughly the same place
    this.$addPirateBase = function $addPirateBase(sysID, factor, seed, locationType) {
        this._bases.push({
            systemID: sysID,
            factor: factor,
            seed: seed,
            locationType: locationType
        });
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$addPirateBaseDirect = function $addPirateBaseDirect(sysID, x, y, z, coord, locationType) {
        this._bases.push({
            systemID: sysID,
            x: x,
            y: y,
            z: z,
            coordSystem: coord,
            locationType: locationType
        });
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$systemHasPirateBase = function $systemHasPirateBase(sysID) {
        for (var i = 0; i < this._bases.length; i++) {
            if (this._bases[i].systemID === sysID) return this._bases[i];
        }
        return null;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$getSystemPirateBase = function $getSystemPirateBase() {
        var bases = system.shipsWithRole("gcm_pirate_base");
        if (bases.length === 0) return null;
        return bases[0];
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$fixPirateBaseData = function $fixPirateBaseData() {
        var gcm = worldScripts.GalCopBB_Missions;
        var b = this._bases;
        for (var i = 0; i < b.length; i++) {
            if (b[i].systemID === system.ID && b[i].hasOwnProperty("factor") === true) {
                var info = gcm.$getRandomPosition(b[i].locationType, b[i].factor, b[i].seed);
                b[i].x = info.x;
                b[i].y = info.y;
                b[i].z = info.z;
                b[i].coordSystem = info.coordSystem;
                delete b[i].factor;
                delete b[i].seed;
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // used if we're manually creating a base in the current system
    this.$forceSpawn = function $forceSpawn() {
        var b = this._bases;
        for (var i = 0; i < b.length; i++) {
            if (b[i].systemID === system.ID) {
                var pos = Vector3D(b[i].x, b[i].y, b[i].z).fromCoordinateSystem(b[i].coordSystem)
                // add some asteroids around the base
                system.addShips("asteroid", 30, pos, 40E3);
                // add the base itself
                var key = "gcm-pirate-base-core";
                if (Ship.shipDataForKey("griff_rockhermit") != null) key = "gcm-pirate-base-griff";
                var checkShips = system.addShips("[" + key + "]", 1, pos, 0);
                var base = null;
                if (checkShips) base = checkShips[0];
                if (base) {
                    base.script.shipDied = this.$pb_shipDied;
                    this.$addMissionIDToBase(base);
                }
                // create some lurking goons
                worldScripts.GalCopBB_GoonSquads.$createGoonSquad(parseInt((Math.random() * 4) + 3), pos, player.ship.scannerRange * 0.9);
                return pos;
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$pb_shipDied = function $pb_shipDied(whom, why) {
        // remove the base from the data so i can't be recreated
        var pb = worldScripts.GalCopBB_PirateBases;
        var b = pb._bases;
        for (var i = 0; i < b.length; i++) {
            if (b[i].systemID === system.ID) {
                b.splice(i, 1);
                break;
            }
        }
        // check for a related mission and mark it complete
        if (this.ship.script.hasOwnProperty("_missionID") && this.ship.script._missionID != 0) {
            var gcm = worldScripts.GalCopBB_Missions;
            var bb = worldScripts.BulletinBoardSystem;
            var item = bb.$getItem(this.ship.script._missionID);
            if (item) {
                // if the mission has been accepted, mark it complete now
                if (item.accepted === true && item.data.destroyedQuantity === 0 && item.data.quantity === 0) {
                    item.data.quantity = 1;
                    bb.$updateBBMissionPercentage(item.ID, 1);
    
                    gcm.$logMissionData(item.ID);
                    player.consoleMessage(expandDescription("[goal_updated]"));
                }
                // if the mission wasn't accepted, just delete it
                if (item.accepted === false) {
                    bb.$removeBBMission(item.ID);
                }
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$addMissionIDToBase = function $addMissionIDToBase(base) {
        var gcm = worldScripts.GalCopBB_Missions;
        var list = gcm.$getListOfMissions(true, 140);
        // there should only ever be 1
        if (list.length > 0) {
            if (list[0].destination === system.ID &&
                list[0].data.quantity === 0 &&
                list[0].data.destroyedQuantity === 0 &&
                (list[0].expiry === -1 || list[0].expiry > clock.adjustedSeconds)) {
    
                base.script._missionID = list[0].ID;
            }
        }
    }
    Scripts/galcopbb_rangefinder.js
    "use strict";
    this.name = "GalCopBB_RangeFinder_MFD";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Controls the Range Finder MFD";
    this.license = "CC BY-NC-SA 4.0";
    
    // idea: include rock hermits as separate scan item
    
    this._mode = -1; // 0 = Escape capsules, 1 = Black boxes, 2 = asteroids, 3 = ships, 4 = communication relays
    this._maxRange = 0; // maximum scan distance for the range finder (returned from script_info data)
    this._maxDirectionRange = 0; // maximum distance for giving directional pointers to targets (returned from script_info data)
    this._running = false;
    this._scanTimer = null; // timer of the scan
    this._scanTime = 2; // number of seconds it will take for a scan
    this._energyTimer = null; // timer to deduct energy (if heat is not being applied)
    this._pointer = -1; // pointer to which scan indicators are in use
    this._heatAmount = 0.93; // amount of heat generated when used with ShipConfiguration OXP
    this._energyAmount = 3; // amount of energy to deduct each quarter second
    this._lastScan = ""; // results of the last scan
    this._lastScanData = {};
    this._next_uniqueID = 0;
    this._scInstalled = false; // flag to indicate that ShipConfig is installed
    this._heatSettings = {
    	200000: 0.88,
    	500000: 0.94
    }; // heat buildup settings
    this._energySettings = { // energy drain settings
    	"type1": {
    		200000: 2.0,
    		500000: 2.7
    	}, // no EEU or NEU
    	"type2": {
    		200000: 2.7,
    		500000: 3.5
    	}, // EEU only
    	"type3": {
    		200000: 3.2,
    		500000: 4
    	}, // NEU
    };
    
    this._firstCol = 1;
    this._secondCol = 6;
    this._thirdCol = 8;
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function () {
    	var h = worldScripts.hudselector;
    	if (h) h.$HUDSelectorAddMFD(this.name, "RangeFinderMFD");
    
    	this._lastScan = expandDescription("[gcm_rangefinder_nodata]");
    
    	if (worldScripts.ShipConfiguration_Core) this._scInstalled = true;
    
    	this._maxRange = this.$getScannerRange();
    	this._maxDirectionRange = this.$getScannerDirectionalRange();
    	if (this._maxRange > 0) {
    		player.ship.setMultiFunctionText("RangeFinderMFD", "Range Finder:\n  " + this._lastScan, false);
    	}
    	this._heatAmount = this._heatSettings[this._maxRange];
    	this._energyAmount = this.$getEnergySetting();
    
    	var chrs = ["+", "-", "X", "x", "O", "<", ">", "^", "v", "A"];
    	var first = 0;
    	for (var i = 0; i < chrs.length; i++) {
    		var wid = defaultFont.measureString(chrs[i]);
    		if (wid > first) first = wid;
    	}
    	this._firstCol = first + 0.3;
    	this._thirdCol = 15 - (this._secondCol + this._firstCol);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillEnterWitchspace = function () {
    	if (this._scanTimer && this._scanTimer.isRunning) {
    		if (this._scInstalled) {
    			player.ship.script.thirdPartyHeat -= this._heatAmount;
    			if (player.ship.script.thirdPartyHeat < 0) player.ship.script.thirdPartyHeat = 0;
    		}
    		this._scanTimer.stop();
    		this._scanTimer = null;
    	}
    	if (this._energyTimer && this._energyTimer.isRunning) {
    		this._energyTimer.stop();
    		this._energyTimer = null;
    	}
    	this._lastScan = expandDescription("[gcm_rangefinder_nodata]");
    	this._lastScanData = {};
    	this._next_uniqueID = 0;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillLaunchFromStation = function (station) {
    	this._lastScan = expandDescription("[gcm_rangefinder_nodata]");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = function (whom, why) {
    	if (this._scanTimer && this._scanTimer.isRunning) {
    		this._scanTimer.stop();
    		this._scanTimer = null;
    	}
    	if (this._energyTimer && this._energyTimer.isRunning) {
    		this._energyTimer.stop();
    		this._energyTimer = null;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.equipmentDamaged = function (equipmentKey) {
    	if (equipmentKey === "EQ_GCM_RANGE_FINDER_MFD" || equipmentKey === "EQ_GCM_RANGE_FINDER_EXT_REMOVAL") {
    		if (this._scanTimer && this._scanTimer.isRunning === true) {
    			this._scanTimer.stop();
    			this._scanTimer = null;
    			if (this._scInstalled) player.ship.script.thirdPartyHeat -= this._heatAmount;
    		}
    		if (this._energyTimer && this._energyTimer.isRunning) {
    			this._energyTimer.stop();
    			this._energyTimer = null;
    		}
    		this._maxRange = this.$getScannerRange();
    		this._maxDirectionRange = this.$getScannerDirectionalRange();
    		this._heatAmount = this._heatSettings[this._maxRange];
    		this._energyAmount = this.$getEnergySetting();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.equipmentRepaired = function (equipmentKey) {
    	if (equipmentKey === "EQ_GCM_RANGE_FINDER_MFD" || equipmentKey === "EQ_GCM_RANGE_FINDER_EXT_REMOVAL") {
    		this._maxRange = this.$getScannerRange();
    		this._maxDirectionRange = this.$getScannerDirectionalRange();
    		this._heatAmount = this._heatSettings[this._maxRange];
    		this._energyAmount = this.$getEnergySetting();
    		this._lastScan = expandDescription("[gcm_rangefinder_nodata]");
    		this._lastScanData = {};
    		if (this._mode >= 0) {
    			player.ship.setMultiFunctionText("RangeFinderMFD", expandDescription("[gcm_rangefinder_mode_short_" + this._mode + "]") + this._lastScan, false);
    		} else {
    			player.ship.setMultiFunctionText("RangeFinderMFD", "Range Finder:\n  " + this._lastScan, false);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.equipmentRemoved = this.equipmentAdded = function (equipmentKey) {
    	if (equipmentKey === "EQ_GCM_RANGE_FINDER_MFD" || equipmentKey === "EQ_GCM_RANGE_FINDER_EXTENDER") {
    		this._maxRange = this.$getScannerRange();
    		this._heatAmount = this._heatSettings[this._maxRange];
    		this._energyAmount = this.$getEnergySetting();
    		player.ship.setMultiFunctionText("RangeFinderMFD", null);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerBoughtEquipment = function (equipmentKey) {
    	if (equipmentKey === "EQ_GCM_RANGE_FINDER_REMOVAL" || equipmentKey === "EQ_GCM_RANGE_FINDER_EXT_REMOVAL") {
    		var p = player.ship;
    		p.removeEquipment("EQ_GCM_RANGE_FINDER_REMOVAL");
    		p.removeEquipment("EQ_GCM_RANGE_FINDER_EXT_REMOVAL");
    		p.removeEquipment("EQ_GCM_RANGE_FINDER_EXTENDER");
    		p.removeEquipment("EQ_GCM_RANGE_FINDER_MFD");
    		p.setMultiFunctionText("RangeFinderMFD", null);
    		return;
    	}
    	this._lastScan = expandDescription("[gcm_rangefinder_nodata]");
    	this._lastScanData = {};
    	player.ship.setMultiFunctionText("RangeFinderMFD", "Range Finder:\n  " + this._lastScan, false);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.mode = function () {
    	var rf = worldScripts.GalCopBB_RangeFinder_MFD;
    	if (rf._scanTimer && rf._scanTimer.isRunning) {
    		player.consoleMessage(expandDescription("[gcm_rangefinder_changemodefail]"));
    		return;
    	}
    	if (rf._maxRange === 0) return;
    
    	rf._mode += 1;
    	if (rf._mode === 6) rf._mode = 0;
    	
    	rf._lastScan = "";
    
    	player.consoleMessage(expandDescription("[gcm_rangefinder_mode_description_" + rf._mode + "]"));
    	player.ship.setMultiFunctionText("RangeFinderMFD", expandDescription("[gcm_rangefinder_mode_short_" + rf._mode + "]"));
    	worldScripts.GalCopBB_Missions.$playSound("mode");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.activated = function () {
    	var rf = worldScripts.GalCopBB_RangeFinder_MFD;
    	if (rf._running == true) {
    		rf._running = false;
    		rf._scanTimer.stop();
    		rf._scanTimer = null;
    		if (rf._energyTimer && rf._energyTimer.isRunning) {
    			rf._energyTimer.stop();
    			rf._energyTimer = null;
    		}
    		if (rf._scInstalled) {
    			player.ship.script.thirdPartyHeat -= rf._heatAmount;
    			if (player.ship.script.thirdPartyHeat < 0) player.ship.script.thirdPartyHeat = 0;
    		}
    		player.ship.setMultiFunctionText("RangeFinderMFD", expandDescription("[gcm_rangefinder_mode_short_" + rf._mode + "][gcm_rangefinder_terminated]") + rf._lastScan, false);
    		worldScripts.GalCopBB_Missions.$playSound("stop");
    		return;
    	}
    	if (rf._mode === -1) rf._mode = 0;
    	if (rf._maxRange === 0) return;
    	rf._pointer = -1;
    	rf._running = true;
    
    	if (rf._energyTimer && rf._energyTimer.isRunning) rf._energyTimer.stop();
    	rf._energyTimer = new Timer(rf, rf.$depleteEnergy, 0.25, 0.25);
    	if (rf._scInstalled) player.ship.script.thirdPartyHeat += rf._heatAmount;
    
    	rf._scanTimer = new Timer(rf, rf.$performScan, rf._scanTime, rf._scanTime);
    	player.ship.setMultiFunctionText("RangeFinderMFD", expandDescription("[gcm_rangefinder_mode_short_" + rf._mode + "][gcm_rangefinder_active_initial]"), false);
    	worldScripts.GalCopBB_Missions.$playSound("activate");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$performScan = function $performScan() {
    	function compare(a, b) {
    		return a.dist > b.dist;
    	}
    
    	function gcm_findships(entity) {
    		return (entity.isShip && entity.isStation === false && !entity.isRock && entity.isPlayer === false && !entity.hasRole("escape-capsule") && entity.dataKey != "telescopemarker");
    	}
    
    	function gcm_findcapsules(entity) {
    		return (entity.isShip && entity.hasRole("escape-capsule"));
    	}
    
    	function gcm_findblackboxes(entity) {
    		// looking for black boxes or ships that have a black box
    		return (entity.isShip && entity.shipClassName.toLowerCase().indexOf("black box") >= 0 || entity.primaryRole === "gcm_derelict");
    	}
    
    	function gcm_findasteroids(entity) {
    		return (entity.isShip && entity.isRock === true && entity.isBoulder === false && entity.dataKey != "telescopemarker");
    	}
    
    	function gcm_findcommsrelays(entity) {
    		return (entity.isShip && entity.primaryRole === "commsrelay");
    	}
    
    	function gcm_findcargo(entity) {
    		return (entity.isCargo && entity.hasRole("cargopod"));
    	}
    
    	this._pointer += 1;
    	if (this._pointer === 3) this._pointer = 0;
    	var leftside = "";
    	var rightside = "";
    
    	switch (this._pointer) {
    		case 0:
    			leftside = "..|";
    			rightside = "|..";
    			break;
    		case 1:
    			leftside = ".|.";
    			rightside = ".|.";
    			break;
    		case 2:
    			leftside = "|..";
    			rightside = "..|";
    			break;
    	}
    	var text = expandDescription("[gcm_rangefinder_mode_short_" + this._mode + "]") + leftside + expandDescription("[gcm_rangefinder_active]") + rightside + "\n";
    	switch (this._mode) {
    		case 0:
    			var targets = system.filteredEntities(this, gcm_findcapsules);
    			break;
    		case 1:
    			var targets = system.filteredEntities(this, gcm_findblackboxes);
    			break;
    		case 2:
    			var targets = system.filteredEntities(this, gcm_findasteroids);
    			break;
    		case 3:
    			var targets = system.filteredEntities(this, gcm_findships);
    			break;
    		case 4:
    			var targets = system.filteredEntities(this, gcm_findcommsrelays);
    			break;
    		case 5:
    			var targets = system.filteredEntities(this, gcm_findcargo);
    			break;
    	}
    
    	this._lastScan = "";
    	if (targets && targets.length > 0) {
    		var list = [];
    		for (var i = 0; i < targets.length; i++) {
    			var d = player.ship.position.distanceTo(targets[i]);
    			if (d < this._maxRange) {
    				// occasionally skip items that are on the edge of scanner range
    				if ((this._maxRange - d) < (this._maxRange / 5) && Math.random() > ((this._maxRange - d) / (this._maxRange / 5))) continue;
    				// get the last distance for this item
    				if (targets[i].script.hasOwnProperty("_uniqueID") === false) {
    					targets[i].script._uniqueID = this._next_uniqueID;
    					this._next_uniqueID += 1;
    				}
    				var last = this._lastScanData[targets[i].script._uniqueID];
    				if (!last) last = 0;
    				list.push({
    					tgt: targets[i],
    					dist: d,
    					range: (d < this._maxDirectionRange ? this.$getPointer(targets[i]) : (d > last ? "+" : "-"))
    				});
    				this._lastScanData[targets[i].script._uniqueID] = d;
    			}
    		}
    		if (list.length > 0) {
    			list.sort(compare);
    			for (var i = 0; i < list.length; i++) {
    				if (i >= 7) break;
    				this._lastScan += this.$padTextRight(list[i].range, this._firstCol) +
    					this.$padTextRight((list[i].dist / 1000).toFixed(2) + "kms", this._secondCol) +
    					(this._mode === 3 ? this.$padTextRight(list[i].tgt.shipClassName, this._thirdCol) : "") + // displays the class name (eg "Python") of the ship detected
    					"\n";
    			}
    		} else {
    			this._lastScan += expandDescription("[gcm_rangefinder_empty]");
    		}
    	} else {
    		this._lastScan += expandDescription("[gcm_rangefinder_empty]");
    	}
    	text += this._lastScan;
    
    	player.ship.setMultiFunctionText("RangeFinderMFD", text, false);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$getPointer = function(ent) {
    	var p = player.ship;
    	var pointer = "";
    	if (p && p.vectorForward) {
    		var f_dev = p.vectorForward.angleTo(ent.position.subtract(p));
    		var r_dev = p.vectorRight.angleTo(ent.position.subtract(p));
    		var u_dev = p.vectorUp.angleTo(ent.position.subtract(p));
    		var s = " ";
    		// > 1.56 means oppposite side (3.12 exact opposite)
    		// so, f_dev < 0.01, r_dev = 1.57, u_dev = 1.57  -- aligned
    		if (f_dev < 0.25) {
    			if (f_dev < 0.25) s = "O";
    			if (f_dev < 0.12) s = "o";
    			if (f_dev < 0.05) s = "X";
    		}
    		if (f_dev >= 0.25) {
    			if (r_dev > 1.69) {
    				s = "<";
    			}
    			if (u_dev < 1.45) {
    				s = "^";
    			} else if(u_dev > 1.69) {
    				s = "v";
    			}
    			if (r_dev < 1.45) {
    				s = ">";
    			}
    			if (f_dev > 1.57) s= "A";
    		}
    		pointer = s;
    	}
    	return pointer;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // appends space to currentText to the specified length in 'em'
    this.$padTextRight = function $padTextRight(currentText, desiredLength, leftSwitch) {
    	if (currentText == null) currentText = "";
    	var hairSpace = String.fromCharCode(31);
    	var ellip = "…";
    	var currentLength = defaultFont.measureString(currentText);
    	var hairSpaceLength = defaultFont.measureString(hairSpace);
    	// calculate number needed to fill remaining length
    	var padsNeeded = Math.floor((desiredLength - currentLength) / hairSpaceLength);
    	if (padsNeeded < 1) {
    		// text is too long for column, so start pulling characters off
    		var tmp = currentText;
    		do {
    			tmp = tmp.substring(0, tmp.length - 2) + ellip;
    			if (tmp === ellip) break;
    		} while (defaultFont.measureString(tmp) > desiredLength);
    		currentLength = defaultFont.measureString(tmp);
    		padsNeeded = Math.floor((desiredLength - currentLength) / hairSpaceLength);
    		currentText = tmp;
    	}
    	// quick way of generating a repeated string of that number
    	if (!leftSwitch || leftSwitch === false) {
    		return currentText + new Array(padsNeeded).join(hairSpace);
    	} else {
    		return new Array(padsNeeded).join(hairSpace) + currentText;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns the current scan limit for the range finder
    this.$getScannerRange = function $getScannerRange() {
    	var eq1 = "EQ_GCM_RANGE_FINDER_MFD";
    	var eq2 = "EQ_GCM_RANGE_FINDER_EXTENDER";
    	if (player.ship.equipmentStatus(eq2) === "EQUIPMENT_OK") return parseInt(EquipmentInfo.infoForKey(eq2).scriptInfo.scan_range) * 1000;
    	if (player.ship.equipmentStatus(eq1) === "EQUIPMENT_OK") return parseInt(EquipmentInfo.infoForKey(eq1).scriptInfo.scan_range) * 1000;
    	return 0;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$getScannerDirectionalRange = function $getScannerDirectionalRange() {
    	var eq1 = "EQ_GCM_RANGE_FINDER_MFD";
    	var eq2 = "EQ_GCM_RANGE_FINDER_EXTENDER";
    	if (player.ship.equipmentStatus(eq2) === "EQUIPMENT_OK") return parseInt(EquipmentInfo.infoForKey(eq2).scriptInfo.directional_range) * 1000;
    	if (player.ship.equipmentStatus(eq1) === "EQUIPMENT_OK") return parseInt(EquipmentInfo.infoForKey(eq1).scriptInfo.directional_range) * 1000;
    	return 0;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$depleteEnergy = function $depleteEnergy() {
    	var p = player.ship;
    	p.energy -= this._energyAmount;
    	if (p.energy < 64) {
    		this.activated();
    		player.consoleMessage("Insufficient energy to continue scanning.");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$getEnergySetting = function $getEnergySetting() {
    	var typ = "type1";
    	if (player.ship.equipmentStatus("EQ_ENERGY_UNIT") === "EQUIPMENT_OK") typ = "type2";
    	if (player.ship.equipmentStatus("EQ_NAVAL_ENERGY_UNIT") === "EQUIPMENT_OK") typ = "type3";
    	return this._energySettings[typ][this._maxRange];
    }
    Scripts/galcopbb_reputation.js
    "use strict";
    this.name = "GalCopBB_Reputation";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Handles changes to entity reputation when missions are completed";
    this.license = "CC BY-NC-SA 4.0";
    
    /*
    	These routines keep track of the players progress with various galactic entities
    
    	The reputation system is designed to be as feature-rich as possible, allow for a wide variety of combinations and adjustments with just a small amount of coding
    	It starts with an entity definition. These are templates that are used to control what happens with that entity.
    	Entity definitions can be created with different scopes. So an entity with a system-based scope could be created multiple times in the players actual reputations list
    	Then there are the actual reputations. These are what the player has achieved for any given entity definition.
    	Finally, there are the mission results, which control whether reputations increase or decrease.
    
    	Entities can be defined to degrade over time.
    	Entities can have complementary entities, which are links between two entities. 
    	So, if entity 1 is complementary to entity 2 in a negative way, then any positive change to the players reputation with entity 1 will result in a negative change to their reputation with entity 2.
    
    	TODO:
    	- add reputation entries to descriptions.plist
    	- work out grids for each entity
    		- Local Government
    		- GalCop
    		- GCDC
    	For BETA:
    	- Create Liberation Front entities
    	- Create Religious entities use CCL_NamesGods
    
    	Mission definitions in "descriptions.plist" need the following lines added and populated for any reputation change to kick in.		
    		missionType0_reputationEntities = ""; // comma separated list of entities involved with this mission
    		missionType0_reputationTarget = "source"; // comma separated list of the target system for each of the entities listed , either "source" or "destination"
    		missionType0_reputationSuccess = ""; // comma separated list of amounts of reputation to apply to each entity if the mission is completed successfully
    		missionType0_reputationFailure = ""; // comma separated list of amounts of reputation to apply to each entity if the mission is failed/terminated
    
    	Entities have the following properties
    		name 				name of the entity
    		scope 				either local, region, chart or galactic
    		display				boolean to indicate whether the entity will be displayed on the reward screen
    		regionSystems		(region scope only) array of system id's relating to the region
    		regionGalaxy		(region scope only) galaxy number for the region
    		getValueWS			worldScript name of function to get rep value from
    		getValueFN			function name to get reputation value from
    		maxValue			maximum value of the reputation
    		minValue			minimum value of the reputation
    		degrade				degrading profile of two values split by "|". first = how much to degrade, second = days between degrading events
    		complementary		list of complementary entities that will be updated at the same time
    							two values split by "|". 
    								first = entity name, 
    								second = value from -1 to 1 being percentage of original rep change to be applied to comp entity
    								eg "Local Government|0.5" would mean adding half the reputation change to the "Local Government" reputation
    								"Pirate|-0.25" would mean deducting a quarter of the reputation change to the "Pirate" reputation
    		impact				defines what (if any) impact type will be applied. Choices are:
    								markets = adjust prices of legal goods.
    								illegals = adjust prices of illegal goods
    								equipment = applies a rebate to equipment purchases
    								ships = applies a rebate to ship purchases
    								bounty = reduces players offender status
    							multiple impacts can be defined by using comma delimited entries (eg "markets,equipment")
    							For price adjustments, the adjustment will be away from the price_average. 
    							For instance, if the current price for computers is 88cr, and the price_average for this station is 80cr,
    								the adjustment will increase the price.
    							If the current price for computers is 72cr, and the price_average for this station is 80cr, 
    								the adjustment will decrease the price.
    		impactAllegiance	defines what stations the impact will be applies to. Possible choices are:
    								galcop,pirate,neutral,chaotic,hunter,private,restricted,thargoid
    							multiple allegiances can be defined by using comma delimited entries (eg "galcop,neutral")
    							does not apply to bounty impact
    		rewardGrid			dictionary list of value/description pairs defining reward structure
    			value			minimum reputation required for this reward
    			description		description of the award
    			achievementWS	worldscript of achievement function to call when player reaches this level
    			achievementFN	function name of the function to call when the player reaches this level
    							function will be passed entity name and reputation level
    							note: the function will be called any time there is a change in rep even if it leaves the player
    							at the same level. In short, destination function needs to handle being called multiple times
    			impactValue		If an impact has been defined for this entity, this value sets the amount of impact to be applied at this level
    	
    	Setting the "getValueWS" and "getValueFN" allows third party reputations to be added to the list. These will not be updated directly.
    
    	Entities can have text substitutions in the name property. So "LocalGov$G" will be translated to "[LocalGov4]" (in a Communist system) and looked up in descriptions.plist
    	The substitution will take place for display purposes only - the name of the entity will still be "LocalGov$G" when referenced in other entities (eg complementary items)
    */
    
    this._entities = {
    	"Cargo Services": {
    		scope: "galactic",
    		display: true,
    		getValueWS: "GalCopBB_Reputation",
    		getValueFN: "$cargoReputation",
    		maxValue: 7,
    		minValue: -1000,
    		rewardGrid: [{
    				value: -1000,
    				description: "Penniless"
    			},
    			{
    				value: 0,
    				description: "Mostly Penniless"
    			},
    			{
    				value: 1,
    				description: "Peddler"
    			},
    			{
    				value: 2,
    				description: "Dealer"
    			},
    			{
    				value: 3,
    				description: "Merchant"
    			},
    			{
    				value: 4,
    				description: "Broker"
    			},
    			{
    				value: 5,
    				description: "Entrepreneur"
    			},
    			{
    				value: 6,
    				description: "Tycoon"
    			},
    			{
    				value: 7,
    				description: "Elite"
    			}
    		]
    	},
    	"Courier Services": {
    		scope: "galactic",
    		display: true,
    		getValueWS: "GalCopBB_Reputation",
    		getValueFN: "$parcelReputation",
    		maxValue: 7,
    		minValue: -1000,
    		rewardGrid: [{
    				value: -1000,
    				description: "Gopher"
    			},
    			{
    				value: 0,
    				description: "Mailperson"
    			},
    			{
    				value: 1,
    				description: "Carrier"
    			},
    			{
    				value: 2,
    				description: "Agent"
    			},
    			{
    				value: 3,
    				description: "Envoy"
    			},
    			{
    				value: 4,
    				description: "Attaché"
    			},
    			{
    				value: 5,
    				description: "Emmisary"
    			},
    			{
    				value: 6,
    				description: "Diplomat"
    			},
    			{
    				value: 7,
    				description: "Elite"
    			}
    		]
    	},
    	"Passenger Services": {
    		scope: "galactic",
    		display: true,
    		getValueWS: "GalCopBB_Reputation",
    		getValueFN: "$passengerReputation",
    		maxValue: 7,
    		minValue: -1000,
    		rewardGrid: [{
    				value: -1000,
    				description: "Hack"
    			},
    			{
    				value: 0,
    				description: "Bus driver"
    			},
    			{
    				value: 1,
    				description: "Operator"
    			},
    			{
    				value: 2,
    				description: "Jockey"
    			},
    			{
    				value: 3,
    				description: "Cabbie"
    			},
    			{
    				value: 4,
    				description: "Coachman"
    			},
    			{
    				value: 5,
    				description: "Silver Service"
    			},
    			{
    				value: 6,
    				description: "Chauffeur"
    			},
    			{
    				value: 7,
    				description: "Elite"
    			}
    		]
    	},
    	"GalCop": {
    		scope: "galactic",
    		display: false,
    		getValueWS: "",
    		getValueFN: "",
    		maxValue: 7,
    		minValue: 0,
    		degrade: "0.5|30",
    		rewardGrid: []
    	},
    	"Local Government": {
    		scope: "system",
    		display: true,
    		getValueWS: "",
    		getValueFN: "",
    		maxValue: 110,
    		minValue: 0,
    		degrade: "0.1|30",
    		impact: "markets,equipment,ships",
    		impactAllegiance: "galcop",
    		complementary: "`PGN|-0.5",
    		rewardGrid: [{
    				value: 0,
    				description: "",
    				impactValue: 0
    			},
    			{
    				value: 1,
    				description: "Casual Acquaintance",
    				impactValue: 0
    			},
    			{
    				value: 5,
    				description: "Associate",
    				impactValue: 0.01
    			},
    			{
    				value: 15,
    				description: "Close Friend",
    				impactValue: 0.02
    			},
    			{
    				value: 30,
    				description: "Stong Ally",
    				impactValue: 0.04
    			},
    			{
    				value: 50,
    				description: "Faithful Supporter",
    				impactValue: 0.07
    			},
    			{
    				value: 75,
    				description: "Noble Benefactor",
    				impactValue: 0.11
    			},
    			{
    				value: 105,
    				description: "Honorary Citizen",
    				impactValue: 0.15
    			},
    		]
    	},
    	"`PGN": {
    		scope: "system",
    		display: true,
    		getValueWS: "",
    		getValueFN: "",
    		maxValue: 140,
    		minValue: 0,
    		degrade: "1|30",
    		impact: "illegals,equipment,ships",
    		impactAllegiance: "pirate,chaotic",
    		complementary: "Local Government|-0.5",
    		rewardGrid: [{
    				value: 0,
    				description: "",
    				impactValue: 0
    			},
    			{
    				value: 1,
    				description: "Low-Level Thug",
    				impactValue: 0
    			},
    			{
    				value: 10,
    				description: "Petty Thief",
    				impactValue: 0.01
    			},
    			{
    				value: 25,
    				description: "Armed Bandit",
    				impactValue: 0.04
    			},
    			{
    				value: 45,
    				description: "Skilled Outlaw",
    				impactValue: 0.075
    			},
    			{
    				value: 70,
    				description: "Feared Brigand",
    				impactValue: 0.1
    			},
    			{
    				value: 100,
    				description: "Dangerous Gangster",
    				impactValue: 0.15
    			},
    			{
    				value: 135,
    				description: "Elite Pirate",
    				impactValue: 0.2
    			},
    		]
    	},
    };
    
    // dictionary of default trophies and medal overlays
    this._medalTypes = {
    	1: "gcm_trophy_1.png",
    	2: "gcm_trophy_1_star.png",
    	3: "gcm_trophy_2.png",
    	4: "gcm_trophy_2.star.png",
    	5: "gcm_badge_round.png",
    	6: "gcm_badge_star.png",
    	7: "gcm_medal_round_badge.png",
    	8: "gcm_medal_star_badge.png",
    	9: "gcm_medal_round_ribbon.png",
    	10: "gcm_medal_star_ribbon.png",
    };
    
    this._debug = false;
    this._disabled = false;
    this._convertOtherReputations = false; // flag to indicate whether reputation information from other OXP's should be included
    this._maxpage = 0; // total number of pages available for display
    this._curpage = 0; // the current page being displayed
    this._display = 0;
    this._selected = 0;
    this._itemColor = "yellowColor";
    this._menuColor = "orangeColor";
    this._exitColor = "yellowColor";
    this._disabledColor = "darkGrayColor";
    this._lastDat = 0;
    this._trueValues = ["yes", "1", 1, "true", true];
    this._pricesAdjusted = false;
    this._ignoreEquipment = ["EQ_SHIP_RESPRAY", "EQ_SMUGGLING_COMPARTMENT"];
    this._emailGetClientName = null; // reference to function that will return a client nameto be attached to emails
    this._reputations = []; // current player reputation with various entities
    // entity:			name of entity
    // galaxy:			the galaxy in which this reputation applies
    // system:			the system in which this reputation applies
    // reputation:		the decimal value of the reputation
    // reputationText:	text version of the reputation (used as holding spot)
    // lastDegrade:		date (in seconds) of the last degrade event for this reputation
    // bonusNotified:   boolean indicating if a message has been sent to the player
    //                      informing them of the potential bonus available by donating
    
    this._awards = []; // any special awards the player has received
    
    // title: 			name of award
    // worldScript:		the name of the worldscript where his award originated
    // description: 	any descriptive text that goes with the award
    // source:			planet name (not ID) where the award was given
    // galaxy:			galaxy number where planet is
    // received:		date the award was received
    // image:			optional background overlay (dimensions 2048x1024)
    // imageType:		number specifying one of the default medal images (1-10)
    // array of client name items, used to provide some consistency between missions 
    this._clientNames = [];
    // configuration settings for use in Lib_Config
    this._gcmRepConfig = {
    	Name: this.name,
    	Alias: "GalCop Missions - Reputations",
    	Display: "Configuration",
    	Alive: "_gcmRepConfig",
    	Bool: {
    		B0: {
    			Name: "_convertOtherReputations",
    			Def: false,
    			Desc: "Import other reps"
    		},
    		Info: "Imports reputations reputation from other OXP's."
    	},
    };
    
    //-------------------------------------------------------------------------------------------------------------
    // adds an award into the array
    this.$addAward = function $addAward(awd) {
    	if (awd.hasOwnProperty("title") === false || awd.title === "") {
    		throw "Invalid award settings: 'title' must be specified";
    	}
    	if (awd.hasOwnProperty("description") === false || awd.description === "") {
    		throw "Invalid award settings: 'description' must be specified.";
    	}
    	if (awd.hasOwnProperty("entity") === false || awd.entity === "") {
    		throw "Invalid award settings: 'entity' must be specified";
    	}
    	if (awd.hasOwnProperty("system") === false) awd.system = system.ID;
    	if (awd.hasOwnProperty("galaxy") === false) awd.galaxy = galaxyNumber;
    	if (awd.hasOwnProperty("received") === false) awd.received = clock.adjustedSeconds;
    
    	var titl = this.$transformText(awd.title, awd.system);
    
    	// make sure the award isn't already there	
    	if (this.$checkForAward(awd.entity, titl, awd.system) === true) return;
    
    	this._awards.push({
    		title: titl,
    		description: this.$transformText(awd.description, awd.system),
    		entity: awd.entity,
    		source: System.systemNameForID(awd.system),
    		galaxy: awd.galaxy,
    		received: awd.received,
    		image: (awd.hasOwnProperty("image") ? awd.image : ""),
    		imageType: (awd.hasOwnProperty("imageType") ? awd.imageType : 0)
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.systemWillPopulate = function () {
    	this._performedAdjustmentCheck = false;
    	if (player.ship.docked === false) {
    		this._adjustTimer = new Timer(this, this.$performPriceAdjustment, 2, 0);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function () {
    	// register our settings, if Lib_Config is present
    	if (worldScripts.Lib_Config) worldScripts.Lib_Config._registerSet(this._gcmRepConfig);
    
    	// load stored information
    	if (missionVariables.GalCopBBMissions_PricesAdjusted)
    		this._pricesAdjusted = (this._trueValues.indexOf(missionVariables.GalCopBBMissions_PricesAdjusted) >= 0 ? true : false);
    	if (missionVariables.GalCopBBMissions_Reputations) {
    		this._reputations = JSON.parse(missionVariables.GalCopBBMissions_Reputations);
    		delete missionVariables.GalCopBBMissions_Reputations;
    	}
    	if (missionVariables.GalCopBBMissions_Awards) {
    		this._awards = JSON.parse(missionVariables.GalCopBBMissions_Awards);
    		delete missionVariables.GalCopBBMissions_Awards;
    	}
    	if (missionVariables.GalCopBBMissions_RepImport) {
    		this._convertOtherReputations = (this._trueValues.indexOf(missionVariables.GalCopBBMissions_RepImport) >= 0 ? true : false);
    		delete missionVariables.GalCopBBMissions_RepImport;
    	}
    	if (missionVariables.GalCopBBMissions_ClientNames) {
    		this._clientNames = JSON.parse(missionVariables.GalCopBBMissions_ClientNames);
    		delete missionVariables.GalCopBBMissions_ClientNames;
    	}
    	if (this._convertOtherReputations === true) {
    		// add in the player's current rep for cargo/parcels/passengers
    		if (this.$isReputationInList("Cargo Services") === false) {
    			this._reputations.push({
    				entity: "Cargo Services",
    				galaxy: -1,
    				system: -1,
    				reputation: this.$cargoReputation()
    			});
    		}
    		if (this.$isReputationInList("Courier Services") === false) {
    			this._reputations.push({
    				entity: "Courier Services",
    				galaxy: -1,
    				system: -1,
    				reputation: this.$parcelReputation()
    			});
    		}
    		if (this.$isReputationInList("Passenger Services") === false) {
    			this._reputations.push({
    				entity: "Passenger Services",
    				galaxy: -1,
    				system: -1,
    				reputation: this.$passengerReputation()
    			});
    		}
    		// if the player has either of the display reputation OXP's installed, use their terminology
    		if (!worldScripts.display_reputations && !worldScripts["display-reputation-contract"]) {
    			this._entities["Cargo Services"].rewardGrid = [{
    					value: -1000,
    					description: "display_reputation_contract_minus"
    				},
    				{
    					value: 0,
    					description: "display_reputation_contract_0"
    				},
    				{
    					value: 1,
    					description: "display_reputation_contract_1"
    				},
    				{
    					value: 2,
    					description: "display_reputation_contract_2"
    				},
    				{
    					value: 3,
    					description: "display_reputation_contract_3"
    				},
    				{
    					value: 4,
    					description: "display_reputation_contract_4"
    				},
    				{
    					value: 5,
    					description: "display_reputation_contract_5"
    				},
    				{
    					value: 6,
    					description: "display_reputation_contract_6"
    				},
    				{
    					value: 7,
    					description: "display_reputation_contract_7"
    				}
    			];
    			this._entities["Courier Services"].rewardGrid = [{
    					value: -1000,
    					description: "display_reputation_parcel_minus"
    				},
    				{
    					value: 0,
    					description: "display_reputation_parcel_0"
    				},
    				{
    					value: 1,
    					description: "display_reputation_parcel_1"
    				},
    				{
    					value: 2,
    					description: "display_reputation_parcel_2"
    				},
    				{
    					value: 3,
    					description: "display_reputation_parcel_3"
    				},
    				{
    					value: 4,
    					description: "display_reputation_parcel_4"
    				},
    				{
    					value: 5,
    					description: "display_reputation_parcel_5"
    				},
    				{
    					value: 6,
    					description: "display_reputation_parcel_6"
    				},
    				{
    					value: 7,
    					description: "display_reputation_parcel_7"
    				}
    			];
    			this._entities["Passenger Services"].rewardGrid = [{
    					value: -1000,
    					description: "display_reputation_passenger_minus"
    				},
    				{
    					value: 0,
    					description: "display_reputation_passenger_0"
    				},
    				{
    					value: 1,
    					description: "display_reputation_passenger_1"
    				},
    				{
    					value: 2,
    					description: "display_reputation_passenger_2"
    				},
    				{
    					value: 3,
    					description: "display_reputation_passenger_3"
    				},
    				{
    					value: 4,
    					description: "display_reputation_passenger_4"
    				},
    				{
    					value: 5,
    					description: "display_reputation_passenger_5"
    				},
    				{
    					value: 6,
    					description: "display_reputation_passenger_6"
    				},
    				{
    					value: 7,
    					description: "display_reputation_passenger_7"
    				}
    			];
    		}
    
    		// add the random hits reputation in
    		if (worldScripts.Random_Hits) {
    			// RH has a rather complecated ranking system ... I'm just going to get the end result of it, rather than trying to reproduce the calc here
    			// thus the rewardGrid is an empty array
    			this._entities["Society of Bounty Hunters"] = {
    				scope: "galactic",
    				display: true,
    				getValueWS: "GalCopBB_Reputation",
    				getValueFN: "$randomHitsReputation",
    				maxValue: 200,
    				minValue: -200,
    				rewardGrid: []
    			};
    			if (this.$isReputationInList("Society of Bounty Hunters") === false) {
    				this._reputations.push({
    					entity: "Society of Bounty Hunters",
    					galaxy: -1,
    					system: -1,
    					reputation: this.$randomHitsReputation()
    				});
    			}
    		}
    		// add the ups reputation in
    		if (worldScripts.ups_parcel) {
    			this._entities["UPS"] = {
    				scope: "galactic",
    				display: true,
    				getValueWS: "GalCopBB_Reputation",
    				getValueFN: "$upsReputation",
    				maxValue: 7,
    				minValue: -2000,
    				rewardGrid: [{
    						value: -2000,
    						description: "Not trustworthy"
    					}, // for all other negative numbers
    					{
    						value: -3,
    						description: "Irresponsible"
    					},
    					{
    						value: -2,
    						description: "Unreliable"
    					},
    					{
    						value: -1,
    						description: "Incompetent"
    					},
    					{
    						value: 0,
    						description: "Rookie"
    					},
    					{
    						value: 1,
    						description: "Average"
    					},
    					{
    						value: 2,
    						description: "Above average"
    					},
    					{
    						value: 3,
    						description: "Good"
    					},
    					{
    						value: 4,
    						description: "Very good"
    					},
    					{
    						value: 5,
    						description: "Dedicated"
    					},
    					{
    						value: 6,
    						description: "Excellent"
    					},
    					{
    						value: 7,
    						description: "Outstanding"
    					}
    				]
    			};
    			if (this.$isReputationInList("UPS") === false) {
    				this._reputations.push({
    					entity: "UPS",
    					galaxy: -1,
    					system: -1,
    					reputation: this.$upsReputation()
    				});
    			}
    		}
    		// add escort contracts reputation in
    		if (worldScripts.Escort_Contracts) {
    			this._entities["I.T.H.A."] = {
    				scope: "galactic",
    				display: true,
    				getValueWS: "GalCopBB_Reputation",
    				getValueFN: "$escortContractsReputation",
    				maxValue: 20,
    				minValue: -1000,
    				rewardGrid: [{
    						value: -1000,
    						description: "Very poor"
    					},
    					{
    						value: -5,
    						description: "Poor"
    					},
    					{
    						value: 0,
    						description: "Neutral"
    					},
    					{
    						value: 1,
    						description: "Good"
    					},
    					{
    						value: 6,
    						description: "Very good"
    					},
    					{
    						value: 11,
    						description: "Excellent"
    					}
    				]
    			};
    			if (this.$isReputationInList("I.T.H.A.") === false) {
    				this._reputations.push({
    					entity: "I.T.H.A.",
    					galaxy: -1,
    					system: -1,
    					reputation: this.$escortContractsReputation()
    				});
    			}
    		}
    		// add rescue stations reputation in
    		if (worldScripts["Rescue Stations"]) {
    			this._entities["RRS Group"] = {
    				scope: "galactic",
    				display: true,
    				getValueWS: "GalCopBB_Reputation",
    				getValueFN: "$rescueStationsReputation",
    				maxValue: 320,
    				minValue: -1000,
    				rewardGrid: [{
    						value: -1000,
    						missiontext: "rescue_rank_0"
    					},
    					{
    						value: -10,
    						missiontext: "rescue_rank_1"
    					},
    					{
    						value: 0,
    						missiontext: "rescue_rank_2"
    					},
    					{
    						value: 10,
    						missiontext: "rescue_rank_3"
    					},
    					{
    						value: 20,
    						missiontext: "rescue_rank_4"
    					},
    					{
    						value: 40,
    						missiontext: "rescue_rank_5"
    					},
    					{
    						value: 80,
    						missiontext: "rescue_rank_6"
    					},
    					{
    						value: 160,
    						missiontext: "rescue_rank_7"
    					},
    					{
    						value: 320,
    						missiontext: "rescue_rank_8"
    					}
    				]
    			};
    			if (this.$isReputationInList("RRS Group") === false) {
    				this._reputations.push({
    					entity: "RRS Group",
    					galaxy: -1,
    					system: -1,
    					reputation: this.$rescueStationsReputation()
    				});
    			}
    		}
    	}
    
    	//this._awards.push({title:"Medal of Honour", entity:"Local Government", description:"Award for bravery above and beyond the call of duty, in service to the citizens of Lave.",
    	//	source:"Lave", galaxy:0, received:clock.adjustedSeconds - 86400, imageType:2});
    	// set up the interface screen, if required
    	var p = player.ship;
    	if (p.dockedStation) this.$initInterface(p.dockedStation);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillSaveGame = function () {
    	missionVariables.GalCopBBMissions_PricesAdjusted = this._pricesAdjusted;
    	if (this._reputations.length > 0) missionVariables.GalCopBBMissions_Reputations = JSON.stringify(this._reputations);
    	if (this._awards.length > 0) missionVariables.GalCopBBMissions_Awards = JSON.stringify(this._awards);
    	if (this._convertOtherReputations === true) missionVariables.GalCopBBMissions_RepImport = this._convertOtherReputations;
    	if (this._clientNames.length > 0) missionVariables.GalCopBBMissions_ClientNames = JSON.stringify(this._clientNames);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerBoughtEquipment = function (equipment) {
    	if (this._ignoreEquipment.indexOf(equipment) >= 0) return;
    
    	var stn = player.ship.dockedStation;
    	if (!stn) return;
    
    	var ent = this.$getImpactEntity("equipment", this.$getStationAllegiance(stn));
    	var pct = 0;
    	if (ent !== "") pct = this.$getImpactValue(ent);
    
    	if (pct > 0) {
    		var equip = EquipmentInfo.infoForKey(equipment);
    		if (equip.price <= 0) return;
    
    		var cost = (equip.price / 10);
    		var refund = Math.floor((cost * pct) / 10) * 10;
    		if (refund > 0) {
    			player.credits += refund;
    			player.consoleMessage("Your reputation has given you a refund of " + formatCredits(refund, false, true) + ".");
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerBoughtNewShip = function (ship, price) {
    	var stn = player.ship.dockedStation;
    	if (!stn) return;
    
    	var ent = this.$getImpactEntity("ships", this.$getStationAllegiance(stn));
    	var pct = 0;
    	if (ent !== "") pct = this.$getImpactValue(ent);
    
    	if (pct > 0) {
    		var refund = Math.floor((price * pct) / 10) * 10;
    		if (refund > 0) {
    			player.credits += refund;
    			player.consoleMessage("Your reputation has given you a refund of " + formatCredits(refund, false, true) + ".");
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillDockWithStation = function (station) {
    	if (worldScripts.GalCopBB_Missions._simulator === true) return;
    	// have we just completed the nova mission? 
    	// we can't just look for "NOVA_HERO" as that text will be applied at the end of the mission no matter what the player did
    	if (missionVariables.nova === "NOVA_ESCAPED_SYSTEM") {
    		// we're adding it manually, as the galaxy may have changed between launching and docking, so we can't rely on the correct system name being attached.
    		this._awards.push({
    			title: "Hero of Leenra",
    			worldScript: "oolite-nova",
    			description: "Awarded by the survivors of Leenra, for participating in the rescue and transportation of refugees during the tragic and catastrophic demise of the sun. Your contributions will not be forgotten.",
    			source: "Leenra",
    			galaxy: 3,
    			received: clock.adjustedSeconds,
    			imageType: 6
    		});
    	}
    
    	if (station.script.hasOwnProperty("_gcm_marketEntity") === true) {
    		if (station.script._gcm_pricesAdjusted === true) {
    			player.addMessageToArrivalReport((stn._gcm_marketAdjustType === "markets" ? "Commodity prices" : "Prices of illegal goods") +
    				" have been adjusted by " + parseInt(station.script._gcm_priceAdjustPercent * 100) +
    				"%. The " + this.$transformText(station.script._gcm_marketEntity, system.ID) + " thanks you for your continued support.");
    			station.script._gcm
    		}
    	}
    	var alleg = this.$getStationAllegiance(station);
    	var ent = this.$getImpactEntity("equipment", alleg);
    	var pct = 0;
    	if (ent !== "") pct = this.$getImpactValue(ent);
    	if (pct > 0) {
    		player.addMessageToArrivalReport("You'll get a " + parseInt(pct * 100) + "% refund on all equipment purchased here. " +
    			"The " + this.$transformText(ent, system.ID) + " thanks you for your continued support.");
    	}
    	ent = this.$getImpactEntity("ships", alleg);
    	pct = 0;
    	if (ent !== "") pct = this.$getImpactValue(ent);
    	if (pct > 0) {
    		player.addMessageToArrivalReport("You'll get a " + parseInt(pct * 100) + "% refund on any ships purchased here. " +
    			"The " + this.$transformText(ent, system.ID) + " thanks you for your continued support.");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDockedWithStation = function (station) {
    	this.$initInterface(station);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.dayChanged = function (newDay) {
    	if (newDay != this._lastDay) {
    		this._lastDay = newDay;
    		// look for any reputations that have a degrading policy
    		for (var i = 0; i < this._reputations.length; i++) {
    			var rep = this._reputations[i];
    			var ent = this._entities[rep.entity];
    			if (ent && ent.hasOwnProperty("degrade") && ent.degrade !== "" && ent.degrade !== "0|0") {
    				var amt = parseFloat(ent.degrade.split("|")[0]);
    				var days = parseFloat(ent.degrade.split("|")[1]);
    				if (rep.hasOwnProperty("lastDegrade") === false) rep.lastDegrade = clock.adjustedSeconds;
    				if ((rep.lastDegrade + (days * 86400)) < clock.adjustedSeconds) {
    					rep.reputation -= amt;
    					if (rep.reputation < ent.minValue) rep.reputation = ent.minValue;
    					if (this._debug) log(this.name, "degrading " + rep.entity + ", new value = " + rep.reputation);
    				}
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.guiScreenWillChange = function (to, from) {
    	if (to === "GUI_SCREEN_MANIFEST" && this._convertOtherReputations === true) {
    		// remove these items so they can be displayed with other reputation entries
    		if (worldScripts.display_reputations) mission.setInstructions(null, "display_reputations");
    		if (worldScripts["display-reputation-contract"]) mission.setInstructions(null, "display-reputation-contract");
    		if (worldScripts["display-reputation-parcel"]) mission.setInstructions(null, "display-reputation-parcel");
    		if (worldScripts["display-reputation-passenger"]) mission.setInstructions(null, "display-reputation-passenger");
    		// RH, EC, RRS UPS Parcel alternate between a mission description and a reputation, depending on what's active.
    		// so only remove the item if there's no active mission
    		if (worldScripts.Random_Hits)
    			if (!missionVariables.random_hits_status || missionVariables.random_hits_status !== "RUNNING")
    				mission.setInstructions(null, "Random_Hits");
    		if (worldScripts.Escort_Contracts)
    			if (worldScripts.Escort_Contracts.ec_currentcontract === false)
    				mission.setInstructions(null, "Escort_Contracts_Rep");
    		if (worldScripts["Rescue Stations"])
    			if ((!missionVariables.rescuestation_scenario || missionVariables.rescuestation_scenario === "") && missionVariables.rescuestation_stage === 0)
    				mission.setInstructions(null, "Rescue Stations");
    		var opts = ["NO", "NOT_NOW"];
    		if (worldScripts["ups_parcel"])
    			if (opts.indexOf(worldScripts["ups_parcel"].ups_parcel) >= 0)
    				mission.setInstructions(null, "ups_parcel");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // initialise the F4 screen entries
    this.$initInterface = function $initInterface(station) {
    	station.setInterface(this.name, {
    		title: "Reputation and Awards",
    		category: "Logs",
    		summary: "Lists current reputation level with various galactic bodies, and also lists any awards received.",
    		callback: this.$showScreen.bind(this)
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$showScreen = function $showScreen() {
    	this._curpage = 0;
    	this._display = 0;
    	// update/populate rep list
    	this.$updateReputationValues();
    	this.$showPage();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$showPage = function $showPage() {
    	function compareEntityName(a, b) {
    		return ((a.entity > b.entity) ? 1 : -1);
    	}
    
    	function compareSystem(a, b) {
    		return ((a.system > b.system) ? 1 : -1);
    	}
    
    	// todo add sort function for entities
    	// sort galactic/chart entities by name
    	// sort system entities by system name or ID
    	// sort regional entites by 
    	var lines = [];
    	var pagetitle = "";
    	var curChoices = {};
    	var def = "99_EXIT";
    	var ovrly = {
    		name: "gcm_trophy_1.png",
    		height: 546
    	};
    	var text = "";
    	var maxlines = 16;
    	if (this.$isBigGuiActive() === true) maxlines = 22;
    
    	// lists all reputations
    	if (this._display === 0) {
    		var headers = ["Galactic Reputations", "Chart Reputations", "Regional Reputations", "System Reputations"];
    		pagetitle = "Reputation";
    
    		// go through the list 4 times: 1 for galactic reps, 1 for chart reps, 1 for regional reps, 1 for system reps
    		// we won't show reputations for chart-wide entities outside the current chart, or for system entities not in the current chart
    		for (var j = 0; j <= 3; j++) {
    			var scope = "";
    			var header = false;
    			var lastHdr = "";
    			switch (j) {
    				case 0:
    					scope = "galactic";
    					this._reputations.sort(compareEntityName);
    					break;
    				case 1:
    					scope = "chart";
    					this._reputations.sort(compareEntityName);
    					break;
    				case 2:
    					scope = "region";
    					this._reputations.sort(compareSystem);
    					break;
    				case 3:
    					scope = "system";
    					this._reputations.sort(compareSystem);
    					break;
    			}
    
    			for (var i = 0; i < this._reputations.length; i++) {
    				if (this._entities[this._reputations[i].entity]) {
    					log(this.name, "checking " + this._reputations[i].entity);
    					if (this._entities[this._reputations[i].entity].display === true && this._entities[this._reputations[i].entity].scope === scope) {
    						var include = false;
    						switch (scope) {
    							case "chart":
    							case "region":
    							case "system":
    								if (parseInt(this._reputations[i].galaxy) === galaxyNumber) include = true;
    								break;
    							case "galactic":
    								include = true;
    								break;
    						}
    						log(this.name, "include " + include);
    						if (include === true) {
    							var ins = "";
    							// for system-level entities, because the same entity could exist in many systems, add the system name to the front of the title
    							if (scope === "system") ins = "(" + System.systemNameForID(this._reputations[i].system) + ") ";
    
    							var rep = this.$getReputation(this._reputations[i].entity, parseInt(this._reputations[i].galaxy), this._reputations[i].system);
    							log(this.name, "rep = " + rep);
    							if (rep !== 0) {
    								var txt = (this.$isNumeric(rep) === true ? this.$getReputationReward(this._reputations[i].entity, rep) : rep);
    								if (txt !== "") {
    									if (header === false) {
    										lines.push(headers[j]);
    										header = true;
    									}
    									lines.push("• " + ins + this.$transformText(this._reputations[i].entity, this._reputations[i].system) + ": " + txt);
    								}
    							}
    						}
    					}
    				}
    			}
    		}
    
    		// we should now have our complete list of reps
    		// display the current page to the player
    		//log(this.name, "min value = " + (this._curpage * maxlines - maxlines) + ", max value = " + (this._curpage * maxlines - 1));
    		for (var i = 0; i < lines.length; i++) {
    			if (i >= (this._curpage * maxlines) && i <= (this._curpage * maxlines + maxlines - 1)) text += lines[i] + "\n";
    		}
    
    		this._maxpage = Math.ceil(lines.length / maxlines);
    		if (this._maxpage > 1) {
    			pagetitle = " (Page " + (this._curpage + 1) + " of " + this._maxpage + ")";
    		}
    
    		if (this._maxpage > 1) {
    			if (this._curpage < this._maxpage - 1) {
    				curChoices["10_GOTONEXT"] = {
    					text: "[gcm_option_nextpage]",
    					color: this._menuColor
    				};
    			} else {
    				curChoices["10_GOTONEXT"] = {
    					text: "[gcm_option_nextpage]",
    					color: this._disabledColor,
    					unselectable: true
    				};
    			}
    			if (this._curpage > 0) {
    				curChoices["11_GOTOPREV"] = {
    					text: "[gcm_option_prevpage]",
    					color: this._menuColor
    				};
    			} else {
    				curChoices["11_GOTOPREV"] = {
    					text: "[gcm_option_prevpage]",
    					color: this._disabledColor,
    					unselectable: true
    				};
    			}
    		}
    		//if (this._awards.length > 0) {
    		curChoices["20_AWARDS"] = {
    			text: "[gcm_option_awards]",
    			color: this._menuColor
    		};
    		// } else {
    		//	curChoices["20_AWARDS"] = {text:"[gcm_option_awards]", color:this._disabledColor, unselectable:true};
    		// }
    		curChoices["99_EXIT"] = {
    			text: "[gcm_option_exit]",
    			color: this._exitColor
    		};
    
    	}
    
    	// list of awards received
    	if (this._display === 1) {
    		pagetitle = "Awards Received";
    		var added = 0;
    		if (this._awards.length === 0) {
    			text = expandDescription("[gcm_no_awards]");
    		} else {
    			for (var i = 0; i < this._awards.length; i++) {
    				// we'll be adding these as menu items
    				var item = this.$padTextRight(this._awards[i].title, 22) +
    					this.$padTextRight(this._awards[i].source + (parseInt(this._awards[i].galaxy) !== galaxyNumber ? " (G" + (parseInt(this._awards[i].galaxy) + 1) + ")" : ""), 9);
    
    				lines.push("01_AWARD~" + (i < 10 ? "0" : "") + i + "|" + item);
    			}
    			// display the current page to the player
    			for (var i = 0; i < lines.length; i++) {
    				if (i >= (this._curpage * maxlines) && i <= (this._curpage * maxlines + maxlines - 1)) {
    					curChoices[lines[i].split("|")[0]] = {
    						text: lines[i].split("|")[1],
    						color: this._itemColor
    					};
    					added += 1;
    				}
    			}
    
    			this._maxpage = Math.ceil(lines.length / maxlines);
    			if (this._maxpage > 1) {
    				pagetitle = " (Page " + (this._curpage + 1) + " of " + this._maxpage + ")";
    			}
    
    			if (this._maxpage > 1) {
    				if (this._curpage < this._maxpage - 1) {
    					curChoices["10_GOTONEXT"] = {
    						text: "[gcm_option_nextpage]",
    						color: this._menuColor
    					};
    					added += 1;
    				} else {
    					curChoices["10_GOTONEXT"] = {
    						text: "[gcm_option_nextpage]",
    						color: this._disabledColor,
    						unselectable: true
    					};
    					added += 1;
    				}
    				if (this._curpage > 0) {
    					curChoices["11_GOTOPREV"] = {
    						text: "[gcm_option_prevpage]",
    						color: this._menuColor
    					};
    					added += 1;
    				} else {
    					curChoices["11_GOTOPREV"] = {
    						text: "[gcm_option_prevpage]",
    						color: this._disabledColor,
    						unselectable: true
    					};
    					added += 1;
    				}
    			}
    			/*if (this._awards.length > 0) {
    				curChoices["20_AWARDS"] = {text:"[gcm_option_awards]", color:this._menuColor};
    				added += 1;
    			} else {
    				curChoices["20_AWARDS"] = {text:"[gcm_option_awards]", color:this._disabledColor, unselectable:true};
    				added += 1;
    			}*/
    
    			added += 2; // for the exit menu item
    			for (var i = 0; i < ((maxlines + 5) - added); i++) {
    				curChoices["02_SPACER_" + i] = "";
    			}
    		}
    		curChoices["21_REPUTATION"] = {
    			text: "[gcm_option_reputation]",
    			color: this._menuColor
    		};
    		curChoices["99_EXIT"] = {
    			text: "[gcm_option_exit]",
    			color: this._exitColor
    		};
    	}
    
    	// details of particular award
    	if (this, _display === 2) {
    		pagetitle = "Award";
    		def = "98_EXIT";
    		var awd = this._awards[this._selected];
    		text = this.$padTextRight(expandDescription("[gcm_award_title]"), 10) + this.$padTextRight(awd.title, 22) + "\n";
    		var cols = this.$columnText(awd.description, 22);
    		for (var i = 0; i < cols.length; i++) {
    			if (i === 0) {
    				text += this.$padTextRight(expandDescription("[gcm_award_description]"), 10) + cols[i] + "\n";
    			} else {
    				text += this.$padTextRight(" ", 10) + cols[i] + "\n";
    			}
    		}
    		text += this.$padTextRight(expandDescription("[gcm_award_system]"), 10) + this.$padTextRight(awd.source + (parseInt(awd.galaxy) !== galaxyNumber ? " (G" + parseInt(awd.galaxy) + 1 + ")" : ""), 22) + "\n";
    		text += this.$padTextRight(expandDescription("[gcm_award_time]"), 10) + this.$padTextRight(clock.clockStringForTime(awd.received), 22) + "\n";
    
    		if (awd.image !== "") {
    			ovrly = {
    				name: awd.image,
    				height: 546
    			};
    		}
    		if (awd.imageType > 0) {
    			ovrly = {
    				name: this._medalTypes[awd.imageType],
    				height: 546
    			};
    		}
    		curChoices["98_EXIT"] = {
    			text: "[gcm_option_exit]",
    			color: this._exitColor
    		};
    	}
    
    	var opts = {
    		screenID: "oolite-gcm-reputation-map",
    		title: pagetitle,
    		allowInterrupt: true,
    		exitScreen: "GUI_SCREEN_INTERFACES",
    		choices: curChoices,
    		overlay: ovrly,
    		initialChoicesKey: (this._lastchoice ? this._lastchoice : def),
    		message: text
    	};
    
    	mission.runScreen(opts, this.$screenHandler, this);
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$screenHandler = function $screenHandler(choice) {
    	delete this._lastchoice;
    	var newChoice = "";
    
    	switch (choice) {
    		case "11_GOTOPREV":
    			this._curpage -= 1;
    			if (this._curpage === 0) newChoice = "10_GOTONEXT";
    			break;
    		case "10_GOTONEXT":
    			this._curpage += 1;
    			if (this._curpage === this._maxpage - 1) newChoice = "11_GOTOPREV";
    			break;
    		case "20_AWARDS":
    			this._curpage = 0;
    			this._display = 1;
    			break;
    		case "21_REPUTATION":
    			this._curpage = 0;
    			this._display = 0;
    			break;
    		case "98_EXIT":
    			this._display = 1;
    			break;
    	}
    	if (choice.indexOf("01_AWARD") >= 0) {
    		this._display = 2;
    		this.selected = parseInt(choice.split("~")[1], 10);
    	}
    
    	this._lastchoice = choice;
    	if (newChoice !== "") {
    		this._lastchoice = newChoice;
    	}
    
    	if (choice !== "99_EXIT") {
    		this.$showPage();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns the player's current reputation with the specified entity in the specified system
    // system is ignored for chart-wide entities
    // galaxy and system are ignored for galactic-type entities
    this.$getReputation = function $getReputation(entity, galaxyID, systemID) {
    	var gal = -1;
    	var sys = -1;
    	switch (this._entities[entity].scope) {
    		case "chart":
    			gal = galaxyID;
    			break;
    		case "system":
    			gal = galaxyID;
    			sys = systemID;
    			break;
    	}
    	for (var i = 0; i < this._reputations.length; i++) {
    		if (this._reputations[i].entity === entity && isNaN(this._reputations[i].reputation) === false && parseInt(this._reputations[i].galaxy) === gal && this._reputations[i].system === sys) return this._reputations[i].reputation;
    	}
    	return 0;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns the human readable version of the players reputation with an entity.
    this.$getReputationReward = function $getReputationReward(entity, reputation) {
    	var grid = this._entities[entity].rewardGrid;
    	if (grid.length === 0) return "";
    	var result = "";
    	for (var i = 0; i < grid.length; i++) {
    		if (parseFloat(reputation) >= parseFloat(grid[i].value)) {
    			if (grid[i].hasOwnProperty("description") === true) {
    				result = grid[i].description;
    				// check to see if there is text that needs expanding.
    				if (result != null && result !== "") {
    					var check = expandDescription(result);
    					result = check;
    				} else {
    					result = "";
    				}
    			}
    			if (grid[i].hasOwnProperty("missiontext") === true) {
    				result = grid[i].missiontext;
    				// check to see if there is text that needs expanding.
    				if (result != null && result !== "") {
    					var check = expandMissionText(result);
    					result = check;
    				} else {
    					result = "";
    				}
    			}
    		}
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns an array of integers being the number of systems where the player has earned a reputation with a particular entity
    this.$getReputationSystems = function $getReputationSystems(entity, galaxy) {
    	var result = [];
    	for (var i = 0; i < this._reputations.length; i++) {
    		if (this._reputations[i].entity === entity && this._reputations[i].galaxy === galaxy && result.indexOf(this._reputations[i].system) === -1) {
    			result.push(this._reputations[i].system)
    		}
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // apply the success reputation to all entities
    this.$adjustReputationSuccess = function $adjustReputationSuccess(missType, sourceSysID, destSysID, percentComplete) {
    	// if we've been allowed to complete a mission at 0%, don't count it as a success - switch to failure
    	if (percentComplete === 0) {
    		this.$adjustReputation(missType, sourceSysID, destSysID, "Failure", percentComplete);
    		return;
    	}
    	this.$adjustReputation(missType, sourceSysID, destSysID, "Success", percentComplete);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // apply the failure reputation to all entities
    this.$adjustReputationFailure = function $adjustReputationFailure(missType, sourceSysID, destSysID, percentComplete) {
    	this.$adjustReputation(missType, sourceSysID, destSysID, "Failure", percentComplete);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // apply the adjustment to reputation
    this.$adjustReputation = function $adjustReputation(missType, sourceSysID, destSysID, adjType, percentComplete) {
    	// get list of entities for this mission type
    	var itemlist = expandDescription("[missionType" + missType + "_reputationEntities]");
    	if (itemlist === "") return;
    	var items = itemlist.split(",");
    	// get list of reputation targets
    	var targetlist = expandDescription("[missionType" + missType + "_reputationTarget]");
    	var targets = targetlist.split(",");
    	// get rep value of successful mission for each entity
    	var valuelist = expandDescription("[missionType" + missType + "_reputation" + adjType + "]");
    	var values = valuelist.split(",");
    	var compCheck = [];
    	var sysID = -1;
    	var hs_dest_check = false;
    	var hs_rep_decrease = 0;
    
    	// apply value to existing entity
    	for (var i = 0; i < this._reputations.length; i++) {
    		var rep = this._reputations[i];
    		for (var j = 0; j < items.length; j++) {
    			if (rep.entity === items[j]) {
    				sysID = -1;
    				hs_dest_check = false;
    				switch (targets[j]) {
    					default:
    						case "source":
    						sysID = sourceSysID;
    					break;
    					case "destination":
    							sysID = destSysID;
    						hs_dest_check = true;
    						break;
    				}
    
    				var ent = this._entities[rep.entity];
    				if (ent) {
    					var adding = false;
    					if (ent.scope === "galactic") adding = true;
    					if (ent.scope === "region" && parseInt(rep.galaxy) === galaxyNumber && ent.regionSystems.indexOf(sysID) >= 0) adding = true;
    					if (ent.scope === "chart" && parseInt(rep.galaxy) === galaxyNumber) adding = true;
    					if (ent.scope === "system" && parseInt(rep.galaxy) === galaxyNumber && rep.system === sysID) adding = true;
    					if (adding === true) {
    						var amt = parseFloat(values[j]) * (adjType === "Success" ? 1 * percentComplete : -1 * (1 - percentComplete))
    						if (hs_dest_check === true && amt < 0) hs_rep_decrease = values[j];
    						rep.reputation += amt;
    						// make sure we don't go outside any limits
    						if (rep.reputation > ent.maxValue) rep.reputation = ent.maxValue;
    						if (rep.reputation < ent.minValue) rep.reputation = ent.minValue;
    						values[j] = 0;
    						compCheck.push({
    							ent: rep.entity,
    							amount: amt,
    							sys: sysID
    						});
    						//this.$checkForComplimentary(rep.entity, amt, sysID);
    						this.$checkForAchievement(rep.entity, rep.reputation, sysID);
    
    						// check for home system
    					}
    				}
    			}
    		}
    	}
    	// look for any new entity records and add them
    	for (var i = 0; i < items.length; i++) {
    		var ent = this._entities[items[i]];
    		sysID = -1;
    		hs_dest_check = false;
    		switch (targets[i]) {
    			default:
    				case "source":
    				sysID = sourceSysID;
    			break;
    			case "destination":
    					sysID = destSysID;
    				hs_dest_check = true;
    				break;
    		}
    		if (ent.scope != "region" || (ent.regionGalaxy === galaxyNumber && ent.regionSystems.indexOf(sysID) >= 0)) {
    			if (values[i] != 0) {
    				var amt = parseFloat(values[i]) * (adjType === "Success" ? 1 * percentComplete : -1 * (1 - percentComplete));
    				if (hs_dest_check === true && amt < 0) hs_rep_decrease = values[i];
    				// make sure we don't go outside any limits
    				if (amt > ent.maxValue) amt = ent.maxValue;
    				if (amt < ent.minValue) amt = ent.minValue;
    
    				this._reputations.push({
    					entity: items[i],
    					galaxy: (ent.scope != "galactic" ? galaxyNumber : -1),
    					system: (ent.scope === "system" ? sysID : -1),
    					reputation: amt
    				});
    
    				compCheck.push({
    					ent: items[i],
    					amount: amt,
    					sys: sysID
    				});
    				//this.$checkForComplimentary(items[i], amt, sysID);
    				this.$checkForAchievement(items[i], amt, sysID);
    			}
    		}
    	}
    	var hs = worldScripts.HomeSystem;
    	if (hs) {
    		// is this a home system
    		if (hs.$isHomeSystem(destSysID) === true) {
    			// we aren't ever going to increase our hs rep, but doing a mission against a home sys will definitely decrease your rep
    			var rep = parseInt(hs._dockCounts[galaxyNumber][destSysID]);
    			rep -= parseInt(hs_rep_decrease);
    			hs._dockCounts[galaxyNumber][destSysID] = rep;
    		}
    	}
    	// apply any complementary values
    	if (compCheck.length > 0) {
    		for (var i = 0; i < compCheck.length; i++) {
    			this.$checkForComplimentary(compCheck[i].ent, compCheck[i].amount, compCheck[i].sys);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // adjust the reputation in any complementary entities that current exist in the players reputation array
    this.$checkForComplimentary = function $checkForComplimentary(entity, amount, sysID) {
    	var ent = this._entities[entity];
    	if (ent.hasOwnProperty("complementary") === false || ent.complementary === "") return;
    	var entlist = ent.complementary.split("~");
    	var hs_rep_decrease = 0;
    
    	for (var j = 0; j < entlist.length; j++) {
    		var comp = entlist[j].split("|");
    		var c_ent = comp[0];
    		var c_pct = parseFloat(comp[1]);
    		var compent = this._entities[c_ent];
    		var found = false;
    		for (var i = 0; i < this._reputations.length; i++) {
    			if (this._reputations[i].entity === c_ent &&
    				(compent.scope === "galactic" ||
    					(compent.scope === "chart" && this._reputations[i].galaxy === galaxyNumber) ||
    					(compent.scope === "region" && this._reputations[i].galaxy === galaxyNumber && compent.regionSystems.indexOf(sysID) >= 0) ||
    					(compent.scope === "system" && this._reputations[i].galaxy === galaxyNumber && this._reputations[i].system === sysID))) {
    				this._reputations[i].reputation += (amount * c_pct);
    				if (this._reputations[i].reputation > compent.maxValue) this._reputations[i].reputation = compent.maxValue;
    				if (this._reputations[i].reputation < compent.minValue) this._reputations[i].reputation = compent.minValue;
    				found = true;
    			}
    		}
    		if (found === false) {
    			var newamt = (amount * c_pct)
    			if (newamt < compent.minValue) newamt = compent.minValue;
    			if (newamt > compent.maxValue) newamt = compent.maxValue
    			this._reputations.push({
    				entity: c_ent,
    				galaxy: (compent.scope != "galactic" ? galaxyNumber : -1),
    				system: (compent.scope === "system" ? sysID : -1),
    				reputation: newamt
    			});
    		}
    		// special case for a home system
    		if (c_ent === "Local Government" && (amount * c_pct) < 0) hs_rep_decrease = 1;
    	}
    	var hs = worldScripts.HomeSystem;
    	if (hs && hs_rep_decrease !== 0) {
    		// is this a home system
    		if (hs.$isHomeSystem(sysID) === true) {
    			// we aren't ever going to increase our hs rep, but doing a mission against a home sys will definitely decrease your rep
    			var rep = parseInt(hs._dockCounts[galaxyNumber][sysID]);
    			rep -= hs_rep_decrease;
    			hs._dockCounts[galaxyNumber][sysID] = rep;
    		}
    	}
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$checkForAchievement = function $checkForAchievement(entity, reputation, sysID) {
    	var grid = this._entities[entity].rewardGrid;
    	if (grid.length === 0) return;
    	var item;
    	// find the item in the grid the player has reached
    	for (var i = 0; i < grid.length; i++) {
    		if (reputation >= parseFloat(grid[i].value)) item = grid[i];
    	}
    	// if this item has an achievement WS and FN, call it, passing the entity and rep values
    	if (item && item.hasOwnProperty("achievementWS") && item.hasOwnProperty("achievementFN")) {
    		worldScripts[item.achievementWS][item.achievementFN](entity, sysID, reputation);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$checkForAward = function $checkForAward(entity, title, sysID) {
    	if (this._awards.length > 0) {
    		for (var i = 0; i < this._awards.length; i++) {
    			if (this._awards[i].entity === entity &&
    				this._awards[i].title === title &&
    				this._awards[i].galaxy === galaxyNumber &&
    				this._awards[i].source === System.systemNameForID(sysID))
    				return true;
    		}
    	}
    	return false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // update any reputations that have an update link (ie. reps that are actually external to this system but we report here anyway)
    this.$updateReputationValues = function $updateReputationValues() {
    	for (var i = 0; i < this._reputations.length; i++) {
    		var ent = this._entities[this._reputations[i].entity];
    		if (ent && ent.getValueWS !== "") {
    			this._reputations[i].reputation = worldScripts[ent.getValueWS][ent.getValueFN]();
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns true if the entity has been added to the reputations list. otherwise false
    this.$isReputationInList = function $isReputationInList(entity) {
    	var result = false;
    	for (var i = 0; i < this._reputations.length; i++) {
    		if (this._reputations[i].entity === entity) {
    			var ent = this._entities[entity];
    			if (ent && (ent.scope === "galactic" || parseInt(this._reputations[i].galaxy) === galaxyNumber)) result = true;
    		}
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns the entity that has the required impact type, and for which the player has the highest reputation
    this.$getImpactEntity = function $getImpactEntity(impactType, allegiance) {
    	var sel = 0;
    	var ent = "";
    	for (var i in this._entities) {
    		if (this._entities[i].hasOwnProperty("impact") === true &&
    			this._entities[i].impact.indexOf(impactType) >= 0 &&
    			this._entities[i].impactAllegiance.indexOf(allegiance) >= 0) {
    
    			var result = this.$getReputation(i, galaxyNumber, system.ID);
    			if (result > sel) {
    				ent = i;
    				sel = result;
    			}
    		}
    	}
    	return ent;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns the impact value for a given entity, based on the player's current reputation
    this.$getImpactValue = function $getImpactValue(entity) {
    	var details = this._entities[entity];
    	var grid = details.rewardGrid;
    	var rep = this.$getReputation(entity, galaxyNumber, system.ID);
    	var result = 0;
    	for (var i = 0; i < grid.length; i++) {
    		if (grid[i].value < rep) result = grid[i].impactValue;
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$performPriceAdjustment = function $performPriceAdjustment() {
    	// are there any price adjustments to apply? check the reputation
    	var found = false;
    	for (var j = 0; j < system.stations.length; j++) {
    		var stn = system.stations[j];
    		var alleg = this.$getStationAllegiance(stn);
    		var legal_pct = 0;
    		var legal_ent = this.$getImpactEntity("markets", alleg);
    		if (legal_ent !== "") legal_pct = this.$getImpactValue(legal_ent);
    		var illegal_pct = 0;
    		var illegal_ent = this.$getImpactEntity("illegals", alleg);
    		if (illegal_ent !== "") illegal_pct = this.$getImpactValue(illegal_ent);
    		if (legal_pct > 0 && legal_pct > illegal_pct) {
    			// only update the prices if they haven't been done already
    			// a save/load sequence should not redo the price change
    			if (stn.script.hasOwnProperty("_gcm_pricesAdjusted") === false || stn.script._gcm_pricesAdjusted === false) {
    				var mkt = stn.market;
    				var adjust = [];
    				for (var i in mkt) {
    					// for a std "markets" adj, only update legal goods - leave illegals alone
    					if (mkt[i].legality_import == 0 && mkt[i].legality_export == 0) {
    						var price = parseFloat(mkt[i].price) / 10;
    						if ((parseFloat(mkt[i].price_average) / 10) > price)
    							adjust.push({
    								commodity: i,
    								old: mkt[i].price,
    								new: parseInt((price * (1 - legal_pct)) * 10)
    							});
    						if ((parseFloat(mkt[i].price_average) / 10) < price)
    							adjust.push({
    								commodity: i,
    								old: mkt[i].price,
    								new: parseInt((price * (1 + legal_pct)) * 10)
    							});
    					}
    				}
    				for (var i = 0; i < adjust.length; i++) {
    					stn.setMarketPrice(adjust[i].commodity, adjust[i].new);
    					if (this._debug) log(this.name, "adjusting " + stn.displayName + " price for " + adjust[i].commodity + ": avg=" + mkt[adjust[i].commodity].price_average + ", old=" + adjust[i].old + ", new=" + adjust[i].new);
    				}
    			}
    			stn.script._gcm_marketEntity = legal_ent;
    			stn.script._gcm_pricesAdjusted = true;
    			stn.script._gcm_priceAdjustPercent = legal_pct;
    			stn.script._gcm_marketAdjustType = "markets";
    			found = true;
    		}
    
    		// only update illegals prices if we haven't also updated legals.
    		if (illegal_pct > 0 && illegal_pct > legal_pct) {
    			// only update the prices if they haven't been done already
    			// a save/load sequence should not redo the price change
    			if (stn.script.hasOwnProperty("_gcm_pricesAdjusted") === false || stn.script._gcm_pricesAdjusted === false) {
    				var mkt = stn.market;
    				var adjust = [];
    				for (var i in mkt) {
    					// for a std "markets" adj, only update legal goods - leave illegals alone
    					if (mkt[i].legality_import !== 0 || mkt[i].legality_export !== 0) {
    						var price = parseFloat(mkt[i].price) / 10;
    						if ((parseFloat(mkt[i].price_average) / 10) > price)
    							adjust.push({
    								commodity: i,
    								old: mkt[i].price,
    								new: parseInt((price * (1 - illegal_pct)) * 10)
    							});
    						if ((parseFloat(mkt[i].price_average) / 10) < price)
    							adjust.push({
    								commodity: i,
    								old: mkt[i].price,
    								new: parseInt((price * (1 + illegal_pct)) * 10)
    							});
    					}
    				}
    				for (var i = 0; i < adjust.length; i++) {
    					stn.setMarketPrice(adjust[i].commodity, adjust[i].new);
    					if (this._debug) log(this.name, "adjusting " + stn.displayName + " price for " + adjust[i].commodity + ": avg=" + mkt[adjust[i].commodity].price_average + ", old=" + adjust[i].old + ", new=" + adjust[i].new);
    				}
    			}
    			stn.script._gcm_marketEntity = illegal_ent;
    			stn.script._gcm_pricesAdjusted = true;
    			stn.script._gcm_priceAdjustPercent = illegal_pct;
    			stn.script._gcm_marketAdjustType = "illegals";
    			found = true;
    		}
    
    	}
    	this._pricesAdjusted = found;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$performBountyAdjustment = function $performBountyAdjustment(entity) {
    	// ? Should the reduction happen each time the player has a change to rep? Or once per level?
    
    	// if the Bounty System is not installed, it's just a straight reduction.
    	// if the Bounty System is installed, perform a reduction in bounty across all player's outstanding bounties.
    	// for repLevel's 4, 5, 6, 7 only: 25% for 4, 50% for 5, 75% for 6, and 100% for 7.
    	// do this each time the player changes their rep in a +ive way
    
    	var recover = this.$getImpactValue(entity);
    	if (recover === 0) return;
    
    	var update = false;
    
    	if (worldScripts.BountySystem) {
    		var b = worldScripts.BountySystem;
    		if (b._bountyDelta > 0) b._bountyDelta = parseInt(b._bountyDelta * (1 - recover));
    		if (recover === 1) {
    			b._offences.length = 0;
    		} else {
    			if (b._offences.length > 0) {
    				// don't worry about whether the offences are transferred or not - just reduce them all
    				for (var i = 0; i < b._offences.length; i++) {
    					b._offences[i].bounty = parseInt(b._offences[i].bounty * (1 - recover));
    					update = true;
    				}
    			}
    		}
    	}
    	if (player.bounty > 0) {
    		player.ship.setBounty(parseInt(player.bounty * (1 - recover)));
    		update = true;
    	}
    
    	if (update === true) {
    		//send email
    		var email = worldScripts.EmailSystem;
    		if (email) {
    			var client = this.$getClientName(system.ID, entity);
    			email.$createEmail({
    				sender: client,
    				subject: "Bounty reduction",
    				date: global.clock.adjustedSeconds,
    				message: this.$transformText(expandDescription("[gcm_bounty_reduction_email]", {
    					reduction: parseInt(recover * 100),
    					orgname: entity,
    					name: client
    				}), system.ID)
    			});
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // internal function to return the cargo reputation
    this.$cargoReputation = function $cargoReputation() {
    	return player.contractReputation;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // internal function to return the parcel reputation
    this.$parcelReputation = function $parcelReputation() {
    	return player.parcelReputation;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // internal function to return the passenger reputation
    this.$passengerReputation = function $passengerReputation() {
    	return player.passengerReputation;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$randomHitsReputation = function $randomHitsReputation() {
    	return "You are rated as " + missionVariables.random_hits_currentrank + " " + missionVariables.random_hits_playertitle;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$upsReputation = function $upsReputation() {
    	return "You are currently rated as '" + this.$getReputationReward("UPS", worldScripts.ups_parcel._reputation()) + "'";
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$escortContractsReputation = function $escortContractsReputation() {
    	return worldScripts.Escort_Contracts.ec_escortrep;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$rescueStationsReputation = function $rescueStationsReputation() {
    	return missionVariables.rescuestation_reputation;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // appends space to currentText to the specified length in 'em'
    this.$padTextRight = function $padTextRight(currentText, desiredLength, leftSwitch) {
    	if (currentText == null) currentText = "";
    	var hairSpace = String.fromCharCode(31);
    	var ellip = "…";
    	var currentLength = defaultFont.measureString(currentText.replace(/%%/g, "%"));
    	var hairSpaceLength = defaultFont.measureString(hairSpace);
    	// calculate number needed to fill remaining length
    	var padsNeeded = Math.floor((desiredLength - currentLength) / hairSpaceLength);
    	if (padsNeeded < 1) {
    		// text is too long for column, so start pulling characters off
    		var tmp = currentText;
    		do {
    			tmp = tmp.substring(0, tmp.length - 2) + ellip;
    			if (tmp === ellip) break;
    		} while (defaultFont.measureString(tmp.replace(/%%/g, "%")) > desiredLength);
    		currentLength = defaultFont.measureString(tmp.replace(/%%/g, "%"));
    		padsNeeded = Math.floor((desiredLength - currentLength) / hairSpaceLength);
    		currentText = tmp;
    	}
    	// quick way of generating a repeated string of that number
    	if (!leftSwitch || leftSwitch === false) {
    		return currentText + new Array(padsNeeded).join(hairSpace);
    	} else {
    		return new Array(padsNeeded).join(hairSpace) + currentText;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // appends space to currentText to the specified length in 'em'
    this.$padTextLeft = function $padTextLeft(currentText, desiredLength) {
    	return this.$padTextRight(currentText, desiredLength, true);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // arranges text into a array of strings with a particular column width
    this.$columnText = function $columnText(originalText, columnWidth) {
    	var returnText = [];
    	if (defaultFont.measureString(originalText) > columnWidth) {
    		var hold = originalText;
    		do {
    			var newline = "";
    			var remain = "";
    			var point = hold.length;
    			do {
    				point = hold.lastIndexOf(" ", point - 1);
    				newline = hold.substring(0, point).trim();
    				remain = hold.substring(point + 1).trim();
    			} while (defaultFont.measureString(newline) > columnWidth);
    			returnText.push(newline);
    			if (remain != "") {
    				if (defaultFont.measureString(remain) <= columnWidth) {
    					returnText.push(remain);
    					hold = "";
    				} else {
    					hold = remain;
    				}
    			} else {
    				hold = "";
    			}
    		} while (hold != "");
    	} else {
    		returnText.push(originalText);
    	}
    	return returnText;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns true if a HUD with allowBigGUI is enabled, otherwise false
    this.$isBigGuiActive = function $isBigGuiActive() {
    	if (oolite.compareVersion("1.83") <= 0) {
    		return player.ship.hudAllowsBigGui;
    	} else {
    		var bigGuiHUD = ["XenonHUD.plist", "coluber_hud_ch01-dock.plist"]; // until there is a property we can check, I'll be listing HUD's that have the allow_big_gui property set here
    		if (bigGuiHUD.indexOf(player.ship.hud) >= 0) {
    			return true;
    		} else {
    			return false;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // calculates the player's bonus factor for a particular mission type by looking at their reputation
    this.$playerMissionReputation = function $playerMissionReputation(sysID, missType) {
    	// get the list of entities related to this mission type
    	var entities = expandDescription("[missionType" + missType + "_reputationEntities]");
    	if (entities != "") {
    		var entlist = entities.split(",");
    		var rep = 0;
    		var max = 0;
    		var min = 0;
    		var total = 0;
    		// for each entity, add the player's current reputation, calculated as a percentage based on the maxValue/minValue settings for the entity
    		for (var i = 0; i < entlist.length; i++) {
    			var ent = this._entities[entlist[i]];
    			// just get the single value if we've either asked for a single system, or the entity is chart/galactic based.
    			if (sysID !== -1 || ent.scope !== "system") {
    				rep = this.$getReputation(entlist[i], galaxyNumber, sysID);
    				max = ent.maxValue;
    				min = ent.minValue;
    				if (max - min !== 0) total += (rep - min) / (max - min);
    			} else {
    				// if we've been passed a "-1", this means get the total for this entity in all systems in the current chart
    				// we should only do this if the scope of the entity is system based.
    				var syslist = this.$getReputationSystems(entlist[i], galaxyNumber);
    				var subtotal = 0;
    				if (syslist.length > 0) {
    					for (var j = 0; j < syslist.length; j++) {
    						rep = this.$getReputation(entlist[i], galaxyNumber, syslist[j]);
    						max = ent.maxValue;
    						min = ent.minValue;
    						if (max - min !== 0) subtotal += (rep - min) / (max - min);
    					}
    					total += (subtotal / syslist.length);
    				}
    			}
    		}
    		// final percentage = sum of rep percentages / count of entities involved
    		var result = (total / entlist.length);
    		// convert this percentage into a number betwen 0.5 and 2.0
    		return (1.5 * result) + 0.5;
    	} else {
    		return 0.5; // if no rep - return min value
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // adds or updates a client name in our stored list.
    this.$addNameToClientList = function $addNameToClientList(cname, sysID, entity, idx) {
    	var found = false;
    	for (var i = 0; i < this._clientNames.length; i++) {
    		var item = this._clientNames[i];
    		if (item.galaxy == galaxyNumber && item.systemID == sysID && item.entity == entity && item.index == idx) {
    			// update name
    			item.clientName = cname;
    			found = true;
    			break;
    		}
    	}
    	if (found === false) {
    		// new name
    		this._clientNames.push({
    			clientName: cname,
    			galaxy: galaxyNumber,
    			systemID: sysID,
    			entity: entity,
    			index: idx,
    			created: clock.adjustedSeconds,
    		});
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // gets a stored name, or generates a new one if no stored name found
    this.$getClientName = function $getClientName(sysID, entity) {
    	var cname = "";
    	// possible 4 names for local governments
    	var idx = Math.floor(Math.random() * 4);
    	// only 2 from other station types
    	if (entity.indexOf("Local Government") === -1) idx = Math.floor(Math.random() * 2);
    
    	for (var i = 0; i < this._clientNames.length; i++) {
    		var item = this._clientNames[i];
    		if (item.galaxy == galaxyNumber && item.systemID == sysID && item.entity == entity && item.index == idx) {
    			// start to change names after a random number of days (between 14 and 28)
    			if (clock.adjustedSeconds - item.created < (86400 * (Math.floor(Math.random() * 14) + 14)))
    				cname = item.clientName;
    		}
    	}
    	if (cname === "") {
    		cname = this.$generateName();
    		if (entity !== "") this.$addNameToClientList(cname, sysID, entity, idx);
    	}
    	return cname;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$generateName = function $generateName() {
    	var text = worldScripts.GNN_PhraseGen._makePhrase(worldScripts.GNN_PhraseGen.$pool["GNN_Names"])
    	return text;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // process any text lookup or replacements in a text item
    // this is to allow an reputations or awards to be defined in a generic way, but appear unique for a particular system
    // eg, the same award could be called different things based on the government type
    // thus the definition in descriptions.plist would be "AngelOfMercy^G"
    // this would result in a expansion lookup of "[AngelOfMercy7]" for a Corp State award
    this.$transformText = function $transformText(text, sysID, destID) {
    	var sys = null;
    	var result = text;
    	var controlChar = ["\[", "~", "$"];
    	//look for any lookup expansions based on gov, eco, or TL,
    	var found = false;
    	for (var i = 0; i < controlChar.length; i++) {
    		if (result.indexOf(controlChar[i]) >= 0) {
    			found = true;
    			break;
    		}
    	}
    	if (found === true) {
    		sys = System.infoForSystem(galaxyNumber, sysID);
    		var lookup = result;
    		lookup = lookup.replace(/\$G/g, sys.government); // insert government number (0-7)
    		lookup = lookup.replace(/\$E/g, sys.economy); // insert economy number (0-7)
    		lookup = lookup.replace(/\$T/g, sys.techLevel); // insert techlevel (0-14)
    		lookup = lookup.replace(/\$H/g, sys.name); // insert system name (eg Lave)
    		lookup = lookup.replace(/\$S/g, galaxyNumber + "-" + sysID); // insert system ID (eg 0-7, where first digit is galaxy num, second is system id)
    
    		if (lookup.indexOf("~") >= 0 || lookup.indexOf("] ") >= 0) {
    			// replace our custom brackets with real ones
    			// a space and ~ char is an opening bracket.
    			lookup = lookup.replace(/ ~/g, " [");
    			// a ~ char and a space is a closing bracket
    			lookup = lookup.replace(/~ /g, "] ");
    			// because of the need for a space after a closing bracket symbol, look for any "] ." or "] ," and remove the space
    			lookup = lookup.replace(/\] \./g, "].");
    			lookup = lookup.replace(/\] \,/g, "],");
    			lookup = lookup.replace(/\] \!/g, "]!");
    			lookup = lookup.replace(/\] \?/g, "]?");
    		} else {
    			lookup = "[" + lookup + "]";
    		}
    		// perform the expansion with the new text
    		//log(this.name, "checking transform " + lookup);
    		result = expandDescription(lookup);
    	} else {
    		result = expandDescription(result);
    	}
    	// look for any inplace expansions
    	if (result.indexOf("`") >= 0) {
    		if (sys == null) sys = System.infoForSystem(galaxyNumber, sysID);
    		var pgn = this.$getPirateGroupName(galaxyNumber, sysID);
    		var d_pgn = "";
    		if (destID && destID >= 0 && destID <= 255) d_pgn = this.$getPirateGroupName(galaxyNumber, destID);
    		var repl = result;
    		do {
    			repl = repl.replace(/`H/g, sys.name);
    			repl = repl.replace(/`I/g, sys.name + "ian");
    			repl = repl.replace(/`PGN/g, pgn);
    			repl = repl.replace(/`DPGN/g, d_pgn);
    		} while (repl.indexOf("`") >= 0);
    		result = repl;
    	}
    	return result;
    }
    
    //--------------------------------------------------------------------------------------------------------------
    // returns true if the passed value is numeric, otherwise false
    this.$isNumeric = function $isNumeric(n) {
    	return !isNaN(parseFloat(n)) && isFinite(n);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$getStationAllegiance = function $getStationAllegiance(station) {
    	var alleg = station.allegiance;
    	if (!alleg || alleg === "") alleg = "neutral";
    	if (station.isMainStation) alleg = "galcop";
    	return alleg;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns a random but system-consistent pirate band name for a particular system
    this.$getPirateGroupName = function $getPirateGroupName(galID, sysID) {
    	var seed = (galID + 1) * 1000 + sysID;
    	var sysName = System.infoForSystem(galID, sysID).name;
    	var def = {
    		fieldA: ["Bloody", "Silver", "Wild", "Crazy", "Cyclone", "Black","Blue","Dark","Gray","Iron","Red","Steel","White"],
    		fieldB: ["dread", "lost", "alligator","angel","asura","banshee","basilisk","beast","bull","bunyip","butcher","chimera","cockatrice","crocodile","cuttlefish","cyclops","daimon","demon","deva","devil","dragon","executioner","fiend","gargoyle","&genie","ghost","ghoul","goblin","god","goddess","golem","gorgon","guardian","harpy","hellcat","hippogriff","hydra","hyena","jinn","judge","kelpie","kraken","lion","lioness","loup-garou","minotaur","monster","octopus","ogre","&oni","orca","owl","phantom","pharaoh","prophet","rakshasa","reaper","revenant","sasquatch","scorpion","&seraph","serpent","shadow","shoggoth","shrike","sorceror","spectre","sphinx","spider","spirit","&squid","tarantula","tengu","tiger","tigress","troll","vampire","vulture","walrus","warlock","warthog","wendigo","werewolf","witch","wizard","wolf","wolverine","wyrm","wyvern"],
    		fieldC: ["bandit$", "pillager$", "pirate$", "plunderer$", "raider$", "renegade$", "marauder$", "sinner$", "ravager$", "buccaneer$", "drifter$", "corsair$", "rover$"],
    		fieldD: ["bloodied", "giant", "stray", "abandoned","abominable","ageless","ancient","archaic","boundless","bright","burning","dead","dark","deserted","dread","eternal","everlasting","fiery","forgotten","forsaken","golden","grey","hallowed","hidden","hoary","holy","horned","icy","infinite","lightless","living","lost","millenial","misty","never-ending","primal","primeval","primordial","pristine","sacred","shining","silver","sunless","sunken","timeless","unbroken","undying","unholy"],
    		fieldE: ["adder", "bear", "cobra", "dog", "dragon", "eagle", "hawk", "jackal", "lion", "octopus", "panther", "python", "rat", "scorpion", "shark", "spider", "squid", "stag", "tiger", "wolf"],
    		fieldF: ["claw", "cove", "dagger", "eye", "fist", "flag", "flame", "hammer", "hand", "heart", "knife", "mountain", "skull", "sword"],
    		fieldG: [],
    		fieldH: [],
    		fieldI: [],
    		fieldJ: [],
    		fieldK: [],
    		fieldL: [],
    		fieldM: [],
    		fieldN: [],
    		fieldO: [],
    		fieldP: [],
    		fieldQ: [],
    		fieldR: [],
    		sentences: [
    			"The +4 >+3 of " + sysName,
    			"The +4 >+3",
    			"The >+5",
    			"The >+5 of " + sysName,
    			"The 1 +5 >+3 of " + sysName,
    			"The 1 +5 >+3",
    			"The +2 >+5 of " + sysName,
    			"The +2 >+5",
    			">3 of the +4 +6",
    			"The 1 >+3 of " + sysName,
    			"The 1 >+3",
    			"The " + sysName + "ian >+3",
    			"The 1 " + sysName + "ian >+3",
    			"The +2 >+3 of " + sysName,
    			"The +2 >+3"
    		],
    	};
    	var text = worldScripts.GNN_PhraseGen._makePhrase(def, null, 0, seed, null);
    	return text;
    }
    Scripts/galcopbb_satellites.js
    "use strict";
    this.name = "GalCopBB_Satellites";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Uses Satellites as basis for missions (110-129)";
    this.license = "CC BY-NC-SA 4.0";
    
    /*
    	TODO: 
    	check what happens to left over attackers when all sats destroyed
    	need to add government bias to mission availability
    
    	Missions involving satellites from Satellites.OXP
    
    	110/111/112/113/114 - Extract data
    		*type a: sat 1 - security data (galcop - foreign - risk)
    		*type b: sats 2,3 - comm logs (galcop - foreign - risk)
    		*type c: sat 4 - scan data from telescope (galcop - foreign - risk)
    		*type d: sat 1 - security data (pirate - local - risk)
    			where is the risk? no chance for witchpoint assassins if the task is local
    			need initial step which is risky - how to get passcode?
    			maybe transmit an encrypted packet to a police ship with device while police is occupied (fighting)
    			doing it if they aren't fighting will mean a mission fail and legal penalty
    		type e: sats 2,3 - comm logs (pirate - local - norisk)
    	115/116/117 - Install new firmware
    		*type a - local system update (galcop - no risk maintenance)
    		type b - local system update (pirate - high risk subvert)
    		type c - foreign system update (galcop - high risk subvert)
    	118/119/120/121 - insert data
    		*type a - sat 1 (galcop - foreign - hi risk) 
    		type b - sats 2,3 (galcop - foreign - hi risk)
    		type c - sat 1 (pirate - local - hi risk)
    		type d - sats 2,3 (pirate - local - hi risk)
    	122/123 - Destroy sats
    		*type a - destroy all type 1,2,3 (galcop, foreign)
    		type b - destroy all type 1,2,3 (pirate, local)
    		todo: get manifest entry to have number of satellites left to destroy
    	124 - Defend satellites
    		*type a - defend a satellite(s) from a number of enemies (galcop, local)
    		might need signal/comms message to let player know when to move from one satellite to another if there is more than one.
    		mission ends when all attackers have been eliminated
    		attacked spawned just our of scanner range, away from the player's current view
    
    		adjust beacon on specific target satellite with "**"
    		give player x minutes to reach satellite, then start spawning ships
    		need "invader" AI to target the satellite unless the player intervenes
    
    	for data interface types, player will be given a one-time passcode, that can only be used between particular times
    	(period will be 15 min)
    	using the code outside those times will invalidate the mission and potentially give player offender status
    	(if any galcop ships are in range when passcode sent)
    	all mission text should say "all surveillance satellites" or "all COM satellites" in system, as there may be more than 1
    
    	TODO:
    	need differentiation between missions - different challenges
    	firmware will be a longer install period
    	destroy mission - make sure there are some police on orbit patrol
    	police orbit patrol ai - similar to satellite ai, but with ability to switch to standard police ai when offence committed
    	test guard AI
    	Idea: mission to get passcode? fly to way point, wait for ship?
    */
    
    this._debug = false;
    this._rsnInstalled = false;
    this._initial = false;
    this._satArray = {};
    this._secCodeMissionTypes = [110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121];
    this._secCodeIllegalTypes = [110, 111, 112, 113, 114, 116, 117, 118, 119, 120, 121];
    this._functionAMissionTypes = [110, 111, 112, 113, 114];
    this._functionBMissionTypes = [115, 116, 117];
    this._functionCMissionTypes = [118, 119, 120, 121];
    this._satTarget = null;
    this._satRespondTimer = null;
    this._satRange = 2000;
    this._codeExpiry = 0;
    this._dataReceived = 0;
    this._dataTransferTimer = null;
    this._dataTransferFreq = 0;
    this._dataTransferCounter = 0;
    this._updateMessage = "";
    this._msgCounter = 0;
    this._seenByPolice = false;
    this._satDefenders = [];
    this._satDefenderTimer = null;
    this._satToDefend = null;
    this._satAttackers = [];
    this._attackers = [];
    this._satAttackerTimer = null;
    this._beginAttackWaves = null;
    this._wavesToCreate = 0;
    this._preferredAssassinLightShips = [];
    this._preferredAssassinMediumShips = [];
    this._countDelay = 0;
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
    	// disable the default scripts for satellites - we'll create them during system population
    	if (worldScripts["Satellite"]) {
    		if (missionVariables.GalCopBBMissions_Satellites) {
    			this._satArray = JSON.parse(missionVariables.GalCopBBMissions_Satellites);
    		}
    
    		// prepopulate for testing
    		//for (var i = 0; i <= 255; i++) {
    		//    this._satArray[i] = [1,1,1,1];
    		//}
    
    		var gcm = worldScripts.GalCopBB_Missions;
    		// add these mission types into the main control
    		var list = [110, 111, 112, 115, 118, 122, 123, 124];
    		gcm._availableMissionTypes = gcm._availableMissionTypes.concat(list);
    		//gcm._availableMissionTypes.push(113); disabled for the moment
    		this._debug = gcm._debug;
    		this.$getRandomShipName = gcm.$getRandomShipName;
    
    	} else {
    		delete this.systemWillPopulate;
    		delete this.systemWillRepopulate;
    		delete this.startUpComplete;
    		delete this.playerWillSaveGame;
    		delete this.playerEnteredNewGalaxy;
    	}
    	// set a flag if random ship names is installed
    	if (worldScripts["randomshipnames"]) this._rsnInstalled = true;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerEnteredNewGalaxy = function () {
    	this._satArray = {};
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillSaveGame = function () {
    	missionVariables.GalCopBBMissions_Satellites = JSON.stringify(this._satArray);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.systemWillPopulate = function () {
    	this._initial = true;
    	this.$getAssassinShipLists();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.systemWillRepopulate = function () {
    	if (this._initial === true) {
    		this._initial = false;
    		this.$checkSatellitesInSystem();
    		this.$checkForActiveMissions();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillLaunchFromStation = function (station) {
    	this.$checkForActiveMissions();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillEnterWitchspace = function (cause, destination) {
    	this.$stopTimers();
    	this._codeExpiry = 0;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = function (whom, why) {
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$stopTimers = function $stopTimers() {
    	if (this._satRespondTimer && this._satRespondTimer.isRunning) this._satRespondTimer.stop();
    	delete this._satRespondTimer;
    	if (this._dataTransferTimer && this._dataTransferTimer.isRunning) this._dataTransferTimer.stop();
    	delete this._dataTransferTimer;
    	if (this._satDefenderTimer && this._satDefenderTimer.isRunning) this._satDefenderTimer.stop();
    	delete this._satDefenderTimer;
    	if (this._satAttackerTimer && this._satAttackerTimer.isRunning) this._satAttackerTimer.stop();
    	delete this._satAttackerTimer;
    	if (this._beginAttackWaves && this._beginAttackWaves.isRunning) this._beginAttackWaves.stop();
    	delete this._beginAttackWaves;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$checkSatellitesInSystem = function $checkSatellitesInSystem() {
    	var sats = system.shipsWithRole("RSsatellite");
    	if (sats.length > 0) {
    		var ary = [0, 0, 0, 0];
    		for (var i = 0; i < sats.length; i++) {
    			// find all satellites and add a bcc message to them
    			this.$resetSatComms(sats[i]);
    			// update array values
    			if (sats[i].name === "Surveillance-Satellite" || sats[i].name === "Surveillance Satellite") ary[0] += 1;
    			if (sats[i].name === "COMLR-Satellite" || sats[i].name === "COMLR Satellite") ary[1] += 1;
    			if (sats[i].name === "COM-Satellite" || sats[i].name === "COM Satellite") ary[2] += 1;
    			if (sats[i].name === "Satellite Telescope") ary[3] += 1;
    		}
    		this._satArray[system.ID] = ary;
    	} else {
    		this._satArray[system.ID] = [0, 0, 0, 0];
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$checkForActiveMissions = function $checkForActiveMissions() {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var list = gcm.$getListOfMissions(true, [110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 122, 123, 124]);
    	if (list.length > 0) {
    		// loop through all active missions and see if any need to be set up for this system
    		for (var i = 0; i < list.length; i++) {
    			// *** type 110/111/112/113/114/115/116 - extract data/update firmware/upload data
    			if (this._secCodeMissionTypes.indexOf(list[i].data.missionType) >= 0 &&
    				list[i].destination === system.ID &&
    				list[i].data.quantity === 0 &&
    				list[i].data.destroyedQuantity === 0 &&
    				(list[i].expiry === -1 || list[i].expiry > clock.adjustedSeconds)) {
    
    				// find the right satellite
    				var sats = system.shipsWithRole("RSsatellite");
    				var pcode = expandDescription("[gcm_passcode]");
    				if (this._codeExpiry === 0) this._codeExpiry = clock.adjustedSeconds + (16 * 60);
    				// for the 115 mission there is no expiry on the passcode
    				if (list[i].data.missionType === 115) this._codeExpiry = clock.adjustedSeconds + (86400 * 10);
    				for (var j = 0; j < sats.length; j++) {
    					// we're going to slow down the max speed of these satellites so the player has some chance of keeping up no matter what they're flying
    					var add = false;
    					if (sats[j].name === "Surveillance-Satellite" && list[i].data.satelliteTypes.indexOf("1") >= 0) {
    						sats[j].maxSpeed = player.ship.maxSpeed - 30;
    						add = true;
    					}
    					if (sats[j].name === "COMLR-Satellite" && list[i].data.satelliteTypes.indexOf("2") >= 0) {
    						sats[j].maxSpeed = player.ship.maxSpeed - 50;
    						add = true;
    					}
    					if (sats[j].name === "COM-Satellite" && list[i].data.satelliteTypes.indexOf("3") >= 0) {
    						sats[j].maxSpeed = player.ship.maxSpeed - 70;
    						add = true;
    					}
    					if (sats[j].name === "Satellite Telescope" && list[i].data.satelliteTypes.indexOf("4") >= 0) add = true;
    
    					// set up broadcast comms interface
    					if (add === true) {
    						// make sure our mission satellites have a beacon, and a meaningful label
    						this.$addBeaconToSatellite(sats[j]);
    						var scr = sats[j].script;
    						// add the mission id and passcode
    						scr._missionID = list[i].ID;
    						scr._passcode = pcode;
    						if (this._secCodeIllegalTypes.indexOf(list[i].data.missionType) >= 0) {
    							scr._checkPolice = true;
    						}
    						// plug in our ship scripts
    						if (scr.shipBeingAttacked && !scr.$gcm_hold_shipBeingAttacked) scr.$gcm_hold_shipBeingAttacked = scr.shipBeingAttacked;
    						scr.shipBeingAttacked = this.$sat_shipBeingAttacked;
    						if (scr.shipAttackedWithMissile && !scr.$gcm_hold_shipAttackedWithMissile) scr.$gcm_hold_shipAttackedWithMissile = scr.shipAttackedWithMissile;
    						scr.shipAttackedWithMissile = this.$sat_shipAttackedWithMissile;
    					}
    				}
    			}
    
    			// defend
    			if (list[i].data.missionType === 124 &&
    				list[i].destination === system.ID &&
    				list[i].data.quantity < (list[i].data.targetQuantity - list[i].data.destroyedQuantity) &&
    				(list[i].expiry === -1 || list[i].expiry > clock.adjustedSeconds)) {
    				// slow down any satellites currently in system so the player and attackers can catch them
    				var sats = system.shipsWithRole("RSsatellite");
    				for (var j = 0; j < sats.length; j++) {
    					var scr = sats[j].script;
    					if (scr.shipDied && !scr.$gcm_hold_shipDied) scr.$gcm_hold_shipDied = scr.shipDied;
    					scr.shipDied = this.$sat_shipDied;
    					this.$addBeaconToSatellite(sats[j]);
    					scr._missionID = list[i].ID;
    					if (sats[j].name === "Surveillance-Satellite" || sats[j].name === "Surveillance Satellite") sats[j].maxSpeed = 200;
    					if (sats[j].name === "COMLR-Satellite" || sats[j].name === "COMLR Satellite") sats[j].maxSpeed = 170;
    					if (sats[j].name === "COM-Satellite" || sats[j].name === "COM Satellite") sats[j].maxSpeed = 150;
    					if (sats[j].name === "Satellite Telescope") sats[j].maxSpeed = 100;
    				}
    				var satIndex = Math.floor(Math.random() * sats.length);
    				sats[satIndex].beaconLabel = "**" + sats[satIndex].beaconLabel;
    				this._satToDefend = sats[satIndex];
    				this._wavesToCreate = Math.floor(Math.random() * 3) + 1;
    				if (!this._beginAttackWaves || this._beginAttackWaves.isRunning === false)
    					this._beginAttackWaves = new Timer(this, this.$beginAttacks, 10, 10);
    				this._countDelay = 0;
    			}
    
    			// destroy satellites
    			if ((list[i].data.missionType === 122 || list[i].data.missionType === 123) &&
    				list[i].destination === system.ID &&
    				list[i].data.quantity < (list[i].data.targetQuantity - list[i].data.destroyedQuantity) &&
    				list[i].expiry > clock.adjustedSeconds) {
    				// add some lurking assassins around the satellites
    				var sats = system.shipsWithRole("RSsatellite");
    				for (var j = 0; j < sats.length; j++) {
    					// we're going to slow down the max speed of these satellites so the guards have some chance of keeping up
    					var add = false;
    					if ((sats[j].name === "Surveillance-Satellite" || sats[j].name === "Surveillance Satellite") && list[i].data.satelliteTypes.indexOf("1") >= 0) {
    						sats[j].maxSpeed = 200;
    						add = true;
    					}
    					if ((sats[j].name === "COMLR-Satellite" || sats[j].name === "COMLR Satellite") && list[i].data.satelliteTypes.indexOf("2") >= 0) {
    						sats[j].maxSpeed = 170;
    						add = true;
    					}
    					if ((sats[j].name === "COM-Satellite" || sats[j].name === "COM Satellite") && list[i].data.satelliteTypes.indexOf("3") >= 0) {
    						sats[j].maxSpeed = 150;
    						add = true;
    					}
    					if (add === true) {
    						var scr = sats[j].script;
    						if (scr.shipDied && !scr.$gcm_hold_shipDied) scr.$gcm_hold_shipDied = scr.shipDied;
    						scr.shipDied = this.$sat_shipDied;
    						if (scr.shipBeingAttacked && !scr.$gcm_hold_shipBeingAttacked) scr.$gcm_hold_shipBeingAttacked = scr.shipBeingAttacked;
    						scr.shipBeingAttacked = this.$sat_shipBeingAttacked;
    						if (scr.shipBeingAttackedUnsuccessfully && !scr.$gcm_hold_shipBeingAttackedUnsuccessfully) scr.$gcm_hold_shipBeingAttackedUnsuccessfully = scr.shipBeingAttackedUnsuccessfully;
    						scr.shipBeingAttackedUnsuccessfully = this.$sat_shipBeingAttackedUnsuccessfully;
    						if (scr.shipAttackedWithMissile && !scr.$gcm_hold_shipAttackedWithMissile) scr.$gcm_hold_shipAttackedWithMissile = scr.shipAttackedWithMissile;
    						scr.shipAttackedWithMissile = this.$sat_shipAttackedWithMissile;
    
    						// make sure our mission satellites have a beacon, and a meaningful label
    						this.$addBeaconToSatellite(sats[j]);
    						// add the guards for this satellite
    						this.$addGuards(sats[j]);
    						// add our mission ID
    						scr._missionID = list[i].ID;
    					}
    				}
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$addBeaconToSatellite = function $addBeaconToSatellite(sat) {
    	sat.beaconCode = "RSSatellite_location";
    	sat.beaconLabel = sat.name;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$resetSatComms = function $resetSatComms(sat) {
    	var bcc = worldScripts.BroadcastCommsMFD;
    	this._msgCounter += 1;
    	var msg = "";
    	if (sat.script._passcode) {
    		msg = "Requesting access " + sat.script._passcode;
    	} else {
    		msg = expandDescription("[gcm_transmitting_sat_passcode]");
    	}
    	bcc.$createMessage({
    		messageName: "gcm_transmit_sat_passcode_" + this._msgCounter,
    		callbackFunction: this.$transmitSecurityCode.bind(this),
    		displayText: "Transmit passcode",
    		messageText: msg,
    		ship: sat,
    		transmissionType: "target",
    		deleteOnTransmit: true,
    		delayCallback: 2,
    		hideOnConditionRed: true
    	});
    	if (bcc.$checkMessageExists("gcm_transmit_function_A") === true) bcc.$removeMessage("gcm_transmit_function_A");
    	if (bcc.$checkMessageExists("gcm_transmit_function_B") === true) bcc.$removeMessage("gcm_transmit_function_B");
    	if (bcc.$checkMessageExists("gcm_transmit_function_C") === true) bcc.$removeMessage("gcm_transmit_function_C");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$clearSatFunctions = function $clearSatFunctions(sat) {
    	var bcc = worldScripts.BroadcastCommsMFD;
    	if (bcc.$checkMessageExists("gcm_transmit_function_A") === true) bcc.$removeMessage("gcm_transmit_function_A");
    	if (bcc.$checkMessageExists("gcm_transmit_function_B") === true) bcc.$removeMessage("gcm_transmit_function_B");
    	if (bcc.$checkMessageExists("gcm_transmit_function_C") === true) bcc.$removeMessage("gcm_transmit_function_C");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$transmitSecurityCode = function $transmitSecurityCode() {
    	var target = player.ship.target;
    	if (player.ship.position.distanceTo(target) > this._satRange) {
    		player.consoleMessage("Satellite out of range");
    		this.$resetSatComms(target);
    		return;
    	}
    	target.commsMessage(expandDescription("[gcm_analysing_code_satellite]"), player.ship);
    	// start a timer to respond to passcode
    	this._satTarget = target;
    	this._satRespondTimer = new Timer(this, this.$checkPasscode.bind(this), 10, 0);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$checkPasscode = function $checkPasscode() {
    	if (player.ship.position.distanceTo(this._satTarget) > this._satRange) {
    		player.consoleMessage("Satellite out of range");
    		this.$resetSatComms(this._satTarget);
    		this._satTarget = null;
    	}
    	if (player.ship.target != this._satTarget) {
    		player.consoleMessage("Communication link disconnected");
    		this.$resetSatComms(this._satTarget);
    		this._satTarget = null;
    	}
    	if (this._codeExpiry === 0 || clock.adjustedSeconds > this._codeExpiry || !this._satTarget.script._passcode) {
    		this._satTarget.commsMessage("Invalid code. Disconnecting.", player.ship);
    		// result? penalise player if police in range?
    		this.$resetSatComms(this._satTarget);
    		this._satTarget = null;
    		return;
    	}
    	this._satTarget.commsMessage(expandDescription("[gcm_passcode_success_sat]"), player.ship);
    	var bcc = worldScripts.BroadcastCommsMFD;
    	bcc.$createMessage({
    		messageName: "gcm_transmit_function_A",
    		callbackFunction: this.$transmitFunctionA.bind(this),
    		displayText: "--Download data",
    		messageText: "Download data",
    		ship: this._satTarget,
    		transmissionType: "target",
    		deleteOnTransmit: false,
    		delayCallback: 3,
    		hideOnConditionRed: true
    	});
    	var bcc = worldScripts.BroadcastCommsMFD;
    	bcc.$createMessage({
    		messageName: "gcm_transmit_function_B",
    		callbackFunction: this.$transmitFunctionB.bind(this),
    		displayText: "--Upgrade firmware",
    		messageText: "Upgrade firmware",
    		ship: this._satTarget,
    		transmissionType: "target",
    		deleteOnTransmit: false,
    		delayCallback: 3,
    		hideOnConditionRed: true
    	});
    	var bcc = worldScripts.BroadcastCommsMFD;
    	bcc.$createMessage({
    		messageName: "gcm_transmit_function_C",
    		callbackFunction: this.$transmitFunctionC.bind(this),
    		displayText: "--Upload data",
    		messageText: "Upload data",
    		ship: this._satTarget,
    		transmissionType: "target",
    		deleteOnTransmit: false,
    		delayCallback: 3,
    		hideOnConditionRed: true
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$transmitFunctionA = function $transmitFunctionA() {
    	if (this._dataTransferTimer && this._dataTransferTimer.isRunning) return;
    	if (player.ship.position.distanceTo(this._satTarget) > this._satRange) {
    		player.consoleMessage("Satellite out of range");
    		this.$resetSatComms(this._satTarget);
    		this._satTarget = null;
    	}
    	var item = worldScripts.BulletinBoardSystem.$getItem(this._satTarget.script._missionID);
    	if (this._functionAMissionTypes.indexOf(item.data.missionType) >= 0) {
    		this._updateMessage = "Data transfer";
    		this._satTarget.commsMessage(this._updateMessage + " commencing.", player.ship);
    		this._dataTransferFreq = 5;
    		this._dataTransferCounter = 0;
    		this._dataTransferTimer = new Timer(this, this.$checkRangeToSatellite.bind(this), 1, 1);
    		this._dataReceived = 0;
    		this.$clearSatFunctions(this._satTarget);
    	} else {
    		this._satTarget.commsMessage("Invalid function for passcode.", player.ship);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$transmitFunctionB = function $transmitFunctionB() {
    	if (this._dataTransferTimer && this._dataTransferTimer.isRunning) return;
    	if (player.ship.position.distanceTo(this._satTarget) > this._satRange) {
    		player.consoleMessage("Satellite out of range");
    		this.$resetSatComms(this._satTarget);
    		this._satTarget = null;
    	}
    	var item = worldScripts.BulletinBoardSystem.$getItem(this._satTarget.script._missionID);
    	if (this._functionBMissionTypes.indexOf(item.data.missionType) >= 0) {
    		this._updateMessage = "Firmware upgrade";
    		this._satTarget.commsMessage(this._updateMessage + " pipeline established. Ready to receive data.", player.ship);
    		this._dataTransferFreq = 10;
    		this._dataTransferCounter = 0;
    		this._dataTransferTimer = new Timer(this, this.$checkRangeToSatellite.bind(this), 1, 1);
    		this._dataReceived = 0;
    		this.$clearSatFunctions(this._satTarget);
    	} else {
    		this._satTarget.commsMessage("Invalid function for passcode.", player.ship);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$transmitFunctionC = function $transmitFunctionC() {
    	if (this._dataTransferTimer && this._dataTransferTimer.isRunning) return;
    	if (player.ship.position.distanceTo(this._satTarget) > this._satRange) {
    		player.consoleMessage("Satellite out of range");
    		this.$resetSatComms(this._satTarget);
    		this._satTarget = null;
    	}
    	var item = worldScripts.BulletinBoardSystem.$getItem(this._satTarget.script._missionID);
    	if (this._functionCMissionTypes.indexOf(item.data.missionType) >= 0) {
    		this._updateMessage = "Data upload";
    		this._satTarget.commsMessage(this._updateMessage + " pipeline established. Ready to receive data.", player.ship);
    		this._dataTransferFreq = 10;
    		this._dataTransferTimer = new Timer(this, this.$checkRangeToSatellite.bind(this), 1, 1);
    		this._dataReceived = 0;
    		this._dataTransferCounter = 0;
    		this.$clearSatFunctions(this._satTarget);
    	} else {
    		this._satTarget.commsMessage("Invalid function for passcode.", player.ship);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$checkRangeToSatellite = function $checkRangeToSatellite() {
    	if (this._satTarget.isValid === false) {
    		this._dataTransferTimer.stop();
    		return;
    	}
    	var p = player.ship;
    	var pos = p.position;
    	var sat = this._satTarget;
    	if (pos.distanceTo(sat) > this._satRange) {
    		player.consoleMessage("Satellite out of range. " + this._updateMessage + " interrupted.");
    		this._dataTransferTimer.stop();
    		this.$resetSatComms(sat);
    		return;
    	}
    	if (p.target != sat) {
    		player.consoleMessage("Communication link disconnected.");
    		this._dataTransferTimer.stop();
    		this.$resetSatComms(sat);
    		return;
    	}
    	// being spotted by a police ship will gain you a bounty, but won't fail the mission
    	if (sat._checkPolice && sat._checkPolice === true && this._seenByPolice === false) {
    		var police = this.$findLawVessels(p);
    		if (police.length > 0) {
    			this._seenByPolice = true;
    			msg = expandDescription("[gcm_wbsa_detected_police]");
    			penalty = (Math.random() * 20) + 10;
    			p.setBounty(player.bounty + penalty, "seen by police");
    			police[0].commsMessage(msg, p);
    		}
    	}
    	// the timer runs every second, by the actual data transfer happens at another frequency
    	this._dataTransferCounter += 1;
    	if (this._dataTransferCounter < this._dataTransferFreq) return;
    	this._dataTransferCounter = 0;
    	this._dataReceived += 1;
    	player.consoleMessage(this._updateMessage + " " + (this._dataReceived * 10) + "% complete.");
    	if (this._dataReceived >= 10) {
    		this._dataReceived = 0;
    		this._dataTransferTimer.stop();
    		this._dataTransferTimer = null;
    		this.$dataTransferComplete();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$dataTransferComplete = function $dataTransferComplete() {
    	this._satTarget.commsMessage(this._updateMessage + " complete.", player.ship);
    	var missID = this._satTarget.script._missionID;
    	if (!missID) {
    		this.$resetSatComms(this._satTarget);
    		this._satTarget = null;
    		return;
    	}
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	item.data.quantity += 1;
    	// the mission can now be updated
    	bb.$updateBBMissionPercentage(missID, item.data.quantity / item.data.targetQuantity);
    
    	worldScripts.GalCopBB_Missions.$logMissionData(item.ID);
    	player.consoleMessage(expandDescription("[goal_updated]"));
    
    	delete this._satTarget.script._missionID;
    	delete this._satTarget.script._passcode;
    	this.$resetSatComms(this._satTarget);
    	this._satTarget = null;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // find all police in range of ship
    this.$findLawVessels = function $findLawVessels(npc) {
    	var ships = npc.checkScanner(true);
    	for (var i = ships.length - 1; i >= 0; i--) {
    		if (ships[i].isPolice === false) ships.splice(i, 1);
    	}
    	return ships;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptedMission = function $acceptedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if (this._debug) log(this.name, "accepted mission id = " + missID);
    
    	if (!item) {
    		log(this.name, "!!ERROR: BB returned null value from $getItem on mission ID " + missID);
    		return;
    	}
    	if (item.data.missionType === 115) {
    		// put a serious passcode on all the local satellites
    		var sats = system.shipsWithRole("RSsatellite");
    		if (sats.length > 0) {
    			for (var i = 0; i < sats.length; i++) {
    				sats[i].script._passcode = expandDescription("[gcm_passcode]");
    			}
    		}
    	}
    	gcm.$updateLastMissionDate(item.source, item.data.missionType);
    	gcm.$updateGeneralSettings(item);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$completedMission = function $completedMission(missID) {
    	var p = player.ship;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// update mission history
    	gcm.$updateSuccessHistoryReputation(item);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$confirmCompleted = function $confirmCompleted(missID) {
    	var p = player.ship;
    	var result = "";
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (item) {
    
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$terminateMission = function $terminateMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// adjust reputation only when the terminatePenalty flag is set to true 
    	if (item.data.terminatePenalty === true) {
    		// update mission history
    		gcm.$updateFailedHistoryReputation(item);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$failedMission = function $failedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var eq = "";
    
    	if (!sbm) {
    		// if there's no blackmarket option, just remove the equipment
    		this.$terminateMission(missID);
    		return;
    	}
    
    	var item = bb.$getItem(missID);
    	// update mission history
    	gcm.$updateFailedHistoryReputation(item);
    
    	if (eq != "") gcm._equipmentFromFailedMissions.push({
    		missionType: item.data.missionType,
    		source: item.source,
    		equip: eq,
    		date: clock.adjustedSeconds
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateManifestEntry = function $updateManifestEntry(missID) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	gcm.$updateManifestEntry(missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
    	return worldScripts.GalCopBB_Missions.$missionAvailability(missID, missType, origSysID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 110 - extract data from surveillance satellites
    this.$missionType110_Values = function $missionType110_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = this._satArray[destSysInfo.systemID][0];
    	result["price"] = parseInt((parseInt(Math.random() * 1000) + 1000) / 10) * 10 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + (workTime * 2); // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price * 0.5); // failure is costly in this case
    	result["satelliteTypes"] = ["1"];
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 111 - extract data from comms satellites
    this.$missionType111_Values = function $missionType111_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = this._satArray[destSysInfo.systemID][1] + this._satArray[destSysInfo.systemID][2];
    	result["price"] = parseInt((parseInt(Math.random() * 1000) + 1000) / 10) * 10 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + (workTime * 2); // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price * 0.5); // failure is costly in this case
    	result["satelliteTypes"] = ["2", "3"];
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 112 - extract data from telescope satellites
    this.$missionType112_Values = function $missionType112_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = this._satArray[destSysInfo.systemID][3];
    	result["price"] = parseInt((parseInt(Math.random() * 500) + 500) / 10) * 10 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + (workTime * 2); // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price * 0.5); // failure is costly in this case
    	result["satelliteTypes"] = ["4"];
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 113 - extract data from sys and comms satellites
    this.$missionType113_Values = function $missionType113_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = this._satArray[destSysInfo.systemID][0] + this._satArray[destSysInfo.systemID][1] + this._satArray[destSysInfo.systemID][2];
    	result["price"] = parseInt((parseInt(Math.random() * 1000) + 1000 + parseInt(Math.random() * 1000) + 1000) / 10) * 10 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + (workTime * 2); // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price * 0.5); // failure is costly in this case
    	result["satelliteTypes"] = ["1", "2", "3"];
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 115 - update firmware of satellites
    this.$missionType115_Values = function $missionType115_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = this._satArray[destSysInfo.systemID][0] + this._satArray[destSysInfo.systemID][1] + this._satArray[destSysInfo.systemID][2] + this._satArray[destSysInfo.systemID][3];
    	//log(this.name, "quantity " + result.quantity);
    	result["price"] = (parseInt((parseInt(Math.random() * 40) + 40) / 10) * 10) * result.quantity +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + 3600 + (900 * result.quantity); // transit time + 60 mins + extra 15 mins per satellite to complete
    	result["penalty"] = 0;
    	result["satelliteTypes"] = ["1", "2", "3", "4"];
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 118 - upload data to surveillance satellite
    this.$missionType118_Values = function $missionType118_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 1000) + 1000 + parseInt(Math.random() * 1000) + 1000) / 10) * 10 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price * 0.5); // failure is costly in this case
    	result["satelliteTypes"] = ["1"];
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 122 - destroy sys and comms satellites
    this.$missionType122_Values = function $missionType122_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = this._satArray[destSysInfo.systemID][0] + this._satArray[destSysInfo.systemID][1] + this._satArray[destSysInfo.systemID][2];
    	result["price"] = parseInt(parseInt(Math.random() * 1000 + 500) * result.quantity) + (parseInt(Math.random() * 1000 + 1000) / 10) * 10 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price * 0.5); // failure is costly in this case
    	result["satelliteTypes"] = ["1", "2", "3"];
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 123 - destroy sys and comms satellites (pirate)
    this.$missionType123_Values = function $missionType123_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = this._satArray[destSysInfo.systemID][0] + this._satArray[destSysInfo.systemID][1] + this._satArray[destSysInfo.systemID][2];
    	result["price"] = parseInt(parseInt(Math.random() * 1000 + 1000) * result.quantity) + (parseInt(Math.random() * 1000 + 1000) / 10) * 10 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price * 0.5); // failure is costly in this case
    	result["satelliteTypes"] = ["1", "2", "3"];
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 124 - defend satellites
    this.$missionType124_Values = function $missionType124_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = this._satArray[destSysInfo.systemID][0] + this._satArray[destSysInfo.systemID][1] + this._satArray[destSysInfo.systemID][2] + this._satArray[destSysInfo.systemID][3];
    	result["price"] = parseInt((parseInt(Math.random() * 1000 + 800) * result.quantity + parseInt(Math.random() * 1000) + 1000) / 10) * 10 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	result["expiry"] = -1;
    	result["penalty"] = parseInt(result.price * 0.5); // failure is costly in this case
    	result["satelliteTypes"] = ["1", "2", "3", "4"];
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$sat_shipBeingAttacked = function $sat_shipBeingAttacked(whom, alt) {
    	if (!alt)
    		if (this.ship.script.$gcm_hold_shipBeingAttacked) this.ship.script.$gcm_hold_shipBeingAttacked(whom);
    	this.ship.target = whom;
    	this.ship.script._attacker = whom;
    	var p = player.ship;
    	var s = worldScripts.GalCopBB_Satellites;
    	if (s._satTarget === this.ship) {
    		if (s._dataTransferTimer && s._dataTransferTimer.isRunning) {
    			s._dataTransferTimer.stop();
    			s._satTarget.script._passcode = "";
    			if (p.position.distanceTo(s._satTarget) < p.scannerRange) {
    				s._satTarget.commsMessage("Satellite under attack. Communication lines shut down.", p);
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$sat_shipBeingAttackedUnsuccessfully = function $sat_shipBeingAttackedUnsuccessfully(whom) {
    	if (this.ship.script.$gcm_hold_shipBeingAttackedUnsuccessfully) this.ship.script.$gcm_hold_shipBeingAttackedUnsuccessfully(whom);
    	this.ship.script.shipBeingAttacked(whom, true);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$sat_shipAttackedWithMissile = function $sat_shipAttackedWithMissile(missile, whom) {
    	if (this.ship.script.$gcm_hold_shipAttackedWithMissile) this.ship.script.$gcm_hold_shipAttackedWithMissile(missile, whom);
    	this.ship.script.shipBeingAttacked(whom, true);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$sat_shipDied = function $sat_shipDied(whom, why) {
    	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);
    	if (this.ship.script._missionID) {
    		var bb = worldScripts.BulletinBoardSystem;
    		var item = bb.$getItem(this.ship.script._missionID);
    		if (item.data.missionType === 122) {
    			item.data.quantity += 1;
    			// the mission can now be updated
    			bb.$updateBBMissionPercentage(item.ID, item.data.quantity / item.data.targetQuantity);
    
    			worldScripts.GalCopBB_Missions.$logMissionData(item.ID);
    			player.consoleMessage(expandDescription("[goal_updated]"));
    		}
    		if (item.data.missionType === 124) {
    			item.data.destroyedQuantity += 1;
    			var s = worldScripts.GalCopBB_Satellites;
    			player.consoleMessage("Satellite destroyed", 5);
    			// the mission can now be updated
    			bb.$updateBBMissionPercentage(item.ID, item.data.quantity / (item.data.targetQuantity - item.data.destroyedQuantity));
    
    			worldScripts.GalCopBB_Missions.$logMissionData(item.ID);
    			player.consoleMessage(expandDescription("[goal_updated]"));
    			s.$setNextSatellite();
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$attacker_shipDied = function $attacker_shipDied(whom, why) {
    	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);
    	if (this.ship.script._missionID) {
    		var bb = worldScripts.BulletinBoardSystem;
    		var item = bb.$getItem(this.ship.script._missionID);
    		if (item.data.missionType === 124) {
    			var s = worldScripts.GalCopBB_Satellites;
    			for (var i = 0; i < s._attackers.length; i++) {
    				if (s._attackers[i] == this.ship) {
    					s._attackers.splice(i, 1);
    					break;
    				}
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // makes the guards follow a particular target
    this.$giveSatDefenderDefendTarget = function $giveSatDefenderDefendTarget() {
    	var retry = false;
    	for (var i = 0; i < this._satDefenders.length; i++) {
    		var shps = this._satDefenders[i].group.ships;
    		var tgt = this._satDefenders[i].target;
    		for (var j = 0; j < shps.length; j++) {
    			var shp = shps[j];
    			if (shp.AIScript.oolite_priorityai) {
    				shp.AIScript.oolite_priorityai.setParameter("oolite_defendTarget", tgt);
    				shp.AIScript.oolite_priorityai.reconsiderNow();
    			} else {
    				retry = true;
    				break;
    			}
    		}
    		if (retry === true) break;
    	}
    	if (retry === true) {
    		this._satDefenderTimer = new Timer(this, this.$giveSatDefenderDefendTarget, 1, 0);
    	} else {
    		this._satDefenders.length = 0;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // makes the attackers attack a particular target
    this.$giveSatAttackersTarget = function $giveSatAttackersTarget() {
    	var retry = false;
    	for (var i = 0; i < this._satAttackers.length; i++) {
    		var shps = this._satAttackers[i].group.ships;
    		var tgt = this._satAttackers[i].target;
    		for (var j = 0; j < shps.length; j++) {
    			var shp = shps[j];
    			if (shp.AIScript.oolite_priorityai) {
    				shp.AIScript.oolite_priorityai.setParameter("oolite_attackTarget", tgt);
    				shp.AIScript.oolite_priorityai.configurationResetWaypoint();
    				shp.AIScript.oolite_priorityai.reconsiderNow();
    			} else {
    				retry = true;
    				break;
    			}
    		}
    		if (retry === true) break;
    	}
    	if (retry === true) {
    		this._satAttackerTimer = new Timer(this, this.$giveSatAttackersTarget, 1, 0);
    	} else {
    		this._satAttackers.length = 0;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$beginAttacks = function $beginAttacks() {
    	var p = player.ship;
    	if (!p || !p.position) return;
    	var dist = p.position.distanceTo(this._satToDefend);
    	// don't add any if we're too far away
    	if (dist > (p.scannerRange * 0.75)) {
    		this._countDelay += 1;
    		// has it been too long? (1.16 minutes for each scanner range distance), minimum of 5 minutes
    		var delay = Math.floor((dist / p.scannerRange) * 70);
    		if (delay < 300) delay = 300;
    		if ((this._countDelay * 6) >= delay) {
    			// how far away are we
    			if (dist > p.scannerRange * 3) {
    				// oh dear ... too long
    				this._satToDefend.explode();
    				this._countDelay = 0;
    			}
    		}
    		return;
    	}
    	// reset the countdown so we can keep the satellite alive
    	this._countDelay = 0;
    	var atk = this._attackers;
    	var gcm = worldScripts.GalCopBB_Missions;
    	// how many left from last wave?
    	// check: what happens if ship flees and therefore this._attackers doesn't get updated
    	if (atk.length === 0 && this._wavesToCreate <= 0) {
    		if (this._satToDefend.isValid === true) {
    			var bb = worldScripts.BulletinBoardSystem;
    			var item = bb.$getItem(this._satToDefend.script._missionID);
    			// the mission can now be updated
    			item.data.quantity += 1;
    			bb.$updateBBMissionPercentage(item.ID, item.data.quantity / (item.data.targetQuantity - item.data.destroyedQuantity));
    
    			worldScripts.GalCopBB_Missions.$logMissionData(item.ID);
    			player.consoleMessage(expandDescription("[goal_updated]"));
    			this._satToDefend.beaconLabel = this._satToDefend.beaconLabel.replace("**", "");
    			delete this._satToDefend.script._missionID;
    			this._satToDefend = null;
    			// do we need to go on?
    			if (item.data.quantity < (item.data.targetQuantity - item.data.destroyedQuantity)) this.$setNextSatellite();
    		}
    		// didn't find a satellite to protect? we're done!
    		if (this._satToDefend == null) this._beginAttackWaves.stop();
    	} else {
    		// look for, and clean up, any non-participants
    		for (var i = atk.length - 1; i >= 0; i--) {
    			// remove derelict ships
    			if (atk[i].isDerelict) {
    				atk.splice(i, 1);
    			} else {
    				// ...and ships more than 60000 distance
    				var pos1 = atk[i].position;
    				if (pos1.distanceTo(this._satToDefend) > (p.scannerRange * 4) && pos1.distanceTo(p) > (p.scannerRange * 4)) {
    					atk[i].remove();
    					atk.splice(i, 1);
    				}
    			}
    		}
    	}
    
    	if (this._satToDefend && atk.length <= Math.floor(Math.random() * 2)) {
    		var spwn = gcm.$findPosition(this._satToDefend.script._orbiting.position);
    		if (spwn) this.$addAttackers(spwn);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // set up the next satellite to defend
    this.$setNextSatellite = function $setNextSatellite() {
    	// move on to the next satellite
    	var sats = system.shipsWithRole("RSsatellite");
    	this._satToDefend = null;
    	for (var i = 0; i < sats.length; i++) {
    		if (sats[i].isValid && sats[i].script._missionID && sats[i].script._missionID > 0) {
    			this._satToDefend = sats[i];
    			this._satToDefend.beaconLabel = "**" + this._satToDefend.beaconLabel;
    			this._wavesToCreate = Math.floor(Math.random() * 3) + 1;
    			break;
    		}
    	}
    	if (this._satToDefend) {
    		this._countDelay = 0;
    		player.commsMessage("Attackers heading for new satellite,\nmarked with '**' on your Space Compass", 6);
    		for (var i = 0; i < this._attackers.length; i++) {
    			this._attackers[i].AIScript.oolite_priorityai.setParameter("oolite_attackTarget", this._satToDefend);
    			this._attackers[i].AIScript.oolite_priorityai.configurationResetWaypoint();
    			this._attackers[i].AIScript.oolite_priorityai.reconsiderNow();
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // adds a variable number of guards around a particular satellite
    // then sets a timer to trigger ai config
    this.$addGuards = function $addGuards(sat) {
    	var num = Math.floor(Math.random() * 4) + 1;
    	var assassin = this._preferredAssassinLightShips;
    	if (system.government >= 5) assassin = this._preferredAssassinMediumShips;
    	// we're going to create our group manually, to avoid any really slow escort ships
    	var gn = new ShipGroup();
    	for (var i = 0; i < num; i++) {
    		var checkShips = system.addShips("[" + assassin[Math.floor(Math.random() * assassin.length)] + "]", 1, sat.position, 10000);
    		if (checkShips) var a = checkShips[0];
    		if (a) {
    			if (this._rsnInstalled) a.shipUniqueName = this.$getRandomShipName(a, "assassin-light");
    			// remove any escorts that came with the ship
    			if (a.escorts) {
    				for (var j = a.escorts.length - 1; j >= 0; j--) a.escorts[j].remove(true);
    			}
    			gn.addShip(a);
    		}
    	}
    	if (gn.ships && gn.ships.length > 0) {
    		var pop = worldScripts["oolite-populator"];
    		for (var i = 0; i < gn.ships.length; i++) {
    			var shp = gn.ships[i];
    			// configure our guards
    			shp.setCrew({
    				name: randomName() + " " + randomName(),
    				bounty: 0,
    				insurance: 0
    			});
    			if (shp.hasHyperspaceMotor) {
    				pop._setWeapons(shp, 1.75); // bigger ones sometimes well-armed
    			} else {
    				pop._setWeapons(shp, 1.3); // rarely well-armed
    			}
    			pop._setSkill(shp, 4 - system.info.government);
    			if (Math.random() * 16 < system.info.government) {
    				pop._setMissiles(shp, -1);
    			}
    			// make sure the AI is switched
    			shp.switchAI("gcm-guardAI.js");
    		}
    		// need to get the lurk position to move
    		this._satDefenders.push({
    			group: gn,
    			target: sat
    		});
    		if (!this._satDefenderTimer || this._satDefenderTimer.isRunning === false) {
    			this._satDefenderTimer = new Timer(this, this.$giveSatDefenderDefendTarget, 1, 0);
    		}
    	} else {
    		log(this.name, "!!ERROR: Satellite guards not spawned!");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$addAttackers = function $addAttackers(pos) {
    	var sat = this._satToDefend;
    	var num = Math.floor(Math.random() * 3) + 1;
    	var assassin = this._preferredAssassinLightShips;
    	if (system.government >= 5) assassin = this._preferredAssassinMediumShips;
    	// we're going to create our group manually, to avoid any really slow escort ships
    	var gn = new ShipGroup();
    	for (var i = 0; i < num; i++) {
    		var checkShips = system.addShips("[" + assassin[Math.floor(Math.random() * assassin.length)] + "]", 1, pos, 500);
    		if (checkShips) var a = checkShips[0];
    		if (a) {
    			if (this._rsnInstalled) a.shipUniqueName = this.$getRandomShipName(a, "assassin-light");
    			// remove any escorts that came with the ship
    			if (a.escorts) {
    				for (var j = a.escorts.length - 1; j >= 0; j--) a.escorts[j].remove(true);
    			}
    			gn.addShip(a);
    		}
    	}
    	if (gn.ships && gn.ships.length > 0) {
    		this._wavesToCreate -= 1;
    		if (this._wavesToCreate <= 0) this._wavesToCreate = 0;
    		var pop = worldScripts["oolite-populator"];
    		for (var i = 0; i < gn.ships.length; i++) {
    			var shp = gn.ships[i];
    			var scr = shp.script;
    			if (scr.shipDied && !scr.$gcm_hold_shipDied) scr.$gcm_hold_shipDied = scr.shipDied;
    			scr.shipDied = this.$attacker_shipDied;
    			// configure our attackers
    			shp.setCrew({
    				name: randomName() + " " + randomName(),
    				bounty: Math.floor(Math.random() * 15 + 5),
    				insurance: 0
    			});
    			scr._missionID = sat.script._missionID;
    			if (shp.hasHyperspaceMotor) {
    				pop._setWeapons(shp, 1.75); // bigger ones sometimes well-armed
    			} else {
    				pop._setWeapons(shp, 1.3); // rarely well-armed
    			}
    			pop._setSkill(shp, 4 - system.info.government);
    			if (Math.random() * 16 < system.info.government) {
    				pop._setMissiles(shp, -1);
    			}
    			// make sure they have an escape pod 
    			shp.awardEquipment("EQ_ESCAPE_POD");
    			// make sure the AI is switched
    			shp.switchAI("gcm-attackerAI.js");
    			this._attackers.push(shp);
    		}
    		// need to get the satellite position to move
    		this._satAttackers.push({
    			group: gn,
    			target: sat
    		});
    		if (!this._satAttackerTimer || this._satAttackerTimer.isRunning === false) {
    			this._satAttackerTimer = new Timer(this, this.$giveSatAttackersTarget, 1, 0);
    		}
    	} else {
    		log(this.name, "!!ERROR: Satellite attackers not spawned!");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // populates an array with ship data keys for use by the populator routines
    this.$getAssassinShipLists = function $getAssassinShipLists() {
    	this._preferredAssassinLightShips.length = 0;
    	var shipkeys = Ship.keysForRole("assassin-light");
    	for (var i = 0; i < shipkeys.length; i++) {
    		this._preferredAssassinLightShips.push(shipkeys[i]);
    	}
    	this._preferredAssassinMediumShips.length = 0;
    	var shipkeys = Ship.keysForRole("assassin-medium");
    	for (var i = 0; i < shipkeys.length; i++) {
    		this._preferredAssassinMediumShips.push(shipkeys[i]);
    	}
    }
    Scripts/galcopbb_secsoftware.js
    "use strict";
    this.name = "GalCopBB_SoftwareInstall";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Control of the F4 Interface for installing security software (mission 62)";
    this.license = "CC BY-NC-SA 4.0";
    
    this._debug = false;
    this._pirateHermitInfo = {}; // dictionary of systems known to have a Rock Hermit, and the time security software was installed
    this._wolfPackOnLaunch = false;
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
    	if (missionVariables.GalCopBBMissions_PirateHermitInfo) {
    		this._pirateHermitInfo = JSON.parse(missionVariables.GalCopBBMissions_PirateHermitInfo);
    		delete missionVariables.GalCopBBMissions_PirateHermitInfo;
    	}
    	if (missionVariables.GalCopBBMissions_WolfPackOnLaunch) {
    		this._wolfPackOnLaunch = true;
    		delete missionVariables.GalCopBBMissions_WolfPackOnLaunch;
    	}
    	var gcm = worldScripts.GalCopBB_Missions;
    	// add these mission types into the main control
    	gcm._availableMissionTypes.push(62);
    	this._debug = gcm._debug;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function () {
    	this.$doesSystemHavePirateHermit();
    
    	this.$initInterface(player.ship.dockedStation);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillSaveGame = function () {
    	if (this._pirateHermitInfo.length > 0) missionVariables.GalCopBBMissions_PirateHermitInfo = JSON.stringify(this._pirateHermitInfo);
    	if (this._wolfPackOnLaunch === true) missionVariables.GalCopBBMissions_WolfPackOnLaunch = true;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerEnteredNewGalaxy = function (galaxyNumber) {
    	this._pirateHermitInfo = {};
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // set up interstellar space mission entities here
    this.shipExitedWitchspace = function () {
    	if (system.ID !== -1) {
    		this.$doesSystemHavePirateHermit();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDockedWithStation = function (station) {
    	this.$initInterface(station);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillLaunchFromStation = function (station) {
    	if (this._wolfPackOnLaunch === true) {
    		var num = Math.floor(player.score / 1000) + 1;
    		var ships = system.addGroup("pirate", num, player.ship.position.add(player.ship.vectorForward.multiply(10000)), 10000);
    		// make sure they will attack the player
    		for (var i = 0; i < ships.length; i++) {
    			ships[i].switchAI("gcm-pirateAI.js");
    		}
    		this._wolfPackOnLaunch = false;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.dayChanged = function (newday) {
    	this.$checkRockHermitSecSoftware();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.alertConditionChanged = function (newCondition, oldCondition) {
    	function gcm_findhermits(entity) {
    		return (entity.isShip === true && entity.isStation === true && entity.hasRole("rockhermit-pirate"));
    	}
    	if (newCondition === 3 && this._pirateHermitInfo[system.ID] && this._pirateHermitInfo[system.ID] >= 0) {
    		var targets = system.filteredEntities(this, gcm_findhermits);
    		for (var i = 0; i < targets.length; i++) {
    			if (targets[i].position.distanceTo(player.ship) < 50000) {
    				var gcm = worldScripts.GalCopBB_Missions;
    				var list = gcm.$getListOfMissions(true, 62);
    				for (var i = 0; i < list.length; i++) {
    					if (list[i].destination === system.ID &&
    						list[i].data.quantity === 0 &&
    						list[i].data.targetQuantity === 1) {
    						// we got spotted! mission failed!
    						list[i].data.targetQuantity = 0;
    					}
    				}
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$initInterface = function $initInterface(station) {
    	var inst = false;
    	if (station.hasRole("rockhermit-pirate")) {
    		var gcm = worldScripts.GalCopBB_Missions;
    		var list = gcm.$getListOfMissions(true, 62);
    		for (var i = 0; i < list.length; i++) {
    			if (list[i].destination === system.ID &&
    				list[i].data.targetQuantity > 0 &&
    				(this._pirateHermitInfo[system.ID] == null || this._pirateHermitInfo[system.ID] > 0)) {
    				inst = true;
    				station.setInterface(this.name, {
    					title: "Install security software",
    					category: "Station Interfaces",
    					summary: "Uploads GalCop's security software package into the Rock Hermits computers.",
    					callback: this.$installSoftware.bind(this)
    				});
    			}
    		}
    	}
    	if (inst === false) {
    		station.setInterface(this.name, null);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$installSoftware = function $installSoftware() {
    	this._pirateHermitInfo[system.ID] = clock.adjustedSeconds;
    
    	// remove the interface screen
    	player.ship.dockedStation.setInterface(this.name, null);
    
    	// flag the mission as complete
    	var bb = worldScripts.BulletinBoardSystem;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var list = gcm.$getListOfMissions(true, 62);
    	for (var i = 0; i < list.length; i++) {
    		if (list[i].destination === system.ID &&
    			list[i].data.quantity === 0) {
    			// we got spotted! mission failed!
    			list[i].data.quantity = 1;
    			bb.$updateBBMissionPercentage(list[i].ID, 1);
    
    			gcm.$logMissionData(list[i].ID);
    			player.consoleMessage(expandDescription("[goal_updated]"));
    
    			if (Math.random() > 0.6) this._wolfPackOnLaunch = true;
    		}
    	}
    
    	// confirm the action to the player
    	mission.runScreen({
    		screenID: "oolite-gcm-secsoftware-summary",
    		title: "GalCop Security Software",
    		message: "The security software package has been installed successfully.",
    		exitScreen: "GUI_SCREEN_INTERFACES",
    		overlay: {
    			name: "gcm-ok.png",
    			height: 546
    		}
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // check for and remove security software installed at rock hermits
    this.$checkRockHermitSecSoftware = function $checkRockHermitSecSoftware() {
    	var keys = Object.keys(this._pirateHermitInfo);
    	for (var i = 0; i < keys.length; i++) {
    		// if it's been a month since the software was installed at the hermit, remove it now.
    		if (this._pirateHermitInfo[keys[i]] !== 0 && clock.adjustedSeconds - this._pirateHermitInfo[keys[i]] > 2592000) {
    			delete this._pirateHermitInfo[keys[i]];
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // checks if the current system has a pirate rock hermit
    this.$doesSystemHavePirateHermit = function $doesSystemHavePirateHermit() {
    	function gcm_findhermits(entity) {
    		return (entity.isStation && entity.hasRole("rockhermit-pirate"));
    	}
    	var targets = system.filteredEntities(this, gcm_findhermits);
    	if (targets.length > 0) {
    		if (this._pirateHermitInfo[system.ID] == null) this._pirateHermitInfo[system.ID] = 0;
    		// attach a shipDied event to the hermits so we can remove them from the array if they are destroyed
    		for (var i = 0; i < targets.length; i++) {
    			// monkey patch if necessary
    			if (targets[i].script.shipDied && targets[i].script.$gcm_hold_shipDied == null) {
    				targets[i].script.$gcm_hold_shipDied = targets[i].script.shipDied;
    			}
    			targets[i].script.shipDied = this.$gcm_pirateHermit_shipDied;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_pirateHermit_shipDied = function $gcm_pirateHermit_shipDied(whom, why) {
    	function gcm_findhermits(entity) {
    		return (entity.isShip && entity.hasRole("rockhermit-pirate"));
    	}
    
    	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);
    
    	var targets = system.filteredEntities(this, gcm_findhermits);
    	if (targets.length === 0) {
    		delete worldScripts.GalCopBB_SoftwareInstall._pirateHermitInfo[system.ID];
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptedMission = function $acceptedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if (this._debug) log(this.name, "accepted mission id = " + missID);
    
    	if (!item) {
    		log(this.name, "!!ERROR: BB returned null value from $getItem on mission ID " + missID);
    		return;
    	}
    	gcm.$updateLastMissionDate(item.source, item.data.missionType);
    	gcm.$updateGeneralSettings(item);
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$completedMission = function $completedMission(missID) {
    	var p = player.ship;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// update mission history
    	gcm.$updateSuccessHistoryReputation(item);
    
    	if (item.data.missionType === 62) {
    		var sbm = worldScripts.Smugglers_BlackMarket;
    		if (sbm) sbm.$removeSaleItem("DTA_GALCOP_SEC_SOFTWARE:" + missID);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$confirmCompleted = function $confirmCompleted(missID) {
    	return "";
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$terminateMission = function $terminateMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// adjust reputation only when the terminatePenalty flag is set to true 
    	if (item.data.terminatePenalty === true) {
    		// update mission history
    		gcm.$updateFailedHistoryReputation(item);
    	}
    
    	// *** type 62 - galcop security software for pirate hermit monitoring
    	if (item.data.missionType === 62) {
    		var sbm = worldScripts.Smugglers_BlackMarket;
    		if (sbm) sbm.$removeSaleItem("DTA_GALCOP_SEC_SOFTWARE:" + missID);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$failedMission = function $failedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var eq = "";
    
    	if (!worldScripts.Smugglers_BlackMarket) {
    		// if there's no blackmarket option, just remove the equipment
    		gcm.$terminateMission(missID);
    		return;
    	}
    
    	var item = bb.$getItem(missID);
    	// update mission history
    	gcm.$updateFailedHistoryReputation(item);
    
    	if (item.data.missionType === 62 && item.data.quantity === 0) eq = "DTA_GALCOP_SEC_SOFTWARE";
    
    	if (eq != "") gcm._equipmentFromFailedMissions.push({
    		missionType: item.data.missionType,
    		source: item.source,
    		equip: eq,
    		date: clock.adjustedSeconds
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateManifestEntry = function $updateManifestEntry(missID) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	gcm.$updateManifestEntry(missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
    	return worldScripts.GalCopBB_Missions.$missionAvailability(missID, missType, origSysID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 62 - security software install
    this.$missionType62_Values = function $missionType62_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 500) + 500) / 10) * 10 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price / 4);
    	return result;
    }
    Scripts/galcopbb_shipinteractions.js
    "use strict";
    this.name = "GalCopBB_ShipInteractions";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Controls ship interactions for missions 33-36";
    this.license = "CC BY-NC-SA 4.0";
    
    /*
    	TODO:
    	- For 33, add more pirates on the return trip who demand you drop the recovered items
    	- For 33, need some sort of cue that you've missed the ship when it jumped in
    
    	*30 (ship-to-ship) - random trader along space lane or at witchpoint tells you to meet another ship (mission 30)
    		- determine if this system will have one of these ships (only in safer systems 5,6,7)
    		- pick one or two random ships in the system, and start monitoring range to player
    		- if the range drops below 20000 then signal the player with the info about the contact mission
    		- player won't have the option of declining the mission, but there will be no penalty for terminating it.
    		
    	*33 - intercept ship carrying stolen item,  (use Broadcast comms to demand data pack), which must then be scooped
    		- player is told ship is heading to Y, a planet to which it is possible to travel two ways, one quicker, one slower. Player must get there first by taking the quicker route
    		- once in system, player needs to wait at the witchpoint for the ship to arrive
    		- spawn ship after a variable delay (3-5 mins?), give player the chance to request cache
    		- if ship is destroyed, cache will be ejected in a cargopod (scripted)
    		- ship may eject cargo if fighting starts
    		- Player must scoop cargo pod and return it to X.
    				
    	34 - track ship from witchpoint to station, then return
    			- player must wait at witchpoint and target all incoming vessels
    			- one of these ships will be the target and a console message will display confirmation.
    			- if player gets with 25 km of target and appears to be following them (eg heading is roughly the same and the player is behind them) they will detect player and flee (mission failed)
    		- spawn ship at witchpoint
    			- check if ship and player are within scanner range of wp beacon. if not, start monitoring player ship distance to target, 
    			- if it drops below 24500 the mission is ended
    		
    	36 - Find ship in system X by sending a pass phrase to any/all ships - one of which will reply with a message with details of where to find special cargo to be returned to system Y
    		- pick one or two random ships in the system to be the target 
    		- add custom message to BCC
    		- have a variety of negative responses
    		- once the right ship is found, send details to player of special cargo location in sys Y
    
    */
    
    this._type33ShipKey = "";
    this._type33StolenType = "";
    this._type33Ship = null;
    this._type33Created = false;
    this._type33MissionID = 0;
    this._addGetawayShipTimer = null;
    this._type30Ships = [];
    this._type30ScanTimer = null;
    this._type30MissionInfo = [];
    this._type30MissionData = null;
    this._type30CommsTimer = null;
    this._type30Message = "";
    this._type30MessageIndex = -1;
    this._type30Ship = null;
    this._type30RangeCounter = 0;
    this._debug = true;
    this._ignoreShipTypes = ["Hognose"];
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
    	var gcm = worldScripts.GalCopBB_Missions;
    	// add these mission types into the main control
    	gcm._availableMissionTypes.push(33);
    	//gcm._availableMissionTypes.push(34);
    	//gcm._availableMissionTypes.push(36);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function () {
    	this._type33Created = missionVariables.GalCopBBMissions_Type33Created;
    	if (missionVariables.GalCopBBMissions_type30MissionInfo) {
    		this._type30MissionInfo = JSON.parse(missionVariables.GalCopBBMissions_Type30MissionInfo);
    		delete missionVariables.GalCopBBMissions_Type30MissionInfo;
    	}
    	this.$setupTimers();
    	this.$setupType30();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillSaveGame = function () {
    	missionVariables.GalCopBBMissions_Type33Created = this._type33Created;
    	if (this._type30MissionInfo.length > 0) missionVariables.GalCopBBMissions_Type30MissionInfo = JSON.stringify(this._type30MissionInfo);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillEnterWitchspace = function (cause, destination) {
    	this._type30Ships.length = 0;
    	this._type30MissionInfo.length = 0;
    	if (this._type33Created === true) {
    		var bb = worldScripts.BulletinBoardSystem;
    		var item = bb.$getItem(this._type33MissionID);
    		item.data.destroyedQuantity = 1;
    		this._type33Created = false;
    		this._type33Ship = null;
    		this._type33StolenType = "";
    		this._type33MissionID = 0;
    	}
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipExitedWitchspace = function () {
    	this.$setupTimers();
    	this.$setupType30();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = function (whom, why) {
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$addNotifyMission = function $addNotifyMission(data) {
    	this._type30MissionInfo.push(data);
    	// start the scan timer, if it isn't already started, and we have some ships in our array
    	if ((!this._type30ScanTimer || this._type30ScanTimer.isRunning === false) && this._type30Ships.length > 0)
    		this._type30ScanTimer = new Timer(this, this.$type30Scanner, 2, 2);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$setupTimers = function $setupTimers() {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var list = gcm.$getListOfMissions(true, 33);
    	for (var i = 0; i < list.length; i++) {
    		if (list[i].destination === system.ID &&
    			list[i].expiry > clock.adjustedSeconds &&
    			list[i].data.destroyedQuantity === 0 &&
    			list[i].data.quantity === 0) {
    			this._type33ShipKey = list[i].data.targetShipKey;
    			this._type33StolenType = list[i].data.stolenItemType;
    			this._type33MissionID = list[i].ID;
    			if (this._type33Created === true) {
    				this.$addType33ShipToSystem();
    			} else {
    				this._addGetawayShipTimer = new Timer(this, this.$addType33ShipToSystem, parseInt((Math.random() * 120) + 180), 0);
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$stopTimers = function $stopTimers() {
    	if (this._addGetawayShipTimer && this._addGetawayShipTimer.isRunning) this._addGetawayShipTimer.stop();
    	delete this._addGetawayShipTimer;
    	this._type33Created = false;
    	if (this._type30ScanTimer && this._type30ScanTimer.isRunning) this._type30ScanTimer.stop();
    	delete this._type30ScanTimer;
    	if (this._type30CommsTimer && this._type30CommsTimer.isRunning) this._type30CommsTimer.stop();
    	delete this._type30CommsTimer;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$setupType30 = function $setupType30() {
    	function gcm_findtraders(entity) {
    		return (entity.isShip && entity.primaryRole.indexOf("trader") >= 0 && entity.bounty === 0 && this._ignoreShipTypes.indexOf(entity.shipClassName) === -1);
    	}
    	// only in safer systems
    	if (system.info.government >= 0) {
    		// find some traders
    		var plt = system.mainPlanet;
    		var targets = system.filteredEntities(this, gcm_findtraders);
    		if (targets.length > 0) {
    			for (var i = 0; i < targets.length; i++) {
    				var vect = targets[i].position.toCoordinateSystem("wpu");
    				if (vect.z >= 0 && vect.z <= 1) {
    					var a = targets[i].position;
    					if (a && plt && a.magnitude() * Math.sin(a.angleTo(plt.position)) < 51200) {
    						if (Math.random() > 0.7) {
    							this._type30Ships.push(targets[i]);
    							if (worldScripts.GalCopBB_Missions._debug) {
    								targets[i].beaconCode = "E";
    								targets[i].beaconLabel = "Comms target";
    							}
    							if (this._type30Ships.length >= 2) break; // we only want a couple of ships set up for this
    						}
    					}
    				}
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$type30Scanner = function $type30Scanner() {
    	if (this._type30MissionData != null) {
    		this._type30ScanTimer.stop();
    		this._type30ScanTimer = null;
    		return;
    	}
    	// if we don't have any missions to communicate, stop the scanner now.
    	if (this._type30MissionInfo.length === 0) {
    		this._type30Ships.length = 0;
    		this._type30ScanTimer.stop();
    		this._type30ScanTimer = null;
    		return;
    	}
    	// to cope with player ejecting
    	if (!player.ship || !player.ship.position) return;
    	for (var i = this._type30Ships.length - 1; i >= 0; i--) {
    		if (this._type30Ships[i].isValid == false || this._type30Ships[i].isInSpace === false) {
    			this._type30Ships.splice(i, 1);
    			continue;
    		}
    		if (this._type30Ships[i].position.distanceTo(player.ship) < 23000 && this._type30Ships[i].alertCondition != 3 && player.alertHostiles === false) {
    			// we have a winner
    			// send transmission
    			this._type30Ship = this._type30Ships[i];
    			// select a random item from the list of available missions
    			this._type30MissionData = this._type30MissionInfo[Math.floor(Math.random() * this._type30MissionInfo.length)];
    			this._type30Message = expandDescription("[missionType" + this._type30MissionData.missionType + "_shipBriefing]", {
    				destination: this._type30MissionData.destination,
    				amount: formatCredits(this._type30MissionData.payment, false, true),
    				name: this._type30MissionData.name
    			});
    			this._type30MessageIndex = 0;
    			// start a timer for transmitting
    			this._type30CommsTimer = new Timer(this, this.$transmitMessage, 5, 5);
    			// clear array - once someone has sent a request, don't ask again
    			this._type30Ships.length = 0;
    			this._type30ScanTimer.stop();
    			this._type30ScanTimer = null;
    
    			break;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$transmitMessage = function $transmitMessage(ship, msg) {
    	if (this._type30Ship && this._type30Ship.isInSpace) {
    		if (player.ship.position.distanceTo(this._type30Ship) < player.ship.scannerRange && this._type30Ship.alertCondition != 3) {
    			// yay! comms is working!
    			var items = this._type30Message.split("\n");
    			this._type30Ship.commsMessage(items[this._type30MessageIndex], player.ship);
    			// remove the greeting option from BCC
    			if (this._type30MessageIndex === 0) {
    				var bcc = worldScripts.BroadcastCommsMFD;
    				bcc.$addShipToArray(this._type30Ship, bcc._greeted);
    				bcc.$buildMessageList();
    			}
    			// increase the pointer for the next part of the message
    			this._type30MessageIndex += 1;
    			// have we got to the end?
    			if (this._type30MessageIndex >= items.length) {
    				this._type30CommsTimer.stop();
    				this._type30CommsTimer = null;
    				worldScripts.GalCopBB_Missions.$acceptMissionFromOther(this._type30MissionData);
    				this._type30MissionData = null;
    				this._type30Ship = null;
    			}
    		} else {
    			this._type30RangeCounter += 1;
    			if (this._type30RangeCounter >= 10) {
    				this._type30CommsTimer.stop();
    				this._type30CommsTimer = null;
    				this._type30MissionData = null;
    				this._type30Ship = null;
    			}
    		}
    	} else {
    		this._type30CommsTimer.stop();
    		this._type30CommsTimer = null;
    	}
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$addType33ShipToSystem = function $addType33ShipToSystem() {
    	// if the player is more than 25k (scanner range) from the witchpoint, wait a bit more
    	if (player.ship.position.distanceTo([0, 0, 0]) > player.ship.scannerRange) {
    		this._addGetawayShipTimer = new Timer(this, this.$addType33ShipToSystem, parseInt((Math.random() * 30) + 30), 0);
    		return;
    	}
    	var pop = worldScripts["oolite-populator"];
    	// get the ship datakey from the mission data
    	var checkShips = system.addShips("[" + this._type33ShipKey + "]", 1, player.ship.position, 20000);
    	var shp = null;
    	if (checkShips) shp = checkShips[0];
    	if (shp) {
    		shp.entityPersonality = Math.floor(system.scrambledPseudoRandomNumber(this._type33MissionID) * 32768);
    
    		shp.primaryRole = "trader";
    		shp.accuracy = 7;
    
    		pop._setWeapons(shp, 4);
    		pop._setMissiles(shp, 2);
    		shp.heatInsulation = 2;
    		shp.setBounty(parseInt(Math.random() * 20 + 20), "setup actions");
    		shp.homeSystem = Math.floor(Math.random() * 256);
    
    		// so the ship can't accidentally scoop the barrel it ejects
    		shp.removeEquipment("EQ_FUEL_SCOOPS");
    
    		shp.switchAI("oolite-traderAI.js");
    
    		// attach some scripts to monitor what happens to ship
    		if (shp.script.shipDied) shp.script.$gcm_hold_shipint_shipDied = shp.script.shipDied;
    		shp.script.shipDied = this.$gcm_shipint_shipDied;
    
    		if (shp.script.shipBeingAttacked) shp.script.$gcm_hold_shipint_shipBeingAttacked = shp.script.shipBeingAttacked;
    		shp.script.shipBeingAttacked = this.$gcm_shipint_shipBeingAttacked;
    
    		if (shp.script.shipBeingAttackedByCloaked) ship.script.$gcm_hold_shipint_shipBeingAttackedByCloaked = shp.script.shipBeingAttackedByCloaked;
    		shp.script.shipBeingAttackedByCloaked = this.$gcm_shipint_shipBeingAttackedByCloaked;
    
    		shp.script._missionID = this._type33MissionID;
    		shp.script._stolenItemType = this._type33StolenType;
    		shp.script._cargoSpawned = false;
    
    		this._type33Ship = shp;
    		this._type33Created = true;
    
    		// add BCC message options
    		var bcc = worldScripts.BroadcastCommsMFD;
    		bcc.$createMessage({
    			messageName: "gcm_address_thief",
    			callbackFunction: this.$thiefResponse.bind(this),
    			displayText: "Demand return of stolen item",
    			messageText: expandDescription("[gcm_address_thief]"),
    			transmissionType: "target",
    			ship: shp,
    			deleteOnTransmit: true,
    			delayCallback: 5
    		});
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_shipint_shipDied = function $gcm_shipint_shipDied(whom, why) {
    	if (this.ship.script.$gcm_hold_shipint_shipDied) this.ship.script.$gcm_hold_shipint_shipDied(whom, why);
    	// make sure the cargo gets created even if the ship dies at someone elses hands
    	worldScripts.GalCopBB_ShipInteractions.$createCargo(this.ship);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_shipint_shipBeingAttacked = function $gcm_shipint_shipBeingAttacked(whom) {
    	if (this.ship.script.$gcm_hold_shipint_shipBeingAttacked) this.ship.script.$gcm_hold_shipint_shipBeingAttacked(whom);
    	if (whom.isPlayer === false) return;
    	if (this.ship.isFleeing) worldScripts.GalCopBB_ShipInteractions.$createCargo(this.ship);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_shipint_shipBeingAttackedByCloaked = function $gcm_shipint_shipBeingAttackedByCloaked() {
    	if (this.ship.script.$gcm_hold_shipint_shipBeingAttackedByCloaked) this.ship.script.$gcm_hold_shipint_shipBeingAttackedByCloaked();
    	if (this.ship.isFleeing) worldScripts.GalCopBB_ShipInteractions.$createCargo(this.ship);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$createCargo = function $createCargo(ship) {
    	// cargo was created, so we don't want any more ships created
    	// if the player doesn't find the cargo now, mission is a bust
    	worldScripts.GalCopBB_ShipInteractions._type33Created = false;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(ship.script._missionID);
    	if (ship.script._cargoSpawned === false && item.data.targetQuantity === 1 && item.data.quantity === 0) {
    		//var sw = ship.spawnOne("gcm_stolen_items");
    		// we're doing it manually because if the ship is running and goes into a wormhole, the pod may follow. This way it stays behind.
    		var checkShips = system.addShips("gcm_stolen_items", 1, ship.position.add(ship.vectorUp.multiply((ship.boundingBox.y + 15) * -1)), 0);
    		var sw = null;
    		if (checkShips) sw = checkShips[0];
    		if (sw) {
    			sw.setScript("oolite-default-ship-script.js");
    			sw.script.shipWasScooped = worldScripts.GalCopBB_ShipInteractions.$gcm_stolenCargo_shipWasScooped;
    			sw.script.shipDied = worldScripts.GalCopBB_ShipInteractions.$gcm_stolenCargo_shipDied;
    			sw.script._missionID = ship.script._missionID;
    			sw.script._stolenItemType = ship.script._stolenItemType;
    			ship.script._cargoSpawned = true;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_stolenCargo_shipWasScooped = function $gcm_stolenCargo_shipWasScooped(scooper) {
    	var bb = worldScripts.BulletinBoardSystem;
    	if (worldScripts.GalCopBB_ShipInteractions._debug) log(this.name, "cargo scooped!");
    	if (!this.ship.script._missionID || this.ship.script._missionID === 0) return;
    	// if someone other that the player scoops it - bad luck! mission can't be completed.
    	if (scooper.isPlayer === false) {
    		var item = bb.$getItem(this.ship.script._missionID);
    		item.data.destroyedQuantity = 1;
    		this._type33Created = false;
    		return;
    	}
    	var eq = "";
    	switch (this.ship.script._stolenItemType) {
    		case "prototype ship schematics":
    			eq = "EQ_GCM_STOLEN_SCHEMATICS";
    			break;
    		case "high-level security codes":
    			eq = "EQ_GCM_STOLEN_CODES";
    			break;
    		case "classified documents":
    			eq = "EQ_GCM_STOLEN_DOCUMENTS";
    			break;
    		case "military weapon designs":
    			eq = "EQ_GCM_STOLEN_WEAPONDESIGNS";
    			break;
    	}
    	var gcm = worldScripts.GalCopBB_Missions;
    	player.ship.awardEquipment(eq);
    	if (bb.$percentCompleted(this.ship.script._missionID) < 1) {
    		var item = bb.$getItem(this.ship.script._missionID);
    		bb.$updateBBMissionPercentage(this.ship.script._missionID, 1);
    		item.data.quantity = 1;
    		gcm.$logMissionData(item.ID);
    		player.consoleMessage(expandDescription("[goal_updated]"));
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_stolenCargo_shipDied = function $gcm_stolenCargo_shipDied(whom, why) {
    	if (worldScripts.GalCopBB_ShipInteractions._debug) log(this.name, "!!OUCH! Special cargo destroyed");
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(this.ship.script._missionID);
    	item.data.destroyedQuantity = 1;
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$thiefResponse = function $thiefResponse() {
    	// turn off the normal greeting option
    	var bcc = worldScripts.BroadcastCommsMFD;
    	bcc.$addShipToArray(this._type33Ship, bcc._greeted);
    	bcc.$buildMessageList();
    
    	if (this._type33Ship.threatAssessment() < player.ship.threatAssessment()) {
    		// response OK
    		this._type33Ship.commsMessage(expandDescription("[gcm_thief_response_ok]"), player.ship);
    		// then flee
    		this._type33Ship.target = player.ship;
    		this._type33Ship.performFlee();
    		this.$createCargo(this._type33Ship);
    		this._type33Ship = null;
    	} else {
    		// response No
    		this._type33Ship.commsMessage(expandDescription("[gcm_thief_response_no]"), player.ship);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptedMission = function $acceptedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if (this._debug) log(this.name, "accepted mission id = " + missID);
    
    	if (!item) {
    		log(this.name, "!!ERROR: BB returned null value from $getItem on mission ID " + missID);
    		return;
    	}
    	gcm.$updateLastMissionDate(item.source, item.data.missionType);
    
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$completedMission = function $completedMission(missID) {
    	var p = player.ship;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// update mission history
    	gcm.$updateSuccessHistoryReputation(item);
    
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	if (item.data.missionType === 33) {
    		var eq = "";
    		var typ = item.data.stolenItemType;
    		switch (typ) {
    			case "prototype ship schematics":
    				eq = "EQ_GCM_STOLEN_SCHEMATICS";
    				break;
    			case "high-level security codes":
    				eq = "EQ_GCM_STOLEN_CODES";
    				break;
    			case "classified documents":
    				eq = "EQ_GCM_STOLEN_DOCUMENTS";
    				break;
    			case "military weapon designs":
    				eq = "EQ_GCM_STOLEN_WEAPONDESIGNS";
    				break;
    		}
    		p.removeEquipment(eq);
    		if (sbm) sbm.$removeSaleItem(eq + ":" + missID);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$confirmCompleted = function $confirmCompleted(missID) {
    	var p = player.ship;
    	var result = "";
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (item) {
    		// *** type 33 - stolen items
    		if (item.data.missionType === 33) {
    			var eq = "";
    			var typ = item.data.stolenItemType;
    			switch (typ) {
    				case "prototype ship schematics":
    					eq = "EQ_GCM_STOLEN_SCHEMATICS";
    					break;
    				case "high-level security codes":
    					eq = "EQ_GCM_STOLEN_CODES";
    					break;
    				case "classified documents":
    					eq = "EQ_GCM_STOLEN_DOCUMENTS";
    					break;
    				case "military weapon designs":
    					eq = "EQ_GCM_STOLEN_WEAPONDESIGNS";
    					break;
    			}
    			var chk = p.equipmentStatus(eq);
    			if (chk !== "EQUIPMENT_OK") {
    				result += (result === "" ? "" : "\n") + "The stolen " + typ + " are not on board your ship.";
    			}
    		}
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$terminateMission = function $terminateMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// adjust reputation only when the terminatePenalty flag is set to true 
    	if (item.data.terminatePenalty === true) {
    		// update mission history
    		gcm.$updateFailedHistoryReputation(item);
    	}
    
    	// *** type 33 - recovered stolen items
    	if (item.data.missionType === 33) {
    		// if the player terminates the mission after they have the software package, it can only mean the player is going to sell it
    		if (item.data.quantity === 1) {
    			var eq = "";
    			typ = item.data.stolenItemType;
    			switch (typ) {
    				case "prototype ship schematics":
    					eq = "EQ_GCM_STOLEN_SCHEMATICS";
    					break;
    				case "high-level security codes":
    					eq = "EQ_GCM_STOLEN_CODES";
    					break;
    				case "classified documents":
    					eq = "EQ_GCM_STOLEN_DOCUMENTS";
    					break;
    				case "military weapon designs":
    					eq = "EQ_GCM_STOLEN_WEAPONDESIGNS";
    					break;
    			}
    			var eqsts = player.ship.equipmentStatus(eq);
    			if (eqsts === "EQUIPMENT_OK") {
    				player.ship.removeEquipment(eq);
    				if (sbm) sbm.$removeSaleItem(eq + ":" + missID);
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$failedMission = function $failedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var eq = "";
    
    	if (!sbm) {
    		// if there's no blackmarket option, just remove the equipment
    		this.$terminateMission(missID);
    		return;
    	}
    
    	var item = bb.$getItem(missID);
    	// update mission history
    	gcm.$updateFailedHistoryReputation(item);
    
    	if (item.data.missionType === 33) {
    		if (item.data.quantity === 1) {
    			var typ = item.data.stolenItemType;
    			switch (typ) {
    				case "prototype ship schematics":
    					eq = "EQ_GCM_STOLEN_SCHEMATICS";
    					break;
    				case "high-level security codes":
    					eq = "EQ_GCM_STOLEN_CODES";
    					break;
    				case "classified documents":
    					eq = "EQ_GCM_STOLEN_DOCUMENTS";
    					break;
    				case "military weapon designs":
    					eq = "EQ_GCM_STOLEN_WEAPONDESIGNS";
    					break;
    			}
    			if (player.ship.equipmentStatus(eq) != "EQUIPMENT_OK") eq = "";
    		}
    	}
    
    	if (eq != "") gcm._equipmentFromFailedMissions.push({
    		missionType: item.data.missionType,
    		source: item.source,
    		equip: eq,
    		date: clock.adjustedSeconds
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateManifestEntry = function $updateManifestEntry(missID) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	gcm.$updateManifestEntry(missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
    	return "";
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 33 - recover stolen item
    this.$missionType33_Values = function $missionType33_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 400) + 500) / 10) * 10 + (7 - destSysInfo.government) * 50 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(200) // plus a possible bonus price, based on player score 
    		+
    		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 20); // plus a distance bonus
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price / 2);
    	return result;
    }
    Scripts/galcopbb_solaractivity.js
    "use strict";
    this.name = "GalCopBB_SolarActivity";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Controls all solar activity-system mission settings (70-73)";
    this.license = "CC BY-NC-SA 4.0";
    
    /*
    	*70 - Use scan equipment to take close up measurements of sun
    			get to fuel scoop dist = ((system.sun.collisionRadius * system.sun.collisionRadius) / system.sun.position.squaredDistanceTo(player.ship.position)) > 0.75;
    			prime scanning device, activate
    			await completion
    			If player retreats before scan is complete, scan will stop, but will restart automatically when in range again (ie it won't start from the beginning)
    	*71 - Recover data cache in low orbit over sun
    			find data cache
    			transmit security code
    			await data package
    			put two nav beacons in low orbit around sun on system population, regardless of mission status. if mission is activated, turn on beacon labels
    	72 - Launch monitoring devices (special missiles)
    			must deploy 2 special missiles from within fuel scoop range, one on the planet side, the other on the far side of the sun.
    			may need alternate launch mechanism because the player will be firing without a target and the standard controls don't allow for this.
    			TODO 
    			- how to file missile without a target?
    			- plot out what happens if the player sells the solar missile
    	*73 - Same as 70, but need to collect scanner from nearby system first, then perform scan
    	*74 - Delivery solar scan data to hi-tech system for analysis
    
    	Ideas for solar activity systems
    		- create severe solar activity events, noted via Snoopers
    		- during events, make it hard to jump into/out of system
    			- jump out possible but only when distance from sun is 2 times planet-sun distance
    			- jump in possible, but will occasionally fail and send the player to interstellar space. Warning could be given at start of jump sequence.
    			- if player is tail-gating another ship, still apply the chance of interstellar space
    		
    */
    
    this._debug = false;
    this._solarActivitySystems = []; // systems where there is solar activity (for missions 70-72)
    this._scanTimer = null; // timer object used when scanning 
    this._scanPosition = 0; // current position of the scan
    this._outOfRangeCount = 0; // counts number of times scan attempted to start but was out of range
    this._missEquipStore1 = ""; // stores equip key of first missile that was replaced for a special missile
    this._missEquipStore2 = ""; // stores equip key of second missile that was replaced for a special missile
    this._energy = null; // timer to deduct energy
    this._energyReduction = 3; // amount of energy to deduct each quarter second
    this._galJump = false;
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
    	this._solarActivitySystems = this.$findSolarActivitySystems();
    
    	var gcm = worldScripts.GalCopBB_Missions;
    	// add these mission types into the main control
    	var list = [70, 71, 73, 74];
    	gcm._availableMissionTypes = gcm._availableMissionTypes.concat(list);
    	gcm._multiStageMissionTypes.push(73);
    	this._debug = gcm._debug;
    	// add our reputation entities
    	var ent = worldScripts.GalCopBB_Reputation._entities;
    	ent["Beacon Of Hope"] = {
    		scope: "system",
    		display: true,
    		getValueWS: "",
    		getValueFN: "",
    		maxValue: 7,
    		minValue: 0,
    		degrade: "0.05|30",
    		rewardGrid: [{
    				value: 0,
    				description: ""
    			},
    			{
    				value: 4,
    				description: "",
    				achievementWS: "GalCopBB_SolarActivity",
    				achievementFN: "$beaconOfHopeReward"
    			},
    			{
    				value: 6,
    				description: "",
    				achievementWS: "GalCopBB_SolarActivity",
    				achievementFN: "$beaconOfHopeReward"
    			},
    			{
    				value: 7,
    				description: "",
    				achievementWS: "GalCopBB_SolarActivity",
    				achievementFN: "$beaconOfHopeReward"
    			}
    		]
    	};
    	// exclude some equipment items from being sold/stored by ShipConfig
    	var sc = worldScripts.ShipConfiguration_Core;
    	if (sc) {
    		sc._noSell.push("EQ_GCM_SOLAR_SCANNER");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function () {
    	if (missionVariables.GalCopBBMissions_MissileStore1) this._missEquipStore1 = missionVariables.GalCopBBMissions_MissileStore1;
    	if (missionVariables.GalCopBBMissions_MissileStore2) this._missEquipStore2 = missionVariables.GalCopBBMissions_MissileStore2;
    	worldScripts.BulletinBoardSystem.$registerBBEvent("GalCopBB_SolarActivity", "$custom_shipWillDockWithStation", "shipWillDockWithStation_start");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillSaveGame = function () {
    	if (this._missEquipStore1 != "") {
    		missionVariables.GalCopBBMissions_MissileStore1 = this._missEquipStore1;
    	} else {
    		delete missionVariables.GalCopBBMissions_MissileStore1;
    	}
    	if (this._missEquipStore2 != "") {
    		missionVariables.GalCopBBMissions_MissileStore2 = this._missEquipStore2;
    	} else {
    		delete missionVariables.GalCopBBMissions_MissileStore2;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.systemWillPopulate = function () {
    	if (this._solarActivitySystems.indexOf(system.ID) >= 0) {
    		// put our two data gatherers in place regardless of whether there's an active mission or not
    		// adds some ambience to systems. or something, anyway...
    		var position1 = Vector3D(0, 0, 1.1).fromCoordinateSystem("sps");
    		system.setPopulator("gcm-solarbeacon1", {
    			callback: function (pos) {
    				var checkShips = system.addShips("commsrelay", 1, pos, 100);
    				var sm = null;
    				if (checkShips) sm = checkShips[0];
    				if (sm) {
    					sm.displayName = "Solar Monitor Alpha";
    					sm.energyRechargeRate = 500;
    					sm.heatInsulation = 100;
    					sm.script._missionID = 0;
    					if (sm.script.shipDied) sm.script.$gcm_hold_shipDied = sm.script.shipDied;
    					sm.script.shipDied = worldScripts.GalCopBB_Missions.$gcm_entity_shipDied;
    				}
    			}.bind(this),
    			location: "COORDINATES",
    			coordinates: position1
    		});
    		var position2 = Vector3D(0, 0, -1.1).fromCoordinateSystem("sps");
    		system.setPopulator("gcm-solarbeacon2", {
    			callback: function (pos) {
    				var checkShips = system.addShips("commsrelay", 1, pos, 100);
    				var sm = null;
    				if (checkShips) sm = checkShips[0];
    				if (sm) {
    					sm.displayName = "Solar Monitor Beta";
    					sm.energyRechargeRate = 500;
    					sm.heatInsulation = 100;
    					sm.script._missionID = 0;
    					if (sm.script.shipDied) sm.script.$gcm_hold_shipDied = sm.script.shipDied;
    					sm.script.shipDied = worldScripts.GalCopBB_Missions.$gcm_entity_shipDied;
    				}
    			}.bind(this),
    			location: "COORDINATES",
    			coordinates: position2
    		});
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillExitWitchspace = function () {
    	this._scanPosition = 0;
    	this.$stopTimer();
    	if (this._galJump === true) {
    		this._galJump = false;
    		this._solarActivitySystems = this.$findSolarActivitySystems();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillLaunchFromStation = function (station) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var list = gcm.$getListOfMissions(true, 71);
    	for (var i = 0; i < list.length; i++) {
    		if (system.ID === list[i].source &&
    			list[i].data.targetQuantity > 0 &&
    			list[i].data.quantity < list[i].data.targetQuantity &&
    			list[i].expiry > clock.adjustedSeconds) {
    
    			var targets = system.shipsWithPrimaryRole("commsrelay");
    			for (var j = 0; j < targets.length; j++) {
    				if (targets[j].displayName.indexOf("Solar Monitor") >= 0) {
    					targets[j].script._missionID = list[i].ID;
    					var m_num = 1;
    					targets[j].beaconCode = "M";
    					if (targets[j].displayName.indexOf("Beta") >= 0) m_num = 2;
    
    					targets[j].beaconLabel = targets[j].displayName;
    					// set up bradocast comms interface
    					var bcc = worldScripts.BroadcastCommsMFD;
    					if (bcc.$checkMessageExists("gcm_sm_transmit_seccode_" + m_num) === false) {
    						bcc.$createMessage({
    							messageName: "gcm_sm_transmit_seccode_" + m_num,
    							callbackFunction: worldScripts.GalCopBB_DataCache.$transmitSecurityCode.bind(worldScripts.GalCopBB_DataCache),
    							displayText: "Transmit security code",
    							messageText: expandDescription("[gcm_transmitting_securitycode]"),
    							ship: targets[j],
    							transmissionType: "target",
    							deleteOnTransmit: true,
    							delayCallback: 5,
    							hideOnConditionRed: true
    						});
    					}
    				}
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.equipmentDamaged = function (equipmentKey) {
    	if (equipmentKey === "EQ_GCM_UNPROCESSED_SCAN_DATA") {
    		var list = worldScripts.GalCopBB_Missions.$getListOfMissions(true, 74);
    		for (var i = 0; i < list.length; i++) {
    			if (list[i].data.destroyedQuantity === 0) {
    				list[i].data.destroyedQuantity = 1;
    				list[i].payment /= 2;
    				break;
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$custom_shipWillDockWithStation = function (station) {
    	// do we have any 73 missions that will get their scanner here?
    	var gcm = worldScripts.GalCopBB_Missions;
    	if (gcm._simulator === true) return;
    	var bb = worldScripts.BulletinBoardSystem;
    	var list = gcm.$getListOfMissions(true, [73, 74]);
    	for (var i = 0; i < list.length; i++) {
    		if (list[i].data.missionType === 73 &&
    			list[i].destination === system.ID &&
    			station.isMainStation === true &&
    			list[i].data.stage === 0) {
    
    			// make sure there's enough room for the scanner
    			if (player.ship.cargoSpaceAvailable === 0) gcm.$freeCargoSpace(1, "")
    
    			// load up the scanner
    			player.ship.awardEquipment("EQ_GCM_SOLAR_SCANNER");
    			// add the arrival report text
    			player.addMessageToArrivalReport(expandDescription("[missionType73_arrivalReport_pickup]"));
    			// update the mission stage
    			list[i].data.stage += 1;
    			list[i].data.quantity += 1;
    			list[i].destination = list[i].source;
    
    			// force the chart marker to update
    			bb.$revertChartMarker(list[i].ID)
    			item.destination = list[i].source;
    			bb.$updateBBMissionPercentage(list[i].ID, list[i].data.quantity / list[i].data.targetQuantity);
    
    			gcm.$logMissionData(list[i].ID);
    			player.consoleMessage(expandDescription("[goal_updated]"));
    		}
    		if (list[i].data.missionType === 74) {
    			if (station.isMainStation && list[i].destination === system.ID) {
    				if (list[i].expiry >= clock.adjustedSeconds) {
    					if (player.ship.equipmentStatus("EQ_GCM_UNPROCESSED_SCAN_DATA") === "EQUIPMENT_OK") {
    						list[i].arrivalReportText = expandDescription("[missionType74_arrivalReport_complete]", {
    							payment: formatCredits(list[i].payment, false, true)
    						});
    					} else {
    						list[i].arrivalReportText = expandDescription("[missionType74_arrivalReport_corrupt]", {
    							payment: formatCredits(list[i].payment, false, true)
    						});
    					}
    				} else {
    					list[i].arrivalReportText = expandDescription("[missionType74_arrivalReport_late]");
    				}
    
    				list[i].data.quantity = 1;
    				bb.$updateBBMissionPercentage(list[i].ID, 1);
    
    				gcm.$logMissionData(list[i].ID);
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerEnteredNewGalaxy = function (galaxyNumber) {
    	this._solarActivitySystems = this.$findSolarActivitySystems();
    	this._galJump = true;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = function (whom, why) {
    	this.$stopTimer();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.mode = function () {
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.activated = function () {
    	worldScripts.GalCopBB_SolarActivity.$startScanner();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$startScanner = function $startScanner() {
    	if (this._scanTimer && this._scanTimer.isRunning) {
    		player.consoleMessage("Solar scan process stopped.");
    		worldScripts.GalCopBB_Missions.$playSound("stop");
    		this.$stopTimer();
    		return;
    	}
    	player.consoleMessage("Solar scan started.");
    	this._scanTimer = new Timer(this, this.$performScan, 1, 1);
    	if (this._energy && this._energy.isRunning) this._energy.stop();
    	this._energy = new Timer(this, this.$depleteEnergy, 0.25, 0.25);
    	worldScripts.GalCopBB_Missions.$playSound("activate");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$performScan = function $performScan() {
    	var p = player.ship;
    	if (p.equipmentStatus("EQ_GCM_SOLAR_SCANNER") === "EQUIPMENT_DAMAGED") {
    		player.consoleMessage("Scanner equipment is inoperable.");
    		this.$stopTimer();
    		return;
    	}
    	if (this.$isPlayerWithinRange() === false) {
    		this._outOfRangeCount += 1;
    		if (this._outOfRangeCount === 10) this._outOfRangeCount = 0;
    		if (this._outOfRangeCount === 1) player.consoleMessage("Out of range of sun. Get within fuel scoop range to continue scan.");
    		return;
    	}
    	this._outOfRangeCount = 0;
    	this._scanPosition += 1;
    	if (parseInt(this._scanPosition / 5) === this._scanPosition / 5) {
    		player.consoleMessage("Solar scan: " + parseInt((this._scanPosition / 60) * 100) + "% complete");
    	}
    	// check for complete
    	if (this._scanPosition >= 60) {
    		this._scanPosition = 0;
    		this.$stopTimer();
    		var gcm = worldScripts.GalCopBB_Missions;
    		var bb = worldScripts.BulletinBoardSystem;
    		var list = gcm.$getListOfMissions(true, [70, 73]);
    		for (var i = 0; i < list.length; i++) {
    			if (system.ID === list[i].source &&
    				list[i].expiry > clock.adjustedSeconds) {
    
    				list[i].data.quantity += 1;
    				bb.$updateBBMissionPercentage(list[i].ID, list[i].data.quantity / list[i].data.targetQuantity);
    
    				gcm.$logMissionData(list[i].ID);
    				player.consoleMessage(expandDescription("[goal_updated]"));
    				break;
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$depleteEnergy = function $depleteEnergy() {
    	var p = player.ship;
    	p.energy -= this._energyReduction;
    	if (p.energy < 64) {
    		this.$stopTimer()
    		player.consoleMessage("Insufficient energy to continue scanning.");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns true if player is within fuel scoop range, otherwise false
    this.$isPlayerWithinRange = function $isPlayerWithinRange() {
    	return (((system.sun.collisionRadius * system.sun.collisionRadius) / system.sun.position.squaredDistanceTo(player.ship.position)) > 0.75);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$stopTimer = function $stopTimer() {
    	if (this._scanTimer && this._scanTimer.isRunning) {
    		this._scanTimer.stop();
    	}
    	delete this._scanTimer;
    	if (this._energy && this._energy.isRunning) {
    		this._energy.stop();
    	}
    	delete this._energy;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$beaconOfHopeReward = function $beaconOfHopeReward(entity, sysID, rep) {
    	var reputation = worldScripts.GalCopBB_Reputation;
    	if (rep >= 4 && rep < 6) {
    		if (reputation.$checkForAward(entity, "Beacon Of Support", 0) === false) {
    			reputation.$addAward({
    				title: "Beacon Of Support",
    				description: "Awarded for the exceptional help and support provided to our Solar Scanning Data Collection Unit, assisting in the vital data collection functions that help keep our citizens safe.",
    				entity: entity,
    				system: 0,
    				imageType: 4
    			});
    		}
    	}
    	if (rep >= 6 && rep < 7) {
    		if (reputation.$checkForAward(entity, "Beacon Of Courage", 0) === false) {
    			reputation.$addAward({
    				title: "Beacon Of Courage",
    				description: "Awarded for the tremendous efforts undertaken in supporting our Solar Scanning Data Collection unit, and thereby contributing in a significant way to the on-going safety of our citizens.",
    				entity: entity,
    				system: 0,
    				imageType: 5
    			});
    		}
    	}
    	if (rep >= 7) {
    		if (reputation.$checkForAward(entity, "Beacon Of Hope", 0) === false) {
    			reputation.$addAward({
    				title: "Beacon Of Hope",
    				description: "The highest award we can give to a pilot. Awarded for the Herculean help and support given to our Solar Scanning Data Collection unit, helping them to effectively monitor our sun for dangerous solar events, and by doing so, assisting in the protection of all our citizens.",
    				entity: entity,
    				system: 0,
    				imageType: 6
    			});
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns a list of sysInfos where solar activity is present
    this.$findSolarActivitySystems = function $findSolarActivitySystems() {
    	return SystemInfo.filteredSystems(this, function (other) {
    		return (other.description.indexOf("solar activity") >= 0);
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptedMission = function $acceptedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if (this._debug) log(this.name, "accepted mission id = " + missID);
    
    	if (!item) {
    		log(this.name, "!!ERROR: BB returned null value from $getItem on mission ID " + missID);
    		return;
    	}
    	gcm.$updateLastMissionDate(item.source, item.data.missionType);
    	gcm.$updateGeneralSettings(item);
    
    	// give the player the solar scanner
    	if (item.data.missionType === 70) {
    		player.ship.awardEquipment("EQ_GCM_SOLAR_SCANNER");
    	}
    	// mission 72 is incomplete at the moment...
    	if (item.data.missionType === 72) {
    		// award the 2 missiles
    		// if existing missiles need to be replaced, replace the cheapest ones first
    		var flag = false;
    		try {
    			var miss = player.ship.missiles;
    			flag = true;
    		} catch (err) {
    			if (this._debug) log(this.name, "!!ERROR: " + err);
    		}
    		// we're going to hold onto the missiles, though, and give them back when the mission is completed
    		if (flag === true && (miss.length + 2) > player.ship.missileCapacity) {
    			// get a list of installed missiles, sorted by price
    			var checklist = [];
    			for (var j = 0; j < miss.length; j++) {
    				var misseq = EquipmentInfo.infoForKey(miss[j].equipmentKey);
    				checklist.push({
    					eq: miss[j].equipmentKey,
    					price: misseq.price
    				});
    			}
    			checklist.sort(compare);
    
    			// work out what the player has got
    			var mincost = 200000; // set a high value here so we are guaranteed to come in underneath it
    			var key1 = "";
    			var key2 = "";
    			for (var j = 0; j < checklist.length; j++) {
    				// see if the cost of this missile is less than or equal to the last mincost value we stored
    				// we're doing less than or equal to because there could be more than 1 missile at the same price and we want both points loaded
    				if (checklist[j].price <= mincost) {
    					if (key1 != "") {
    						key2 = key1;
    					}
    					mincost = checklist[j].price;
    					key1 = checklist[j].eq;
    				}
    			}
    			// only store the 2nd one if there are no slots left at all
    			if (key2 != "" && (miss.length + 1) > player.ship.missileCapacity) {
    				worldScripts.GalCopBBMissions_SolarActivity._missEquipStore2 = key2;
    				// remove first instance of this key
    				player.ship.removeEquipment(key2);
    			}
    			if (key1 != "") {
    				worldScripts.GalCopBBMissions_SolarActivity._missEquipStore1 = key1;
    				// remove first instance of this key
    				player.ship.removeEquipment(key1);
    			}
    		}
    		// give the player the missiles now
    		player.ship.awardEquipment("EQ_GCM_SOLAR_MISSILE");
    		player.ship.awardEquipment("EQ_GCM_SOLAR_MISSILE");
    	}
    
    	if (item.data.missionType === 74) {
    		player.ship.awardEquipment("EQ_GCM_UNPROCESSED_SCAN_DATA");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$completedMission = function $completedMission(missID) {
    	var p = player.ship;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    
    	// update mission history
    	gcm.$updateSuccessHistoryReputation(item);
    
    	// *** type 70 - solar scanner
    	if (item.data.missionType === 70 || item.data.missionType === 73) {
    		p.removeEquipment("EQ_GCM_SOLAR_SCANNER");
    		if (sbm) sbm.$removeSaleItem("EQ_GCM_SOLAR_SCANNER:" + missID);
    	}
    	// *** type 72 - give back the player's original missiles, if possible (they might have bought new ones)
    	if (item.data.missionType === 72) {
    		var sa = worldScripts.GalCopBB_SolarActivity;
    		if (sa._missEquipStore1 != "") {
    			p.awardEquipment(sa._missEquipStore1);
    			sa._missEquipStore1 = "";
    		}
    		if (sa._missEquipStore2 != "") {
    			p.awardEquipment(sa._missEquipStore2);
    			sa._missEquipStore2 = "";
    		}
    		// make sure any remaining missiles are removed
    		for (var i = 0; i < p.missileCapacity; i++) {
    			p.removeEquipment("EQ_GCM_SOLAR_MISSILE");
    		}
    	}
    	// *** type 74 - solar scan data
    	if (item.data.missionType === 74) {
    		p.removeEquipment("EQ_GCM_SOLAR_SCAN_DATA");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$confirmCompleted = function $confirmCompleted(missID) {
    	var p = player.ship;
    	var result = "";
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (item) {
    		if (item.data.missionType === 74) {
    			var chk = p.equipmentStatus("EQ_GCM_UNPROCESSED_SCAN_DATA")
    			if (chk !== "EQUIPMENT_OK" && chk !== "EQUIPMENT_DAMAGED") result += (result === "" ? "" : "\n") + "Solar scan data not on board";
    		}
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$terminateMission = function $terminateMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// adjust reputation only when the terminatePenalty flag is set to true 
    	if (item.data.terminatePenalty === true) {
    		// update mission history
    		gcm.$updateFailedHistoryReputation(item);
    	}
    
    	// *** type 70/73 - solar scanner
    	if (item.data.missionType === 70 || item.data.missionType === 73) {
    		var eqsts = player.ship.equipmentStatus("EQ_GCM_SOLAR_SCANNER");
    		if (eqsts === "EQUIPMENT_OK" || eqsts === "EQUIPMENT_DAMAGED") {
    			player.ship.removeEquipment("EQ_GCM_SOLAR_SCANNER");
    			if (sbm) sbm.$removeSaleItem("EQ_GCM_SOLAR_SCANNER:" + missID);
    		}
    	}
    	if (item.data.missionType === 74) {
    		player.ship.removeEquipment("EQ_GCM_UNPROCESSED_SCAN_DATA");
    	}
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$failedMission = function $failedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var eq = "";
    
    	if (!sbm) {
    		// if there's no blackmarket option, just remove the equipment
    		this.$terminateMission(missID);
    		return;
    	}
    
    	var item = bb.$getItem(missID);
    	// update mission history
    	gcm.$updateFailedHistoryReputation(item);
    
    	// *** type 70/73 - solar scanner
    	if (item.data.missionType === 70 || item.data.missionType === 73) {
    		var eqsts = player.ship.equipmentStatus("EQ_GCM_SOLAR_SCANNER");
    		if (eqsts === "EQUIPMENT_OK" || eqsts === "EQUIPMENT_DAMAGED") {
    			eq = "EQ_GCM_SOLAR_SCANNER";
    		}
    	}
    
    	if (eq != "") gcm._equipmentFromFailedMissions.push({
    		missionType: item.data.missionType,
    		source: item.source,
    		equip: eq,
    		date: clock.adjustedSeconds
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateManifestEntry = function $updateManifestEntry(missID) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	gcm.$updateManifestEntry(missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
    	return worldScripts.GalCopBB_Missions.$missionAvailability(missID, missType, origSysID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 70/72 
    this.$missionType7072_Values = function $missionType7072_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var missValues = worldScripts.GalCopBB_CoreMissionValues;
    	var result = {};
    	result["quantity"] = 1;
    	result["price"] = 100 + parseInt((8 - destSysInfo.government) * (Math.random() * 50));
    	result["expiry"] = clock.adjustedSeconds + (3600 * 3); // 3 hours to complete
    	result["penalty"] = result.price / 2;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 71 
    this.$missionType71_Values = function $missionType71_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var missValues = worldScripts.GalCopBB_CoreMissionValues;
    	var result = {};
    	result["quantity"] = 2;
    	result["price"] = 100 + parseInt((8 - destSysInfo.government) * (Math.random() * 50));
    	result["expiry"] = clock.adjustedSeconds + (3600 * 3); // 3 hours to complete
    	result["penalty"] = result.price / 2;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 73
    this.$missionType73_Values = function $missionType73_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var missValues = worldScripts.GalCopBB_CoreMissionValues;
    	var result = {};
    	result["quantity"] = 2;
    	result["price"] = 500 + parseInt((8 - destSysInfo.government) * (Math.random() * 90));
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime;
    	result["penalty"] = result.price / 2;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 74
    this.$missionType74_Values = function $missionType74_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var missValues = worldScripts.GalCopBB_CoreMissionValues;
    	var result = {};
    	result["quantity"] = 1;
    	result["price"] = 500 + parseInt((8 - destSysInfo.government) * (Math.random() * 90)) + missValues.$calcDistanceBonus(routeDistance, 10);
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime;
    	result["penalty"] = result.price / 2;
    	return result;
    }
    Scripts/galcopbb_stationdisrupt.js
    "use strict";
    this.name = "GalCopBB_StationDisrupt";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Control of the F4 Interface and various functions for disrupting stations (mission 63,64,65)";
    this.license = "CC BY-NC-SA 4.0";
    
    this._debug = false;
    this._disruptInstalled = []; // list of systems where the disruption software has been installed
    this._typeToInstall = 0; // type of software being installed either 63 or 64
    this._checkTimer = null; // timer started when player installs software to see if they are caught by GalCop
    this._redockCatch = false; // flag to indicate player redocked within 24 hours of software installation
    this._distCheckTimer = null; // timer to inform player when they've reached a suitable distance from main station
    this._policeSelfDestructTimer = null; // timer to control the self-destruction of the police vessel
    this._policeShip = null; // ship object of police vessel that will self-destruct
    this._dataStreamTimer = null; // timer to control sending data stream to main station
    this._dataStreamCounter = 0; // current position of the data stream
    this._playerIsDocking = false; // flag to indicate when the player has requested docking clearance
    this._mission6364MissionID = 0; // mission ID link for a 63/64 type mission
    this._currDist = 0; // player's current distance from the main station
    this._miss65EjectDist = 50000; // distance player must be from main station before ejecting
    this._checkDistMessage = false; // flag to indicate the suitable distance message has been sent to the player
    this._activeMissionID = 0; // mission ID link for the software that has just been installed at a station
    this._cancelTimer = null; // timer to control when the window of opportunity for installing the software expires
    this._abortTime = 0; // time when window of opportunity for software installation closes
    this._simulator = false;
    
    //	Must initiate software upload within 30 seconds of finishing data stream
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
    	if (missionVariables.GalCopBBMissions_StationDisruption) {
    		this._disruptInstalled = JSON.parse(missionVariables.GalCopBBMissions_StationDisruption);
    		delete missionVariables.GalCopBBMissions_StationDisruption;
    	}
    	var gcm = worldScripts.GalCopBB_Missions;
    	// add these mission types into the main control
    	var list = [63, 64, 65];
    	gcm._availableMissionTypes = gcm._availableMissionTypes.concat(list);
    	gcm._multiStageMissionTypes = gcm._multiStageMissionTypes.concat(list);
    	this._debug = gcm._debug;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function () {
    	this.$initInterface(player.ship.dockedStation);
    	if (worldScripts.BountySystem_Core) {
    		// add the specialised bounty item
    		var bs = worldScripts.BountySystem_Core;
    		bs._offenceTypes["galcop station interference"] = {
    			description: "Illegally accessing and/or updating of critical station functions for purposes of personal gain",
    			severity: 3
    		};
    	}
    	this.$initInterface(player.ship.dockedStation);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillSaveGame = function () {
    	if (this._disruptInstalled.length > 0) {
    		missionVariables.GalCopBBMissions_StationDisruption = JSON.stringify(this._disruptInstalled);
    	} else {
    		delete missionVariables.GalCopBBMissions_StationDisruption;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.systemWillPopulate = function () {
    	// this system is disrupted...
    	// work out the type of disruption and apply 
    	if (this.$disruptCommodities(system.ID) === true) this.$adjustCommodityPrices();
    
    	var gcm = worldScripts.GalCopBB_Missions;
    	var list = gcm.$getListOfMissions(true, [63, 64]);
    	for (var i = 0; i < list.length; i++) {
    		if (list[i].destination === system.ID &&
    			list[i].data.quantity === 0 &&
    			list[i].data.destroyedQuantity === 0 &&
    			list[i].expiry > clock.adjustedSeconds) {
    			this._mission6364MissionID = list[i].ID;
    			this.$createDataStreamOption();
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.guiScreenChanged = function (to, from) {
    	if (guiScreen === "GUI_SCREEN_EQUIP_SHIP" && player.ship.docked && player.ship.dockedStation.isMainStation && this.$disruptEquipment(system.ID) === true) {
    		mission.runScreen({
    			screenID: "oolite-gcm-disabled-summary",
    			title: "Services Unavailable",
    			message: expandDescription("[gcm_disrupt_shipyard]"),
    			overlay: {
    				name: "gcm-bandaid.png",
    				height: 546
    			}
    		});
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDockedWithStation = function (station) {
    	if (this._simulator === true) return;
    
    	this._playerIsDocking = false;
    	this._dataStreamCounter = 0;
    	this.$stopTimers();
    	this.$initInterface(station);
    
    	// check if we're docking inside of 48hours of disruption
    	if (station.isMainStation) {
    		for (var i = 0; i < this._disruptInstalled.length; i++) {
    			if (this._disruptInstalled[i].system === system.ID && (this._disruptInstalled[i].installTime + (86400 * 2)) > clock.adjustedSeconds && this._disruptInstalled[i].fined === false) {
    				// oops! redocked within 48 hours
    				this._activeMissionID = this._disruptInstalled[i].originalMissionID;
    				this._redockCatch = true;
    			}
    		}
    	}
    
    	// check for a mission type 65 at stage 0
    	var bb = worldScripts.BulletinBoardSystem;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var list = gcm.$getListOfMissions(true, 65);
    	for (var i = 0; i < list.length; i++) {
    		if (list[i].destination === system.ID &&
    			list[i].expiry > clock.adjustedSeconds &&
    			list[i].data.stage === 0 &&
    			list[i].data.destroyedQuantity === 0 &&
    			station.allegiance !== "galcop") {
    
    			var msg = expandDescription("[missionType65_arrivalReport_pickup]");
    			// does the player already have an escape pod?
    			if (player.ship.equipmentStatus("EQ_ESCAPE_POD") === "EQUIPMENT_OK") {
    				player.ship.removeEquipment("EQ_ESCAPE_POD");
    				msg = msg.replace("*existing*", ", removing your existing escape pod and ");
    			} else {
    				msg = msg.replace("*existing*", "");
    			}
    			player.ship.awardEquipment("EQ_GCM_ESCAPE_POD");
    			list[i].data.stage += 1;
    			list[i].data.quantity += 1;
    			bb.$updateBBMissionPercentage(list[i].ID, list[i].data.quantity / list[i].data.targetQuantity);
    
    			bb.$revertChartMarker(list[i].ID);
    
    			gcm.$logMissionData(list[i].ID);
    			player.consoleMessage(expandDescription("[goal_updated]"));
    
    			player.addMessageToArrivalReport(msg);
    			break;
    		}
    
    		if (list[i].destination === system.ID &&
    			list[i].data.stage === 2 &&
    			list[i].data.destroyedQuantity === 0) {
    
    			// reset the expiry (because the escape pod rescue time is completely variable)
    			list[i].expiry = clock.adjustedSeconds + (3600);
    			gcm.$updateManifestEntry(list[i].ID);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipLaunchedFromStation = function (station) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	this._simulator = false;
    	if (gcm.$simulatorRunning() === true) {
    		this._simulator = true;
    		return;
    	}
    
    	var list = gcm.$getListOfMissions(true, [63, 64, 65]);
    	for (var i = 0; i < list.length; i++) {
    		if (list[i].data.missionType === 65 && list[i].source === system.ID && station.isMainStation) {
    
    			if (list[i].data.stage === 1 && list[i].data.destroyedQuantity === 0) {
    				this._distCheckTimer = new Timer(this, this.$distCheck, 5, 5);
    				break;
    			}
    			if (list[i].data.stage === 2 && list[i].data.destroyedQuantity === 0) {
    				// monkey-patch the station
    				if (station.script.stationLaunchedShip && station.script.stationLaunchedShip != this.$gcm_stationLaunchedShip) {
    					station.script.$gcm_hold_stationLaunchedShip = station.script.stationLaunchedShip;
    				}
    				station.script.stationLaunchedShip = this.$gcm_stationLaunchedShip;
    				break;
    			}
    		}
    		if ((list[i].data.missionType === 63 || list[i].data.missionType === 64) &&
    			list[i].destination === system.ID &&
    			list[i].data.stage === 0 &&
    			list[i].data.destroyedQuantity === 0 &&
    			list[i].expiry > clock.adjustedSeconds) {
    			this._mission6364MissionID = list[i].ID;
    			this.$createDataStreamOption();
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipExitedWitchspace = function () {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var list = gcm.$getListOfMissions(true, 65);
    	for (var i = 0; i < list.length; i++) {
    		if (list[i].source === system.ID) {
    			if (list[i].data.stage === 1 && list[i].data.destroyedQuantity === 0) {
    				this._distCheckTimer = new Timer(this, this.$distCheck, 5, 5);
    				break;
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillEnterWitchspace = function () {
    	this.$stopTimers();
    	if (this._cancelTimer && this._cancelTimer.isRunning) {
    		this._cancelTimer.stop();
    		this._cancelTimer = null;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = function (whom, why) {
    	this.$stopTimers();
    	if (this._cancelTimer && this._cancelTimer.isRunning) {
    		this._cancelTimer.stop();
    		this._cancelTimer = null;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.dayChanged = function (newday) {
    	for (var i = this._disruptInstalled.length - 1; i >= 0; i--) {
    		// has 1 month passed - if so, remove the disruption
    		if ((this._disruptInstalled[i].installTime + (30 * 86400)) < clock.adjustedSeconds) this._disruptInstalled.splice(i, 1);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.missionScreenOpportunity = function () {
    	if (this._redockCatch === true) {
    		this._redockCatch = false;
    		this.$checkForCapture();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipLaunchedEscapePod = function (escapepod) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var bb = worldScripts.BulletinBoardSystem;
    	var list = gcm.$getListOfMissions(true, 65);
    	for (var i = 0; i < list.length; i++) {
    		if (list[i].source === system.ID &&
    			list[i].data.stage === 1 &&
    			list[i].data.destroyedQuantity === 0) {
    
    			if (this._currDist > this._miss65EjectDist) {
    				// reset the expiry (because the escape pod rescue time is completely variable)
    				// we'll reset it again when we hit the shipWilldockwithstation routine, but for now just put it enough into the future to avoid failing it.
    				list[i].expiry = clock.adjustedSeconds + (86400 * 20);
    				list[i].data.stage += 1;
    				list[i].data.quantity += 1;
    				bb.$updateBBMissionPercentage(list[i].ID, list[i].data.quantity / list[i].data.targetQuantity);
    
    				gcm.$logMissionData(list[i].ID);
    				player.consoleMessage(expandDescription("[goal_updated]"));
    			} else {
    				list[i].destroyedQuantity = 1;
    			}
    			break;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerRequestedDockingClearance = function (message) {
    	if (message === "DOCKING_CLEARANCE_GRANTED" || message === "DOCKING_CLEARANCE_EXTENDED") {
    		this._playerIsDocking = true;
    	} else {
    		this._playerIsDocking = false;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerDockingRefused = function () {
    	this._playerIsDocking = false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // this works in v1.85/6 only
    this.playerDockingClearanceGranted = function () {
    	this._playerIsDocking = true;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerDockingClearanceExpired = function () {
    	this._playerIsDocking = false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$stopTimers = function $stopTimers() {
    	if (this._checkTimer && this._checkTimer.isRunning) {
    		this._checkTimer.stop();
    		this._checkTimer = null;
    	}
    	if (this._distCheckTimer && this._distCheckTimer.isRunning) {
    		this._distCheckTimer.stop();
    		this._distCheckTimer = null;
    	}
    	if (this._dataStreamTimer && this._dataStreamTimer.isRunning) {
    		this._dataStreamTimer.stop();
    		this._dataStreamTimer = null;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_stationLaunchedShip = function $gcm_stationLaunchedShip(whom) {
    	if (this.ship.script.$gcm_hold_stationLaunchedShip) this.ship.script.$gcm_hold_stationLaunchedShip(whom);
    
    	if (whom.isPolice) {
    		// ok, got one!
    		var gcmsd = worldScripts.GalCopBB_StationDisrupt;
    		whom.awardEquipment("EQ_ESCAPE_POD");
    		gcmsd._policeShip = whom;
    		gcmsd.$startSelfDestructTimer();
    
    		// un-monkey-patch station
    		if (this.ship.script.$gcm_hold_stationLaunchedShip) {
    			this.ship.script.stationLaunchedShip = this.ship.script.$gcm_hold_stationLaunchedShip;
    			delete this.ship.script.$gcm_hold_stationLaunchedShip;
    		} else {
    			delete this.ship.script.stationLaunchedShip;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$startSelfDestructTimer = function $startSelfDestructTimer() {
    	if (this._policeShip) {
    		// attach some failsafe scripts
    		if (this._policeShip.script.shipDied && this._policeShip.script.shipDied != this.$gcm_police_shipDied) {
    			this._policeShip.script.$gcm_hold_police_shipDied = this._policeShip.script.shipDied;
    		}
    		this._policeShip.script.shipDied = this.$gcm_police_shipDied;
    
    		if (this._policeShip.script.shipDockedWithStation && this._policeShip.script.shipDockedWithStation != this.$gcm_police_shipDockedWithStation) {
    			this._policeShip.script.$gcm_hold_police_shipDockedWithStation = this._policeShip.script.shipDockedWithStation;
    		}
    		this._policeShip.script.shipDockedWithStation = this.$gcm_police_shipDockedWithStation;
    	}
    	this._policeSelfDestructTimer = new Timer(this, this.$policeSelfDestruct, parseInt(Math.random() * 240 + 180), 0);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$policeSelfDestruct = function $policeSelfDestruct() {
    	if (this._policeShip.crew && this._policeShip.crew[0].name === "") {
    		this._policeShip.crew[0].name = randomName() + " " + randomName();
    	}
    	this._policeShip.script.shipLaunchedEscapePod = this.$gcm_police_shipLaunchedEscapePod;
    	this._policeShip.abandonShip();
    	this._policeShip.explode();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_police_shipDied = function $gcm_police_shipDied(whom, why) {
    	if (this.ship.script.$gcm_hold_police_shipDied) this.ship.script.$gcm_hold_police_shipDied(whom, why);
    	worldScripts.GalCopBB_StationDisrupt.$createCargo(this.ship);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_police_shipLaunchedEscapePod = function $gcm_police_shipLaunchedEscapePod(pod, passengers) {
    	pod.script._gcm_asked_already = true;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_police_shipDockedWithStation = function $gcm_police_shipDockedWithStation(station) {
    	if (this.ship.script.$gcm_hold_police_shipDockedWithStation) this.ship.script.$gcm_hold_police_shipDockedWithStation(station);
    	//log(this.name, "police ship docked before self destruct");
    	// if the police ship docks before the self destruct fires, reset the station monkey patching so we can try again
    	// redo the monkey patch on the station
    	if (station.script.stationLaunchedShip && station.script.stationLaunchedShip != worldScripts.GalCopBB_StationDisrupt.$gcm_stationLaunchedShip) {
    		station.script.$gcm_hold_stationLaunchedShip = station.script.stationLaunchedShip;
    	}
    	station.script.stationLaunchedShip = worldScripts.GalCopBB_StationDisrupt.$gcm_stationLaunchedShip;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$gcm_softwarecargo_shipWasScooped = function $gcm_softwarecargo_shipWasScooped(scooper) {
    	if (scooper.isPlayer) {
    		// complete mission
    		player.ship.awardEquipment("EQ_GCM_SOFTWARE");
    		var bb = worldScripts.BulletinBoardSystem;
    		var gcm = worldScripts.GalCopBB_Missions;
    		var list = gcm.$getListOfMissions(true, 65);
    		for (var i = 0; i < list.length; i++) {
    			if (list[i].source === system.ID &&
    				list[i].data.stage === 2 &&
    				list[i].data.destroyedQuantity === 0) {
    
    				list[i].data.stage += 1;
    				list[i].data.quantity += 1;
    				bb.$updateBBMissionPercentage(list[i].ID, list[i].data.quantity / list[i].data.targetQuantity);
    
    				gcm.$logMissionData(list[i].ID);
    				player.consoleMessage(expandDescription("[goal_updated]"));
    				break;
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$createCargo = function $createCargo(ship) {
    	var sw = ship.spawnOne("gcm_software_package");
    	sw.script.shipWasScooped = this.$gcm_softwarecargo_shipWasScooped;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$distCheck = function $distCheck() {
    	if (player.ship && player.ship.isValid && player.ship.isInSpace) {
    		this._currDist = player.ship.position.distanceTo(system.mainStation) / 1000;
    	}
    	if (this._currDist > this._miss65EjectDist && this._checkDistMessage === false) {
    		player.consoleMessage("Suitable distance reached for ejection point.");
    		this._checkDistMessage = true;
    		//this._distCheckTimer.stop();
    		//delete this._distCheckTimer;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$initInterface = function $initInterface(station) {
    	var inst = false;
    	if (station.isMainStation) {
    		var gcm = worldScripts.GalCopBB_Missions;
    		var list = gcm.$getListOfMissions(true, [63, 64]);
    		for (var i = 0; i < list.length; i++) {
    			if (list[i].destination === system.ID &&
    				((list[i].data.missionType === 63 && this.$disruptCommodities(system.ID) === false) || (list[i].data.missionType === 64 && this.$disruptEquipment(system.ID) === false)) &&
    				list[i].data.targetQuantity > 0 && this._cancelTimer && this._cancelTimer.isRunning) {
    
    				inst = true;
    				this._typeToInstall = list[i].data.missionType;
    
    				station.setInterface(this.name, {
    					title: "Install security software",
    					category: "Station Interfaces",
    					summary: "Uploads software package into the stations computers to disrupt " + (this._typeToInstall === 63 ? "station market prices" : "station ship outfitting facilities") + ".",
    					callback: this.$installSoftware.bind(this)
    				});
    			}
    		}
    	}
    	if (inst === false) {
    		station.setInterface(this.name, null);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$adjustCommodityPrices = function $adjustCommodityPrices() {
    	for (var j = 0; j < this._disruptInstalled.length; j++) {
    		if (this._disruptInstalled[j].system === system.ID && this._disruptInstalled[j].type === 1) {
    			var price = this._disruptInstalled[j].price;
    			var mkt = system.mainStation.market;
    			for (var key in mkt) {
    				if (mkt.hasOwnProperty(key)) {
    					system.mainStation.setMarketPrice(key, price);
    				}
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$disruptCommodities = function $disruptCommodities(sysID) {
    	var result = false;
    	for (var i = 0; i < this._disruptInstalled.length; i++) {
    		if (this._disruptInstalled[i].system === sysID && this._disruptInstalled[i].type === 1) result = true;
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$disruptEquipment = function $disruptEquipment(sysID) {
    	var result = false;
    	for (var i = 0; i < this._disruptInstalled.length; i++) {
    		if (this._disruptInstalled[i].system === sysID && this._disruptInstalled[i].type === 2) result = true;
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$installSoftware = function $installSoftware() {
    	// remove the interface screen
    	player.ship.dockedStation.setInterface(this.name, null);
    	this._cancelTimer.stop()
    
    	// theoretically the player can't get here if they sat on the F4 interface screen until the time ran out, then tried an install
    	// F4 interface would be invalid and they couldn't select it
    	// flag the mission as complete
    	var bb = worldScripts.BulletinBoardSystem;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var list = gcm.$getListOfMissions(true, this._typeToInstall);
    	for (var i = 0; i < list.length; i++) {
    		if (list[i].destination === system.ID && list[i].data.stage === 1) {
    			// add the system to the disrupt list
    			this._disruptInstalled.push({
    				system: system.ID,
    				originalMissionID: list[i].ID,
    				installTime: clock.adjustedSeconds,
    				type: (this._typeToInstall === 63 ? 1 : 2),
    				price: 10.0, // decicredits, means 1cr
    				fined: false
    			});
    
    			if (this._typeToInstall === 63) this.$adjustCommodityPrices();
    
    			// note the missionID so we can update the disrupt list if the player stays too long
    			this._activeMissionID = list[i].ID;
    
    			list[i].data.stage += 1;
    			list[i].data.quantity += 1;
    			bb.$updateBBMissionPercentage(list[i].ID, 1);
    
    			gcm.$logMissionData(list[i].ID);
    			player.consoleMessage(expandDescription("[goal_updated]"));
    		}
    	}
    
    	// confirm the action to the player
    	mission.runScreen({
    		screenID: "oolite-gcm-disruptcomplete-summary",
    		title: "Disruption Software",
    		message: "The disruption software package has been installed successfully.",
    		overlay: {
    			name: "gcm-ok.png",
    			height: 546
    		},
    		exitScreen: "GUI_SCREEN_INTERFACES",
    	});
    
    	this._checkTimer = new Timer(this, this.$doCapture, parseInt(Math.random() * 15 + 30), 0);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$doCapture = function $doCapture() {
    	this._redockCatch = true;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$checkForCapture = function $checkForCapture() {
    	// skip if the player has launched
    	if (player.ship.isInSpace) return;
    	// skip if this isn't the main station we're docked at
    	if (player.ship.dockedStation.isMainStation == false) return;
    
    	var found = false;
    	if (this._activeMissionID != 0) {
    		for (var i = 0; i < this._disruptInstalled.length; i++) {
    			if (this._disruptInstalled[i].originalMissionID === this._activeMissionID && this._disruptInstalled[i].fined === false) {
    				this._disruptInstalled[i].fined = true;
    				found = true;
    				break;
    			}
    		}
    	}
    	// the player has already be fined at this station for this offence
    	if (found === false) return;
    
    	var fine = 10000;
    	if (player.credits < 10000) fine = Math.floor(player.credits);
    
    	player.credit -= fine;
    	player.ship.setBounty(player.bounty + 100, "galcop station interference");
    
    	var curChoices = {};
    	curChoices["99_EXIT"] = "Press space commander";
    
    	mission.runScreen({
    		screenID: "oolite-gcm-disruptcaught-summary",
    		title: "Security Breach",
    		overlay: {
    			name: "gcm-warning.png",
    			height: 546
    		},
    		message: expandDescription("[gcm_disrupt_caught]", {
    			type: (this._typeToInstall === 63 ? "commodity market systems" : "ship maintenance systems"),
    			fine: (fine > 0 ? "fined " + formatCredits(fine, false, true) + " and " : "")
    		}),
    		exitScreen: "GUI_SCREEN_STATUS"
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$createDataStreamOption = function $createDataStreamOption() {
    	var bcc = worldScripts.BroadcastCommsMFD;
    	if (bcc.$checkMessageExists("gcm_disrupt_station") === false) {
    		bcc.$createMessage({
    			messageName: "gcm_disrupt_station",
    			callbackFunction: this.$transmitDataStream.bind(this),
    			displayText: "<Transmit data stream>",
    			messageText: "",
    			ship: system.mainStation,
    			transmissionType: "target",
    			delayCallback: 1,
    			deleteOnTransmit: true,
    			hideOnConditionRed: true
    		});
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$transmitDataStream = function $transmitDataStream() {
    	// check if we're under docking clearance
    	// if not, reset the datastream option
    	if (this._playerIsDocking === false) {
    		player.consoleMessage("Station has not issued docking clearance - unable to start data stream")
    		this._dataStreamCounter = 0;
    		this.$createDataStreamOption();
    		return;
    	}
    	var p = player.ship;
    	// is the station still targetted?
    	if (p.target !== system.mainStation) {
    		player.consoleMessage("Main station no longer targeted - data stream interrupted");
    		this._dataStreamCounter = 0;
    		this.$createDataStreamOption();
    		return;
    	}
    	// are we out of range?
    	if (p.position.distanceTo(system.mainStation) > p.scannerRange) {
    		player.consoleMessage("Main station out of range - data stream interrupted");
    		this._dataStreamCounter = 0;
    		this.$createDataStreamOption();
    		return;
    	}
    	// check that the player is not in range of police ships
    	var chk = p.checkScanner(true);
    	for (var i = 0; i < chk.length; i++) {
    		if (chk[i].isPolice && p.position.distanceTo(chk[i]) <= (p.scannerRange * 0.6)) {
    			var item = worldScripts.BulletinBoardSystem.$getItem(this._mission6364MissionID);
    
    			var fine = 5000;
    			if (player.credits < 5000) fine = Math.floor(player.credits);
    
    			player.credit -= fine;
    			p.setBounty(player.bounty + 50, "galcop station interference");
    
    			item.data.destroyedQuantity += 1;
    			var msg = expandDescription("[gcm_datastream_detected_police]");
    			chk[i].commsMessage(msg, p);
    			return;
    		}
    	}
    	this._dataStreamCounter += 1;
    	if (this._dataStreamCounter === 1) {
    		player.consoleMessage("Starting data stream transmission.", 3);
    	}
    	player.consoleMessage("Data stream " + ((this._dataStreamCounter / 20) * 100).toFixed(0) + "\% complete.", 2);
    	if (this._dataStreamCounter === 20) {
    		player.consoleMessage("Data stream transmission complete.", 3);
    		var bb = worldScripts.BulletinBoardSystem;
    		var item = bb.$getItem(this._mission6364MissionID);
    		item.data.stage += 1;
    		item.data.quantity += 1;
    		bb.$updateBBMissionPercentage(item.ID, item.data.quantity / item.data.targetQuantity);
    
    		worldScripts.GalCopBB_Missions.$logMissionData(item.ID);
    		player.consoleMessage(expandDescription("[goal_updated]"));
    		// set the abort time for the software install
    		this._abortTime = clock.adjustedSeconds + 30;
    		this._cancelTimer = new Timer(this, this.$cancelUpload, 1, 1);
    		return;
    	}
    	// restart the timer (total time to 100% should be 100 seconds - docking clearance lasts for 120)
    	this._dataStreamTimer = new Timer(this, this.$transmitDataStream, 5, 0);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$cancelUpload = function $cancelUpload() {
    	if (this._abortTime > clock.adjustedSeconds) return;
    
    	system.mainStation.setInterface(this.name, null);
    	player.consoleMessage("Software upload time exceeded.")
    	var item = worldScripts.BulletinBoardSystem.$getItem(this._mission6364MissionID);
    	item.data.quantity = 0;
    	item.data.stage = 0;
    	this._cancelTimer.stop();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptedMission = function $acceptedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if (this._debug) log(this.name, "accepted mission id = " + missID);
    
    	if (!item) {
    		log(this.name, "!!ERROR: BB returned null value from $getItem on mission ID " + missID);
    		return;
    	}
    	gcm.$updateLastMissionDate(item.source, item.data.missionType);
    	gcm.$updateGeneralSettings(item);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$completedMission = function $completedMission(missID) {
    	var p = player.ship;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// update mission history
    	gcm.$updateSuccessHistoryReputation(item);
    
    	// *** type 65 - acquiring galcop station sec software
    	if (item.data.missionType === 65) {
    		if (p.equipmentStatus("EQ_GCM_SOFTWARE") === "EQUIPMENT_OK") {
    			p.removeEquipment("EQ_GCM_SOFTWARE");
    			var sbm = worldScripts.Smugglers_BlackMarket;
    			if (sbm) sbm.$removeSaleItem("EQ_GCM_SOFTWARE:" + missID);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$confirmCompleted = function $confirmCompleted(missID) {
    	var p = player.ship;
    	var result = "";
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (item) {
    		// *** type 65 - acquire galcop station sec software
    		if (item.data.missionType === 65) {
    			var chk = p.equipmentStatus("EQ_GCM_SOFTWARE");
    			if (chk !== "EQUIPMENT_OK") {
    				result += (result === "" ? "" : "\n") + "The software package is not on board your ship.";
    			}
    		}
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$terminateMission = function $terminateMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// adjust reputation only when the terminatePenalty flag is set to true 
    	if (item.data.terminatePenalty === true) {
    		// update mission history
    		gcm.$updateFailedHistoryReputation(item);
    	}
    
    	// *** type 65 - acquiring galcop station security software
    	if (item.data.missionType === 65) {
    		// if the player terminates the mission after they have the software package, it can only mean the player is going to sell it
    		if (item.data.quantity === 3) {
    			var eqsts = player.ship.equipmentStatus("EQ_GCM_SOFTWARE");
    			if (eqsts === "EQUIPMENT_OK") {
    				player.ship.removeEquipment("EQ_GCM_SOFTWARE");
    				if (sbm) sbm.$removeSaleItem("EQ_GCM_SOFTWARE:" + missID);
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$failedMission = function $failedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var eq = "";
    
    	if (!sbm) {
    		// if there's no blackmarket option, just remove the equipment
    		this.$terminateMission(missID);
    		return;
    	}
    
    	var item = bb.$getItem(missID);
    	// update mission history
    	gcm.$updateFailedHistoryReputation(item);
    
    	if (item.data.missionType === 65) {
    		// if the player terminates the mission after they have the software package, it can only mean the player is going to sell it
    		if (item.data.quantity === 3) {
    			var eqsts = player.ship.equipmentStatus("EQ_GCM_SOFTWARE");
    			if (eqsts === "EQUIPMENT_OK") eq = "EQ_GCM_SOFTWARE";
    		}
    	}
    
    	if (eq != "") gcm._equipmentFromFailedMissions.push({
    		missionType: item.data.missionType,
    		source: item.source,
    		equip: eq,
    		date: clock.adjustedSeconds
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateManifestEntry = function $updateManifestEntry(missID) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	gcm.$updateManifestEntry(missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
    	return worldScripts.GalCopBB_Missions.$missionAvailability(missID, missType, origSysID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 63 - disrupt commodities, 64 - disrupt ship maintenance
    this.$missionType6364_Values = function $missionType6364_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = 2;
    	result["price"] = parseInt((parseInt(Math.random() * 500) + 500) / 10) * 10 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price / 4);
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 65 - acquire security software
    this.$missionType65_Values = function $missionType65_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = 3;
    	result["price"] = parseInt((parseInt(Math.random() * 500) + 500) / 10) * 10 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500) // plus a possible bonus price, based on player score 
    		+
    		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 20); // plus a distance bonus
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price / 2);
    	return result;
    }
    Scripts/galcopbb_warzone.js
    "use strict";
    this.name = "GalCopBB_Warzone";
    this.author = "phkb";
    this.copyright = "2018 phkb";
    this.description = "Controls all civil warzone missions (90-99)";
    this.license = "CC BY-NC-SA 4.0";
    
    /*
    	*90 - evacuate refugees
    		>> have option of taking them to a anarchy/feudal system and selling them as slaves?
        *91 - transfer injured
    	92 - import weapons
    		limit to non-galcop stations? link to smugglers? make it a smuggling mission?
    
    	define elements of civil war, so they can be referenced in text (eg "Free Raxxla Party", "Destroy All Thargoids Forever Group" )
    */
    
    this._warSystems = []; // systems where there is civil war
    this._galJump = false;
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function () {
    	this._warSystems = this.$findWarSystems();
    
    	var gcm = worldScripts.GalCopBB_Missions;
    	// add these mission types into the main control
    	var list = [90, 91];
    	gcm._availableMissionTypes = gcm._availableMissionTypes.concat(list);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerEnteredNewGalaxy = function (galaxyNumber) {
    	this._warSystems = this.$findWarSystems();
    	this._galJump = true;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillExitWitchspace = function () {
    	if (this._galJump === true) {
    		this._galJump = false;
    		this._warSystems = this.$findWarSystems();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillDockWithStation = function (station) {
    	// check for other mission types being completed
    	if (station.isMainStation) {
    		var gcm = worldScripts.GalCopBB_Missions;
    		var bb = worldScripts.BulletinBoardSystem;
    		var list = gcm.$getListOfMissions(true, [90, 91]);
    		for (var i = 0; i < list.length; i++) {
    			if (list[i].data.missionType === 90 && list[i].destination === system.ID) {
    				if (player.ship.equipmentStatus("EQ_GCM_PASSENGER_TRANSPORT") === "EQUIPMENT_OK" && list[i].expiry >= clock.adjustedSeconds) {
    					list[i].arrivalReportText = expandDescription("[missionType90_arrivalReport_complete]", {
    						payment: formatCredits(list[i].payment, false, true)
    					});
    				} else {
    					list[i].arrivalReportText = expandDescription("[missionType90_arrivalReport_late]");
    				}
    
    				list[i].data.quantity = 1;
    				bb.$updateBBMissionPercentage(list[i].ID, 1);
    				gcm.$logMissionData(list[i].ID);
    			}
    			if (list[i].data.missionType === 91 && list[i].destination === system.ID) {
    				if (player.ship.equipmentStatus("EQ_GCM_PATIENT_TRANSPORT") === "EQUIPMENT_OK" && list[i].expiry >= clock.adjustedSeconds) {
    					list[i].arrivalReportText = expandDescription("[missionType91_arrivalReport_complete]", {
    						payment: formatCredits(list[i].payment, false, true)
    					});
    				} else {
    					list[i].arrivalReportText = expandDescription("[missionType91_arrivalReport_late]");
    				}
    
    				list[i].data.quantity = 1;
    				bb.$updateBBMissionPercentage(list[i].ID, 1);
    				gcm.$logMissionData(list[i].ID);
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptedMission = function $acceptedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if (this._debug) log(this.name, "accepted mission id = " + missID);
    
    	if (!item) {
    		log(this.name, "!!ERROR: BB returned null value from $getItem on mission ID " + missID);
    		return;
    	}
    	gcm.$updateLastMissionDate(item.source, item.data.missionType);
    
    	// give player passenger transport for mission type 90
    	if (item.data.missionType === 90) {
    		var eq = EquipmentInfo.infoForKey("EQ_GCM_PASSENGER_TRANSPORT");
    		if (player.ship.cargoSpaceAvailable < eq.requiredCargoSpace) this.$freeCargoSpace(eq.requiredCargoSpace, "");
    		player.ship.awardEquipment("EQ_GCM_PASSENGER_TRANSPORT");
    	}
    	
    	// give player patient transport for mission type 91
    	if (item.data.missionType === 91) {
    		var eq = EquipmentInfo.infoForKey("EQ_GCM_PATIENT_TRANSPORT");
    		if (player.ship.cargoSpaceAvailable < eq.requiredCargoSpace) this.$freeCargoSpace(eq.requiredCargoSpace, "");
    		player.ship.awardEquipment("EQ_GCM_PATIENT_TRANSPORT");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$completedMission = function $completedMission(missID) {
    	var p = player.ship;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if ([90, 91].indexOf(item.data.missionType) >= 0) {
    		// if the player has a bounty, doing these missions will reduce it
    		if (player.ship.bounty > 0) {
    			for (var i = 0; i < 5; i++) {
    				if (player.ship.bounty > 0) player.ship.setBounty(player.ship.bounty - 1, "community service");
    			}
    			player.ship.consoleMessage("Your bounty has been reduced.");
    		}
    	}
    
    	// update mission history
    	gcm.$addMissionHistory(item.source, item.data.missionType, 1, 0);
    
    	gcm.$checkForFollowupMission(item);
    
    	// adjust reputation with entities
    	var rep = worldScripts.GalCopBB_Reputation;
    	if (rep._disabled === false) {
    		rep.$adjustReputationSuccess(item.data.missionType, item.source, item.destination, item.percentComplete);
    	}
    
    	// *** type 90 - passenger transport (shouldn't need this, but just in case)
    	if (item.data.missionType === 90) {
    		p.removeEquipment("EQ_GCM_PASSENGER_TRANSPORT");
    	}
    
    	// *** type 91 - patient transport (shouldn't need this, but just in case)
    	if (item.data.missionType === 91) {
    		p.removeEquipment("EQ_GCM_PATIENT_TRANSPORT");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$confirmCompleted = function $confirmCompleted(missID) {
    	var p = player.ship;
    	var result = "";
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	if (item) {
    		// *** type 91 - patient transport
    		if (item.data.missionType === 91) {
    			var chk = p.equipmentStatus("EQ_GCM_PATIENT_TRANSPORT");
    			if (chk !== "EQUIPMENT_OK" && chk !== "EQUIPMENT_DAMAGED") {
    				result += (result === "" ? "" : "\n") + "The emergency patient transport unit is not on board your ship.";
    			}
    			if (chk === "EQUIPMENT_DAMAGED") {
    				result += (result === "" ? "" : "\n") + "The emergency patient transport unit has been damaged. You can't determine the condition of the patients inside.";
    			}
    		}
    	}
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$terminateMission = function $terminateMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// adjust reputation only when the terminatePenalty flag is set to true 
    	if (item.data.terminatePenalty === true) {
    		// update mission history
    		gcm.$updateFailedHistoryReputation(item);
    	}
    	// *** type 90 - patient transport
    	if (item.data.missionType === 90) {
    		player.ship.removeEquipment("EQ_GCM_PASSENGER_TRANSPORT");
    	}
    	// *** type 91 - patient transport
    	if (item.data.missionType === 91) {
    		player.ship.removeEquipment("EQ_GCM_PATIENT_TRANSPORT");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$failedMission = function $failedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var eq = "";
    
    	if (!sbm) {
    		// if there's no blackmarket option, just remove the equipment
    		this.$terminateMission(missID);
    		return;
    	}
    
    	var item = bb.$getItem(missID);
    	// update mission history
    	gcm.$updateFailedHistoryReputation(item);
    
    	if (item.data.missionType === 90) {
    		// we'll just remove this one
    		player.ship.removeEquipment("EQ_GCM_PASSENGER_TRANSPORT");
    		eq = "";
    	}
    
    	if (item.data.missionType === 91) {
    		// we'll just remove this one
    		player.ship.removeEquipment("EQ_GCM_PATIENT_TRANSPORT");
    		eq = "";
    	}
    
    	if (eq != "") gcm._equipmentFromFailedMissions.push({
    		missionType: item.data.missionType,
    		source: item.source,
    		equip: eq,
    		date: clock.adjustedSeconds
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateManifestEntry = function $updateManifestEntry(missID) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	gcm.$updateManifestEntry(missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
    	return worldScripts.GalCopBB_Missions.$missionAvailability(missID, missType, origSysID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 90 - refugee transport
    this.$missionType90_Values = function $missionType90_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var missValues = worldScripts.GalCopBB_CoreMissionValues;
        var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 50) + 75) / 10) * 10 + (7 - destSysInfo.government) * 20 + missValues.$calcDistanceBonus(routeDistance, 10);
    	result["expiry"] = clock.adjustedSeconds + routeTime + 10000; 
    	result["penalty"] = result.price / 2;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 91 - emergency patient transport
    this.$missionType91_Values = function $missionType91_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var missValues = worldScripts.GalCopBB_CoreMissionValues;
        var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 50) + 75) / 10) * 10 + (7 - destSysInfo.government) * 20 + missValues.$calcDistanceBonus(routeDistance, 10);
    	result["expiry"] = clock.adjustedSeconds + routeTime + 1600; // we want the time to be tight for the patient transport -- transit time + 10 mins
    	result["penalty"] = result.price / 2;
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns a list of sysInfos where civil war is present
    this.$findWarSystems = function $findWarSystems() {
    	return SystemInfo.filteredSystems(this, function (other) {
    		return (other.description.indexOf("civil war") >= 0);
    	});
    }
    
    Scripts/galcopbb_wbsa.js
    "use strict";
    this.name = "GalCopBB_WBSA";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Control of the Witchpoint Beacon Security Access device";
    this.license = "CC BY-NC-SA 4.0";
    
    /*
    *60 - Hacking witchpoints
    	System authorities in system X are concerned about the flow of pirate traffic through their system. They want to hack into the witchpoint beacons of all systems within a 7LY range, that will enable them to monitor traffic flows into their system, outside of GalCop's purview.
    		- will require getting within 200m of witchpoint marker, targeting the marker and initiating the special equipment (prime, activate)
    
    *61 - Restoring witchpoints
    	GalCop has become aware of an illegal data stream emanating from witchpoint markers around system X. You need to go to each of the systems listed, and clean the illegal code from the units. 
    		- will require getting within 200m of witchpoint marker, targeting the marker and initiating the special equipment (prime, activate)
    		- will only be available once mission 60 has been performed at a system
    */
    
    this._debug = false;
    this._uploadCount = 0; // percentage of upload completed
    this._uploadTimer = null; // timer to perform upload
    this._mode = 0; // 0 = hijack mode, 1 = reset mode
    this._seenByPolice = false;
    this._seenByHunters = false;
    this._wpHacks = []; // list of systems who have had their witchpoint beacon hacked (missions 60-61)
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function () {
    	var gcm = worldScripts.GalCopBB_Missions;
    	// add these mission types into the main control
    	var list = [60, 61];
    	gcm._availableMissionTypes = gcm._availableMissionTypes.concat(list);
    	this._debug = gcm._debug;
    	// exclude some equipment items from being sold/stored by ShipConfig
    	var sc = worldScripts.ShipConfiguration_Core;
    	if (sc) {
    		sc._noSell.push("EQ_GCM_WBSA");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function () {
    	if (missionVariables.GalCopBBMissions_HackedWP) {
    		this._wpHacks = JSON.parse(missionVariables.GalCopBBMissions_HackedWP);
    		delete missionVariables.GalCopBBMissions_HackedWP;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillSaveGame = function () {
    	// save any data we currently have
    	if (this._wpHacks.length > 0) missionVariables.GalCopBBMissions_HackedWP = JSON.stringify(this._wpHacks);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillEnterWitchspace = function (cause, destination) {
    	// once a witchpoint has been hacked, there is a high chance of a misjump (but not from interstellar space)
    	if (this._wpHacks.length > 0 && cause !== "galactic jump") {
    		for (var i = 0; i < this._wpHacks.length; i++) {
    			if (this._wpHacks[i] === destination && system.ID !== -1 && Math.random() > 0.7) player.ship.scriptedMisjump = true;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerEnteredNewGalaxy = function (galNumber) {
    	this._wpHacks.length = 0;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillExitWitchspace = function () {
    	this._seenByHunters = false;
    	this._seenByPolice = false;
    	if (this._uploadTimer && this._uploadTimer.isRunning) {
    		this._uploadTimer.stop();
    		this._uploadTimer = null;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.mode = function () {
    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.activated = function () {
    	var wbsa = worldScripts.GalCopBB_WBSA;
    	if (wbsa._uploadTimer && wbsa._uploadTimer.isRunning) {
    		wbsa._uploadTimer.stop();
    		delete wbsa._uploadTimer;
    		player.consoleMessage("Security Access upload stopped.");
    		worldScripts.GalCopBB_Missions.$playSound("stop");
    		return;
    	}
    
    	var p = player.ship;
    
    	// is the equipment OK?
    	if (p.equipmentStatus("EQ_GCM_WBSA") !== "EQUIPMENT_OK") {
    		worldScripts.GalCopBB_Missions.$playSound("stop");
    		return;
    	}
    	// are any of the stop conditions met?
    	if (wbsa.$checkConstraints() === false) {
    		worldScripts.GalCopBB_Missions.$playSound("stop");
    		return;
    	}
    
    	var mode = wbsa.$checkWBSAMode();
    	// if we try to use the device in a system not contracted to
    	if (mode === -1) {
    		player.consoleMessage("Unable to establish data link. Security codes invalid.");
    		worldScripts.GalCopBB_Missions.$playSound("stop");
    		return;
    	}
    
    	player.consoleMessage("Data link established. Commencing upload.");
    	worldScripts.GalCopBB_Missions.$playSound("activate");
    
    	// have to start again each time you stop the upload
    	wbsa._uploadCount = 0;
    	if (wbsa._uploadTimer && wbsa._uploadTimer.isRunning) wbsa._uploadTimer.stop();
    	wbsa._uploadTimer = new Timer(wbsa, wbsa.$scanProcess, 3, 3);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$scanProcess = function $scanProcess() {
    
    	var wbsa = worldScripts.GalCopBB_WBSA;
    
    	if (wbsa.$checkConstraints() === false) {
    		wbsa._uploadTimer.stop();
    		wbsa._uploadTimer = null;
    		player.consoleMessage("Security Access upload stopped.");
    		return;
    	}
    
    	var p = player.ship;
    	var msg = "";
    
    	// is there any police vessel in range?
    	var police = this.$findLawVessels(p);
    	if (this._mode === 0 && police.length > 0 && this._seenByPolice === false) {
    		this._seenByPolice = true;
    		msg = expandDescription("[gcm_wbsa_detected_police]");
    		penalty = (Math.random() * 20) + 10;
    		p.setBounty(player.bounty + penalty, "seen by police");
    		//this.$sendEmail(penalty);
    		police[0].commsMessage(msg, p);
    		worldScripts.GalCopBB_WBSA.$witchpointHackMissionFailed();
    	}
    
    	// are there any hunters in range? 
    	var hunters = this.$findHunters(p);
    	var mode = wbsa.$checkWBSAMode();
    	if (mode === 0 && hunters.length > 0 && wbsa._seenByHunters === false && wbsa._seenByPolice === false) {
    		wbsa._seenByHunters = true;
    		msg = expandDescription("[gcm_wbsa_detected_hunter]");
    		hunters[0].commsMessage(msg, p);
    		for (var i = 0; i < hunters.length; i++) {
    			hunters[i].target = p;
    			hunters[i].performAttack();
    		}
    	}
    
    	wbsa._uploadCount += 2;
    	player.consoleMessage("Upload " + wbsa._uploadCount + "\% complete.", 2);
    
    	if (wbsa._uploadCount >= 100) {
    		wbsa._uploadTimer.stop();
    		wbsa._uploadTimer = null;
    		if (mode === 0) wbsa.$witchpointHackMissionComplete();
    		if (mode === 1) wbsa.$witchpointRestoreMissionComplete();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$checkConstraints = function $checkConstraints() {
    	var p = player.ship;
    
    	// do we have a witchpoint beacon targeted?
    	if (p.target.hasRole("buoy-witchpoint") == false) {
    		player.consoleMessage("Security Access device will only work when targeting the witchpoint beacon.");
    		return false;
    	}
    	if (p.isCloaked) {
    		player.consoleMessage("Security Access device will not work when cloaked.");
    		return false;
    	}
    	if (p.position.distanceTo(p.target) > (p.scannerRange * 0.02)) {
    		//log(this.name, "dist = " + p.position.distanceTo(p.target));
    		player.consoleMessage("Out of range to use Security Access device.");
    		return false;
    	}
    
    	return true;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$witchpointHackMissionComplete = function $witchpointHackMissionComplete() {
    	// add the system to the list of hacked systems
    	// this can be done regardless of whether the player was spotted by police or not.
    	var gcm = worldScripts.GalCopBB_Missions;
    	this._wpHacks.push(system.ID);
    	var bb = worldScripts.BulletinBoardSystem;
    	var list = gcm.$getListOfMissions(true, 60);
    	for (var i = 0; i < list.length; i++) {
    		if (system.ID === list[i].destination &&
    			list[i].expiry > clock.adjustedSeconds &&
    			list[i].data.quantity < list[i].data.targetQuantity) {
    
    			list[i].data.quantity = 1;
    			bb.$updateBBMissionPercentage(list[i].ID, 1);
    
    			gcm.$logMissionData(list[i].ID);
    			player.consoleMessage(expandDescription("[goal_updated]"));
    			break;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$witchpointHackMissionFailed = function $witchpointHackMissionFailed() {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var list = gcm.$getListOfMissions(true, 60);
    	for (var i = 0; i < list.length; i++) {
    		if (system.ID === list[i].destination) {
    			// reduce the target quantity, so that the mission complete criteria can never be met, forcing the player to terminate the mission instead.
    			list[i].data.targetQuantity = 0;
    			break;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$witchpointRestoreMissionComplete = function $witchpointRestoreMissionComplete() {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var bb = worldScripts.BulletinBoardSystem;
    	var list = gcm.$getListOfMissions(true, 61);
    	for (var i = 0; i < list.length; i++) {
    		if (system.ID === list[i].destination &&
    			list[i].expiry > clock.adjustedSeconds &&
    			list[i].data.quantity < list[i].data.targetQuantity) {
    
    			list[i].data.quantity = 1;
    			bb.$updateBBMissionPercentage(list[i].ID, 1);
    
    			// remove the system from the list of hacks
    			this._wpHacks.splice(this._wpHacks.indexOf(system.ID), 1);
    
    			gcm.$logMissionData(list[i].ID);
    			player.consoleMessage(expandDescription("[goal_updated]"));
    			break;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$checkWBSAMode = function $checkWBSAMode() {
    	var gcm = worldScripts.GalCopBB_Missions;
    	var id = gcm.$getActiveMissionIDByType(60);
    	var type = 0;
    	if (id === -1) {
    		id = gcm.$getActiveMissionIDByType(61);
    		if (id != -1) {
    			type = 1;
    		} else {
    			type = -1;
    		}
    	}
    	return type;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // find all police or main stations within 15500km of of ship
    this.$findLawVessels = function $findLawVessels(npc) {
    	function _ships(entity) {
    		return entity.isShip && entity.isPolice && !entity.isPlayer && !entity.isStation && !entity.isMainStation;
    	}
    	return system.filteredEntities(this, _ships, npc, 21500);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$findHunters = function $findHunters(npc) {
    	function _hunters(entity) {
    		return entity.isShip && Ship.roleIsInCategory(entity.primaryRole, "oolite-hunter");
    	}
    	return system.filteredEntities(this, _hunters, npc, 15000);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptedMission = function $acceptedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	if (this._debug) log(this.name, "accepted mission id = " + missID);
    
    	if (!item) {
    		log(this.name, "!!ERROR: BB returned null value from $getItem on mission ID " + missID);
    		return;
    	}
    	gcm.$updateLastMissionDate(item.source, item.data.missionType);
    	gcm.$updateGeneralSettings(item);
    
    	// give player the witchpoint marker security access device
    	if (item.data.missionType === 60 || item.data.missionType === 61) {
    		player.ship.awardEquipment("EQ_GCM_WBSA");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$completedMission = function $completedMission(missID) {
    	var p = player.ship;
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var gcm = worldScripts.GalCopBB_Missions;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    
    	// update mission history
    	gcm.$updateSuccessHistoryReputation(item);
    
    	// *** type 60 & 61 - hacking/repairing witchpoint markers
    	if (item.data.missionType === 60 || item.data.missionType === 61) {
    		if (p.equipmentStatus("EQ_GCM_WBSA") === "EQUIPMENT_OK" || p.equipmentStatus("EQ_GCM_WBSA") === "EQUIPMENT_DAMAGED") {
    			p.removeEquipment("EQ_GCM_WBSA");
    			if (sbm) sbm.$removeSaleItem("EQ_GCM_WBSA:" + missID);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$confirmCompleted = function $confirmCompleted(missID) {
    	return "";
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$terminateMission = function $terminateMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var item = bb.$getItem(missID);
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    
    	// adjust reputation only when the terminatePenalty flag is set to true 
    	if (item.data.terminatePenalty === true) {
    		// update mission history
    		gcm.$updateFailedHistoryReputation(item);
    	}
    
    	// *** type 60 & 61 - hacking/repairing witchpoint markers
    	if (item.data.missionType === 60 || item.data.missionType === 61) {
    		var eqsts = player.ship.equipmentStatus("EQ_GCM_WBSA");
    		if (eqsts === "EQUIPMENT_OK" || eqsts === "EQUIPMENT_DAMAGED") {
    			player.ship.removeEquipment("EQ_GCM_WBSA");
    			if (sbm) sbm.$removeSaleItem("EQ_GCM_WBSA:" + missID);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$failedMission = function $failedMission(missID) {
    	var bb = worldScripts.BulletinBoardSystem;
    	var sbm = worldScripts.Smugglers_BlackMarket;
    	var gcm = worldScripts.GalCopBB_Missions;
    	var eq = "";
    
    	if (!sbm) {
    		// if there's no blackmarket option, just remove the equipment
    		this.$terminateMission(missID);
    		return;
    	}
    
    	var item = bb.$getItem(missID);
    	// update mission history
    	gcm.$updateFailedHistoryReputation(item);
    
    	if (item.data.missionType === 60 || item.data.missionType === 61) {
    		var eqsts = player.ship.equipmentStatus("EQ_GCM_WBSA");
    		if (eqsts === "EQUIPMENT_OK" || eqsts === "EQUIPMENT_DAMAGED") eq = "EQ_GCM_WBSA";
    	}
    
    	if (eq != "") gcm._equipmentFromFailedMissions.push({
    		missionType: item.data.missionType,
    		source: item.source,
    		equip: eq,
    		date: clock.adjustedSeconds
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$updateManifestEntry = function $updateManifestEntry(missID) {
    	var gcm = worldScripts.GalCopBB_Missions;
    	gcm.$updateManifestEntry(missID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
    	return worldScripts.GalCopBB_Missions.$missionAvailability(missID, missType, origSysID);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 60 - hack witchpoint beacon
    this.$missionType60_Values = function $missionType60_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 500) + 1000) / 10) * 10 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price * 2); // failure is costly in this case
    	return result;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 61 - restore witchpoint beacon
    this.$missionType61_Values = function $missionType61_Values(workTime, routeTime, routeDistance, destSysInfo) {
    	var result = {};
    	result["quantity"] = 1;
    	result["price"] = parseInt((parseInt(Math.random() * 500) + 200) / 10) * 10 +
    		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(200); // plus a possible bonus price, based on player score 
    	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
    	result["penalty"] = parseInt(result.price / 4);
    	return result;
    }