Back to Index Page generated: May 8, 2024, 6:16:03 AM

Expansion Bounty System

Content

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description (WIP) This OXP seeks to add depth and meaning to the bounty system by making crimes persistent (removing the old mechanic of having bounties slowly disappear over time). Bounties might persist, but they are not always visible in every location. A Warrant Scanner can be used by the player or NPC's to uncover bounties registered in other systems. (WIP) This OXP seeks to add depth and meaning to the bounty system by making crimes persistent (removing the old mechanic of having bounties slowly disappear over time). Bounties might persist, but they are not always visible in every location. A Warrant Scanner can be used by the player or NPC's to uncover bounties registered in other systems.
Identifier oolite.oxp.phkb.BountySystem oolite.oxp.phkb.BountySystem
Title Bounty System Bounty System
Category Mechanics Mechanics
Author phkb phkb
Version 0.13 0.13
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Information URL http://wiki.alioth.net/index.php/Bounty_System n/a
Download URL https://wiki.alioth.net/img_auth.php/5/5d/BountySystem.oxz n/a
License CC-BY-NC-SA 4.0 CC-BY-NC-SA 4.0
File Size n/a
Upload date 1627914066

Documentation

Also read http://wiki.alioth.net/index.php/Bounty%20System

readme.txt

Bounty System
By Nick Rogers

Special thanks to Wildeblood for all his suggestions and input. Hopefully this OXP will restore something of his lost Criminal Record OXP! And yes, I have backups!
Thanks also to rustem, Cody, smivs, Day, Imaginos, and UK_Eliter for all their encouragement and input.

Overview
========
The bounty system in the core game of Oolite is somewhat primitive, a product of its 8-bit roots. If you commit a crime, regardless of the seriousness of that crime, your bounty is halved with each witchspace jump. So you can shoot down a flotilla of police ships in one system, but 8 to 10 jumps later you're clean again. Also, if you want to quickly erase your bounty you can purchase and use an escape pod, or do a galactic jump.

This OXP aims to add depth and meaning to the bounty system by making your crimes persistent. So, if you commit a crime in system A, and then come back to system A later, your bounty will be remembered. It will not degrade over time, nor will it vanish by using an escape pod or a galactic jump, or when buying a new ship.

Also, different crimes have a varying degree of severity, meaning there is a distinction between smuggling illegal goods to murdering a pilot by destroying their ship. The following table describes the different severities and levels of applicability:

Level                Example of crime               Applicability
---------------------------------------------------------------------------
Severity 1 (Minor)   Smuggling illegal goods        local systems only (within 5ly)
Severity 2 (Serious) Shooting main station          Across the current chart
Severity 3 (Major)   Destroying another ship        Across all charts

Local systems only: This means the crime only applies to systems within 5LY of the system where the crime was committed. So if you are caught smuggling goods in system A, then jump to system B (which is less than 5ly from system A), your crime is still applicable. Jump to system C, more than 5 LY from system A, then your crime is no longer applicable.

Across the current chart: This means the crime will apply to all systems in the current chart. So if you shoot the main station in system A, your crime is applicable in system B and system C and all systems in the current chart. But if you use a galactic jump and go to the next chart, those crimes will not be applicable.

Across all charts: This means the crime will apply to all systems in all charts. No matter where you go, your crime will be applicable and you will potentially be labelled a "Fugitive".

The F7 System Data screen can help you in assessing your potential bounty in a given system. When you select a system on the F6 screen and then visit the F7 System Data screen the computer will scan your warrant list and determine how the system will likely view your presence. If you have committed crimes in that system one of the following things will be displayed:

	This system considers you [a criminal/an] offender: This text will be displayed if your overall bounty in the system is less than 50cr.
	This system considers you a [criminal] fugitive: This text will be displayed if your overall bounty in the system in greater than or equal to 50cr.

The "criminal" part of the text will be added if you committed crimes in that system in particular. If the "criminal" element is not present you haven't committed crimes in that system, but your crimes in other systems have become known and you will be considered an offender or fugitive if you visit that system.

GalCop Security Office
======================
All GalCop-aligned stations have either a security office (when at the main station), or a security kiosk (when at other GalCop-aligned stations), in which you can view any outstanding bounties you might have picked up in the current galactic sector, and when/where those crimes were committed. You can also view any outstanding you might have in other sectors. Through this interface you can view the details of each bounty, the crimes that were committed to generate the bounty and, if available, pay the fine to clear your record. The available of the payment option is dependent on the amount of time since the crime was committed, and the location the crime was committed. See Paying for fines below.

Warrant Scanner
===============
While your crimes might be applicable in multiple systems, that doesn't necessarily mean your crimes will be *visible* in all systems automatically. If you shoot at the main station in system A, and jump to system B, your status will initially be "Clean". This means that this system currently doesn't have a record of your crimes.

However, all police and most bounty hunters can be equipped with a "Warrant Scanner", a device that will scan your ship and perform a GalCop Criminal Database lookup based on the specifications of your ship and pilot registration. Should any criminal records be found they will become visible in this system. All GalCop police ships have this device installed and they will actively try to use it on all ships visible to them. Once your crimes have been revealed to system authorities, you will be wanted in that system whenever you visit.

You can purchase and install the Warrant Scanner yourself, and do the same thing on other ships. You must be within 15km of the target (police ships can scan from 20km), and when activated you must stay within that distance for 10 seconds (8 seconds for police) and keep the target locked for the scanner to work. The scanner won't work when your cloaking device is engaged, or if you're travelling at injector or torus speeds. If the target engages a cloak the scanning will stop.

The output of the scanner functions in two ways: MFD mode (which will use a spare MFD slot to display scanning messages), or non-MFD mode (which will output scanning messages to the message log). To switch between output modes, prime the equipment and press the "b" (mode) key. If MFD output mode is selected, but there are no MFD slots available when a scan starts, the Warrant Scanner will automatically switch to Non-MFD output mode.

Pilots will consider use of this scanner as hostile, and so their response may be hostile as well, particularly when not in the main station aegis.

Once you start a scan on a target, if you are forced to target another ship before the scan is completed, when you re-target the first ship the scan will automatically restart.

Further controls for the Warrant Scanner can be found in Library Config. The Warrant Scanner can be set to automatically scan offender and fugitive targets, or all targets. When the scanner is in automatic scan mode, scans will only start when the target is within 15km range. In order for the scan to detect offender or fugitive status, the Scanner Targeting Enhancement must be installed and operational, and weapons must be enabled. Non-combatant targets will not be scanned automatically when your ship's weapons are turned off.

Warrant Scanners can be purchased at TL7 systems, for 2570 Cr.

Paying For Fines
================
While bounties will not degrade over time, it is still possible to remove those fines from your record by paying a requisite fine. Fines can be paid in one of two places: the main stations of the system where the fine was committed, or at the GalCop HQ system in each sector. See below for the locations of GalCop HQ. You can only pay fines collected in other sectors at the GalCop HQ system in your current sector. If you choose to pay your fine at GalCop HQ, a 10% fine processing charge will be added to the total amount.

The amount of fine you pay will be based on the amount of bounty, the type of system where the offence was committed, and the amount of time since the offence was committed.

The calculation is:
For Anarchy, Feudal, Democracy and Corporate States
	100 * (Bounty Amount - (days since offence * severity degrade rate))
For other government types
	50 * (Bounty Amount - (days since offence * severity degrade rate))

Example 1: A bounty of 30 (severity 1), collected in Lave (a Dictatorship), 15 days ago:
	Fine amount = 50 * (30 - (15 * 0.8))	= 900 cr

Example 2: A bounty of 75 (severity 3), collected in Qube (a Corporate State), 90 days ago:
	Fine amount = 100 * (75 - (90 * 0.1))	= 6600 cr

The longer you leave the payment, the more the fine will reduce, but the more likely it is you will be discovered.

Depending on the severity of your offence, there will also be a delay before you will be able to pay. The following table lists the delay periods for each severity level, and how fast the fine will degrade over time:

Level        Delay period    Degrade rate
---------------------------------------------
Severity 1   1 day           0.8cr per day
Severity 2   10 days         0.5cr per day
Severity 3   30 days         0.1cr per day

Special note: If a police vessel flags your ship to pay fines upon docking, the fine you pay will be the full amount and will not be reduced by a degrading component. This will only happen if your bounty is small, however. If you have a large bounty, police ships are more likely to attack you.

GalCop HQ systems
Sector  Planet
--------------------------------------------------------
1       Lave (ID 7, TL 5, Dictatorship)
2       Diti (ID 33, TL 9, Democracy)
3       Rigebi (ID 26, TL 12, Corporate State)
4       Beesed (ID 49, TL 9, Multi-Government)
5       Bisoge (ID 102, TL 12, Corporate State)
6       Celaan (ID 85, TL 11, Corporate State)
7       Ceraxete (ID 202, TL 6, Dictatorship)
8       Maarabi (ID 184, TL 5, Multi-Government)

Appeals process
===============
In the event that you believe a bounty has been incorrectly applied to you, it is possible to submit an appeal against the crime. While viewing the details of the crime from the GalCop Security Office screen, you can select the "Submit appeal against fine" option. This will ask you to confirm that an appeal be lodged. There is a cost involved in the appeal, and it may take quite some time to have the appeal processed, depending on how far away from the GalCop HQ system you are. The results of any appeals will be shown to you when you next dock at a GalCop station.

You can only lodge an appeal against a severity 1 crime up to 10 days after the event, 5 days for a severity 2 crime. Severity 3 crimes cannot be appealed against. 

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/

Shield image from https://www.iconfinder.com/icons/175318/shield_icon#size=512
Gavel image from http://www.freepik.com and http://www.flaticon.com, licensed by CC BY 3.0

With special thanks to Milo for the performance improvements code, updates to the NPC scan function, and for implementing the "no-repeat-bounty" code.

Discussion
==========
This OXP is discussed at this forum link: http://aegidian.org/bb/viewtopic.php?f=4&t=17861

Version History
===============
0.13
- Stopped switching NPC's active target to prevent possible conflicts in the AI routines.
- Added the Warrant Scanner MFD to HUD Selector.

0.12
- Reduced the delay period for severity 3 offences to 30 days, and severity 2 offences to 10 days.
- All defined offence types should now be passed through to the logging routine.

0.11
- Scanner was auto-scanning in manual scan mode.
- Bug fixes.

0.10
- Additional code changes for NPC scanning and interfacing with Broadcast Comms MFD, provided by Milo.

0.9
- Date applied to any offences is the date of the first offence (was previously the date the player left the system).
- Bug fixes for appeal processing.

0.8
- Warrant Scanner in auto-scan mode will now open MFD even if target is out of range.
- Performance improvements for the scanning process.
- If player is scanned in a system and bounties uncovered, and they are forced to deal with those bounties at the station, subsequent scans will not reapply bounty.
- Code refactoring.

0.7
- Fixed issue where missile attacks from the player would not ever be considered for possible illegality.

0.6
- Fixed issue with repairing a damaged passive-mode Warrant scanner.
- Fixed error when attempting to change the configuration of the Warrant scanner.
- Tweaked some descriptions in the equipment.plist file.
- Renamed some variables to prevent possible OXP conflicts.

0.5
- Changing the mode of the Warrant Scanner via Library to an automatic scan will remove the primable equipment.

0.4.4
- Warrant Scanner will switch back from non-MFD mode if an MFD slot becomes available.
- An update to the Most Wanted list will now be requested whenever the Most Wanted list is displayed.

0.4.3
- Improved compatibility with Fuel Injection Cruise Control OXP.

0.4.2
- When a scanned ship goes through a wormhole and has their bounty reset, you can now rescan the ship in the destination system to re-reveal their bounty.
- Corrected misspelled variable "galaxyNumber".
- General source code spelling corrections.

0.4.1
- Added the source ship which has performed a scan as a parameter into the $uncoverBounties function.

0.4.0
- The Warrant Scanner will now automatically continue scanning a ship that has only been partly scanned.
- The Warrant Scanner can now be set to automatically scan a ship as soon as it's targeted (controlled via Library).
- Eliminated need to keep array of scanned ships.
- Option to set course to a particular destination will be suppressed if that destination is on the player's currently plotted path.

0.3.4
- When setting a course to a target system, F7 screen will now display the new destination.
- Bug fixing Most Wanted interfaces.

0.3.3
- Added protection against NPC scan invalid conditions.
- Further tweaks to the Most Wanted interfaces.

0.3.2
- More improvements to the Most Wanted interfaces.
- Corrections to descriptions.plist.

0.3.1
- Further code changes to handle integration with GalCop Most Wanted OXP.
- Fixed issue where using an escape was still clearing any bounty.
- Bug fixes.
- Code refactoring.

0.3.0
- Added an appeals process, so the player can submit a request for minor (sev 1 or 2) crimes to be discarded.
- Further code changes to handle future OXP integration.
- Improvements to text layout of menu options containing distance and # jumps.
- Added the severity level to the fine details page.
- Removed debug message.

0.2.0
- Added number of jumps to various mission screens.
- Code changes to handle future OXP integration.
- Scans performed by police or bounty hunters will be passed on to all ships in range that have the same class, to prevent spamming.
- Cleaned up overlay image.
- Converted offence dates to real dates (eg "25 Jan 3145");
- Security office now available on all GalCop-aligned stations, but is called the "Security Kiosk". No payments can be paid there.
- Updated license.

0.1.14
- Added extra info to readme.txt
- Removed hard-coded scanner range value.
- Bug fixes.

0.1.13
- Better handling of situation where an OXP resets bounty to zero.
- Better handling of "seen by police" bounty changes.
- Fixed name reference bug when monkey patching NPC ships.

0.1.12
- Paying fines on transferred bounties was removing the original bounty, not just the transferred one.
- Better integration with the ANA.
- Police and bounty hunters may now decide not to perform a scan.

0.1.11
- Severity 1 crimes were being added multiple times if scanned by more than 1 police/bounty hunter.

0.1.10
- Medical ships are now excluded from getting a hidden bounty.
- Fixed issue with HUD not becoming visible again when launching while viewing the GalCop security office screens.

0.1.9
- Fixed issue where player destination was getting reset when a chart screen is exited by pressing a function key, rather than via the "Exit" command.
- Fixed Javascript error when player ship is destroyed.

0.1.8
- Fixed a minor issue where some bogus data could sometimes be captured and then acted on. It should now be dropped.

0.1.7
- Fixed Javascript bug "itm is not defined".

0.1.6
- Ships that have had bounties revealed will have their bounty reset when exiting a wormhole in another system, same as the player.
- Added Ship Dock Control integration code, so hidden bounties will be kept after docking and reapplied on launching.
- Added some protection against OXP's adding items to the possible offence list, and then the OXP is subsequently removed.
- Hopefully fixed issue with timers being garbage collected when still running.
- Simplified the data storage method for performance improvements.
- Changed "==" comparisons to "===" for performance improvements.

0.1.5
- Fixed error where same offence can be discovered multiple times for the player.

0.1.4
- Added code to make use of enhanced F7 screen features in Oolite 1.83/4.

0.1.3
- Removed debug messages.

0.1.2
- Added a console message confirmation whenever the course is set to a destination.
- Refined the process of adding "attacked police/innocent" ships. Hopefully accidental hits won't be added to the run-sheet any more.

0.1.1
- Bounties collected in Interstellar space will no longer be added to your overall bounty.

0.1.0
- Initial release.

Equipment

Name Visible Cost [deci-credits] Tech-Level
Warrant Scanner yes 25700 7+
Warrant Scanner yes 25700 7+
Police Warrant Scanner yes 836900 13+
Police Warrant Scanner yes 836900 13+
Remove Warrant Scanner no 3000 1+

Ships

This expansion declares no ships.

Models

This expansion declares no models.

Scripts

Path
Scripts/bountysystem_conditions.js
"use strict";
this.name        = "BountySystem_Conditions";
this.author      = "phkb";
this.copyright   = "2020 phkb";
this.description = "Condition script for determining when items are available";
this.licence     = "CC BY-NC-SA 4.0";

//-------------------------------------------------------------------------------------------------------------
this.allowAwardEquipment = function(equipment, ship, context) {
    if (context === "scripted") return true;
    var p = player.ship;
    switch (equipment) {
        case "EQ_WARRANT_SCANNER":
            if (p.equipmentStatus("EQ_WARRANT_SCANNER_PASSIVE") != "EQUIPMENT_UNAVAILABLE") return false;
            break;
        case "EQ_WARRANT_SCANNER_PASSIVE":
            if (p.equipmentStatus(equipment) === "EQUIPMENT_DAMAGED") return true;
            return false;
    }
    return true;
}
Scripts/bountysystem_core.js
"use strict";
this.name = "BountySystem_Core";
this.author = "phkb";
this.copyright = "2016 phkb";
this.description = "Core routines for New Bounty System.";
this.licence = "CC BY-NC-SA 4.0";

/*
	What this OXP attempts to do is to keep track of offences committed by the player. Based on the type of offence and where the offence was committed,
	different things will occur.

	For "severity 1"-classed offences, the bounty will be local systems only. That is, you will only have a bounty within 5LY of the system you committed the crime.
	For "severity 2"-classed offences, the bounty is chart-wide. That is, you will have a bounty no matter where you go in the chart, but they don't follow you to a new chart
	For "severity 3"-classed offences, the bounty is across all charts.

	Severity 1 crimes are invisible in the current system, but can be transferred if within a 5ly radius of the original offence when scanned with warrant scanner
	Severity 2 and 3 crimes are invisible in the current system (if they didn't originate there) but will be transferred if you are scanned with the warrant scanner.

	You can pay off fines at GalCopHQ or at the system where the crime was committed, but only after a certain time period.
	Paying for fines long distance will incur a 10% increase in fine

	TODO: check the order in which events fire: does shipAttackedOther occur before the bountyChange event?
	If so, we need to check if the player already has a bounty delta in play. If not, we need to skip adding the offence via shipAttackedOther
	If not, we need to send a flag to the shipAttacked other

	Ideas:
	- Statute of limitations on severity 1 crimes: 12 months and then they disappear
	- Appeals process? Give player opportunity to appeal a particular offence (eg if the police see you attacking an assassin and decide you must be the offender)
*/

this._disabled = false;
this._debug = false; // turns on/off debug messages in log file
this._severity1Transfer = true; // indicates whether severity 1 crimes can be transferred over distance

this._offences = []; // holds list of all players offences
this._galCopHQ = [7, 33, 26, 49, 102, 85, 202, 184]; // galcop hq locations in each sector
this._galCopHQFineSurcharge = 0.1; // increase to fine amount when paid at GalCop HQ (currently 10%)
this._lastOffenceDate = 0;
this._changing = false; // flag to indicate that a change to the players bounty is happening internally
this._holding = []; // holding array of offences incurred in the current system
this._holdingDate = 0; // date of first offence of the holding array
this._bountyDelta = 0; // how much the bounty has changed while in the current system
this._system = -1; // system ID where offences held in this._holding occurred
this._severity = 1; // how serious were the offences in this._holding
this._escapePod = false; // flag to indicate the player has just used an escape pod
this._displayType = 0; // which screen is currently being displayed
this._page = 0; // which page is currently being displayed
this._selectedItem = 0; // the index of the item selected fine item from the menu
this._selectedGalaxy = 0; // the galaxy number selected from the menu
this._selectedIndex = -1; // the index of the selected most wanted item
this._lastChoice = ["", "", "", "", "", "", "", "", "", "", "", "", "", ""]; // array to store last choice on menus
this._routeMode = "SHORTEST"; // default mode for long range chart
this._itemColor = "yellowColor";
this._menuColor = "orangeColor";
this._exitColor = "yellowColor";
this._disabledColor = "darkGrayColor";
this._anarchies = false; // flag indicating "Anarchies.OXP" is installed
this._bountyScreen = false; // flag indicating the bounty screen is open
this._holdingItem = {}; // used by the mission screen as the holding spot for offences in the current system
this._markers = [];
this._appeals = [];
this._registrars = ["", "", "", "", "", "", "", ""];
this._appealMessage = "";
this._doAppealScreen = false;
this._secOffClerk = "";
this._warrantScannerMode = 0;
this._alreadyScannedPlayerInThisSystem = false; // do-once-per-system (not cleared when docked, only when jumping out) for importing out-of-system bounties so if player pays off the locals, they don't get a bounty again from a subsequent scan

// configuration settings for use in Lib_Config
this._bountySystemConfig = {
	Name: this.name,
	Alias: "Bounty System",
	Display: "Config",
	Alive: "_bountySystemConfig",
	Notify: "$changeSettings",
	Bool: {
		B0: {
			Name: "_severity1Transfer",
			Def: true,
			Desc: "Sev 1 bounty transfer"
		},
		Info: "0 - When true, severity 1 bounties can be discovered up to 5 LY from point of origin. When false, severity 1 bounties are not transferable"
	},
	SInt: {
		S0: {
			Name: "_warrantScannerMode",
			Def: 0,
			Min: 0,
			Max: 2,
			Desc: "Warrant scanner mode"
		},
		Info: "0: Scanner mode - 0 = manual scan, 1 = automatic scan (offenders only), 2 = automatic scan (all ships)"
	},
};
this._trueValues = ["yes", "1", 1, "true", true];

// list of different offences and details thereof
this._offenceTypes = {
	"killed police": {
		description: "Murder of GalCop law enforcement officer by means of aggravated use of spacecraft weaponry.",
		severity: 3
	},
	"killed innocent": {
		description: "Murder of innocent pilot by means of aggravated use of spacecraft weaponry.",
		severity: 3
	},
	"theft of ship": {
		description: "Grand theft of spacecraft within GalCop space.",
		severity: 3
	},
	"attacked police": {
		description: "Apprehended violence against GalCop law enforcement officer with spacecraft weaponry.",
		severity: 2
	},
	"attacked main station": {
		description: "Apprehended violence against GalCop orbital space station with spacecraft weaponry.",
		severity: 2
	},
	"attacked innocent": {
		description: "Apprehended violence against innocent pilot with spacecraft weaponry.",
		severity: 2
	},
	"illegal exports": {
		description: "Exportation of illegal commodities for personal gain within GalCop-controlled orbital space stations.",
		severity: 1
	},
	"illegal imports": {
		description: "Importation of illegal commodities for personal gain within GalCop-controlled orbital space stations.",
		severity: 1
	},
	"illegal equipment": {
		description: "Installation and/or use of illegal and/or unregistered equipment in GalCop-controlled space.",
		severity: 1
	},
	"assisting offenders": {
		description: "Aiding and abetting known offenders with use of spacecraft weaponry against innocent third parties.",
		severity: 1
	},
	"attempted bribe": {
		description: "Attempting to influence the actions of an official of GalCop by offering money.",
		severity: 1
	},
	"underworld activity": {
		description: "Collusion with known underworld entities to deceive and/or defraud GalCop officials in order to bypass GalCop law, for personal gain.",
		severity: 1
	},
	"act of piracy": {
		description: "Coercing a non-combatant to relinquish materials for personal gain.",
		severity: 1
	},
	"unspecified_small": {
		description: "Unspecified minor offences.",
		severity: 1
	},
	"unspecified_medium": {
		description: "Unspecified serious offences.",
		severity: 2
	},
	"unspecified_large": {
		description: "Unspecified major offences, warranting use of lethal force.",
		severity: 3
	},
	"unspecified_undefined": {
		description: "Unspecified offences.",
		severity: 1
	},
};

// bountyMin is greater than or equal to
// bountyMax is less than or equal to
// degrade rate is rate in decimal credits fine will degrade per day
// transferLimit controls applicability range of sev 1 crimes (this._severity1Transfer must be true for this to apply)
// payableIn is how many days must pass before fine can be paid
// appeal is whether the crime can be appealed
// appealTime is how many days after the crime can it be appealed
this._severityMatrix = {
	1: {
		label: "Minor",
		bountyMin: 1,
		bountyMax: 14,
		degradeRate: 0.8,
		transferLimit: 5,
		payableIn: 1,
		appeal: true,
		appealTime: 10
	},
	2: {
		label: "Serious",
		bountyMin: 15,
		bountyMax: 49,
		degradeRate: 0.5,
		transferLimit: 0,
		payableIn: 10,
		appeal: true,
		appealTime: 5
	},
	3: {
		label: "Major",
		bountyMin: 50,
		bountyMax: -1,
		degradeRate: 0.1,
		transferLimit: 0,
		payableIn: 30,
		appeal: false,
		appealTime: 0
	}
};

//-------------------------------------------------------------------------------------------------------------
this.startUpComplete = function () {
	if (this._disabled) {
		delete this.playerWillSaveGame;
		delete this.shipBountyChanged;
		delete this.shipLaunchedEscapePod;
		delete this.playerBoughtNewShip;
		delete this.shipLaunchedFromStation;
		delete this.shipWillDockWithStation;
		delete this.shipDockedWithStation;
		delete this.shipWillEnterWitchspace;
		delete this.shipExitedWitchspace;
		delete this.shipKilledOther;
		delete this.shipAttackedOther;
		delete this.shipFiredMissile;
		delete this.guiScreenChanged;
		delete this.startUpComplete;
		delete this.infoSystemChanged;
		return;
	}

	// load up player data
	if (missionVariables.BountySystem_Offences) {
		this._offences = JSON.parse(missionVariables.BountySystem_Offences);
		delete missionVariables.BountySystem_Offences;
	}
	if (missionVariables.BountySystem_Holding) {
		this._holding = JSON.parse(missionVariables.BountySystem_Holding);
		delete missionVariables.BountySystem_Holding;
	}
	if (missionVariables.BountySystem_HoldingDate) {
		this._holdingDate = missionVariables.BountySystem_HoldingDate;
		delete missionVariables.BountySystem_HoldingDate;
	}
	if (missionVariables.BountySystem_Severity) {
		this._severity = parseInt(missionVariables.BountySystem_Severity);
		delete missionVariables.BountySystem_Severity;
	}
	if (missionVariables.BountySystem_Delta) {
		this._bountyDelta = missionVariables.BountySystem_Delta;
		delete missionVariables.BountySystem_Delta;
	}
	if (missionVariables.BountySystem_LastOffenceDate) {
		this._lastOffenceDate = missionVariables.BountySystem_LastOffenceDate;
		delete missionVariables.BountySystem_LastOffenceDate;
	}
	if (missionVariables.BountySystem_Appeals) {
		this._appeals = JSON.parse(missionVariables.BountySystem_Appeals);
		delete missionVariables.BountySystem_Appeals;
	}
	if (missionVariables.BountySystem_AppealRegistrars) {
		this._registrars = JSON.parse(missionVariables.BountySystem_AppealRegistrars);
		delete missionVariables.BountySystem_AppealRegistrars;
	}

	this.$resetBogusData();

	this._system = system.ID;

	// the Anarchies OXP also plays around with bounty processing. Rather than make that OXP incompatible with this one,
	// we're going to override Anarchies and essentially nullify what it does, so that all the bounty processing is contained here.
	if (worldScripts.Anarchies) {
		this._anarchies = true;
		// monkey patch the this.$newBountyCalculation2 function so we can override the calculations and apply our own logic
		if (this._debug) log(this.name, "Monkey patching Anarchies...");
		var w = worldScripts.Anarchies;
		w.$saved_newBountyCalculation2 = w.$newBountyCalculation2;
		w.$newBountyCalculation2 = this.$anarchiesBountyCalcOverride;
		// we also don't want the anarchies "shipLaunchedEscapePod" routine.
		delete w.shipLaunchedEscapePod;
	}

	// if the player has a bounty we don't know about, add some generic offences to the array
	// this would happen the first time the player loads a game after installing the OXP.
	if (player.bounty > 0 && this.$calculateBounty(system.ID) === 0) {
		this.shipBountyChanged(player.bounty, "scripted");
	}

	// add a mission screen exception to Xenon UI
	if (worldScripts.XenonUI) {
		var wx = worldScripts.XenonUI;
		wx.$addMissionScreenException("oolite-galcopsecoffice-screen4-map");
		wx.$addMissionScreenException("oolite-galcopsecoffice-screen8-map");
		wx.$addMissionScreenException("oolite-galcopsecoffice-screen9-map");
	}
	// add a mission screen exception to Xenon Redux UI
	if (worldScripts.XenonReduxUI) {
		var wxr = worldScripts.XenonReduxUI;
		wxr.$addMissionScreenException("oolite-galcopsecoffice-screen4-map");
		wxr.$addMissionScreenException("oolite-galcopsecoffice-screen8-map");
		wxr.$addMissionScreenException("oolite-galcopsecoffice-screen9-map");
	}

	// add screen 9 to the display current course OXP
	var dcc = worldScripts.DisplayCurrentCourse;
	if (dcc) dcc._screenIDList.push("oolite-galcopsecoffice-screen9-map");

	this._suspendedDestination = null;
	this.$initInterface(player.ship.dockedStation);

	// clear this flag after the game has been loaded and any starting bounty applied
	this._changing = false;

	if (missionVariables.BountySystem_Sev1Transfer) this._severity1Transfer = (this._trueValues.indexOf(missionVariables.BountySystem_Sev1Transfer) >= 0 ? true : false);

	// register our settings, if Lib_Config is present
	if (worldScripts.Lib_Config) worldScripts.Lib_Config._registerSet(this._bountySystemConfig);

	if (missionVariables.BountySystem_alreadyScannedPlayerInThisSystem) {
		this._alreadyScannedPlayerInThisSystem = missionVariables.BountySystem_alreadyScannedPlayerInThisSystem;
		delete missionVariables.BountySystem_alreadyScannedPlayerInThisSystem;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.playerWillSaveGame = function () {
	if (this._offences.length > 0) {
		missionVariables.BountySystem_Offences = JSON.stringify(this._offences);
	} else {
		delete missionVariables.BountySystem_Offences;
	}
	if (this._bountyDelta > 0) {
		missionVariables.BountySystem_Holding = JSON.stringify(this._holding);
		missionVariables.BountySystem_HoldingDate = this._holdingDate;
		missionVariables.BountySystem_Severity = this._severity;
		missionVariables.BountySystem_Delta = this._bountyDelta;
	}
	missionVariables.BountySystem_Sev1Transfer = this._severity1Transfer;
	if (this._appeals.length > 0) {
		missionVariables.BountySystem_Appeals = JSON.stringify(this._appeals);
	}
	missionVariables.BountySystem_AppealRegistrars = JSON.stringify(this._registrars);
	missionVariables.BountySystem_LastOffenceDate = this._lastOffenceDate;
	missionVariables.BountySystem_alreadyScannedPlayerInThisSystem = this._alreadyScannedPlayerInThisSystem;
}

//-------------------------------------------------------------------------------------------------------------
this.shipBountyChanged = function (delta, reason) {
	/* 
	Because of the way this event fires, there could be lots of messages with a delta of 0.
	Rather than try to work out the difference, I'm just adding all offences to a run-sheet, but each different type is only added once.
	If the severity of the crimes increases, the overall severity of the run-sheet will also increase.
	What this means is that you could have severity 1 crimes listed with severity 3.
	*/

	// check to see if we're setting the bounty ourselves - in which case skip this process
	if (this._changing === true) return;

	// otherwise, check the reason for the change
	switch (reason) {
		case "distress call":
		case "setup actions":
		case "new system":
		case "new galaxy":
			return;
		case "illegal exports":
		case "illegal imports":
		case "illegal equipment":
			// "illegal exports" events will happen with a delta value of zero, so check first
			if (delta > 0) this.$addOffence(reason);
			break;
		case "killed innocent":
		case "killed police":
		case "attempted bribe":
		case "attacked police":
		case "attacked main station":
		case "attacked innocent":
		case "assisting offenders":
			// only add the office if (a) the delta is positive, or (b) we currently have a holding bounty delta that's greater than zero, and we haven't seen this offence yet
			if (delta > 0 || (this._bountyDelta > 0 && this._holding.indexOf(reason) === -1)) {
				this.$addOffence(reason);
			}
			break;
			// removed the seen by police element, in case a crime is noted (ie delta > 0) with this reason
			//case "seen by police":
			// for the moment I'm assuming this reason will always be accompanied by another event, so we don't need to record it separately
			//break;
		default:
			// we should get here for "scripted" and any other unrecognised reason
			// but we only want to do something if there is a positive delta
			if (delta > 0) {
				if (this._debug) log(this.name, "adding unspecified for '" + reason + "', delta " + delta);
				if (this._offenceTypes[reason]) {
					this.$addOffence(reason);
				} else {
					if (delta >= this._severityMatrix[1].bountyMin && delta <= this._severityMatrix[1].bountyMax) this.$addOffence("unspecified_small");
					if (delta >= this._severityMatrix[2].bountyMin && delta <= this._severityMatrix[2].bountyMax) this.$addOffence("unspecified_medium");
					if (delta >= this._severityMatrix[3].bountyMin) this.$addOffence("unspecified_large");
				}
			}
			break;
	}

	if (this._debug) log(this.name, "bounty " + player.bounty + ", change " + delta + ", reason " + reason);
	if (delta > 0) this._bountyDelta += delta;

	// we're reducing our bounty or we're setting it to zero
	if ((delta < 0 && reason !== "escape pod") || (delta === 0 && reason === "scripted")) {
		// if the bounty was reset to 0, clear out all bounties - something has reset the players bounty
		if (player.bounty === 0 && this.$calculateBounty(system.ID) != 0 && reason !== "paid fine") {
			if (this._debug) log(this.name, "full bounty reset occurred");
			this._offences.length = 0;
			this._holding.length = 0;
			this._holdingDate = 0;
			this._bountyDelta = 0;
			// exit at this point so it doesn't flow into the next sections.
			return;
		}
		// player was marked for fines by a police ship
		// note: if the player was scanned and offences were transferred to the current system, and then they were marked for fines
		// paying the fine will only remove the transferred fine from the current system - not the original crime. If the player comes back to 
		// the system again and is scanned again they might have to pay for the fines again.
		if (player.bounty === 0 && this.$calculateBounty(system.ID) != 0 && reason === "paid fine") {
			if (this._debug) log(this.name, "player paid fine");
			// clear out any offences that would apply in the current system
			var tfrlist = [];
			var found = false;
			for (var i = this._offences.length - 1; i >= 0; i--) {
				var itm = this._offences[i];
				// theoretically, the only time this would happen is for severity 1 offences, but we'll check for the others as well
				//if ((itm.severity === 1 && itm.galaxy === galaxyNumber && itm.system === system.ID) || (itm.severity >= 2 && itm.transferred === false)) {
				if (itm.galaxy === galaxyNumber && itm.system === system.ID) {
					// take a copy of the relevant data for transferred offence removal, but only if this is the original offence
					// to catch instances where a bounty has been transferred to another system (not this one), but because we've paid the original, the transferred bounty should also be removed
					// that is, if we're deleting the parent offence, all child offences should be removed as well.
					if (itm.transferred === false) {
						tfrlist.push({
							bounty: itm.bounty,
							severity: itm.severity,
							galaxy: itm.galaxy,
							system: itm.system,
							systemName: itm.systemName,
							government: itm.government,
							date: itm.date
						});
					}
					// the item is paid, so we can remove it.
					if (itm.transferred === true || (itm.originatingGalaxy === galaxyNumber && itm.originatingSystem === system.ID)) {
						this._offences.splice(i, 1);
						found = true;
					}
				}
			}
			// remove any transferred offences
			if (tfrlist.length > 0) {
				for (var i = 0; i < tfrlist.length; i++) {
					this.$removeTransferredOffences(tfrlist[i]);
				}
			}
			// exit at this point so it doesn't flow into the next sections, but only if we found and removed some bounties from the master list and there's nothing on the current run sheet
			if (found === true && this._bountyDelta === 0) return;
			// if we didn't find any bounties in the master list, we might be paying a fine for current offences - we deal with that in the next bit
			if (this._debug) log(this.name, "fine paid on current run sheet - continuing..." + this._bountyDelta);
		}
		// we've already recorded some bounty in this system, so reduce it
		if (this._bountyDelta > Math.abs(delta)) {
			if (this._debug) log(this.name, "adjusting current run sheet");
			this._bountyDelta += delta;
			// exit at this point so it doesn't flow into the next sections.
			return;
		}
		// the reduction will clear out the current run sheet - remove the data.
		if (this._bountyDelta <= Math.abs(delta)) {
			if (this._debug) log(this.name, "current run sheet cleared");
			delta -= this._bountyDelta;
			this._bountyDelta = 0;
			this._holding.length = 0;
			this._holdingDate = 0;
			this._severity = 0;
			// exit at this point so it doesn't flow into the next section.
			//return;
		}
		// we have no pending bounties in this system
		if (this._bountyDelta === 0 && delta != 0) {
			// reduce any recorded bounties for the player, local first
			var amount = this.$reduceBounty(Math.abs(delta), 1, system.ID);
			if (amount > 0) amount = this.$reduceBounty(amount, 2, system.ID);
			if (amount > 0) amount = this.$reduceBounty(amount, 3, system.ID);
			this.$cleanUp();
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipLaunchedEscapePod = function (escapepod) {
	this._escapePod = true;
}

//-------------------------------------------------------------------------------------------------------------
this.playerBoughtNewShip = function (ship, price) {
	this._changing = true;
	player.bounty = this.$calculateBounty(system.ID);
	this._changing = false;
}

//-------------------------------------------------------------------------------------------------------------
this.shipLaunchedFromStation = function (station) {
	delete missionVariables.BountySystem_Offences;
	delete missionVariables.BountySystem_Holding;
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillDockWithStation = function (station) {
	if (this._escapePod === true) {
		this._changing = true;
		player.bounty += this.$calculateBounty(system.ID); // add instead of setting so other OXPs can update it (currently known: Broadcast Comms restores hidden bounty for players who surrendered to police)
		this._changing = false;
		this._escapePod = false;
	}
	if (station.allegiance === "galcop" && this._appeals.length > 0) {
		this._appealMessage = "";
		for (var i = this._appeals.length - 1; i >= 0; i--) {
			var appeal = this._appeals[i];
			if (appeal.completedBy < clock.adjustedSeconds) {
				var result = "";
				if (this._offences.length > 1) result = "other-offences";
				if (this._offences.length === 1 && this._offences[0].date == appeal.data.date && this._offences[0].system === appeal.data.system && this._offences[0].severity == 3) result = "severity-3";
				if (this._offences.length === 0) result = "already-paid";
				if (appeal.data.date - appeal.data.lastOffenceDate < 30) "too-soon";
				if (result === "") {
					for (var j = 0; j < appeal.roles.length; j++) {
						if (appeal.roles[j].indexOf("pirate") >= 0) {
							result = "pirate";
							break;
						}
						if (appeal.roles[j].indexOf("assassin") >= 0) {
							result = "assassin";
							break;
						}
					}
				}
				if (result === "") result = "success";
				var appeal = expandDescription("[galcop-appealresult-" + result + "]", {
					system: appeal.data.systemName,
					date: this.$setDateSince(appeal.data.date * 86400)
				});
				this._appealMessage += (this._appealMessage === "" ? "" : "\n\n") + appeal;

				// send an email confirmation
				var ga = worldScripts.GalCopAdminServices;
				if (ga) {
					if (this._registrars[galaxyNumber] === "") this._registrars[galaxyNumber] = expandDescription("%N [nom]");
					var msg = expandDescription("[galcop-appealresult-email]", {
						result: appeal,
						sender: this._registrars[galaxyNumber]
					});
					var w = worldScripts.EmailSystem;
					w.$createEmail({
						sender: "Appeals Registrar",
						subject: "Appeal Result",
						date: global.clock.adjustedSeconds,
						sentFrom: this._galCopHQ[galaxyNumber],
						message: msg,
						expiryDays: ga._defaultExpiryDays
					});
				}

				// if the appeal was successful, remove the offence
				if (result === "success") {
					for (var j = this._offences.length - 1; j >= 0; j--) {
						var itm = this._offences[j];
						if (itm.galaxy === galaxyNumber && itm.system === appeal.data.system && itm.date === appeal.data.date) {
							// we want to remove all instances of this bounty, regardless of whether it's transferred or not.
							this._offences.splice(j, 1);
						}
					}
				}
				// we'll remove this appeal now that it's been finished
				this._appeals.splice(i, 1);
			}
		}
		if (this._appealMessage !== "") this._doAppealScreen = true;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.missionScreenOpportunity = function () {
	if (this._doAppealScreen === true) {
		this._doAppealScreen = false;
		mission.runScreen({
			screenID: "oolite-bountysystem-appeal-map",
			title: "Appeal Submission Response",
			overlay: {
				name: "galcop_logo.png",
				height: 546
			},
			message: this._appealMessage,
			exitScreen: "GUI_SCREEN_STATUS"
		});
		this._appealMessage = "";
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipDockedWithStation = function (station) {
	this.$initInterface(station);
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillEnterWitchspace = function (cause, destination) {
	this._secOffClerk = "";
	// push data into the array, but only if there is a bounty delta
	if (this._bountyDelta != 0 && system.ID >= 0) {
		// for case where we are about to add an offence but no description has been added
		if (this._holding.length === 0) {
			if (this._bountyDelta >= this._severityMatrix[1].bountyMin && this._bountyDelta <= this._severityMatrix[1].bountyMax) this.$addOffence("unspecified_small");
			if (this._bountyDelta >= this._severityMatrix[2].bountyMin && this._bountyDelta <= this._severityMatrix[2].bountyMax) this.$addOffence("unspecified_medium");
			if (this._bountyDelta >= this._severityMatrix[3].bountyMin) this.$addOffence("unspecified_large");
		}
		this._offences.push({
			offenceList: JSON.stringify(this._holding),
			bounty: this._bountyDelta,
			severity: this._severity,
			galaxy: galaxyNumber,
			system: this._system,
			systemName: System.infoForSystem(galaxyNumber, this._system).name,
			government: System.infoForSystem(galaxyNumber, this._system).government,
			date: this._holdingDate, //global.clock.days,
			transferred: false,
			originatingSystem: this._system,
			originatingGalaxy: galaxyNumber,
			priorOffenceDate: this._lastOffenceDate
		});
		this._lastOffenceDate = this._holdingDate;
	}
	// clean up for next system
	this._holding.length = 0;
	this._holdingDate = 0;
	this._bountyDelta = 0;
	this._severity = 0;
	this._alreadyScannedPlayerInThisSystem = false;
}

//-------------------------------------------------------------------------------------------------------------
this.shipExitedWitchspace = function () {
	this.$resetBogusData();
	// calculate the player's bounty in this system
	this._changing = true;
	player.bounty = this.$calculateBounty(system.ID);
	this._changing = false;
	this._system = system.ID;
}

//-------------------------------------------------------------------------------------------------------------
this.shipKilledOther = function (whom, damageType) {
	this.$targetAttacked(whom, damageType, true);
}

//-------------------------------------------------------------------------------------------------------------
this.shipAttackedOther = function (whom) {
	// only send the targetAttacked message if the player has a current bounty delta
	if (this._bountyDelta > 0) this.$targetAttacked(whom, "energy damage", false);
}

//-------------------------------------------------------------------------------------------------------------
this.shipFiredMissile = function (missile, whom) {
	this.$targetAttacked(whom, "energy damage", false);
}

//-------------------------------------------------------------------------------------------------------------
// processes all the variations on attacking and killing other ships
this.$targetAttacked = function (whom, damageType, killed) {
	// don't record anything if this is a simulator run
	if (this.$simulatorRunning()) return;

	var msgType = "attacked";
	if (killed) msgType = "killed";

	// player shot and killed other with lasers, q-bomb, or by ramming them
	if (whom.isPiloted && !whom.isDerelict && !whom.isThargoid) {
		if (damageType === "energy damage" || damageType === "cascade weapon" || damageType === "scrape damage") {
			// who did we just kill?
			if (whom.isPolice) {
				this.$addOffence(msgType + " police");
			} else {
				// is there a police ship/station in range
				// (check the holding array first though, so we can avoid doing "findLawVessels" with every laser shot)
				if (whom.bounty === 0 && this._holding.indexOf(msgType + "innocent") === -1) {
					var police = this.$findLawVessels(player.ship);
					if (police.length > 0) this.$addOffence(msgType + " innocent");
				}
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.guiScreenChanged = function (to, from) {
	if (to === "GUI_SCREEN_SYSTEM_DATA" && from != "GUI_SCREEN_SYSTEM_DATA") this.$updateSystemInfo();
	if (from === "GUI_SCREEN_MISSION" && this._bountyScreen) {
		this._bountyScreen = false;
		if (this._hudHidden === false && player.ship.hudHidden === true) player.ship.hudHidden = this._hudHidden;
		if (this._suspendedDestination) player.ship.targetSystem = this._suspendedDestination;
		this._suspendedDestination = null;
		// clean up all the marks on the chart
		this.$removeChartData();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.infoSystemChanged = function (to, from) {
	if (guiScreen === "GUI_SCREEN_SYSTEM_DATA") this.$updateSystemInfo();
}

//--------------------------------------------------------------------------------------------------------------
this.$updateSystemInfo = function () {
	var sysID = player.ship.targetSystem;
	if (player.ship.hasOwnProperty("infoSystem")) sysID = player.ship.infoSystem;
	var bounty = this.$calculateBounty(sysID);
	var criminal = this.$playerIsCriminal(sysID);
	if (bounty > 0 && bounty < 50) mission.addMessageText("This system considers you " + (criminal === true ? "a criminal" : "an") + " offender.");
	if (bounty > 50) mission.addMessageText("This system considers you " + (criminal === true ? "a criminal" : "a") + " fugitive.");
}

//-------------------------------------------------------------------------------------------------------------
this.$initInterface = function (station) {
	if (station.allegiance === "galcop") {
		station.setInterface(this.name, {
			title: (station.isMainStation ? "GalCop security office" : "GalCop security kiosk"),
			category: "Station Interfaces",
			summary: "Accesses the GalCop security office services at this station.",
			callback: this.$startSecOfficeScreen.bind(this)
		});
	} else {
		station.setInterface(this.name, null);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$startSecOfficeScreen = function () {
	this._lastChoice = ["", "", "", "", "", "", "", "", "", "", ""];
	this._displayType = 0;
	this._page = 0;
	this.$showSecOffice();
}

//-------------------------------------------------------------------------------------------------------------
this.$showSecOffice = function () {
	function compare(a, b) {
		if (a.date < b.date) return -1;
		if (a.date > b.date) return 1;
		return 0;
	}

	function compareBounty(a, b) {
		return (b.bounty - a.bounty);
	}

	function compareDateDesc(a, b) {
		if (a.createdDate > b.createdDate) return -1;
		if (a.createdDate < b.createdDate) return 1;
		return 0;
	}
	var p = player.ship;

	var text = "";
	var opts;
	var curChoices = {};
	var def = "";
	var rows = 21;
	this._hudHidden = p.hudHidden;
	if (this.$isBigGuiActive() === false) p.hudHidden = true;
	this._bountyScreen = true;
	var jmpIndent = 6.45;

	var govs = new Array();
	for (var i = 0; i < 8; i++)
		govs.push(String.fromCharCode(i));
	var spc = String.fromCharCode(31);

	if (this._displayType === 0) {
		text = expandDescription("[galcop-office-welcome]", {
			officetype: (system.ID === this._galCopHQ[galaxyNumber] ? "HQ" : "Local")
		});
		var itemcount = 2;

		curChoices["01_VIEWSTD"] = {
			text: "[galcop-local-fines]",
			color: this._menuColor
		};
		curChoices["02_VIEWINTER"] = {
			text: "[galcop-inter-fines]",
			color: this._menuColor
		};
		if (worldScripts.BountySystem_MostWanted) {
			curChoices["03_WANTED"] = {
				text: "[galcop-mostwanted]",
				color: this._menuColor
			};
			itemcount += 1;
			if (p.equipmentStatus("EQ_HUNTER_LICENCE") === "EQUIPMENT_OK") {
				curChoices["09_BOUNTY_TIPS"] = {
					text: "[galcop-mostwanted-tips]",
					color: this._menuColor
				};
				itemcount += 1;
			}
			if (worldScripts.BountySystem_MostWanted._completed.length > 0) {
				curChoices["03_WANTED_BONUS"] = {
					text: "[galcop-mostwanted-bonus-menu]",
					color: this._menuColor
				};
				itemcount += 1;
			}
		}
		if (system.ID != this._galCopHQ[galaxyNumber]) {
			curChoices["04_GALCOPHQ"] = {
				text: "[galcop-hq]",
				color: this._menuColor
			};
			itemcount += 1;
		}

		curChoices["99_EXIT"] = {
			text: "[galcop-exit]",
			color: this._exitColor
		};
		var def = "99_EXIT";

		for (var i = 0; i < (rows - itemcount); i++) {
			curChoices["99_SPACER_" + i] = "";
		}

		var opts = {
			screenID: "oolite-galcopsecoffice-screen0-map",
			title: "GalCop " + (system.ID === this._galCopHQ[galaxyNumber] ? "HQ" : "Local") + " Security Office",
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			overlay: {
				name: "galcop_logo.png",
				height: 546
			},
			choices: curChoices,
			initialChoicesKey: (this._lastChoice[this._displayType] && this._lastChoice[this._displayType] != "" ? this._lastChoice[this._displayType] : def),
			message: text
		};
	}

	if (this._displayType === 1) { // outstanding fines
		text = this.$padTextRight(expandDescription("[galcop-fine-hdr-system]"), 6) +
			this.$padTextRight(expandDescription("[galcop-fine-hdr-date]"), 6) +
			this.$padTextLeft(expandDescription("[galcop-fine-hdr-incidents]"), 5) +
			this.$padTextLeft(expandDescription("[galcop-fine-hdr-bounty]"), 4) +
			this.$padTextLeft(expandDescription("[galcop-fine-hdr-fine]"), 5) + "  " + expandDescription("[galcop-fine-hdr-payable]");
		var list = [];
		for (var i = 0; i < this._offences.length; i++) {
			var offence = this._offences[i];
			if (offence.transferred === false && offence.galaxy === galaxyNumber) {
				if (this.$calculateItemBounty(offence, offence.system) > 0) {
					var subitems = JSON.parse(offence.offenceList);
					var days = 0;
					if (this.$itemIsPayable(offence) === false) days = this.$daysUntilPayable(offence);

					var st = this.$setDateSince(parseFloat(offence.date) * 86400);
					list.push({
						text: this.$padTextRight(offence.systemName, 6) +
							this.$padTextRight(st, 6) + // offence.date
							this.$padTextLeft(subitems.length, 5) +
							this.$padTextLeft(formatCredits(this.$calculateItemBounty(offence, offence.system), false, true), 4) +
							(this.$itemIsPayable(offence) ? this.$padTextLeft(formatCredits(this.$calculateFine(offence), false, true), 5) : this.$padTextLeft(" ", 5)) +
							(days === 0 ? "  Yes" : "  " + days + " day" + (days > 1 ? "s" : "")),
						date: offence.date,
						index: i
					});
				}
			}
		}
		// add in any active offences that haven't been added to the main offence list
		if (this._holding.length > 0) {
			this._holdingItem = {
				offenceList: JSON.stringify(this._holding),
				bounty: this._bountyDelta,
				severity: this._severity,
				galaxy: galaxyNumber,
				system: system.ID,
				systemName: system.name,
				government: system.info.government,
				date: this._holdingDate, //global.clock.days,
				transferred: false
			};
			list.push({
				text: this.$padTextRight(this._holdingItem.systemName, 6) +
					this.$padTextRight(this.$setDateSince(this._holdingItem.date * 86400), 6) + // this._holdingItem.date
					this.$padTextLeft(this._holding.length, 5) +
					this.$padTextLeft(formatCredits(this._bountyDelta, false, true), 4) +
					this.$padTextLeft(" ", 5) +
					"  ",
				date: global.clock.days,
				index: 9999
			});
		}

		def = "98_CLOSE";

		if (list.length > 0) {
			list.sort(compare);

			var maxpage = Math.ceil(list.length / (rows + 1));
			var end = ((this._page * (rows + 1)) + (rows + 1));
			var itemcount = 0;
			if (end > list.length) end = list.length;
			for (var i = (this._page * (rows + 1)); i < end; i++) {
				curChoices["01_FINE-" + (i < 10 ? "0" : "") + i + "~" + list[i].index] = {
					text: list[i].text,
					alignment: "LEFT",
					color: this._itemColor
				};
				itemcount += 1;
			}

			curChoices["94_SPACER"] = "";

			if (maxpage > 1 && this._page < (maxpage - 1)) {
				curChoices["95_NEXT"] = {
					text: "[galcop-nextpage]",
					color: this._menuColor
				};
			} else {
				curChoices["95_NEXT"] = {
					text: "[galcop-nextpage]",
					color: this._disabledColor,
					unselectable: true
				};
			}
			if (this._page > 0) {
				curChoices["96_PREV"] = {
					text: "[galcop-prevpage]",
					color: this._menuColor
				};
			} else {
				curChoices["96_PREV"] = {
					text: "[galcop-prevpage]",
					color: this._disabledColor,
					unselectable: true
				};
			}

			for (var i = 0; i < (rows - itemcount); i++) {
				curChoices["99_SPACER_" + i] = "";
			}

			//if (this._lastChoice != "") def = this._lastChoice;

		} else {
			text = expandDescription("[galcop-no-warrants]");
		}

		curChoices["98_CLOSE"] = {
			text: "[galcop-exit]",
			color: this._exitColor
		};

		var opts = {
			screenID: "oolite-galcopsecoffice-screen1-map",
			title: "Outstanding Fines",
			overlay: {
				name: "bounty-gavel.png",
				height: 546
			},
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: (this._lastChoice[this._displayType] && this._lastChoice[this._displayType] != "" ? this._lastChoice[this._displayType] : def),
			message: text
		};
	}

	if (this._displayType === 2) { // outstanding intergalactic fines
		// for this we summarise all the bounties from other sectors
		var hq = System.infoForSystem(galaxyNumber, this._galCopHQ[galaxyNumber]);
		var itemcount = 0;
		for (var i = 0; i <= 7; i++) {
			if (i != galaxyNumber) {
				var bounty = this.$calculateIntergalacticBounty(i);
				if (bounty > 0) {
					var fineamt = 0;
					for (var j = 0; j < this._offences.length; j++) {
						if (this._offences[i].transferred === false && this._offences[j].galaxy === i && this.$itemIsPayable(this._offences[j]) === true) fineamt += this.$calculateFine(this._offences[j]);
					}
					curChoices["05_GAL~" + i] = {
						text: this.$padTextRight("Sector " + (i + 1), 10) + this.$padTextLeft(formatCredits(bounty, false, true), 10) + this.$padTextLeft(formatCredits(fineamt, false, true), 10),
						alignment: "LEFT",
						color: this._itemColor
					};
					itemcount += 1;
				}
			}
		}

		if (itemcount > 0) {
			text = this.$padTextRight(expandDescription("[galcop-sector]"), 10) + this.$padTextLeft(expandDescription("[galcop-bounty]"), 10) + this.$padTextLeft(expandDescription("[galcop-payable]"), 10) + "\n";
			curChoices["06_SPACER"] = "";
			for (var i = 0; i < ((rows + 2) - itemcount); i++) {
				curChoices["99_SPACER_" + i] = "";
			}

		} else {
			text = expandDescription("[galcop-no-inter-warrants]");
		}

		def = "98_CLOSE";
		curChoices["98_CLOSE"] = {
			text: "[galcop-exit]",
			color: this._exitColor
		};

		var opts = {
			screenID: "oolite-galcopsecoffice-screen2-map",
			title: "Outstanding Intergalactic Fines",
			overlay: {
				name: "bounty-gavel.png",
				height: 546
			},
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: (this._lastChoice[this._displayType] && this._lastChoice[this._displayType] != "" ? this._lastChoice[this._displayType] : def),
			message: text
		};
	}

	if (this._displayType === 3) { // most wanted list
		// pilot name, bounty, last seen, updated
		text = this.$padTextRight(expandDescription("[galcop-mostwanted-hdr-pilot]"), 10) +
			this.$padTextRight(expandDescription("[galcop-mostwanted-hdr-lastseen]"), 10) +
			this.$padTextRight(expandDescription("[galcop-mostwanted-hdr-updated]"), 7.25) +
			this.$padTextLeft(expandDescription("[galcop-mostwanted-hdr-bounty]"), 4.5);
		var list = [];
		var mw = worldScripts.BountySystem_MostWanted;
		if (mw._constantUpdates == true) mw.$updateDisplayList();
		var dl = mw._displayList;
		for (var i = 0; i < dl.length; i++) {
			if (dl[i].lastVisibleSystem != -1) {
				list.push({
					pilotName: dl[i].pilotName,
					bounty: dl[i].bounty,
					lastSeen: System.systemNameForID(dl[i].lastVisibleSystem),
					lastVisibleSystem: dl[i].lastVisibleSystem,
					updated: dl[i].updated,
					index: i,
					ref: dl[i].index
				});
			} else {
				list.push({
					pilotName: dl[i].pilotName,
					bounty: dl[i].bounty,
					lastSeen: "--unknown--",
					lastVisibleSystem: -1,
					updated: "",
					index: i,
					ref: dl[i].index
				});
			}
		}

		def = "98_CLOSE";

		if (list.length > 0) {
			list.sort(compareBounty);

			var maxpage = Math.ceil(list.length / rows);
			var end = ((this._page * rows) + rows);
			var itemcount = 0;
			if (end > list.length) end = list.length;
			for (var i = (this._page * rows); i < end; i++) {
				var lastseen = "";
				var updated = "";
				if (list[i].lastVisibleSystem >= 0) {
					var info = System.infoForSystem(galaxyNumber, list[i].lastVisibleSystem);
					var rt = system.info.routeToSystem(info);
					if (rt) {
						var dist = rt.distance;
					} else {
						var dist = system.info.distanceToSystem(info);
					}
					lastseen = list[i].lastSeen + " (" + govs[info.government] + spc + dist.toFixed(1) + "ly)";
					updated = this.$adjustTime(clock.clockStringForTime(list[i].updated));
				} else {
					lastseen = "--unknown--";
					updated = "";
				}

				curChoices["50_MOSTWANTED-" + (i < 10 ? "0" : "") + i + "~" + list[i].index] = {
					text: this.$padTextRight((mw._missionList.indexOf(list[i].ref) >= 0 ? "• " : "") + list[i].pilotName, 10) +
						this.$padTextRight(lastseen, 10) +
						this.$padTextRight(updated, 7.25) +
						this.$padTextLeft(formatCredits(list[i].bounty, false, true), 4.5),
					alignment: "LEFT",
					color: this._itemColor
				};
				itemcount += 1;
			}

			curChoices["94_SPACER"] = "";
			for (var i = 0; i < (rows - itemcount); i++) {
				curChoices["94_SPACER_" + i] = "";
			}

			if (maxpage > 1 && this._page < (maxpage - 1)) {
				curChoices["95_NEXT"] = {
					text: "[galcop-nextpage]",
					color: this._menuColor
				};
			} else {
				curChoices["95_NEXT"] = {
					text: "[galcop-nextpage]",
					color: this._disabledColor,
					unselectable: true
				};
			}
			if (this._page > 0) {
				curChoices["96_PREV"] = {
					text: "[galcop-prevpage]",
					color: this._menuColor
				};
			} else {
				curChoices["96_PREV"] = {
					text: "[galcop-prevpage]",
					color: this._disabledColor,
					unselectable: true
				};
			}
		}

		def = "98_CLOSE";
		curChoices["98_CLOSE"] = {
			text: "[galcop-exit]",
			color: this._exitColor
		};

		var opts = {
			screenID: "oolite-galcopsecoffice-screen3-map",
			title: "GalCop Most Wanted" + (maxpage > 1 ? ": Page " + (this._page + 1) + " of " + maxpage : ""),
			overlay: {
				name: "mw_badge.png",
				height: 546
			},
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: (this._lastChoice[this._displayType] && this._lastChoice[this._displayType] != "" ? this._lastChoice[this._displayType] : def),
			message: text
		};
	}

	if (this._displayType === 4) { // galcop hq location
		// hold the player's destination
		var rtCurr = system.info.routeToSystem(System.infoForSystem(galaxyNumber, p.targetSystem));
		this._suspendedDestination = p.targetSystem;
		// override it for the display
		p.targetSystem = this._galCopHQ[galaxyNumber];

		var bg = "LONG_RANGE_CHART";
		if (p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY")) {
			if (this._galCopHQ[galaxyNumber] != system.ID) {
				var rt = system.info.routeToSystem(System.infoForSystem(galaxyNumber, this._galCopHQ[galaxyNumber]), (this._routeMode === "SHORTEST" ? "OPTIMIZED_BY_JUMPS" : "OPTIMIZED_BY_TIME"));
				if (rt && rt.route.length > 1) {
					text = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + this.$padTextRight("", jmpIndent) + expandDescription("[galcop-jumps]") + " " + (rt.route.length - 1);
				}
			}
			if (this._routeMode === "SHORTEST") {
				bg += "_SHORTEST";
				curChoices["90_SHORTEST"] = {
					text: "[galcop-short-route]",
					color: this._disabledColor,
					unselectable: true
				};
				curChoices["91_QUICKEST"] = {
					text: "[galcop-quick-route]",
					color: this._menuColor
				};
			} else {
				bg += "_QUICKEST";
				curChoices["90_SHORTEST"] = {
					text: "[galcop-short-route]",
					color: this._menuColor
				};
				curChoices["91_QUICKEST"] = {
					text: "[galcop-quick-route]",
					color: this._disabledColor,
					unselectable: true
				};
			}
		}

		if (p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY") && (!rtCurr || rtCurr.route.indexOf(p.targetSystem) === -1)) {
			curChoices["96_SETCOURSE~" + p.targetSystem] = {
				text: expandDescription("[galcop-set-course]", {
					destination: System.systemNameForID(p.targetSystem)
				}),
				color: this._menuColor
			};
		}

		curChoices["98_CLOSE"] = {
			text: "[galcop-exit]",
			color: this._exitColor
		};
		def = "98_CLOSE";

		var opts = {
			screenID: "oolite-galcopsecoffice-screen4-map",
			title: "GalCop HQ in Sector " + (galaxyNumber + 1),
			backgroundSpecial: bg,
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: (this._lastChoice[this._displayType] && this._lastChoice[this._displayType] != "" ? this._lastChoice[this._displayType] : def),
			message: text
		};
	}

	if (this._displayType === 5) { // fine detail
		if (this._selectedItem === 9999) {
			var itm = this._holdingItem;
		} else {
			var itm = this._offences[this._selectedItem];
		}
		var subitems = JSON.parse(itm.offenceList);
		var tl = 0;
		if (itm.galaxy === galaxyNumber) {
			var info = System.infoForSystem(galaxyNumber, itm.system);
			tl = info.techlevel + 1;
		} else {
			var info = null;
		}
		var hq = System.infoForSystem(galaxyNumber, this._galCopHQ[galaxyNumber]);
		text = this.$padTextRight(expandDescription("[galcop-offence-system]"), 10) + itm.systemName + " (" + govs[itm.government] + spc + (itm.galaxy != galaxyNumber ? "G" + (itm.galaxy + 1) + ")" : "TL" + tl + ")") + "\n";
		text += this.$padTextRight(expandDescription("[galcop-offence-date]"), 10) + this.$setDateSince(itm.date * 86400) + "\n";
		text += this.$padTextRight(expandDescription("[galcop-offence-severity]"), 10) + itm.severity + " (" + this._severityMatrix[itm.severity].label + ")\n";

		var bounty = 0;
		if (itm.galaxy != galaxyNumber) {
			bounty = this.$calculateIntergalacticItemBounty(itm);
		} else {
			bounty = this.$calculateItemBounty(itm, itm.system);
		}
		text += this.$padTextRight(expandDescription("[galcop-offence-bounty]"), 10) + formatCredits(bounty, false, true) + "\n";
		var linecount = 0;
		if (this.$itemAlreadyTransferred(itm) === true) {
			text += this.$padTextRight(expandDescription("[galcop-offence-applied]"), 10) + "Yes\n";
			linecount += 1;
		}
		if (itm.system != system.ID) linecount += 1;
		for (var i = 0; i < subitems.length; i++) {
			if (this._offenceTypes[subitems[i]]) {
				var textItems = "- " + this._offenceTypes[subitems[i]].description;
			} else {
				var textItems = "- " + this._offenceTypes["unspecified_undefined"].description;
			}
			var colItems = this.$columnText(textItems, 21);
			for (var j = 0; j < colItems.length; j++) {
				if (i === 0 && j === 0) {
					text += this.$padTextRight(expandDescription("[galcop-offence-charges]"), 10);
				} else {
					text += this.$padTextRight(" ", 10);
				}
				text += colItems[j] + "\n";
				linecount += 1;
				if (linecount >= rows - 1) {
					text += this.$padTextRight(" ", 10) + "<< list overflow >>\n";
					break;
				}
			}
			if (linecount >= rows - 1) break;
		}

		if (itm.system != system.ID) {
			curChoices["53_VIEWCHART"] = {
				text: "[galcop-view-course]",
				color: this._menuColor
			};
		}

		if (this.$itemIsPayable(itm) === false || this._selectedItem === 9999) {
			var days_until = this.$daysUntilPayable(itm);
			if (this._selectedItem === 9999) {
				text += this.$padTextRight(expandDescription("[galcop-offence-days]"), 10) + days_until + " day" + (days_until === 1 ? "" : "s") + " after leaving system\n";
			} else {
				text += this.$padTextRight(expandDescription("[galcop-offence-days]"), 10) + days_until + " day" + (days_until === 1 ? "" : "s") + "\n";
			}
			curChoices["90_PAYFINE"] = {
				text: expandDescription("[galcop-payfine]", {
					fine: "(not available)"
				}),
				color: this._disabledColor,
				unselectable: true
			};
		} else {
			text += this.$padTextRight(expandDescription("[galcop-offence-fine]"), 10) + formatCredits(this.$calculateFine(itm), false, true);
			if (system.ID === this._galCopHQ[galaxyNumber] || (itm.galaxy === galaxyNumber && system.ID === itm.system)) {
				curChoices["90_PAYFINE"] = {
					text: expandDescription("[galcop-payfine]", {
						fine: "(" + formatCredits(this.$calculateFine(itm) * (itm.system != system.ID ? (1 + this._galCopHQFineSurcharge) : 1), true, true) + ")"
					}) + (p.dockedStation.isMainStation ? "" : " (only at main station)"),
					unselectable: (p.dockedStation.isMainStation ? false : true),
					color: this._menuColor
				};
			} else {
				var hqJumps = 0;
				// calc distance to HQ
				var rt1 = system.info.routeToSystem(hq, (this._routeMode === "SHORTEST" ? "OPTIMIZED_BY_JUMPS" : "OPTIMIZED_BY_TIME"));
				if (rt1 && p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY")) {
					var dist1 = rt1.distance;
					hqJumps = rt1.route.length - 1;
				} else {
					var dist1 = system.info.distanceToSystem(hq);
				}
				var offJumps = 0;
				// calc distance to offence planet (if available)
				if (info) {
					var rt2 = system.info.routeToSystem(info, (this._routeMode === "SHORTEST" ? "OPTIMIZED_BY_JUMPS" : "OPTIMIZED_BY_TIME"));
					if (rt2 && p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY")) {
						var dist2 = rt2.distance;
						offJumps = rt2.route.length - 1;
					} else {
						var dist2 = system.info.distanceToSystem(info);
					}
				}
				if (itm.galaxy === galaxyNumber) {
					if (itm.system != this._galCopHQ[galaxyNumber]) {
						text += expandDescription("[galcop-offence-payable-at-both]", {
							destination: info.name
						});
						var warn = expandDescription("[galcop-offence-hq-warning]");
						if (defaultFont.measureString(warn) > 21) {
							var warnItems = this.$columnText(warn, 21);
							for (var i = 0; i < warnItems.length; i++) {
								text += "\n" + this.$padTextRight(" ", 10) + warnItems[i];
							}
						} else {
							text += "\n" + this.$padTextRight(" ", 10) + warn;
						}
						if (p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY") && this.$systemInCurrentPlot(this._galCopHQ[galaxyNumber]) === false) {
							curChoices["96_SETCOURSE~" + this._galCopHQ[galaxyNumber]] = {
								text: expandDescription("[galcop-hq-set-course-dist]", {
									distance: dist1.toFixed(1),
									jumps: (hqJumps > 0 ? ", " + hqJumps + " jump" + (hqJumps > 1 ? "s" : "") : "")
								}),
								color: this._menuColor
							};
						}
					} else {
						text += expandDescription("[galcop-offence-payable-at-local]", {
							destination: info.name
						});
					}
					if (p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY") && this.$systemInCurrentPlot(itm.system) === false) {
						curChoices["96_SETCOURSE~" + itm.system] = {
							text: expandDescription("[galcop-set-course-dist]", {
								destination: info.name,
								distance: dist2.toFixed(1),
								jumps: (offJumps > 0 ? ", " + offJumps + " jump" + (offJumps > 1 ? "s" : "") : "")
							}),
							color: this._menuColor
						};
					}
				} else {
					text += " (payable at GalCop HQ)";
					if (p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY") && this.$systemInCurrentPlot(this._galCopHQ[galaxyNumber]) === false) {
						curChoices["96_SETCOURSE~" + this._galCopHQ[galaxyNumber]] = {
							text: expandDescription("[galcop-hq-set-course-dist]", {
								distance: dist1.toFixed(1),
								jumps: (hqJumps > 0 ? ", " + hqJumps + " jump" + (hqJumps > 1 ? "s" : "") : "")
							}),
							color: this._menuColor
						};
					}
				}
			}
			text += "\n";
		}
		// if we're at a main station, we can offer an appeal process
		if (player.ship.dockedStation && player.ship.dockedStation.isMainStation) {
			if (this._selectedItem !== 9999 &&
				(itm.hasOwnProperty("lastOffenceDate") === false || (clock.days - itm.date) <= this._severityMatrix[itm.severity].appealTime) &&
				this.$checkAppealStatus(itm) === "none" &&
				this._severityMatrix[itm.severity].appeal === true) {
				curChoices["97_APPEAL"] = {
					text: "[galcop-submit-appeal]",
					color: this._menuColor
				};
			}
			if (this.$checkAppealStatus(itm) === "in progress") {
				curChoices["97_APPEAL"] = {
					text: "[galcop-appeal-inprogress]",
					color: this._disabledColor,
					unselectable: true
				};
			}
		}
		if (itm.galaxy === galaxyNumber) {
			curChoices["97_CLOSE"] = {
				text: "[galcop-exit]",
				color: this._exitColor
			};
			def = "97_CLOSE";
		} else {
			curChoices["97A_CLOSE"] = {
				text: "[galcop-exit]",
				color: this._exitColor
			};
			def = "97A_CLOSE";
		}

		var opts = {
			screenID: "oolite-galcopsecoffice-screen5-map",
			title: "Outstanding Fine",
			overlay: {
				name: "bounty-gavel.png",
				height: 546
			},
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: (this._lastChoice[this._displayType] && this._lastChoice[this._displayType] != "" ? this._lastChoice[this._displayType] : def),
			message: text
		};
	}

	if (this._displayType === 6) { // outstanding intergalactic fines
		text = this.$padTextRight(expandDescription("[galcop-fine-hdr-system]"), 6) +
			this.$padTextRight(expandDescription("[galcop-fine-hdr-date]"), 6) +
			this.$padTextLeft(expandDescription("[galcop-fine-hdr-incidents]"), 5) +
			this.$padTextLeft(expandDescription("[galcop-fine-hdr-bounty]"), 4) +
			this.$padTextLeft(expandDescription("[galcop-fine-hdr-fine]"), 5) + "  " + expandDescription("[galcop-fine-hdr-payable]");
		var list = [];
		for (var i = 0; i < this._offences.length; i++) {
			if (this._offences[i].transferred === false && this._offences[i].galaxy === this._selectedGalaxy) {
				if (this.$calculateIntergalacticItemBounty(this._offences[i]) > 0) {
					var subitems = JSON.parse(this._offences[i].offenceList);
					list.push({
						text: this.$padTextRight(this._offences[i].systemName, 6) +
							this.$padTextRight(this.$setDateSince(this._offences[i].date * 86400), 6) +
							this.$padTextLeft(subitems.length, 5) +
							this.$padTextLeft(formatCredits(this.$calculateIntergalacticItemBounty(this._offences[i]), false, true), 4) +
							(this.$itemIsPayable(this._offences[i]) ? this.$padTextLeft(formatCredits(this.$calculateFine(this._offences[i]), false, true), 5) : this.$padTextLeft(" ", 5)) +
							(this.$itemIsPayable(this._offences[i]) ? "  Yes" : "  " + this.$daysUntilPayable(this._offences[i]) + " days"),
						date: this._offences[i].date,
						index: i
					});
				}
			}
		}

		def = "97B_CLOSE";

		if (list.length > 0) {
			list.sort(compare);

			var maxpage = Math.ceil(list.length / (rows + 1));
			var end = ((this._page * (rows + 1)) + (rows + 1));
			var itemcount = 0;
			if (end > list.length) end = list.length;
			for (var i = (this._page * (rows + 1)); i < end; i++) {
				curChoices["01_FINE-" + (i < 10 ? "0" : "") + i + "~" + list[i].index] = {
					text: list[i].text,
					alignment: "LEFT",
					color: this._itemColor
				};
				itemcount += 1;
			}

			curChoices["94_SPACER"] = "";

			if (maxpage > 1 && this._page < (maxpage - 1)) {
				curChoices["95_NEXT"] = {
					text: "[galcop-nextpage]",
					color: this._menuColor
				};
			} else {
				curChoices["95_NEXT"] = {
					text: "[galcop-nextpage]",
					color: this._disabledColor,
					unselectable: true
				};
			}
			if (this._page > 0) {
				curChoices["96_PREV"] = {
					text: "[galcop-prevpage]",
					color: this._menuColor
				};
			} else {
				curChoices["96_PREV"] = {
					text: "[galcop-prevpage]",
					color: this._disabledColor,
					unselectable: true
				};
			}

			for (var i = 0; i < ((rows) - itemcount); i++) {
				curChoices["99_SPACER_" + i] = "";
			}

			if (this._lastChoice != "") def = this._lastChoice;

		} else {
			text = expandDescription("[galcop-no-warrants]");
		}

		curChoices["97B_CLOSE"] = {
			text: "[galcop-exit]",
			color: this._exitColor
		};

		var opts = {
			screenID: "oolite-galcopsecoffice-screen6-map",
			title: "Outstanding Fines in Sector " + (this._selectedGalaxy + 1),
			overlay: {
				name: "bounty-gavel.png",
				height: 546
			},
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: (this._lastChoice[this._displayType] && this._lastChoice[this._displayType] != "" ? this._lastChoice[this._displayType] : def),
			message: text
		};
	}

	if (this._displayType === 7) { // most wanted detail
		var mw = worldScripts.BountySystem_MostWanted;
		var dl = mw._displayList;
		var itm = dl[this._selectedIndex];
		text = this.$padTextRight(expandDescription("[galcop-mostwanted-pilot]"), 10);
		var colText = this.$columnText("Cmdr. " + itm.pilotName + ", " + itm.pilotDescription, 21);
		for (var i = 0; i < colText.length; i++) {
			if (i != 0) text += this.$padTextRight("", 10);
			text += colText[i] + "\n";
		}
		text += this.$padTextRight(expandDescription("[galcop-mostwanted-shiptype]"), 10) + itm.shipType + "\n";
		text += this.$padTextRight(expandDescription("[galcop-mostwanted-shipname]"), 10) + itm.shipName + "\n";
		text += this.$padTextRight(expandDescription("[galcop-mostwanted-bounty]"), 10) + formatCredits(itm.bounty, false, true) + "\n\n";

		if (itm.lastVisibleSystem >= 0) {
			var jmps = 0;
			var info = System.infoForSystem(galaxyNumber, itm.lastVisibleSystem);
			var rt = system.info.routeToSystem(info, (this._routeMode === "SHORTEST" ? "OPTIMIZED_BY_JUMPS" : "OPTIMIZED_BY_TIME"));
			if (rt && p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY")) {
				var dist = rt.distance;
				jmps = rt.route.length - 1;
			} else {
				var dist = system.info.distanceToSystem(info);
			}
			var jmps2 = 0;
			var info2 = System.infoForSystem(galaxyNumber, itm.lastVisibleDestination);
			var rt2 = system.info.routeToSystem(info2, (this._routeMode === "SHORTEST" ? "OPTIMIZED_BY_JUMPS" : "OPTIMIZED_BY_TIME"));
			if (rt2 && p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY")) {
				var dist2 = rt2.distance;
				jmps2 = rt2.route.length - 1;
			} else {
				var dist2 = system.info.distanceToSystem(info2);
			}

			var rt3 = info.routeToSystem(info2, (this._routeMode === "SHORTEST" ? "OPTIMIZED_BY_JUMPS" : "OPTIMIZED_BY_TIME"));

			text += this.$padTextRight(expandDescription("[galcop-mostwanted-lastseen]"), 10) +
				System.systemNameForID(itm.lastVisibleSystem) + " (" + govs[info.government] + spc +
				"TL" + (info.techlevel + 1) + ", " + dist.toFixed(1) + "ly" + (jmps > 0 ? ", " + jmps + " jump" + (jmps > 1 ? "s" : "") : "") +
				(rt && jmps > 0 && p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY") ? ", " + rt.time.toFixed(1) + " hrs" : "") +
				")\n";
			var upd = this.$getTimeSince(itm.updated, false);
			//log(this.name, "check time " + itm.updated + " -- " + clock.clockStringForTime(itm.updated));
			text += this.$padTextRight(expandDescription("[galcop-mostwanted-updated]"), 10) + this.$adjustTime(clock.clockStringForTime(itm.updated)) +
				" (" + (upd !== "" ? upd + " ago" : "just now") + ")\n";

			if (player.ship.equipmentStatus("EQ_HUNTER_LICENCE") === "EQUIPMENT_OK" && itm.departureTime < clock.adjustedSeconds) {
				var arr = itm.departureTime + (rt3.time * 3600);
				text += this.$padTextRight(expandDescription("[galcop-mostwanted-lastseendest]"), 10) +
					System.systemNameForID(itm.lastVisibleDestination) + " (" + govs[info2.government] + spc +
					"TL" + (info2.techlevel + 1) + ", " + dist2.toFixed(1) + "ly" + (jmps2 > 0 ? ", " + jmps2 + " jump" + (jmps2 > 1 ? "s" : "") : "") +
					(rt2 && jmps2 > 0 && p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY") ? ", " + rt2.time.toFixed(1) + " hrs" : "") +
					")\n";
				if (rt3) {
					text += this.$padTextRight(expandDescription("[galcop-mostwanted-arrivetime]"), 10) + this.$adjustTime(clock.clockStringForTime(arr));
					var timeIn = ""
					if (arr < clock.adjustedSeconds) {
						timeIn = this.$getTimeSince(arr, false) + " ago";
					} else {
						timeIn = "In " + this.$getTimeSince(clock.adjustedSeconds - (arr - clock.adjustedSeconds), false);
					}
					text += " (" + timeIn + ")\n";
				}
			}

			if (player.ship.equipmentStatus("EQ_HUNTER_LICENCE") === "EQUIPMENT_OK") {
				if (mw._missionList.indexOf(itm.index) === -1) {
					curChoices["60_ADD~" + itm.index] = {
						text: "[galcop-mostwanted-add]",
						color: this._menuColor
					};
				} else {
					curChoices["61_REMOVE~" + itm.index] = {
						text: "[galcop-mostwanted-remove]",
						color: this._menuColor
					};
				}
			}
		} else {
			text += this.$padTextRight(expandDescription("[galcop-mostwanted-updated]"), 10) + "N/A\n";
			text += this.$padTextRight(expandDescription("[galcop-mostwanted-lastseen]"), 10) + "--unknown--\n";
		}
		text += "\n";

		var hist = "";
		if (player.ship.equipmentStatus("EQ_HUNTER_LICENCE") === "EQUIPMENT_OK") {
			for (var i = 0; i < itm.lastVisibleHistory.length; i++) {
				if (itm.lastVisibleHistory[i] != null && itm.lastVisibleHistory[i].hasOwnProperty("system") === true) {
					if (hist === "") {
						hist = this.$padTextRight(expandDescription("[galcop-mostwanted-history]"), 10);
					} else {
						hist += this.$padTextRight("", 10);
					}
					hist += System.systemNameForID(itm.lastVisibleHistory[i].system) + ", " + this.$getTimeSince(itm.lastVisibleHistory[i].updated, false) + " ago\n";
					hist += this.$padTextRight("", 11) + "Destination: " + System.systemNameForID(itm.lastVisibleHistory[i].destination) + "\n";
				}
			}
			if (hist !== "") text += hist;
		}
		if (itm.lastVisibleSystem >= 0 && itm.lastVisibleSystem != system.ID) {
			if (itm.departureTime < clock.adjustedSeconds && player.ship.equipmentStatus("EQ_HUNTER_LICENCE") === "EQUIPMENT_OK") {
				curChoices["52_VIEWCHART"] = {
					text: "[galcop-mostwanted-next]",
					color: this._menuColor
				};
				if (p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY") && this.$systemInCurrentPlot(itm.lastVisibleDestination) === false) {
					curChoices["96_SETCOURSE~" + itm.lastVisibleDestination] = {
						text: expandDescription("[galcop-set-course]", {
							destination: System.systemNameForID(itm.lastVisibleDestination)
						}),
						color: this._menuColor
					};
				}
			} else {
				curChoices["52_VIEWCHART"] = {
					text: "[galcop-mostwanted-last]",
					color: this._menuColor
				};
				if (p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY") && this.$systemInCurrentPlot(itm.lastVisibleSystem) === false) {
					curChoices["96_SETCOURSE~" + itm.lastVisibleSystem] = {
						text: expandDescription("[galcop-set-course]", {
							destination: System.systemNameForID(itm.lastVisibleSystem)
						}),
						color: this._menuColor
					};
				}
			}
		}

		if (player.ship.equipmentStatus("EQ_HUNTER_LICENCE") === "EQUIPMENT_OK") {
			curChoices["80_WAIT"] = {
				text: "[galcop-wait]",
				color: this._menuColor
			};
		}

		def = "97C_CLOSE";
		curChoices["97C_CLOSE"] = {
			text: "[galcop-exit]",
			color: this._exitColor
		};

		var opts = {
			screenID: "oolite-galcopsecoffice-screen7-map",
			title: "Most Wanted - Details",
			overlay: {
				name: "mw_badge.png",
				height: 546
			},
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: (this._lastChoice[this._displayType] && this._lastChoice[this._displayType] != "" ? this._lastChoice[this._displayType] : def),
			message: text
		};
	}

	if (this._displayType === 8) { // most wanted last seen map
		var mw = worldScripts.BountySystem_MostWanted;
		var dl = mw._displayList;
		var itm = dl[this._selectedIndex];
		// hold the player's destination
		var rtCurr = system.info.routeToSystem(System.infoForSystem(galaxyNumber, p.targetSystem));
		this._suspendedDestination = p.targetSystem;
		// override it for the display
		if (itm.lastVisibleDestination >= 0 && itm.lastVisibleDestination <= 255 &&
			itm.departureTime < clock.adjustedSeconds &&
			player.ship.equipmentStatus("EQ_HUNTER_LICENCE") === "EQUIPMENT_OK") {
			p.targetSystem = itm.lastVisibleDestination;
		} else {
			p.targetSystem = itm.lastVisibleSystem;
		}

		// put markers on the map
		mission.markSystem({
			system: itm.lastVisibleSystem,
			name: this.name,
			markerShape: "MARKER_SQUARE",
			markerColor: "whiteColor",
			markerScale: 2
		});
		this._markers.push(itm.lastVisibleSystem);

		if (player.ship.equipmentStatus("EQ_HUNTER_LICENCE") === "EQUIPMENT_OK") {
			if (itm.departureTime < clock.adjustedSeconds) {
				mission.markSystem({
					system: itm.lastVisibleDestination,
					name: this.name,
					markerShape: "MARKER_SQUARE",
					markerColor: "whiteColor",
					markerScale: 3
				});
				this._markers.push(itm.lastVisibleDestination);
			}
			var colrs = ["lightGrayColor", "grayColor", "darkGrayColor"];
			for (var i = 0; i < itm.lastVisibleHistory.length; i++) {
				if (itm.lastVisibleHistory[i] && itm.lastVisibleHistory[i].hasOwnProperty("system") && this._markers.indexOf(itm.lastVisibleHistory[i].system) === -1) {
					mission.markSystem({
						system: itm.lastVisibleHistory[i].system,
						name: this.name,
						markerShape: "MARKER_SQUARE",
						markerColor: colrs[i],
						markerScale: 2
					});
					this._markers.push(itm.lastVisibleHistory[i].system);
				}
			}
		}

		text = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
		var bg = "LONG_RANGE_CHART";
		var info = System.infoForSystem(galaxyNumber, p.targetSystem);
		if (system.info.distanceToSystem(info) < 7.4) {
			bg = "SHORT_RANGE_CHART";
			text += "\n\n";
		}
		var rt = system.info.routeToSystem(info, (bg === "SHORT_RANGE_CHART" ? p.routeMode : (this._routeMode === "SHORTEST" ? "OPTIMIZED_BY_JUMPS" : "OPTIMIZED_BY_TIME")));
		if (p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY")) {
			if (p.targetSystem != system.ID) {
				if (rt && rt.route.length > 1) {
					text += this.$padTextRight("", jmpIndent) + expandDescription("[galcop-jumps]") + " " + (rt.route.length - 1);
				}
			}
			if (bg === "LONG_RANGE_CHART") {
				if (this._routeMode === "SHORTEST") {
					bg = "LONG_RANGE_CHART_SHORTEST";
					curChoices["90_SHORTEST"] = {
						text: "[galcop-short-route]",
						color: this._disabledColor,
						unselectable: true
					};
					curChoices["91_QUICKEST"] = {
						text: "[galcop-quick-route]",
						color: this._menuColor
					};
				} else {
					bg = "LONG_RANGE_CHART_QUICKEST";
					curChoices["90_SHORTEST"] = {
						text: "[galcop-short-route]",
						color: this._menuColor
					};
					curChoices["91_QUICKEST"] = {
						text: "[galcop-quick-route]",
						color: this._disabledColor,
						unselectable: true
					};
				}
			}
		}
		if (p.targetSystem != system.ID && (!rtCurr || rtCurr.route.indexOf(p.targetSystem) === -1) && p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY")) {
			curChoices["96_SETCOURSE~" + p.targetSystem] = {
				text: expandDescription("[galcop-set-course]", {
					destination: System.systemNameForID(p.targetSystem)
				}),
				color: this._menuColor
			};
		}

		curChoices["97D_CLOSE"] = {
			text: "[galcop-exit]",
			color: this._exitColor
		};
		def = "97D_CLOSE";

		var opts = {
			screenID: "oolite-galcopsecoffice-screen8-map",
			title: "Course to Last Known Location",
			backgroundSpecial: bg,
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: (this._lastChoice[this._displayType] && this._lastChoice[this._displayType] != "" ? this._lastChoice[this._displayType] : def),
			message: text
		};
	}

	if (this._displayType === 9) { // location of fine system
		var itm = this._offences[this._selectedItem];
		// hold the player's destination
		var rtCurr = system.info.routeToSystem(System.infoForSystem(galaxyNumber, p.targetSystem));
		this._suspendedDestination = p.targetSystem;
		// override it for the display
		p.targetSystem = itm.system;

		var bg = "LONG_RANGE_CHART";
		if (p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY")) {
			if (itm.system != system.ID) {
				var rt = system.info.routeToSystem(System.infoForSystem(galaxyNumber, itm.system), (this._routeMode === "SHORTEST" ? "OPTIMIZED_BY_JUMPS" : "OPTIMIZED_BY_TIME"));
				if (rt && rt.route.length > 1) {
					text = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + this.$padTextRight("", jmpIndent) + expandDescription("[galcop-jumps]") + " " + (rt.route.length - 1);
				}
			}
			if (this._routeMode === "SHORTEST") {
				bg = "LONG_RANGE_CHART_SHORTEST";
				curChoices["90_SHORTEST"] = {
					text: "[galcop-short-route]",
					color: this._disabledColor,
					unselectable: true
				};
				curChoices["91_QUICKEST"] = {
					text: "[galcop-quick-route]",
					color: this._menuColor
				};
			} else {
				bg = "LONG_RANGE_CHART_QUICKEST";
				curChoices["90_SHORTEST"] = {
					text: "[galcop-short-route]",
					color: this._menuColor
				};
				curChoices["91_QUICKEST"] = {
					text: "[galcop-quick-route]",
					color: this._disabledColor,
					unselectable: true
				};
			}
		}
		if (itm.system != system.ID && (!rtCurr || rtCurr.route.indexOf(itm.system) === -1) && p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY")) {
			curChoices["96_SETCOURSE~" + itm.system] = {
				text: expandDescription("[galcop-set-course]", {
					destination: System.systemNameForID(itm.system)
				}),
				color: this._menuColor
			};
		}

		curChoices["97E_CLOSE"] = {
			text: "[galcop-exit]",
			color: this._exitColor
		};
		def = "97E_CLOSE";

		var opts = {
			screenID: "oolite-galcopsecoffice-screen9-map",
			title: "Course to Offence System",
			backgroundSpecial: bg,
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: (this._lastChoice[this._displayType] && this._lastChoice[this._displayType] != "" ? this._lastChoice[this._displayType] : def),
			message: text
		};
	}

	if (this._displayType === 10) { // collect bounty receipts
		var mw = worldScripts.BountySystem_MostWanted;
		var lines = [];
		var total = 0;
		lines.push(this.$padTextRight("Name", 23) + this.$padTextLeft("Bonus Payment", 8));
		lines.push("");
		// work out the amount of the receipts
		// bonus for difficulty (based on accuracy)
		for (var i = 0; i < mw._completed.length; i++) {
			var itm = mw._completed[i];
			var amt = 0;
			var pct = 0;
			switch (itm.primaryRole) {
				case "trader":
				case "trader-courier":
				case "trader-smuggler":
					pct = 0.05;
					break;
				case "hunter":
				case "hunter-medium":
				case "hunter-heavy":
					pct = 0.1;
					break;
				case "pirate":
				case "pirate-light-freighter":
				case "pirate-medium-freighter":
				case "pirate-heavy-freighter":
					pct = 0.2;
					break;
				case "assassin-light":
				case "assassin-medium":
				case "assassin-heavy":
					pct = 0.15;
					break;
			}
			pct += itm.accuracy / 50;
			amt = parseInt(itm.realBounty * pct);
			total += amt;
			var colText = this.$columnText(itm.pilotName + ", " + itm.pilotDescription, 21);
			for (var j = 0; j < colText.length; j++) {
				var lineText = this.$padTextRight(colText[j], 21);
				if (j === 0) lineText += this.$padTextLeft(formatCredits(amt, true, true), 10);
				lines.push(lineText);
			}
		}
		if (lines.length > 0) {
			var maxpage = Math.ceil(lines.length / (rows + 1));
			var end = ((this._page * (rows + 1)) + (rows + 1));
			var itemcount = 0;
			if (end > lines.length) end = lines.length;
			for (var i = (this._page * (rows + 1)); i < end; i++) {
				text += lines[i] + "\n";
				itemcount += 1;
			}

			curChoices["94_SPACER"] = "";

			if (maxpage > 1 && this._page < (maxpage - 1)) {
				curChoices["95_NEXT"] = {
					text: "[galcop-nextpage]",
					color: this._menuColor
				};
			} else {
				curChoices["95_NEXT"] = {
					text: "[galcop-nextpage]",
					color: this._disabledColor,
					unselectable: true
				};
			}
			if (this._page > 0) {
				curChoices["96_PREV"] = {
					text: "[galcop-prevpage]",
					color: this._menuColor
				};
			} else {
				curChoices["96_PREV"] = {
					text: "[galcop-prevpage]",
					color: this._disabledColor,
					unselectable: true
				};
			}
		}

		if (system.ID === this._galCopHQ[galaxyNumber]) {
			curChoices["97_CLAIM"] = {
				text: expandDescription("[galcop-bounty-bonus-claim]", {
					amount: formatCredits(total, true, true)
				}),
				color: this._menuColor
			};
		} else {
			curChoices["97_CLAIM"] = {
				text: expandDescription("[galcop-bounty-bonus-claim]", {
					amount: formatCredits(total, true, true)
				}) + " (Only at GalCop HQ)",
				color: this._disabledColor,
				unselectable: true
			};
		}

		curChoices["98_CLOSE"] = {
			text: "[galcop-exit]",
			color: this._exitColor
		};
		def = "98_CLOSE";

		var opts = {
			screenID: "oolite-galcopsecoffice-screen10-map",
			title: "Bounty Claim Receipts",
			overlay: {
				name: "mw_badge.png",
				height: 546
			},
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: (this._lastChoice[this._displayType] && this._lastChoice[this._displayType] != "" ? this._lastChoice[this._displayType] : def),
			message: text
		};

	}

	if (this._displayType === 11) { // confirm appeal submission
		var itm = this._offences[this._selectedItem];
		var subitems = JSON.parse(itm.offenceList);
		var bounty = 0;
		if (itm.galaxy != galaxyNumber) {
			bounty = this.$calculateIntergalacticItemBounty(itm);
		} else {
			bounty = this.$calculateItemBounty(itm, itm.system);
		}
		var cost = (itm.severity * 2) * 400 + (subitems.length * 200);
		var hq = System.infoForSystem(galaxyNumber, this._galCopHQ[galaxyNumber]);
		var dist = system.info.distanceToSystem(hq);
		var time = Math.pow(dist / 2, 2);

		text = expandDescription("[galcop-appeal-confirm]", {
			amount: formatCredits(bounty, false, true),
			system: itm.systemName,
			date: this.$setDateSince(itm.date * 86400),
			eta: parseInt(time),
			cost: formatCredits(cost, false, true)
		});

		curChoices["01_YES"] = {
			text: "[galcop-appeal-yes]",
			color: this._menuColor
		};
		curChoices["02_NO"] = {
			text: "[galcop-appeal-no]",
			color: this._menuColor
		};
		def = "02_NO";

		var opts = {
			screenID: "oolite-galcopsecoffice-screen11-map",
			title: "Confirm Appeal Submission",
			overlay: {
				name: "galcop_logo.png",
				height: 546
			},
			allowInterrupt: false,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: (this._lastChoice[this._displayType] && this._lastChoice[this._displayType] != "" ? this._lastChoice[this._displayType] : def),
			message: text
		};
	}

	if (this._displayType === 12) { // bounty hunters noticeboard
		// bounty name, source name, date
		text = this.$padTextRight(expandDescription("[galcop-mostwanted-tips-bountyname]"), 10) +
			this.$padTextRight(expandDescription("[galcop-mostwanted-tips-sourcename]"), 10) +
			this.$padTextRight(expandDescription("[galcop-mostwanted-tips-date]"), 7.25) + "\n";
		var mw = worldScripts.BountySystem_MostWanted;
		var list = mw._hunterTips;
		list.sort(compareDateDesc);

		def = "98_CLOSE";

		if (list.length > 0) {
			var maxpage = Math.ceil(list.length / rows);
			var end = ((this._page * rows) + rows);
			var itemcount = 0;
			if (end > list.length) end = list.length;
			for (var i = (this._page * rows); i < end; i++) {
				curChoices["70_TIPS-" + (i < 10 ? "0" : "") + i + "~" + i] = {
					text: this.$padTextRight((mw._missionList.indexOf(list[i].index) >= 0 ? "• " : "") + list[i].bountyName, 10) +
						this.$padTextRight(list[i].source, 10) +
						this.$padTextRight(this.$adjustTime(clock.clockStringForTime(list[i].createdDate)), 7.25),
					alignment: "LEFT",
					color: this._itemColor
				};
				itemcount += 1;
			}

			curChoices["94_SPACER"] = "";
			for (var i = 0; i < (rows - itemcount); i++) {
				curChoices["94_SPACER_" + i] = "";
			}

			if (maxpage > 1 && this._page < (maxpage - 1)) {
				curChoices["95_NEXT"] = {
					text: "[galcop-nextpage]",
					color: this._menuColor
				};
			} else {
				curChoices["95_NEXT"] = {
					text: "[galcop-nextpage]",
					color: this._disabledColor,
					unselectable: true
				};
			}
			if (this._page > 0) {
				curChoices["96_PREV"] = {
					text: "[galcop-prevpage]",
					color: this._menuColor
				};
			} else {
				curChoices["96_PREV"] = {
					text: "[galcop-prevpage]",
					color: this._disabledColor,
					unselectable: true
				};
			}
		}

		def = "98_CLOSE";
		curChoices["98_CLOSE"] = {
			text: "[galcop-exit]",
			color: this._exitColor
		};

		var opts = {
			screenID: "oolite-galcopsecoffice-screen12-map",
			title: "Bounty Hunter Noticeboard" + (maxpage > 1 ? ": Page " + (this._page + 1) + " of " + maxpage : ""),
			overlay: {
				name: "mw_badge.png",
				height: 546
			},
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: (this._lastChoice[this._displayType] && this._lastChoice[this._displayType] != "" ? this._lastChoice[this._displayType] : def),
			message: text
		};
	}

	if (this._displayType === 13) { // bounty hunter notice details
		var mw = worldScripts.BountySystem_MostWanted;
		var itm = mw._hunterTips[this._selectedIndex];
		text = this.$padTextRight(expandDescription("[galcop-mostwanted-tips-bountyname]") + ":", 10);
		text += "Cmdr. " + itm.bountyName + "\n";
		text += this.$padTextRight(expandDescription("[galcop-mostwanted-tips-sourcename]") + ":", 10) + itm.source + "\n";
		var upd = this.$getTimeSince(itm.createdDate, false);
		text += this.$padTextRight(expandDescription("[galcop-mostwanted-tips-date]") + ":", 10) + this.$adjustTime(clock.clockStringForTime(itm.createdDate)) + " (" + (upd != "" ? upd + " ago" : "just now") + ")\n\n";
		text += this.$padTextRight("Notice:", 10);
		var colText = this.$columnText(itm.text, 21);
		for (var i = 0; i < colText.length; i++) {
			if (i != 0) text += this.$padTextRight("", 10);
			text += colText[i] + "\n";
		}

		def = "97F_CLOSE";
		if (this._selectedIndex < mw._hunterTips.length - 1) {
			curChoices["71_NEXTTIP"] = {
				text: "Next notice",
				color: this._itemColor
			};
		} else {
			curChoices["71_NEXTTIP"] = {
				text: "Next notice",
				color: this._disabledColor,
				unselectable: true
			};
		}
		if (this._selectedIndex > 0) {
			curChoices["72_PREVTIP"] = {
				text: "Previous notice",
				color: this._itemColor
			};
		} else {
			curChoices["72_PREVTIP"] = {
				text: "Previous notice",
				color: this._disabledColor,
				unselectable: true
			};
		}
		curChoices["97F_CLOSE"] = {
			text: "[galcop-exit]",
			color: this._exitColor
		};

		var opts = {
			screenID: "oolite-galcopsecoffice-screen13-map",
			title: "Bounty Hunter Noticeboard - Details",
			overlay: {
				name: "mw_badge.png",
				height: 546
			},
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: (this._lastChoice[this._displayType] && this._lastChoice[this._displayType] != "" ? this._lastChoice[this._displayType] : def),
			message: text
		};
	}

	mission.runScreen(opts, this.$secOfficeHandler, this);
}

//-------------------------------------------------------------------------------------------------------------
this.$secOfficeHandler = function (choice) {

	if (this._suspendedDestination) player.ship.targetSystem = this._suspendedDestination;
	this._suspendedDestination = null;
	this.$removeChartData();

	if (!choice) return;

	switch (mission.screenID) {
		case "oolite-galcopsecoffice-screen0-map":
			this._lastChoice[0] = choice;
			break;
		case "oolite-galcopsecoffice-screen1-map":
			this._lastChoice[1] = choice;
			break;
		case "oolite-galcopsecoffice-screen2-map":
			this._lastChoice[2] = choice;
			break;
		case "oolite-galcopsecoffice-screen3-map":
			this._lastChoice[3] = choice;
			break;
		case "oolite-galcopsecoffice-screen4-map":
			this._lastChoice[4] = choice;
			break;
		case "oolite-galcopsecoffice-screen5-map":
			this._lastChoice[5] = choice;
			break;
		case "oolite-galcopsecoffice-screen6-map":
			this._lastChoice[6] = choice;
			break;
		case "oolite-galcopsecoffice-screen7-map":
			this._lastChoice[7] = choice;
			break;
		case "oolite-galcopsecoffice-screen8-map":
			this._lastChoice[8] = choice;
			break;
		case "oolite-galcopsecoffice-screen9-map":
			this._lastChoice[9] = choice;
			break;
		case "oolite-galcopsecoffice-screen10-map":
			this._lastChoice[10] = choice;
			break;
		case "oolite-galcopsecoffice-screen11-map":
			this._lastChoice[11] = choice;
			break;
		case "oolite-galcopsecoffice-screen12-map":
			this._lastChoice[12] = choice;
			break;
		case "oolite-galcopsecoffice-screen13-map":
			this._lastChoice[13] = choice;
			break;
	}

	if (choice === "01_VIEWSTD") {
		this._displayType = 1;
		this._page = 0;
	}
	if (choice === "02_VIEWINTER") this._displayType = 2;
	if (choice === "03_WANTED") {
		this._displayType = 3;
		this._page = 0;
	}
	if (choice === "03_WANTED_BONUS") {
		this._displayType = 10;
		this._page = 0;
	}
	if (choice === "04_GALCOPHQ") this._displayType = 4;
	if (choice === "09_BOUNTY_TIPS") {
		this._displayType = 12;
		this._page = 0;
	}

	if (choice === "02_NO") this._displayType = 5;
	if (choice === "01_YES") {
		this._displayType = 5;
		var itm = this._offences[this._selectedItem];
		var subitems = JSON.parse(itm.offenceList);
		var cost = (itm.severity * 2) * 400 + (subitems.length * 200);
		var apl = {};
		var hq = System.infoForSystem(galaxyNumber, this._galCopHQ[galaxyNumber]);
		var dist = system.info.distanceToSystem(hq);
		apl["status"] = "in progress";
		apl["initiatedOn"] = clock.adjustedSeconds;
		apl["completedBy"] = clock.adjustedSeconds + (Math.pow(dist / 2, 2) * 3600); // a rough calculation of the time based on distance to HQ
		apl["data"] = JSON.parse(JSON.stringify(itm)); // make a copy of the bounty details for reference
		apl["roles"] = JSON.parse(JSON.stringify(player.roleWeights)); // grab a current copy of the player's roles
		this._appeals.push(apl);
		player.credits -= cost;
		player.consoleMessage("You appeal has been lodged. " + formatCredits(cost, false, true) + " has been deducted from your account.");
		// send an email confirmation
		var ga = worldScripts.GalCopAdminServices;
		if (ga) {
			//if (this._registrars[galaxyNumber] === "") this._registrars[galaxyNumber] = expandDescription("%N [nom]");
			if (this._secOffClerk === "") this._secOffClerk = expandDescription("%N [nom]");
			var msg = expandDescription("[galcop-appeal-email]", {
				system: itm.systemName,
				date: this.$setDateSince(itm.date * 86400),
				hq: System.systemNameForID(this._galCopHQ[galaxyNumber]),
				cost: formatCredits(cost, true, true),
				sender: this._secOffClerk
			});
			var w = worldScripts.EmailSystem;
			w.$createEmail({
				sender: "GalCop Security Office",
				subject: "Appeal Submission",
				date: global.clock.adjustedSeconds,
				message: msg,
				expiryDays: ga._defaultExpiryDays
			});
		}

	}

	if (choice.indexOf("01_FINE") >= 0) {
		this._selectedItem = parseInt(choice.substring(choice.indexOf("~") + 1));
		this._displayType = 5;
	}

	if (choice.indexOf("05_GAL") >= 0) {
		this._selectedGalaxy = parseInt(choice.substring(choice.indexOf("~") + 1));
		this._displayType = 6;
		this._page = 0;
	}

	if (choice.indexOf("50_MOSTWANTED") >= 0) {
		this._selectedIndex = parseInt(choice.substring(choice.indexOf("~") + 1));
		this._displayType = 7;
	}

	if (choice === "52_VIEWCHART") this._displayType = 8;
	if (choice === "53_VIEWCHART") this._displayType = 9;

	if (choice.indexOf("60_ADD") >= 0) {
		var idx = parseInt(choice.substring(choice.indexOf("~") + 1));
		var mw = worldScripts.BountySystem_MostWanted;
		mw._missionList.push(idx);
		player.consoleMessage("Record has been added to manifest screen");
	}
	if (choice.indexOf("61_REMOVE") >= 0) {
		var idx = parseInt(choice.substring(choice.indexOf("~") + 1));
		var mw = worldScripts.BountySystem_MostWanted;
		var lookup = mw._missionList.indexOf(idx);
		mw._missionList.splice(lookup, 1);
		player.consoleMessage("Record has been removed from manifest screen");
	}
	if (choice.indexOf("70_TIPS") >= 0) {
		this._selectedIndex = parseInt(choice.substring(choice.indexOf("~") + 1));
		this._displayType = 13;
	}
	if (choice === "71_NEXTTIP") {
		this._selectedIndex += 1;
	}
	if (choice === "72_PREVTIP") {
		this._selectedIndex -= 1;
	}

	if (choice === "80_WAIT") {
		var mw = worldScripts.BountySystem_MostWanted;
		mw.$askWaitTime();
		return;
	}
	if (choice === "90_PAYFINE") {
		var itm = this._offences[this._selectedItem];
		// calculate what the fine is
		var calc_fine = this.$calculateFine(itm);
		if (itm.system != system.ID) calc_fine *= (1 + this._galCopHQFineSurcharge);
		// set which screen will be shown on return
		this._displayType = 1;
		if (itm.galaxy != galaxyNumber) this._displayType = 6;
		if (player.credits >= calc_fine) {
			// send a fine email
			var ga = worldScripts.GalCopAdminServices;
			if (ga) {
				var msg = expandDescription("[marked-for-fines-intro]", {
					fine: formatCredits(calc_fine, true, true)
				});
				if (itm.galaxy === galaxyNumber) {
					if (this.$calculateItemBounty(itm) >= 50) {
						msg += expandDescription("[marked-for-fines-fugitive]");
					}
				} else {
					if (this.$calculateIntergalacticItemBounty(itm) >= 50) {
						msg += expandDescription("[marked-for-fines-fugitive]");
					}
				}
				var w = worldScripts.EmailSystem;
				msg += expandDescription("[marked-for-fines-end]");
				w.$createEmail({
					sender: expandDescription("[marked-for-fines-sender]"),
					subject: "Fine payment",
					date: global.clock.adjustedSeconds,
					message: msg,
					expiryDays: ga._defaultExpiryDays
				});
			}
			player.credits -= calc_fine;
			// are there any transferred items that match this one? take a copy of the relevant data before we delete it
			var tfr = {
				bounty: itm.bounty,
				severity: itm.severity,
				galaxy: itm.galaxy,
				system: itm.system,
				systemName: itm.systemName,
				government: itm.government,
				date: itm.date
			};
			// delete the record
			this._offences.splice(this._selectedItem, 1);
			// remove any transferred offences
			this.$removeTransferredOffences(tfr);
			// update the players bounty
			this._changing = true;
			player.bounty = this.$calculateBounty(system.ID);
			this._changing = false;

		} else {
			player.consoleMessage("Insufficient credit to pay fine.");
		}
	}

	if (choice.indexOf("96_SETCOURSE") >= 0) {
		var dest = parseInt(choice.substring(choice.indexOf("~") + 1));
		if (dest >= 0 && dest <= 255) {
			player.ship.targetSystem = dest;
			player.ship.infoSystem = player.ship.targetSystem;
		}
		player.consoleMessage("Course set for " + System.systemNameForID(dest));
		switch (this._displayType) {
			case 5:
				this._displayType = 1;
				break;
			case 7:
				this._displayType = 3;
				break;
			case 8:
				this._displayType = 7;
				break;
			case 9:
				this._displayType = 5;
				break;
			default:
				this._displayType = 0;
				break;
		}
	}

	if (choice === "97_APPEAL") this._displayType = 11;
	if (choice === "97_CLAIM") {
		var mw = worldScripts.BountySystem_MostWanted;
		var total = 0;
		// work out the amount of the receipts
		// bonus for difficulty (based on accuracy)
		var txt = "";
		for (var i = 0; i < mw._completed.length; i++) {
			var itm = mw._completed[i];
			var amt = 0;
			var pct = 0;
			switch (itm.primaryRole) {
				case "trader":
				case "trader-courier":
				case "trader-smuggler":
					pct = 0.05;
					break;
				case "hunter":
				case "hunter-medium":
				case "hunter-heavy":
					pct = 0.1;
					break;
				case "pirate":
				case "pirate-light-freighter":
				case "pirate-medium-freighter":
				case "pirate-heavy-freighter":
					pct = 0.2;
					break;
				case "assassin-light":
				case "assassin-medium":
				case "assassin-heavy":
					pct = 0.15;
					break;
			}
			pct += itm.accuracy / 50;
			amt = parseInt(itm.realBounty * pct);
			total += amt;
			txt += " " + formatCredits(amt, true, true) + " for " + itm.pilotName + ", " + itm.pilotDescription + "\n";
		}
		player.credits += total;
		player.consoleMessage("You have been awarded " + formatCredits(total, true, true))
		// send a fine email
		var w = worldScripts.EmailSystem;
		if (w) {
			var ga = worldScripts.GalCopAdminServices;
			ga.$setupRepNames(player.ship.dockedStation);
			var msg = expandDescription("[galcop-bounty-bonus-email]", {
				amount: formatCredits(total, true, true),
				data: txt,
				sender: ga._galCopBountyOfficer
			});
			w.$createEmail({
				sender: expandDescription("[galcop-bounty-sender]"),
				subject: "Bounty Bonus Claim",
				date: global.clock.adjustedSeconds,
				message: msg,
				expiryDays: ga._defaultExpiryDays
			});
		}
		mw._completed.length = 0;
		this._displayType = 0;
	}
	if (choice === "90_SHORTEST") this._routeMode = "SHORTEST";
	if (choice === "91_QUICKEST") this._routeMode = "QUICKEST";
	if (choice === "95_NEXT") this._page += 1;
	if (choice === "96_PREV") this._page -= 1;
	if (choice === "97_CLOSE") this._displayType = 1;
	if (choice === "97A_CLOSE") this._displayType = 6;
	if (choice === "97B_CLOSE") this._displayType = 2;
	if (choice === "97C_CLOSE") this._displayType = 3;
	if (choice === "97D_CLOSE") this._displayType = 7;
	if (choice === "97E_CLOSE") this._displayType = 5;
	if (choice === "97F_CLOSE") this._displayType = 12;
	if (choice === "98_CLOSE") this._displayType = 0;

	if (choice != "99_EXIT") this.$showSecOffice();
}

//-------------------------------------------------------------------------------------------------------------
// adds an offence to the current run sheet
this.$addOffence = function (offenceType) {
	if (this._debug) log(this.name, "adding offence: " + offenceType);
	// have we added this offence already?
	if (this._holdingDate === 0) this._holdingDate = clock.days;
	if (this._holding.indexOf(offenceType) === -1) {
		this._holding.push(offenceType);
		// update the severity of the current set of offences
		var sev = this._offenceTypes[offenceType].severity;
		if (sev && sev > this._severity) {
			this._severity = sev;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// removes any transferred bounties when the original one is paid off
this.$removeTransferredOffences = function (offenceItem) {
	for (var i = this._offences.length - 1; i >= 0; i--) {
		var itm = this._offences[i];
		if (itm.date === offenceItem.date &&
			itm.severity === offenceItem.severity &&
			itm.bounty === offenceItem.bounty &&
			itm.transferred === true)
			this._offences.splice(i, 1);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$anarchiesBountyCalcOverride = function () {
	// this function intentionally left blank...
}

//-------------------------------------------------------------------------------------------------------------
this.$simulatorRunning = function () {
	var w = worldScripts["Combat Simulator"];
	var result = false;
	if (w && w.$checkFight && w.$checkFight.isRunning) result = true;
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// returns true if item can be paid off, otherwise false
this.$itemIsPayable = function (offenceItem) {
	var payableIn = this._severityMatrix[offenceItem.severity].payableIn;
	if ((offenceItem.date + payableIn) <= global.clock.days) return true;
	return false;
}

//-------------------------------------------------------------------------------------------------------------
// returns the number of days left until the item can be paid off
this.$daysUntilPayable = function (offenceItem) {
	var payableIn = this._severityMatrix[offenceItem.severity].payableIn;
	return parseInt((offenceItem.date + payableIn) - global.clock.days);
}

//-------------------------------------------------------------------------------------------------------------
// returns true if the player has committed a crime in a given system, otherwise false
this.$playerIsCriminal = function (systemID) {
	for (var i = 0; i < this._offences.length; i++) {
		if (this._offences[i].transferred === false && this._offences[i].galaxy === galaxyNumber && this._offences[i].system === systemID) return true;
	}
	return false;
}

//-------------------------------------------------------------------------------------------------------------
// adds transferable bounties to the current system
this.$uncoverBounties = function (source) {
	for (var i = 0; i < this._offences.length; i++) {
		if (((this._offences[i].severity === 2 && this._offences[i].galaxy === galaxyNumber) || this._offences[i].severity === 3) && this._offences[i].transferred === false) {
			// make sure we haven't already transferred this offence locally
			if (this.$itemAlreadyTransferred(this._offences[i]) === false) {
				var itm = this._offences[i];
				this._offences.push({
					offenceList: itm.offenceList,
					bounty: itm.bounty,
					severity: itm.severity,
					galaxy: galaxyNumber,
					system: system.ID,
					systemName: system.info.name,
					government: system.info.government,
					date: itm.date,
					transferred: true,
					originatingSystem: itm.system,
					originatingGalaxy: itm.galaxy
				});
			}
		}
		// severity 1 crimes can be transferred at certain distances
		if (this._severity1Transfer === true && this._offences[i].severity === 1 && this._offences[i].galaxy === galaxyNumber && this._offences[i].transferred === false) {
			var tfr = false;
			// check if the distance to the original offence is within the acceptable range
			var maxdist = this._severityMatrix[this._offences[i].severity].transferLimit;
			var info = System.infoForSystem(galaxyNumber, this._offences[i].system);
			var dist = System.infoForSystem(galaxyNumber, system.ID).distanceToSystem(info);
			if (dist <= maxdist) tfr = true;

			if (tfr === true) {
				// make sure we haven't already transferred this offence locally
				if (this.$itemAlreadyTransferred(this._offences[i]) === false) {
					var itm = this._offences[i];
					this._offences.push({
						offenceList: itm.offenceList,
						bounty: itm.bounty,
						severity: itm.severity,
						galaxy: galaxyNumber,
						system: system.ID,
						systemName: system.info.name,
						government: system.info.government,
						date: itm.date,
						transferred: true,
						originatingSystem: itm.system,
						originatingGalaxy: itm.galaxy
					});
				}
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// checks if the item we're about the transfer has already be transferred to the current system
this.$itemAlreadyTransferred = function (offenceItem) {
	var result = false;

	for (var i = 0; i < this._offences.length; i++) {
		if (this._offences[i].transferred === true &&
			this._offences[i].bounty === offenceItem.bounty &&
			this._offences[i].severity === offenceItem.severity &&
			this._offences[i].system === system.ID &&
			this._offences[i].galaxy === offenceItem.galaxy &&
			this._offences[i].date === offenceItem.date &&
			this._offences[i].offenceList === offenceItem.offenceList) {
			result = true;
			break;
		}
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// removes any zero bounty items from the array
this.$cleanUp = function () {
	for (var i = this._offences.length - 1; i >= 0; i--) {
		if (this._offences[i].bounty === 0) this._offences.splice(i, 1);
	}
}

//-------------------------------------------------------------------------------------------------------------
// removes any bogus data that might end up hanging around
this.$resetBogusData = function () {
	// reset bogus data - holding items without any bountyDelta
	if ((this._holding.length > 0 || this._severity > 1) && this._bountyDelta === 0) {
		this._holding.length = 0;
		this._holdingDate = 0;
		this._severity = 1;
	}
	// reset bogus data - bountyDelta without any holding item offences
	if (this._bountyDelta != 0 && this._holding.length === 0) {
		this._bountyDelta = 0;
		this._severity = 1;
	}
	for (var i = this._offences.length - 1; i >= 0; i--) {
		/*log(this.name, "bounty " + this._offences[i].bounty + 
			", tfr = " + this._offences[i].transferred + 
			", severity = " + this._offences[i].severity + 
			", orig sys = " + this._offences[i].originatingSystem + 
			", sys = " + this._offences[i].system +
			", orig gal = " + this._offences[i].originatingGalaxy +
			", gal = " + this._offences[i].galaxy);*/

		// make sure we don't have any dodgy entries in the list
		if (this._offences[i].severity === 0 || this._offences[i].offenceList === "[]") this._offences.splice(i, 1);
	}
}

//-------------------------------------------------------------------------------------------------------------
// reduces bounty by an amount
this.$reduceBounty = function (amount, severity, systemID) {
	var remain = amount;
	for (var i = 0; i < this._offences.length; i++) {
		var itm = this._offences[i];
		// only update non-transferred bounties
		if (itm.transferred === false) {
			if (severity === 1 && itm.galaxy === galaxyNumber && itm.system === systemID) {
				if (itm.severity === severity) {
					if (itm.bounty < remain) {
						remain = remain - itm.bounty;
						itm.bounty = 0;
						this.$updateTransferredBounty(itm);
					} else {
						itm.bounty -= remain;
						this.$updateTransferredBounty(itm);
						remain = 0;
					}
				}
			}
			if (severity === 2 && itm.galaxy === galaxyNumber) {
				if (itm.severity === severity) {
					if (itm.bounty < remain) {
						remain = remain - itm.bounty;
						itm.bounty = 0;
						this.$updateTransferredBounty(itm);
					} else {
						itm.bounty -= remain;
						remain = 0;
						this.$updateTransferredBounty(itm);
					}
				}
			}
			if (severity === 3) {
				if (itm.severity === severity) {
					if (itm.bounty < remain) {
						remain = remain - itm.bounty;
						itm.bounty = 0;
						this.$updateTransferredBounty(itm);
					} else {
						itm.bounty -= remain;
						this.$updateTransferredBounty(itm);
						remain = 0;
					}
				}
			}
		}
	}
	return remain;
}

//-------------------------------------------------------------------------------------------------------------
// updates any transferred bounties to match the original
this.$updateTransferredBounty = function (offenceItem) {
	for (var i = 0; i < this._offences.length; i++) {
		var itm = this._offences[i];
		if (itm.date === offenceItem.date &&
			itm.severity === offenceItem.severity &&
			itm.bounty != offenceItem.bounty &&
			itm.transferred === true)
			itm.bounty = offenceItem.bounty;
	}
}

//-------------------------------------------------------------------------------------------------------------
// returns the calculated bounty value for the player in the current system
this.$calculateBounty = function (systemID) {
	var total = 0;
	for (var i = 0; i < this._offences.length; i++) {
		var itm = this._offences[i];
		total += this.$calculateItemBounty(itm, systemID);
	}
	// check to see if the player has any active bounties in the current system
	if (systemID === this._system && this._bountyDelta != 0) total += this._bountyDelta;
	return parseInt(total);
}

//-------------------------------------------------------------------------------------------------------------
// returns the calculated bounty value for a particular offence
this.$calculateItemBounty = function (offenceItem, systemID) {
	var bounty = 0;
	var sysID = (systemID ? systemID : system.ID);
	// because we making a copy of warrants when the player is scanned, basically if the galaxy and system ID match, this is visible
	if (offenceItem.system === sysID && offenceItem.galaxy === galaxyNumber && (offenceItem.transferred === false ||
			(offenceItem.transferred === true && (offenceItem.originatingSystem != sysID || offenceItem.originatingGalaxy != galaxyNumber))))
		bounty = offenceItem.bounty;
	return bounty;
}

//-------------------------------------------------------------------------------------------------------------
// calculates the intergalactic bounty on a particular item
this.$calculateIntergalacticItemBounty = function (offenceItem) {
	if (offenceItem.severity === 3) {
		return offenceItem.bounty;
	} else {
		return 0;
	}
}

//-------------------------------------------------------------------------------------------------------------
// returns the calculated intergalactic bounty value for the player
this.$calculateIntergalacticBounty = function (galNum) {
	var total = 0;
	for (var i = 0; i < this._offences.length; i++) {
		var itm = this._offences[i];
		if (itm.galaxy === galNum && itm.transferred === false) {
			total += this.$calculateIntergalacticItemBounty(itm);
		}
	}
	return parseInt(total);
}

//-------------------------------------------------------------------------------------------------------------
// calculates the bounty fine to pay on a particular item
this.$calculateFine = function (offenceItem) {
	var gov = 0;
	var calc_fine = 0;
	if (offenceItem.system === -1) {
		gov = 1;
	} else {
		gov = offenceItem.government;
	}
	calc_fine = 50 + ((gov < 2 || gov > 5) ? 50 : 0);
	calc_fine *= this.$degradeBounty(offenceItem);
	// check for a minimum fine payment
	if (calc_fine < 50) calc_fine = 50;
	return calc_fine;
}

//-------------------------------------------------------------------------------------------------------------
// degrade the bounty for fine calculations
this.$degradeBounty = function (offenceItem) {
	var b = offenceItem.bounty;
	// degrade the bounty by time since offence
	var degrade = this._severityMatrix[offenceItem.severity].degradeRate;
	var timeframe = (global.clock.days - offenceItem.date);
	b -= (timeframe * degrade);
	return (parseInt(b) > 0 ? parseInt(b) : 0);
}

//-------------------------------------------------------------------------------------------------------------
this.$clearTransferredBounties = function () {
	for (var i = this._offences.length - 1; i >= 0; i--) {
		var item = this._offences[i];
		if (item.transferred === true && item.originatingGalaxy != galaxyNumber) {
			this._offences.splice(i, 1);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$checkAppealStatus = function (item) {
	var result = "none";
	if (this._appeals.length > 0) {
		for (var i = 0; i < this._appeals.length; i++) {
			if (this._appeals[i].data.system === item.system && this._appeals[i].data.date === item.date) result = this._appeals[i].status;
		}
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// find all police or main stations in range of ship
this.$findLawVessels = function (npc) {
	function _ships(entity) {
		return entity.isShip && !(entity.isPlayer) && (entity.isPolice || (entity.isStation && entity.isMainStation));
	}
	return system.filteredEntities(this, _ships, npc, npc.scannerRange);
}

//-------------------------------------------------------------------------------------------------------------
// returns true if a HUD with allowBigGUI is enabled, otherwise false
this.$isBigGuiActive = function () {
	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;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// appends space to currentText to the specified length in 'em'
this.$padTextRight = function (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;
	}
}

//-------------------------------------------------------------------------------------------------------------
// appends space to currentText to the specified length in 'em'
this.$padTextLeft = function (currentText, desiredLength) {
	return this.$padTextRight(currentText, desiredLength, true);
}

//-------------------------------------------------------------------------------------------------------------
// arranges text into a array of strings with a particular column width
this.$columnText = function (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;
}

//-------------------------------------------------------------------------------------------------------------
this.$adjustTime = function (itemTime) {
	return itemTime.substring(0, itemTime.length - 3);
}

//-------------------------------------------------------------------------------------------------------------
// returns a string containing the days, hours, minutes (and possibly seconds) since a particular time
this.$getTimeSince = function (lastTime, hoursOnly) {
	var diff = clock.adjustedSeconds - lastTime;
	var result = "";
	if (diff > 0) {
		var days = (hoursOnly && hoursOnly === true ? 0 : Math.floor(diff / 86400));
		var hours = Math.floor((diff - (days * 86400)) / 3600);
		var mins = Math.floor((diff - (days * 86400 + hours * 3600)) / 60);
		var secs = Math.floor(diff - (days * 86400) - (hours * 3600) - (mins * 60));
		// special case - reduce 1 hour down to mins
		if (days === 0 && hours === 1 && mins < 40) {
			hours = 0;
			mins += 60;
		}
		// special case - reduce 1 min down to secs
		if (days === 0 && hours === 0 && mins === 1 && secs < 40) {
			mins = 0;
			secs += 60;
		}
		if (hoursOnly && hoursOnly === true && mins > 30 && hours > 1) hours += 1;
		if (days > 0) result += days + " days";
		if (hours > 0) result += (result === "" ? "" : " ") + hours + " hours";
		if ((hoursOnly == null || hoursOnly === false) || (hours === 0 && mins > 0)) {
			if (mins > 0) result += (result === "" ? "" : " ") + mins + " mins";
		}
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
this.$removeChartData = function () {
	if (this._markers.length > 0) {
		for (var i = 0; i < this._markers.length; i++) {
			mission.unmarkSystem({
				system: this._markers[i],
				name: this.name
			});
		}
	}
	this._markers.length = 0;
}

//-------------------------------------------------------------------------------------------------------------
this.$setDateSince = function (checkDate) {
	var testDate = new Date();
	testDate.setFullYear(3142, 2, 21);

	var secondsPassedTotal = checkDate - 2084004 * 86400.0;
	var hoursPassed = Math.floor(secondsPassedTotal / 3600);
	var minutesPassed = Math.floor((secondsPassedTotal % 3600) / 60);
	var secondsPassed = Math.floor(secondsPassedTotal % 60);

	testDate.setHours(hoursPassed);
	testDate.setMinutes(minutesPassed);
	testDate.setSeconds(secondsPassed);

	var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
	return testDate.getDay() + " " + months[testDate.getMonth()] + " " + testDate.getFullYear();
}

//-------------------------------------------------------------------------------------------------------------
// returns true if the passes System ID is in the player's currently plotted course, otherwise false
this.$systemInCurrentPlot = function (sysID) {
	var result = false;
	var target = player.ship.targetSystem;

	if (oolite.compareVersion("1.81") < 0) {
		// 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, global.system.ID).routeToSystem(System.infoForSystem(galaxyNumber, target), player.ship.routeMode);
		if (myRoute) {
			if (myRoute.route.indexOf(sysID) >= 0) result = true;
		}
	} else {
		if (target === sysID) result = true;
	}

	return result;
}

//-------------------------------------------------------------------------------------------------------------
this.$changeSettings = function () {
	worldScripts.BountySystem_WarrantScanner._scannerMode = this._warrantScannerMode;
	// switch to passive (ie no hands!)
	var p = player.ship;
	var eqsts = p.equipmentStatus("EQ_WARRANT_SCANNER");
	if (eqsts && (eqsts == "EQUIPMENT_OK" || egsts == "EQUIPMENT_DAMAGED")) {
		if (this._warrantScannerMode >= 1) {
			p.removeEquipment("EQ_WARRANT_SCANNER");
			p.awardEquipment("EQ_WARRANT_SCANNER_PASSIVE");
			p.setEquipmentStatus("EQ_WARRANT_SCANNER_PASSIVE", eqsts);
		}
		return;
	}
	eqsts = p.equipmentStatus("EQ_WARRANT_SCANNER_PASSIVE");
	if (eqsts && (eqsts == "EQUIPMENT_OK" || egsts == "EQUIPMENT_DAMAGED")) {
		if (this._warrantScannerMode < 1) {
			p.removeEquipment("EQ_WARRANT_SCANNER_PASSIVE");
			p.awardEquipment("EQ_WARRANT_SCANNER");
			p.setEquipmentStatus("EQ_WARRANT_SCANNER", eqsts);
		}
		return;
	}
	eqsts = p.equipmentStatus("EQ_WARRANT_SCANNER_POLICE");
	if (eqsts && (eqsts == "EQUIPMENT_OK" || egsts == "EQUIPMENT_DAMAGED")) {
		if (this._warrantScannerMode >= 1) {
			p.removeEquipment("EQ_WARRANT_SCANNER_POLICE");
			p.awardEquipment("EQ_WARRANT_SCANNER_POLICE_PASSIVE");
			p.setEquipmentStatus("EQ_WARRANT_SCANNER_POLICE_PASSIVE", eqsts);
		}
		return;
	}
	eqsts = p.equipmentStatus("EQ_WARRANT_SCANNER_POLICE_PASSIVE");
	if (eqsts && (eqsts == "EQUIPMENT_OK" || egsts == "EQUIPMENT_DAMAGED")) {
		if (this._warrantScannerMode < 1) {
			p.removeEquipment("EQ_WARRANT_SCANNER_POLICE_PASSIVE");
			p.awardEquipment("EQ_WARRANT_SCANNER_POLICE");
			p.setEquipmentStatus("EQ_WARRANT_SCANNER_POLICE", eqsts);
		}
		return;
	}	
}
Scripts/bountysystem_npcscan.js
"use strict";
this.name = "BountySystem_NPCScan";
this.author = "phkb";
this.copyright = "2016 phkb";
this.description = "Routines for NPC's using the Warrant scanning";
this.licence = "CC BY-NC-SA 4.0";

//-------------------------------------------------------------------------------------------------------------
this.$initialiseTimer = function (overrideFreq) {
	this.ship.script._debugNPCScan = false;
	this.ship.script._loggingLevel = 2;
	this.ship.script._rangeCheck = 0;

	// grab the scan time and range from the equipment info record
	// but only do this once - once we have the numbers we don't need to look them up again.
	if (!this.ship.script._warrantScannerTime) {
		if (this.ship.equipmentStatus("EQ_WARRANT_SCANNER") === "EQUIPMENT_OK")
			var eq = EquipmentInfo.infoForKey("EQ_WARRANT_SCANNER");
		if (this.ship.equipmentStatus("EQ_WARRANT_SCANNER_POLICE") === "EQUIPMENT_OK")
			var eq = EquipmentInfo.infoForKey("EQ_WARRANT_SCANNER_POLICE");

		if (eq) {
			this.ship.script._warrantScannerTime = eq.scriptInfo.scan_time;
			this.ship.script._warrantScannerRange = this.ship.scannerRange * eq.scriptInfo.scan_range;
			this.ship.script._warrantScannerFrequency = eq.scriptInfo.npc_scan_frequency;
		}
	}

	if ((overrideFreq != null && isNaN(overrideFreq) === false) || isNaN(this.ship.script._warrantScannerFrequency) === false) {
		// run a timer so that, at regular intervals, the NPC ship will consider doing a warrant scan
		this.ship.script._checkTimer = new Timer(this, this.ship.script.$checkScannerForTarget, (overrideFreq == null ? this.ship.script._warrantScannerFrequency : overrideFreq), 0);
		if (!this.ship.script._scannedList) this.ship.script._scannedList = [];
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$checkScannerForTarget = function $checkScannerForTarget() {

	// make sure we're still a valid entity
	if (!this.ship.isValid || !this.ship.isInSpace) return;

	// if we don't have the scanner or its damaged, we don't go any further
	if (this.ship.equipmentStatus("EQ_WARRANT_SCANNER") != "EQUIPMENT_OK" && this.ship.equipmentStatus("EQ_WARRANT_SCANNER_POLICE") != "EQUIPMENT_OK") {
		//log(this.name, "!NOTE: Warrant Scanner equipment failure");
		return;
	}
	// the ship needs a valid AI to work
	if (this.ship.AIScript.name === "Null AI") return;

	// free up the ship's target if it's still on the last target and we aren't fighting it
	//if (this.ship.script._lastTarget && this.ship.script._lastTarget === this.ship.target && this.ship.hasHostileTarget === false) this.ship.target = null;

	// if the ship already has a target don't do anything
	/*if (this.ship.target && this.ship.target != this.ship.script._checkTarget) {
		if (this.ship.script._debugNPCScan && this.ship.script._loggingLevel > 1) log(this.name, "ship already has target (" + this.ship.target + ") :" + this.ship);
		this.ship.script.$initialiseTimer();
		return;
	}*/

	// don't do it under red alert condition
	if (this.ship.alertCondition === 3) {
		if (this.ship.script._debugNPCScan && this.ship.script._loggingLevel > 1) log(this.name, "ship is under attack :" + this.ship);
		this.ship.script.$initialiseTimer();
		return;
	}

	// random chance that the ship won't initiate a scan: 20% for police, 50% for bounty hunters
	if ((this.ship.isPolice && Math.random() > 0.8) || Math.random() > 0.5) {
		if (this.ship.script._debugNPCScan && this.ship.script._loggingLevel > 1) log(this.name, "ship decides not to scan :" + this.ship);
		this.ship.script.$initialiseTimer(15);
		return;
	}

	// get a list of ships
	var ships = this.ship.checkScanner(true);

	// if there's no ships in range, try again later
	if (!ships || ships.length === 0) {
		this.ship.script.$initialiseTimer();
		return;
	}

	for (var i = 0; i < ships.length; i++) {
		var found = false;
		if (this.ship.isPolice) {
			// police will only scan clean ships, and they won't scan pirates - they already know to attack them!
			if (ships[i].status === "STATUS_IN_FLIGHT" && ships[i].bounty === 0 && (Ship.roleIsInCategory(ships[i].primaryRole, "oolite-assassin") || Ship.roleIsInCategory(ships[i].primaryRole, "oolite-trader") || ships[i].isPlayer) && this.ship.script._scannedList.indexOf(ships[i]) === -1) {
				found = true;
			}
		} else {
			// bounty hunters will scan pirates, looking to maximise their profits
			if (ships[i].status === "STATUS_IN_FLIGHT" && (Ship.roleIsInCategory(ships[i].primaryRole, "oolite-pirate") || Ship.roleIsInCategory(ships[i].primaryRole, "oolite-assassin") || Ship.roleIsInCategory(ships[i].primaryRole, "oolite-trader") || ships[i].isPlayer) && this.ship.script._scannedList.indexOf(ships[i]) === -1) {
				found = true;
			}
		}
		if (found === true) {
			// we have a winner!
			if (this.ship.script._debugNPCScan && this.ship.script._loggingLevel >= 1) log(this.name, "scan target selected :" + this.ship);
			if (this.ship.script._debugNPCScan && this.ship.script._loggingLevel >= 1) log(this.name, "target selected = " + ships[i]);
			//this.ship.target = ships[i];
			this.ship.script._checkTarget = ships[i];
			this.ship.script._scanPass = 0;
			this.ship.script._warrantScannerTimer = new Timer(this, this.ship.script.$scanTarget, 1, 1);
			return;
		}
	}
	this.ship.script.$initialiseTimer();
}

//-------------------------------------------------------------------------------------------------------------
this.$scanTarget = function $scanTarget() {
	var that = $scanTarget; // pointer to this function
	var core = (that.core = that.core || worldScripts.BountySystem_Core); // cache worldScript reference in local property on this function
	var w = (that.w = that.w || worldScripts.BountySystem_WarrantScanner); // cache worldScript reference in local property on this function
	var npcscan = (that.npcscan = that.npcscan || worldScripts.BountySystem_NPCScan); // cache worldScript reference in local property on this function
	var bcc = (that.bcc = that.bcc || worldScripts.BroadcastCommsMFD); // cache worldScript reference in local property on this function
	var myShip = this.ship,
		myScript = myShip.script,
		myTarget = myScript._checkTarget;
	if (myScript._debugNPCScan && myScript._loggingLevel > 1) log(this.name, "running scanTarget :" + myShip);
	// make sure ship is valid
	if (!myShip.isValid) {
		myScript._warrantScannerTimer.stop();
		return;
	}
	if (myShip.alertCondition === 3) {
		// ship is in a battle
		if (myScript._debugNPCScan && myScript._loggingLevel > 1) log(this.name, "ship in red alert - cancelling scan :" + myShip);
		myScript._warrantScannerTimer.stop();
		myTarget = null;
		myScript.$initialiseTimer();
		return;
	}
	/*if (myShip.target != myTarget || !myTarget.isValid) {
		// somethings happened and their target has switched
		if (myScript._debugNPCScan && myScript._loggingLevel > 1) log(this.name, "target has shifted - cancelling scan :" + myShip);
		myScript._warrantScannerTimer.stop();
		myTarget = null;
		myScript.$initialiseTimer();
		return;
	}*/
	// is target still valid
	var result = w.$targetIsValid(myShip);
	if (result) {
		// are we in range?
		if (w.$targetInRange(myShip)) {
			myScript._rangeCheck = 0;
			if (myScript._debugNPCScan && myScript._loggingLevel > 1) log(this.name, "scanning ship (" + myTarget + ") :" + myShip);
			// ok, do a pass
			myScript._scanPass += 1;
			if (myScript._debugNPCScan && myScript._loggingLevel > 1) log(this.name, "pass " + this._scanPass + " :" + myShip);
			if (myScript._scanPass === 1 && myTarget.isPlayer) player.consoleMessage(expandDescription("[warrant-scan-detected]"), 5);
			if (myScript._scanPass === 2 && myTarget.isPlayer === false) w.$npcReaction(myTarget, myShip);
			// are we done?
			if (myScript._scanPass >= myScript._warrantScannerTime) {
				myScript._warrantScannerTimer.stop();
				if (!myTarget.isPlayer || !core._alreadyScannedPlayerInThisSystem) { // only apply out-of-system bounties to the player once per system visit
					if (myTarget.isPlayer) core._alreadyScannedPlayerInThisSystem = true; // remember that we did this
					// ok, get the hidden bounty;
					if (myScript._debugNPCScan && myScript._loggingLevel > 1) log(this.name, "scan complete :" + myShip);
					myScript._scannedList.push(myTarget);

					var initial = myTarget.bounty;
					var check = w.$checkBounty(myTarget, true, myShip); // returns sum of out-of-system bounties and imports any transferable bounties to the current system (this affects the calculation in shipWillDockWithStation that sets player bounty)
					if (check > 0) { // an out-of-system bounty was found
						if (myTarget.isPlayer && bcc && bcc._surrenderBounty > 0) { // player has a hidden bounty after surrendering to police with Broadcast Comms
							bcc._surrenderBounty += check; // add bounty amount to Broadcast Comms' "hidden" value to be restored if the player commits another crime or doesn't go to the station
							if (myScript._debugNPCScan && myScript._loggingLevel >= 1) log(this.name, "bounty of " + check + " added to Broadcast Comms' hidden bounty value for surrendered " + myTarget);
						} else { // apply the bounty directly
							myTarget.bounty += check;
							if (myScript._debugNPCScan && myScript._loggingLevel >= 1) log(this.name, "bounty change of " + (myTarget.bounty - initial) + " for " + myTarget);
						}
					}
				}

				// add this target to all vessels in range who have the same scan class
				npcscan.$addTargetToScannedList(myShip, myTarget); // call to worldScript function from ship-attached script

				myScript._lastTarget = myTarget;
				myTarget = null;
				myScript.$initialiseTimer();
			}
		} else {
			myScript._rangeCheck += 1;
			// try to move in range of target so we can continue scanning
			if (myShip.position.distanceTo(myTarget) < (myShip.scannerRange * 1.3) && myScript._rangeCheck < 30) {
				if (myScript._debugNPCScan && myScript._loggingLevel > 1) log(this.name, "attempting to get in range :" + myShip);
				myShip.destination = myTarget.position;
				myShip.desiredRange = myScript._warrantScannerRange - parseInt(Math.random * 1000) + 200;
				myShip.desiredSpeed = myShip.maxSpeed;
			} else {
				// give up if the distance is too great, or the range check counter times out
				if (myScript._debugNPCScan && myScript._loggingLevel > 1) log(this.name, "unable to get in range of target (timeout or distance too far) :" + myShip);
				myScript._warrantScannerTimer.stop();
				//myShip.target = null;
				myTarget = null;
				myScript.$initialiseTimer();
			}
		}
	} else {
		if (myScript._debugNPCScan && myScript._loggingLevel > 1) log(this.name, "target no longer valid :" + myShip);
		myScript._warrantScannerTimer.stop();
		//myShip.target = null;
		myTarget = null;
		myScript.$initialiseTimer();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$addTargetToScannedList = function (source, target) {
	var ships = source.checkScanner(true);
	for (var i = 0; i < ships.length; i++) {
		if (ships[i].scanClass === source.scanClass && (ships[i].equipmentStatus("EQ_WARRANT_SCANNER") === "EQUIPMENT_OK" || ships[i].equipmentStatus("EQ_WARRANT_SCANNER_POLICE") === "EQUIPMENT_OK")) {
			ships[i].script._scannedList.push(target);
		}
	}
}
Scripts/bountysystem_scanner.js
"use strict";
this.name = "BountySystem_WarrantScanner";
this.author = "phkb";
this.copyright = "2016 phkb";
this.description = "Routines for controlling the use of the Warrant scanner.";
this.licence = "CC BY-NC-SA 4.0";

this._debug = false; // turns on debug messages in the log
this._warrantScannerTimer = null; // timer object used when scanning NPC's
this._hideMFDTimer = null; // timer used to hide the MFD after a scan completes
//this._scannedShips = [];		// list of ships scanned by the player
this._scanTimeMidPoint = 4; // point of time during a scan (in seconds) at which initial information is displayed. 
this._lastTarget = null; // holding object of player's target when scanning started (so we can check if the player switches target and stops the scan)
this._mfdID = -1; // ID of the MFD that last held the Warrant Scanner
this._outputMode = 0; // mode = 0 standard MFD view, mode = 1, no MFD
this._scannerMode = 0; // 0 = manual scan, 1 = automatic scan (offenders only), 2 = auto scan everyone
this._overrideMode = false;
this._giveToNPC = true; // flag to control whether NPC's can get the warrant scanner
this._trueValues = ["yes", "1", 1, "true", true];
this._ficcInstalled = false;

//-------------------------------------------------------------------------------------------------------------
this.startUpComplete = function () {
	if (worldScripts.BountySystem_Core._disabled) {
		delete this.shipSpawned;
		delete this.playerBoughtEquipment;
		delete this.shipWillLaunchFromStation;
		delete this.startUpComplete;
		return;
	}
	// ensure station dock control has the important script properties noted
	var sdc = worldScripts.StationDockControl;
	if (sdc && sdc._propertiesToKeep) {
		sdc._propertiesToKeep.push("_hiddenBounty");
		sdc._propertiesToKeep.push("_storedHiddenBounty");
	}

	if (missionVariables.BountySystem_WarrantScanAuto) {
		this._scannerMode = parseInt(missionVariables.BountySystem_WarrantScanAuto);
		worldScripts.BountySystem_Core._warrantScannerMode = this._scannerMode;
	}
	if (worldScripts.FuelInjectionCruiseControl) this._ficcInstalled = true;

	// add interface to HUD selector
	var h = worldScripts.hudselector;
	if (h) h.$HUDSelectorAddMFD(this.name, this.name);
}

//-------------------------------------------------------------------------------------------------------------
this.playerWillSaveGame = function () {
	missionVariables.BountySystem_WarrantScanAuto = this._scannerMode;
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillLaunchFromStation = function (station) {
	var equip = "";
	var p = player.ship;
	if (p.equipmentStatus("EQ_WARRANT_SCANNER") === "EQUIPMENT_OK" || p.equipmentStatus("EQ_WARRANT_SCANNER_PASSIVE") === "EQUIPMENT_OK") equip = "EQ_WARRANT_SCANNER";
	if (p.equipmentStatus("EQ_WARRANT_SCANNER_POLICE") === "EQUIPMENT_OK" || p.equipmentStatus("EQ_WARRANT_SCANNER_POLICE_PASSIVE") === "EQUIPMENT_OK") equip = "EQ_WARRANT_SCANNER_POLICE";
	if (equip != "") {
		var eq = EquipmentInfo.infoForKey(equip);
		p.script._warrantScannerTime = eq.scriptInfo.scan_time;
		p.script._warrantScannerRange = p.scannerRange * eq.scriptInfo.scan_range;
	}
	// if the player doesn't have a way of seeing the target bounty, put the scanner mode into override
	this._overrideMode = false;
	if (this._scannerMode === 1 && p.equipmentStatus("EQ_SCANNER_SHOW_MISSILE_TARGET") != "EQUIPMENT_OK") this._overrideMode = true;
}

//-------------------------------------------------------------------------------------------------------------
this.playerBoughtEquipment = function (equip) {
	var p = player.ship;
	// remove and refund warrant scanner (both versions)
	if (equip === "EQ_WARRANT_SCANNER_REMOVAL") {
		p.removeEquipment(equip);
		var stn = p.dockedStation;
		var eqSts = "";
		var eqSts1 = p.equipmentStatus("EQ_WARRANT_SCANNER");
		if (eqSts1 === "EQUIPMENT_OK" || eqSts1 === "EQUIPMENT_DAMAGED") {
			var eq = EquipmentInfo.infoForKey("EQ_WARRANT_SCANNER");
			p.removeEquipment("EQ_WARRANT_SCANNER");
			eqSts = eqSts1;
		}
		eqSts1 = p.equipmentStatus("EQ_WARRANT_SCANNER_PASSIVE");
		if (eqSts1 === "EQUIPMENT_OK" || eqSts1 === "EQUIPMENT_DAMAGED") {
			var eq = EquipmentInfo.infoForKey("EQ_WARRANT_SCANNER_PASSIVE");
			p.removeEquipment("EQ_WARRANT_SCANNER_PASSIVE");
			eqSts = eqSts1;
		}
		var eqSts2 = p.equipmentStatus("EQ_WARRANT_SCANNER_POLICE");
		if (eqSts2 === "EQUIPMENT_OK" || eqSts2 === "EQUIPMENT_DAMAGED") {
			var eq = EquipmentInfo.infoForKey("EQ_WARRANT_SCANNER_POLICE");
			p.removeEquipment("EQ_WARRANT_SCANNER_POLICE");
			eqSts = eqSts2;
		}
		eqSts2 = p.equipmentStatus("EQ_WARRANT_SCANNER_POLICE_PASSIVE");
		if (eqSts2 === "EQUIPMENT_OK" || eqSts2 === "EQUIPMENT_DAMAGED") {
			var eq = EquipmentInfo.infoForKey("EQ_WARRANT_SCANNER_POLICE_PASSIVE");
			p.removeEquipment("EQ_WARRANT_SCANNER_POLICE_PASSIVE");
			eqSts = eqSts2;
		}
		// refund the cost of the equipment
		if (eq) {
			if (eqSts === "EQUIPMENT_OK") player.credits += (eq.price / 10) * stn.equipmentPriceFactor;
			if (eqSts === "EQUIPMENT_DAMAGED") player.credits += ((eq.price / 10) * stn.equipmentPriceFactor) / 2;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipSpawned = function (newship) {
	if (newship.isShip === false) return;
	// ignore these types
	if (newship.isPlayer || newship.isRock || newship.isThargoid || newship.isCargo || newship.isDerelict || newship.isBeacon ||
		newship.isStation || newship.isBoulder || newship.isWeapon || newship.isWormhole || newship.isVisualEffect || !newship.isPiloted) return;
	// for some standard ships that gets spawned we want to create a secondary bounty value that the player can uncover using the warrant scanner
	// some assassins
	// some traders
	// rarely hunters
	// most pirates
	// no miners/scavengers
	// most of these increases will only be marginal (most 10 points, some 20, very few 30 and above)
	// if the ship already has the property, we can exit
	// this is most likely a relaunch at a station via SDC
	if (newship.script.hasOwnProperty("_hiddenBounty")) return;
	newship.script._hiddenBounty = 0;
	if (newship.isPolice === false) {
		if (Ship.roleIsInCategory(newship.primaryRole, "oolite-pirate")) {
			if (Math.random() > 0.2) {
				var b = parseInt(Math.random() * 10) + 10;
				// some larger bounties
				if (Math.random() > 0.8) {
					b = parseInt(Math.random() * 20) + 30
				}
				// and rarely, some even larger ones
				if (Math.random() > 0.95) {
					b = parseInt(Math.random() * 40) + 50
				}
				newship.script._hiddenBounty = b;
				if (this._debug) log(this.name, "adding pirate bounty for " + newship);
			}
		} else if (Ship.roleIsInCategory(newship.primaryRole, "oolite-assassin")) {
			if (Math.random() > 0.5) {
				var b = parseInt(Math.random() * 10) + 20;
				// rarely some larger bounties
				if (Math.random() > 0.92) {
					b = parseInt(Math.random() * 20) + 40
				}
				// and even rarely, some even larger ones
				if (Math.random() > 0.99) {
					b = parseInt(Math.random() * 40) + 50
				}
				newship.script._hiddenBounty = b;
				if (this._debug) log(this.name, "adding assassin bounty for " + newship);
			}
		} else if (Ship.roleIsInCategory(newship.primaryRole, "oolite-trader")) {
			if (Math.random() > 0.8 && newship.name.indexOf("Medical") === 0) {
				newship.script._hiddenBounty = parseInt(Math.random() * 10) + 5;
				if (this._debug) log(this.name, "adding trader bounty for " + newship);
			}
		} else if (Ship.roleIsInCategory(newship.primaryRole, "oolite-bounty-hunter")) {
			if (Math.random() > 0.98) {
				newship.script._hiddenBounty = parseInt(Math.random() * 10) + 5;
				if (this._debug) log(this.name, "adding hunter bounty for " + newship);
			}
			// most hunters (60%) will have the warrant scanner
			if (this._giveToNPC === true) {
				if (Math.random() > 0.4) {
					if (this._debug) log(this.name, "adding equipment to " + newship);
					newship.awardEquipment("EQ_WARRANT_SCANNER");
					this.$addScannerRoutines(newship);
				}
			}
		}
	}
	// store what the hidden bounty is in a separate spot so we can reset back to it if the ship jumps to another system
	// allows us to simulate the same rules the player gets
	newship.script._storedHiddenBounty = newship.script._hiddenBounty;

	// add our shipExitedWormhole, shipDied and shipRemoved events to the ship
	// monkey patch if necessary
	if (newship.script.shipExitedWormhole) newship.script.$bounty_ovr_shipExitedWormhole = newship.script.shipExitedWormhole;
	newship.script.shipExitedWormhole = this.$bounty_shipExitedWormhole;
	if (newship.script.shipDied) newship.script.$bounty_ovr_shipDied = newship.script.shipDied;
	newship.script.shipDied = this.$bounty_shipDied;
	if (newship.script.shipRemoved) newship.script.$bounty_ovr_shipRemoved = newship.script.shipRemoved;
	newship.script.shipRemoved = this.$bounty_shipRemoved;
	if (newship.script.shipWillEnterWormhole) newship.script.$bounty_ovr_shipWillEnterWormhole = newship.script.shipWillEnterWormhole;
	newship.script.shipWillEnterWormhole = this.$bounty_shipWillEnterWormhole;

	if (this._giveToNPC === true) {
		// only give it to police ships that are piloted, can actually move (ie not stations), and are not satellites.
		if (newship.isPolice && newship.isPiloted && newship.maxSpeed > 0 && newship.hasRole("RSsatellite") === false) {
			// all police vessels have the scanner
			if (this._debug) log(this.name, "adding equipment to " + newship);
			newship.awardEquipment("EQ_WARRANT_SCANNER_POLICE");
			this.$addScannerRoutines(newship);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.equipmentDamaged = function (equipmentKey) {
	// switch to override mode if the STE is damaged
	if (equipmentKey === "EQ_SCANNER_SHOW_MISSILE_TARGET" && this._scannerMode === 1) this._overrideMode = true;
}

//-------------------------------------------------------------------------------------------------------------
this.equipmentRepaired = function (equipmentKey) {
	// switch off override mode if the STE is repaired
	if (equipmentKey === "EQ_SCANNER_SHOW_MISSILE_TARGET" && this._scannerMode === 1) this._overrideMode = false;
}

//-------------------------------------------------------------------------------------------------------------
this.shipTargetAcquired = function (target) {
	var ts = target.script;
	if (!ts) return;
	if (!player.ship.weaponsOnline && !target.hasHostileTarget && target.alertCondition !== 3) return; // don't auto-scan when weapons are offline, unless target won't react (has a hostile target or is in red alert)
	// if the scanner mode is set to automatically scan, and we haven't started scanning this ship, start it now
	if (ts.hasOwnProperty("_warrantScanPosition") === false) {
		// if the scanner mode is set to automatically scan, and we haven't started scanning this ship, start it now
		var doScan = false;
		switch (this._scannerMode) {
			//case 0:
				//if (player.ship.position.distanceTo(target) < player.ship.script._warrantScannerRange) doScan = true;
				//break;
			case 1:
				if (target.bounty > 0 || this._overrideMode === true) doScan = true;
				break;
			case 2:
				doScan = true;
				break;
		}
		if (doScan) {
			this.$startScan();
			return;
		}
	} else {
		// if we're already partway through scanning the target, automatically continue now
		if (ts._warrantScanPosition >= 0 && ts._warrantScanPosition < player.ship.script._warrantScannerTime) {
			this.$startScan();
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.activated = function $activated() {
	var that = $activated; // pointer to this function
	var s = (that.s = that.s || worldScripts.BountySystem_WarrantScanner); // cache reference to worldScript in local property on this function
	if (s._warrantScannerTimer && s._warrantScannerTimer.isRunning) {
		s._warrantScannerTimer.stop();
		if (s._outputMode === 0) s.$turnOffMFD();
		player.consoleMessage("Warrant scanner disengaged.");
		return;
	}

	// is the equipment OK?
	// theoretically the player can only have the standard scanner, but just in case the rules change later...
	if (player.ship.equipmentStatus("EQ_WARRANT_SCANNER") != "EQUIPMENT_OK" && player.ship.equipmentStatus("EQ_WARRANT_SCANNER_POLICE") != "EQUIPMENT_OK" &&
		player.ship.equipmentStatus("EQ_WARRANT_SCANNER_PASSIVE") != "EQUIPMENT_OK" && player.ship.equipmentStatus("EQ_WARRANT_SCANNER_POLICE_PASSIVE") != "EQUIPMENT_OK") return;

	// do we have a station target and is it a main station?
	if (s.$targetIsValid(player.ship) === false) return;

	// automatically switch to Non-MFD mode if there are no available slots
	if (s._outputMode === 0 && s.$isMFDSlotAvailable() === false) s._outputMode = 1;
	// TWEAK: automatically switch to MFD mode if there is an available slot (thanks to Nite Owl)
	if (s._outputMode === 1 && s.$isMFDSlotAvailable() === true) s._outputMode = 0;

	var p = player.ship;

	// have we started scanning this ship already?
	var checkScan = s.$existingShipScanPosition(p.target);
	if (checkScan >= p.script._warrantScannerTime) {
		if (s._outputMode === 1) player.consoleMessage("Ship already scanned.");
		if (s._outputMode === 0) {
			s.$updateMFD("Warrant Scanner:\nShip already scanned.");
			if (s._hideMFDTimer && s._hideMFDTimer.isRunning) s._hideMFDTimer.stop();
			s._hideMFDTimer = new Timer(s, s.$turnOffMFD, 5, 0);
		}
		return;
	}
	if (s._outputMode === 1) player.consoleMessage("Scanning started.");

	s._warrantScannerTimer = new Timer(s, s.$scanProcess, 1, 1);
	s._lastTarget = p.target;
	if (s._hideMFDTimer && s._hideMFDTimer.isRunning) s._hideMFDTimer.stop();
	if (s._outputMode === 0) s.$updateMFD("Warrant Scanner:\nInitialising...\n");
}

//-------------------------------------------------------------------------------------------------------------
this.mode = function $mode() {
	var that = $mode; // pointer to this function
	var s = (that.s = that.s || worldScripts.BountySystem_WarrantScanner); // cache reference to worldScript in local property on this function
	if (s._outputMode === 0) {
		s._outputMode = 1;
		player.consoleMessage("Switched to Non-MFD mode.");
		return;
	}
	if (s._outputMode === 1) {
		s._outputMode = 0;
		player.consoleMessage("Switched to MFD mode.");
		return;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$addScannerRoutines = function $addScannerRoutines(newship) {
	var that = $addScannerRoutines; // pointer to this function
	var w = (that.w = that.w || worldScripts.BountySystem_NPCScan); // cache reference to worldScript in local property on this function
	newship.script.$initialiseTimer = w.$initialiseTimer;
	newship.script.$checkScannerForTarget = w.$checkScannerForTarget;
	newship.script.$scanTarget = w.$scanTarget;
	newship.script.$initialiseTimer();
}

//-------------------------------------------------------------------------------------------------------------
this.$startScan = function () {
	if (this._hideMFDTimer && this._hideMFDTimer.isRunning) this._hideMFDTimer.stop();
	if (this._warrantScannerTimer && this._warrantScannerTimer.isRunning) this._warrantScannerTimer.stop();
	this.activated();
}

//-------------------------------------------------------------------------------------------------------------
this.$scanProcess = function $scanProcess() {
	var p = player.ship;

	// player switched target while scanning or target isn't valid anymore (jumped out/destroyed) - auto-off triggered
	if (p.target != this._lastTarget || this.$targetIsValid(p) === false) {
		player.consoleMessage("Warrant scanner disengaged.");
		if (this._outputMode === 0) this.$turnOffMFD();
		this._warrantScannerTimer.stop();
		return;
	}

	// add this ship to the array
	var scanPos = this.$existingShipScanPosition(p.target);
	//if (scanPos === 0) this._scannedShips.push({ship:p.target, scanPosition:0});

	var mfdText = "Warrant Scanner:\n";
	var outOfRange = false;

	// make sure target is still in range - but put scan on hold until it is
	if (this.$targetInRange(p) === false) {
		mfdText += "Ship out of range.\n";
		outOfRange = true;
	} else {
		scanPos += 1;
	}

	if (this._outputMode === 1 && outOfRange === false) player.consoleMessage(((scanPos / p.script._warrantScannerTime) * 100).toFixed(0) + "% complete");
	if (scanPos === p.script._warrantScannerTime) {
		mfdText += "Scan complete.\n";
	} else {
		mfdText += "Scanning: " + ((scanPos / p.script._warrantScannerTime) * 100).toFixed(0) + "% complete\n";
	}

	if (scanPos >= this._scanTimeMidPoint) {
		var viewrole = this.$translateRole(p.target.primaryRole);
		mfdText += "\nPilot: " + this.$extractPilotName(p.target) + "\n" + (viewrole != "Unknown" ? viewrole + "\n" : "");
		// only display the message once for consoleMessage mode
		if (this._outputMode === 1 && scanPos === this._scanTimeMidPoint && outOfRange === false) {
			player.consoleMessage("Pilot is " + this.$extractPilotName(p.target) + (viewrole != "Unknown" ? " (" + viewrole + ")" : ""));
		}
	}

	p.target.script._warrantScanPosition = scanPos;
	// update the scan position for this ship
	/*for (var i = 0; i < this._scannedShips.length; i++) {
		if (this._scannedShips[i].ship === p.target) this._scannedShips[i].scanPosition = scanPos;
	}*/

	// NPC reaction after 2 seconds
	if (scanPos === 2) {
		// decide if they will flee, attack or ignore
		this.$npcReaction(p.target, p);
	}

	// finish the scan
	if (scanPos >= p.script._warrantScannerTime) {
		if (this._outputMode === 1) player.consoleMessage("Scan complete.");
		this._warrantScannerTimer.stop();
		// will this ship get a bounty?
		var check = this.$checkBounty(p.target, true, p);
		if (check > 0) {
			mfdText += "\nAdditional warrants of " + formatCredits(check, false, true) + "\napplied to target.";
			if (this._outputMode === 1) player.consoleMessage("Additional warrants of " + formatCredits(check, false, true) + " applied to target.");
			p.target.bounty += check;
		} else {
			if (this._outputMode === 1) player.consoleMessage("No additional warrants found for target.");
			mfdText += "\nNo additional warrants\nfound for target.";
		}
		if (this._outputMode === 0) {
			if (this._hideMFDTimer && this._hideMFDTimer.isRunning) this._hideMFDTimer.stop();
			this._hideMFDTimer = new Timer(this, this.$turnOffMFD, 10, 0);
		}
	}

	if (this._outputMode === 0) this.$updateMFD(mfdText);
}

//-------------------------------------------------------------------------------------------------------------
// checks if we have an MFD slot available (a blank spot or a slot currently allocated to this OXP. 
// Returns true if a spot is found, otherwise false
this.$isMFDSlotAvailable = function () {
	var p = player.ship;
	var result = false;
	for (var i = 0; i < p.multiFunctionDisplayList.length; i++) {
		if (!p.multiFunctionDisplayList[i] || (p.multiFunctionDisplayList[i] === "" || p.multiFunctionDisplayList[i] === this.name)) {
			result = true;
		}
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
this.$updateMFD = function (text) {
	var p = player.ship;

	// set the text in the MFD
	p.setMultiFunctionText(this.name, text, false);

	// 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) {
		// 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) p.setMultiFunctionDisplay(this._mfdID, this.name);
	}
}

//-------------------------------------------------------------------------------------------------------------
// records the index of the MFD that currently holds the warrant scanner mfd
this.$findMFDID = function () {
	var p = player.ship;
	if (p.isValid === false || !p.multiFunctionDisplayList) return;
	for (var i = 0; i < p.multiFunctionDisplayList.length; i++) {
		if (p.multiFunctionDisplayList[i] === this.name) this._mfdID = i;
	}
}

//-------------------------------------------------------------------------------------------------------------
// hides all instances of the warrant scanner MFD
this.$turnOffMFD = function $turnOffMFD() {
	var p = player.ship;
	if (p.isValid === false || !p.multiFunctionDisplayList) return;
	for (var i = 0; i < p.multiFunctionDisplayList.length; i++) {
		if (p.multiFunctionDisplayList[i] === this.name) {
			p.setMultiFunctionDisplay(i, "");
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// gets a readable form of the pilot's name
this.$extractPilotName = function (ship) {
	var pilotName = "";
	var pilot = ship.crew[0];
	pilotName = pilot.name + (this._outputMode === 1 ? ", " : "\n") + pilot.description;
	return pilotName;
}

//-------------------------------------------------------------------------------------------------------------
// returns the scan position (ie. how far through the scan we have gotten) for a particular ship
this.$existingShipScanPosition = function (ship) {
	if (ship.script.hasOwnProperty("_warrantScanPosition") === false) ship.script._warrantScanPosition = 0;
	return ship.script._warrantScanPosition;
	/*
	for (var i = 0; i < this._scannedShips.length; i++) {
		if (this._scannedShips[i].ship === ship) return this._scannedShips[i].scanPosition;
	}
	return 0;
	*/
}

//-------------------------------------------------------------------------------------------------------------
// checks the target of the source ship to ensure it is still valid. If so, returns true.
// when false, scanning process will stop
this.$targetIsValid = function (source) {

	var ship = (source.isPlayer ? source.target : source.script._checkTarget);
	var result = true;
	// do we have a station target and is it a main station?
	if (!ship || ship.isValid === false) {
		if (source.isPlayer) player.consoleMessage("No target selected.");
		result = false;
	}
	if (result === true && ship.isStation) {
		if (source.isPlayer && this._scannerMode == 0) player.consoleMessage("Scanner will not work on stations.");
		result = false;
	}
	if (result === true && (ship.isPiloted === false || ship.isThargoid || ship.hasRole("escape-capsule"))) {
		if (source.isPlayer && this._scannerMode == 0) player.consoleMessage("No valid target selected.");
		result = false;
	}
	if (result === true && (source.injectorsEngaged || (source.isPlayer && this._ficcInstalled && source.script._ficcEngaged === true) || source.torusEngaged === true)) {
		if (source.isPlayer) player.consoleMessage("Unable to scan at injector or torus speed.");
		result = false;
	}
	if (result === true && source.isCloaked) {
		if (source.isPlayer) player.consoleMessage("Scanner will not work when cloaked.");
		result = false;
	}
	if (result === true && ship.isCloaked) {
		if (source.isPlayer) player.consoleMessage("Scanner unable to target cloaked ship.");
		result = false;
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// checks range to source's target. when in range, returns true
// otherwise false. When false, scan will continue but be on hold.
this.$targetInRange = function (source) {
	var ship = (source.isPlayer ? source.target : source.script._checkTarget);
	var dist = source.position.distanceTo(ship);
	if (source.script._debugNPCScan === true) log(this.name, "dist to target = " + dist + " for " + source);
	// are we too far away for a good scan?
	if (dist > source.script._warrantScannerRange) {
		if (source.isPlayer) {
			if (this._outputMode === 1) player.consoleMessage("Ship out of range.");
		}
		return false;
	}
	return true;
}

//-------------------------------------------------------------------------------------------------------------
// checks the hidden bounty for a particular ship. doRemove will do an "official" search, in which case the ship will be removed from the list
// basically, once a ship has been scanned with the warrant scanner we don't want the ship to get scanned again and increase their bounty multiple times.
// so we remove it for an official scan.
this.$checkBounty = function $checkBounty (checkship, doRemove, source) {
	var bounty = 0;
	if (checkship.isPlayer) {
		// uncover the player's bounties
		var that = $checkBounty; // pointer to this function
		var w = (that.w = that.w || worldScripts.BountySystem_Core); // cache reference to worldScript in local property on this function
		w.$uncoverBounties(source);
		w.shipExitedWitchspace();
	} else {
		var idx = -1;
		bounty = checkship.script._hiddenBounty;
		// if this is an official scan, reset the ships hiddenBounty value so they don't get multiple bounty increases
		if (doRemove === true && bounty > 0) checkship.script._hiddenBounty = 0;
	}
	return bounty;
}

//-------------------------------------------------------------------------------------------------------------
// updates the hidden bounty for a particular ship, or adds it if the ship isn't in the list already
this.$setBounty = function (checkship, newbounty) {
	checkship.script._hiddenBounty = newbounty;
}

//-------------------------------------------------------------------------------------------------------------
this.$npcReaction = function (ship, source) {
	// don't do anything if the ship is under attack or at condition red - they're too busy to notice
	if (ship.hasHostileTarget || ship.alertCondition === 3) return;

	// check if they've got something to hide
	var check = this.$checkBounty(ship, false, source);
	if (check === 0) return;

	// count up the number of ships in range that might be doing the scan
	var ships = ship.checkScanner(true);
	var inRange = 0;
	if (ships) {
		for (var i = 0; i < ships.length; i++) {
			var groupMbr = false;
			// check if this ship is actually a member of the same group as the target
			if (ships[i].group && ships[i].group.containsShip(ship)) groupMbr = true;

			if (ships[i].isPiloted === true && ships[i].isStation === false && ships[i].isThargoid === false && groupMbr === false &&
				ship.position.distanceTo(ships[i]) <= source.script._warrantScannerRange) inRange += 1;
		}
	}

	if (ship.equipmentStatus("EQ_CLOAKING_DEVICE") === "EQUIPMENT_OK") {
		// turn on the cloak to evade detection
		if (this._debug) log(this.name, "cloaking " + ship);
		ship.isCloaked = true;
	}

	// if there's only one ship in range, then it's obvious who the scanner is
	if (inRange === 1) {
		ship.target = source;
		if (source.isPolice === false && ship.threatAssessment(false) > source.threatAssessment(false) && ship.withinStationAegis === false) {
			if (this._debug) log(this.name, "attack mode(1) engaged for " + ship + " against " + source);
			ship.performAttack();
			return;
		} else {
			if (this._debug) log(this.name, "flee mode(1) engaged for " + ship + " against " + source);
			ship.performFlee();
			return;
		}
	}

	// won't know who the perpetrator is if more than one ship in range
	if (inRange > 1) return;
	// otherwise, they'll know exactly who it is

	// they're clear, but it's still not friendly
	// police will ignore
	// pirates will most likely attack
	// traders will sometimes attack or sometimes flee
	// assassins will attack
	// hunters will sometimes attack

	// if the one scanning is a police vessel, don't do anything.
	if (source.isPolice) return;

	if (Ship.roleIsInCategory(ship.primaryRole, "oolite-pirate") && Math.random() > 0.8) {
		if (this._debug) log(this.name, "attack mode(2) engaged for " + ship + " against " + source);
		ship.target = source;
		ship.performAttack();
		return;
	}
	if (Ship.roleIsInCategory(ship.primaryRole, "oolite-assassin") && ship.withinStationAegis === false && Math.random() > 0.8) {
		if (this._debug) log(this.name, "attack mode(3) engaged for " + ship + " against " + source);
		ship.target = source;
		ship.performAttack();
		return;
	}
	if (Ship.roleIsInCategory(ship.primaryRole, "oolite-trader") && ship.withinStationAegis === false && Math.random() > 0.9 && ship.threatAssessment(false) > source.threatAssessment(false)) {
		if (this._debug) log(this.name, "attack mode(4) engaged for " + ship + " against " + source);
		ship.target = source;
		ship.performAttack();
		return;
	}
	if (Ship.roleIsInCategory(ship.primaryRole, "oolite-trader") && ship.withinStationAegis === false && Math.random() > 0.3) {
		if (this._debug) log(this.name, "flee mode(2) engaged for " + ship + " against " + source);
		ship.target = source;
		ship.performFlee();
		return;
	}
	if (Ship.roleIsInCategory(ship.primaryRole, "oolite-bounty-hunter") && ship.withinStationAegis === false && Math.random() > 0.98) {
		if (this._debug) log(this.name, "attack mode(5) engaged for " + ship + " against " + source);
		ship.target = source;
		ship.performAttack();
		return;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$translateRole = function (role) {
	switch (role) {
		case "hunter":
		case "hunter-medium":
		case "hunter-heavy":
			return "Known bounty hunter";
		case "pirate":
		case "pirate-light-fighter":
		case "pirate-medium-fighter":
		case "pirate-heavy-fighter":
		case "pirate-light-freighter":
		case "pirate-medium-freighter":
		case "pirate-heavy-freighter":
		case "pirate-aegis-raider":
		case "pirate-interceptor":
		case "ftzpirate":
			return "Known pirate";
		case "trader":
		case "trader-courier":
		case "trader-courier+":
		case "trader-smuggler":
			return "Known trader";
		case "assassin-light":
		case "assassin-medium":
		case "assassin-heavy":
			return "Known assassin";
		case "police":
		case "police-station-patrol":
		case "police-witchpoint-patrol":
			return "Known police officer";
	}
	return "Unknown";
}

//-------------------------------------------------------------------------------------------------------------
this.$bounty_shipExitedWormhole = function () {
	if (this.ship.script.$bounty_ovr_shipExitedWormhole) this.ship.script.$bounty_ovr_shipExitedWormhole;
	if (this.ship.script.hasOwnProperty("_hiddenBounty") === false) return;
	// if this ship got scanned and had its hidden bounty applied, remove and reset when we exit a wormhole
	this.ship.script._warrantScanPosition = 0;
	if (this.ship.script._storedHiddenBounty > 0 && this.ship.script._hiddenBounty === 0) {
		this.ship.bounty -= this.ship.script._storedHiddenBounty;
		if (this.ship.bounty > 0) {
			// reset back to zero - assume any bounty in that system was local
			this.ship.bounty = 0;
			// then we need to determine if this ship has a local bounty in the new system as well
			if (Math.random() > 0.4) {
				var sys = System.infoForSystem(galaxyNumber, this.ship.destinationSystem);
				switch (this.ship.primaryRole) {
					case "trader":
						if (Math.random() > 0.6) this.ship.setBounty(Math.ceil(Math.random() * 20), "setup actions");
						break;
					case "trader-smuggler":
					case "trader-smuggler+":
						this.ship.setBounty(Math.ceil(Math.random() * 20), "setup actions");
						if (this.ship.bounty > this.ship.cargoSpaceCapacity * 2) {
							this.ship.bounty = this.ship.cargoSpaceCapacity * 2;
						}
						break;
					case "pirate":
						this.ship.setBounty(20 + sys.government + (this.ship.group ? this.ship.group.count : 0) + Math.floor(Math.random() * 8), "setup actions");
						break;
					case "pirate-interceptor":
						this.ship.setBounty(50 + sys.government + Math.floor(Math.random() * 36), "setup actions");
						break;
					case "pirate-light-fighter":
					case "pirate-medium-fighter":
					case "pirate-heavy-fighter":
						this.ship.setBounty(20 + sys.government + Math.floor(Math.random() * 12), "setup actions");
						break;
					case "pirate-light-freighter":
					case "pirate-medium-freighter":
					case "pirate-heavy-freighter":
						this.ship.setBounty(60 + sys.government + Math.floor(Math.random() * 8), "setup actions");
						break;
					case "pirate-aegis-raider":
						this.ship.setBounty(50 + sys.government + Math.floor(Math.random() * 36), "setup actions");
						break;
				}
			}
		}
		this.ship.script._hiddenBounty = this.ship.script._storedHiddenBounty;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$bounty_shipDied = function (whom, why) {
	if (this.ship.script.$bounty_ovr_shipDied) this.ship.script.$bounty_ovr_shipDied(whom, why);
	// clean up timers
	if (this.ship.script._checkTimer && this.ship.script._checkTimer.isRunning) {
		this.ship.script._checkTimer.stop();
		delete this.ship.script._checkTimer;
	}
	if (this.ship.script._warrantScannerTimer && this.ship.script._warrantScannerTimer.isRunning) {
		this.ship.script._warrantScannerTimer.stop();
		delete this.ship.script._warrantScannerTimer;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$bounty_shipRemoved = function (suppressDeathEvent) {
	if (this.ship.script.$bounty_ovr_shipRemoved) this.ship.script.$bounty_ovr_shipRemoved(suppressDeathEvent);
	// clean up timers
	if (this.ship.script._checkTimer && this.ship.script._checkTimer.isRunning) {
		this.ship.script._checkTimer.stop();
		delete this.ship.script._checkTimer;
	}
	if (this.ship.script._warrantScannerTimer && this.ship.script._warrantScannerTimer.isRunning) {
		this.ship.script._warrantScannerTimer.stop();
		delete this.ship.script._warrantScannerTimer;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$bounty_shipWillEnterWormhole = function () {
	if (this.ship.script.$bounty_ovr_shipWillEnterWormhole) this.ship.script.$bounty_ovr_shipWillEnterWormhole();
	// clean up timers
	if (this.ship.script._checkTimer && this.ship.script._checkTimer.isRunning) {
		this.ship.script._checkTimer.stop();
		delete this.ship.script._checkTimer;
	}
	if (this.ship.script._warrantScannerTimer && this.ship.script._warrantScannerTimer.isRunning) {
		this.ship.script._warrantScannerTimer.stop();
		delete this.ship.script._warrantScannerTimer;
	}
}