Back to Index Page generated: Jun 13, 2026, 7:54:53 PM

Expansion BroadcastComms MFD

Content

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Provides a way to send communications to other ships via a multi-function display and primable equipment. Provides a way to send communications to other ships via a multi-function display and primable equipment.
Identifier oolite.oxp.phkb.BroadcastCommsMFD oolite.oxp.phkb.BroadcastCommsMFD
Title BroadcastComms MFD BroadcastComms MFD
Category HUDs HUDs
Author phkb and Zireael phkb and Zireael
Version 1.4 1.4
Tags mfd, comms, equipment mfd, comms, equipment
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Dependent Expansions
  • oolite.oxp.Cholmondely.BroadcastComms_Digebiti_Variations:1.0
  • oolite.oxp.Norby.Ambiences_recommended_by_Norby:1.6
  • oolite.oxp.phkb.GalCopMissions:0.14
  • oolite.oxp.phkb.QuickVisa:1.5
  • Information URL https://wiki.alioth.net/index.php/BroadcastComms_MFD n/a
    Download URL https://wiki.alioth.net/img_auth.php/a/ac/BroadcastCommsMFD_1.4.oxz n/a
    License CC-BY-NC-SA 4.0 CC-BY-NC-SA 4.0
    File Size n/a
    Upload date 1753243793

    Relationships Diagram

    Documentation

    Also read http://wiki.alioth.net/index.php/BroadcastComms%20MFD

    Resources/BCC_Testbed.oxp/readme.txt

    BCC Testbed
    ===========
    
    This OXP is a testing and demonstrating the mechanism for script_info integrations in BCC. To use it, move the OXP into your AddOns folder. Then, after launching from the station, there will be a stationary Cobra MkIII between the station and the beacon. After targetting this ship, you will then be able to test the various functions BCC is providing.
    
    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/
    
    Discussion
    ==========
    This OXP is discussed at this forum link: http://bb.oolite.space/viewtopic.php?f=4&t=16826
    

    readme.txt

    Broadcast Comms MFD
    ===================
    
    Author: phkb (Nick Rogers) and Zireael
    With thanks to: cim, Norby, Wildeblood and Marte for their suggestions and fixes
    Special thanks to Milo for fixes to the surrender to police method.
    
    Only works with Oolite 1.79 or greater.
    
    Overview
    ========
    This OXP provides a means by which the player can communicate in a real way with other ships. That is, the types of communications the player transmits will have a bearing on the game in some way, depending on the transmission type
    
    There are 11 types of transmissions:
    
    1. Requests for wormholes (broadcast ie. to all ships in range)
       This transmission asks if anyone is going to your hyperspace destination. The message is only available if a destination has been set and the players status is not "red".
       Ships can respond in three ways: (a) Not at all. (b) By telling the player they are headed somewhere else, or (c) by telling the player they are heading to the players destination.
       This message can be sent repeatedly.
    
    2. Send distress message (broadcast ie. to all ships in range)
       This transmission requests immediate assistance against attackers. NPC ships may or may not choose to help.
       Only available when the player ship is under attack.
       This message can be sent repeatedly.
    
    3. Send greeting to target
       This transmission sends a basic "Hello" message to the ship the player is targeting. The other ship may choose to respond or not. Once a response has been received from the other ship, no further greetings can be sent to that ship.
    
    4. Send taunt to target
       This transmission sends a taunt to the other ship, and the other ship may or may not respond. But regardless of a verbal response, the other ship may do one of the following:
          (a) Nothing at all
          (b) If they are currently attacking the player, they might get angry and their accuracy might decrease for a few seconds, or increase for a few seconds.
          (c) If they are not targeting the player at the moment, they might choose to start targeting the player instead.
       Multiple taunts can be sent to other ships.
    
    5. Issue threat to target
       This transmission issues a threat to the other ship. Depending on who that ship is targeting, the response may be different:
          (a) If the other ship is targeting the player, there is a small chance they might choose to flee.
          (b) If the other ship is not targeting the player, they will possibly choose to attack the player.
       Only one threat can be issues to a ship.
    
    6. Offer bribe to target/Offer bribe to nearest attacker
       This transmission offers money to a ship that is attacking the player. That ship can respond in a two ways.
          (a) They can reject the offer. The amount the player can offer will then increase by a factor of 2 or 2.5 and the player can then try the bribe again if they wish. If the amount of bribe increases beyond the players current credit balance, the offer to bribe will be removed.
          (b) They can accept the bribe. In this case, the bribe amount is deducted from the player bank account, and the ship targeting the player will break off their attack. If they are part of a pirate band, the pirate who accepted the bribe will share the spoils and they will all break off their attack.
    
       If the attacking ship is targeted by the player, they will see the next bribe amount in the message text when selecting the message. 
       If the player doesn't have a target (for instance, they are fleeing from a group of ships), the bribe amount will only be displayed when the transmission takes place. This is because it is not known who is the closest attacking ship until the transmission occurs, and the bid amounts are stored for each pirate group or ship. If a ship from one pirate group moves closer than another pirate group, the amount of the bribe will be adjusted accordingly.
       
    7. Demand cargo from target
       This transmission sends a demand for cargo to the other ship. They can either accept or reject the demand. If they accept the demand, they will drop some cargo and flee. If this action is performed in sight of a police vessel or the main station, there is a chance the victim will report the crime and the player will receive an increase to their offender status.
    
    8. Surrender to target/Surrender to nearest attacker
       This transmission sends a surrender message another ship. There is a small chance the attackers might accept the surrender. In that case they will stop attacking the player and wait for the player to dump some cargo. If the player doesn't dump any cargo, the pirates will start attacking the player again. If the player decides to use the lull in hostilities to start attacking the ships the pirates will get really angry and get a temporary boost in their accuracy.
       If the attacker is a police vessel, they might demand the player to disable their weapons. The player will have a few seconds to comply. If they do, the police will most likely accept your surrender and stop attacking.
    
    9. Offer to rescue escape pod (only if fuel scoop is fitted)
       This sends a message to the targeted escape pod, asking the occupant if they would like to be scooped and returned to the main station.
       Commanders should check that they have available cargo space before scooping escape pods.
      
    10. Target last comms message
        This will switch the players target to the ship that last sent a comms message.
    
    11. Keep away from my target
        This will send a message to all ships in range, telling them to stay away from the players current target. Only ships that aren't fighting against the player will potentially respond. Enemy ships will ignore this message.
        
    Usage:
    The BroadcastComms MFD is available for purchase for 200CR at all stations, regardless of techlevel.
    After displaying the BroadcastComms MFD and priming it, press the "b" (Mode) key to select a message from the available options. 
    The selected message is marked with a ">" in the MFD. 
    You don't need the BroadcastComms MFD to be visible, though. There might be situations where it is not be visible, so changes to the selected message will also be displayed as a console message. 
    Once the desired message is selected, press the "n" (Activate) key to send the message.
    
    External access
    ===============
    Provision has been made for other OXP's to access the comms features in this OXP. There are seven methods that can be used:
    
    	var w = worldScripts.BroadcastCommsMFD;
    	w.$createMessage({messageName:string, callbackFunction:Function, displayText:string, messageText:string, ship:Ship, shipDisplayName:string, 
    		transmissionType:string, deleteOnTransmit:Boolean, delayCallback:integer})
    
    		Calling this function will add the message to the display that will be displayed when the conditions are met.
    
    		messageName			The name of this message, internally used for identification purposes. Can be any text. It is recommended to prefix
    							the message name with the name of your worldscript to prevent the possibility of duplicate message names.
    		callbackFunction	The function to call when the player transmits the message.
    		displayText			The message text to display
    		messageText			The message text that will be transmitted, if different to the display text.
    		ship				Reference to a ship object for a ship-to-ship transmission. Player must target the ship for the message to
    							become available.
    							Can be null, and if transmission type is "target" this will create a message that can be sent to any targeted ship.
    							Will be ignored if transmission type is "broadcast".
    		shipDisplayName		Display name of a ship for a ship-to-ship transmission. Player must target the ship for the message to 
    							become available.
    							Alternate method of targeting a specific ship, if the ship object is not available.
    							Will be ignored if transmission type is "broadcast".
    		transmissionType	Transmission type, either "broadcast" (ie. transmitted to all ships) or "target" (the ship targeted by the player)
    							Including a ship property necessitates a "target" value here. Specifying "broadcast" will over override ship reference.
    							If not set, will default to "broadcast".
    		deleteOnTransmit	Boolean value indicating that the message will be removed from the list as soon as the player transmits it.
    							If not set will default to true.
    		delayCallback		Integer value indicating how long (in seconds) to take before calling the callback function. A value of 0 (zero)
    							means immediately.
    							If not set will default to 2.
    		hideOnConditionRed	Boolean value indicating that the message will be hidden from the player if their ship is under attack.
    							Default is false.
    					  
    	Special case: If the displayText starts with any type of bracket (ie. "(", "{", "<" or "[") there will be no external transmission. That is, the callback function will be called, but there will be no comms messages sent to any ships. An external OXP can use this option to, for example, switch comms modes between a specialised set of comms messages, and the standard set.
    	
    	var w = worldScripts.BroadcastCommsMFD;
    	w.$updateMessage({messageName:string, displayText:string, messageText:string, callbackFunction:Function, deleteOnTransmit:boolean, delayCallback:integer});
    
    		This function will update the settings of a particular message.
    		
    		messageName			The name of the message that will be updated
    		displayText			The new message text to be displayed, otherwise null if not being changed.
    		messageText			The new message text that will be transmitted, otherwise null if not being changed.
    		callbackFunction	The new function to call when the player transmits the message, otherwise null if not being changed.
    		deleteOnTransmit	Boolean value indicating that the message will be removed from the list as soon as the player transmits it. 
    							Null if not being changed.
    		delayCallback		Integer value indicating how long (in seconds) to take before calling the callback function. A value of 0 (zero)
    							means immediately. Null if not being changed.
    
    	var w = worldScripts.BroadcastCommsMFD;
    	w.$checkMessageExists(messageName:string);
    
    		This function returns true if messageName exists, otherwise false.
    
    		messageName			The name of the message that will be searched for
    	
    	var w = worldScripts.BroadcastCommsMFD;
    	w.$removeMessage(messageName:string);
    
    		Calling this function will remove the message from the list. Only messages created via the "$createMessage" function can be removed.
    
    		messageName			The name of the message that will be removed
    
    	var w = worldScripts.BroadcastCommsMFD;
    	w.$disableMessage(msgid:int);
    	
    		This function disables one or all of the internal messages, preventing it from being displayed
    		
    		msgid				The ID of the internal message that will be disabled, where
    							0 - all
    							1 - request for wormhole
    							2 - send distress message
    							3 - send greeting
    							4 - send taunt
    							5 - send threat
    							6 - offer bribe
    							7 - demand cargo
    							8 - surrender to target
    							9 - offer to rescue escape pod
    							10 - target last comms message
    							11 - keep away from my target
    
    	var w = worldScripts.BroadcastCommsMFD;
    	w.$enableMessage(msgid:int);
    		
    		This function enables one of all of the internal messages (if they have been disabled by the $disableMessage function), allow it to be displayed
    		
    		msgid				The ID of the internal message that will be enabled, where
    							0 - all
    							1 - request for wormhole
    							2 - send distress message
    							3 - send greeting
    							4 - send taunt
    							5 - send threat
    							6 - offer bribe
    							7 - demand cargo
    							8 - surrender to target
    							9 - offer to rescue escape pod
    							10 - target last comms message
    							11 - keep away from my target
    
    	var w = worldScripts.BroadcastCommsMFD;
    	w.$isMessageEnabled(msgid:int);
    	
    		This function is used to determine if an internal message is enabled or if it has been disabled.
    		Returns true if message is enabled, otherwise false.
    		Passing msgid value of 0 will check if all messages are enabled. Only returns true if all messages are enabled. Otherwise false.
    		
    		msgid				The ID of the internal message that will be evaluated.
    		
    Examples:
    	var w = worldScripts.BroadcastCommsMFD;
    	w.$createMessage({messageName:this.name + "myMessage1", 
    		callbackFunction:this.$broadcastCallback.bind(this), 
    		displayText:"Evacuation message", 
    		messageText:"Attention all ships. Please evacuate the area and move to a safe distance.", 
    		transmissionType:"broadcast", 
    		deleteOnTransmit:true, 
    		delayCallback:4,
    		hideOnConditionRed:false});
    
    		This example adds a broadcast message to the display. It will be removed when transmitted, and it will take 4 seconds for the
    		callback function "this.$broadcastCallback.bind(this)" to be executed. It won't be hidden if the player is under attack.
    
    	var w = worldScripts.BroadcastCommsMFD;
    	w.$createMessage({messageName:this.name + "myMessage2", 
    		callbackFunction:this.$targetCallback.bind(this), 
    		displayText:"Ask native question", 
    		messageText:"Are you a native of these parts?", 
    		ship:myShip, 
    		transmissionType:"target", 
    		deleteOnTransmit:false, 
    		delayCallback:0,
    		hideOnConditionRed:true});
    
    		This example adds a target-specific message to the display, to be shown when the ship object "myShip" is targeted by the player.
    		It will be not removed when transmitted and the callback function this.$targetCallback.bind(this)" will be executed immediately.
    		It will be hidden if the player is under attack.
    
    	var w = worldScripts.BroadcastCommsMFD;
    	w.$removeMessage(this.name + "myMessage2");
    
    		This example will remove the message "myMessage2" from the list of external messages.
    
    	var w = worldScripts.BroadcastCommsMFD;
    	w.$disableMessage(0);
    	
    		This example will disable all internal messages, leaving only the external messages to be displayed.
    		
    	var w = worldScripts.BroadcastCommsMFD;
    	w.$disableMessage(3);
    	
    		This example will disable the "Send greeting to target" message.
    		
    	var w = worldScripts.BroadcastCommsMFD;
    	w.$enableMessage(0);
    		
    		This example will enable all internal messages.
    		
    	var w = worldScripts.BroadcastCommsMFD;
    	w.$enableMessage(3);
    		
    		This example will enable the "Send greeting to target" message.
    		
    External messages, and the status of internal messages, are cleared each time the player loads from a saved game. If a persistent message needs to be shown, or if the status of internal messages needs to be changed in an ongoing manner, ensure the changes are made after the player loads from a saved game.
    
    Using script_info to control messages
    =====================================
    It is also possible to control messages by using the "script_info" property of a ship definition in shipdata.plist. The following options are available:
    
    	script_info = {
    		bcc_disable_defaults = (); // an array of numeric identifiers that correspond to default message id's 
    		bcc_custom_defaults = {}; // a dictionary object with keys corresponding to default message id's
    		bcc_custom_messages = {}; // a dictionary object containing message definitions
    	};
    
    Looking at each of these in more detail:
    	
    	// this would disable message types 3 (greeting), 4 (taunt) and 7 (demand cargo)
    	bcc_disable_defaults = (3,4,7); 
    
    	// this definition includes all possible options for changing the standard transmissions and replies.
    	bcc_custom_defaults = { // note: [xyz] refers to an entry in descriptions.plist. Each one should be unique
    		"1" = { // wormhole request
    			reply_confirm = "[xyz]";
    			reply_decline = "[xyz]";
    		};
    		"3" = { // greeting
    			transmit = "[xyz]";
    			reply = "[xyz]";
    		};
    		"4" = { // taunt
    			transmit = "[xyz]";
    			reply = "[xyz]";
    		};
    		"5" = { // threat
    			transmit = "[xyz]";
    			reply_normal = "[xyz]";
    			reply_fleeing = "[xyz]";
    		};
    		"6" = { // bribe
    			transmit = "[xyz]"; // [amount] && [credittype] must be included in descriptions.plist entry
    			reply_fine = "[xyz]"; // police vessel fines player
    			reply_decline = "[xyz]"; // decline bribe
    			reply_accept = "[xyz]"; // accept bribe
    		};
    		"7" = { // demand cargo
    			reply_refuse = "[xyz]";
    		};
    		"8" = { // surrender
    			transmit = "[xyz]";
    			reply_impatient = "[xyz]";
    			reply_police_accept = "[xyz]";
    			reply_police_reject = "[xyz]";
    		};
    		"9" = { // escape pod offer to rescue
    			transmit = "[xyz]";
    			reply_friendly = "[xyz]";
    			reply_unfriendly = "[xyz]";
    		};
    		"11" = { // keep away from target
    			reply = "[xyz]";
    		};
    	};
    
    	bcc_custom_messages = {
    		"level1" = {
    			// this will show "Level 1 message" in the Broadcast Comms MFD
    			// when selected, it will display the "transmit" message
    			// after 2 seconds, it will display the "reply" message, and run the post_reply_function, passing the ship and messagekey ("level1") to the function
    			// then it will make the messages "level2a" and "level2b" visible
    
    			state = "visible"; // whether the player will be offered this option immediately;
    			display = "Level 1 message"; // what to display in the MFD
    			delete_on_transmit = yes; // indicates whether message option should be removed from MFD after use
    			transmit = "This is the transmitted message for level 1."; // what to transmit when the option is activated
    			delay = 2; // how long to delay after transmission
    			reply = "This is the response to level 1."; // the message that will be sent back to the player.
    			post_reply_function = "$test_reply"; // function to call after sending the reply (optional)
    			post_reply_worldscript = "MyWorldScriptName";
    			post_reply_show_messages = ("level2a", "level2b"); // messages to make visible after the reply is received
    		};
    		"level2a" = {
    			// this will show "Level 2 message A" in the Broadcast Comms MFD
    			// when selected, it will display the "transmit" message
    			// after 2 seconds, it will display the "reply" message, and run the post_reply_function, passing the ship and messagekey ("level2a") to the function
    			// then it will hide "level2b" (it doesn't need to hide itself, as the "delete_on_transmit" is set to "yes")
    
    			state = "hidden";
    			display = "Level 2 message A"; // what to display in the MFD
    			delete_on_transmit = yes; // indicates whether message option should be removed from MFD after use
    			transmit = "This is the transmitted message for level 2A."; // what to transmit when the option is activated
    			delay = 2; // how long to delay after transmission
    			reply = "This is the response to level 2A."; // the message that will be sent back to the player.
    			post_reply_function = "$test_reply"; // function to call after sending the reply (optional)
    			post_reply_worldscript = "MyWorldScriptName";
    			post_reply_hide_messages = ("level2b");
    		};
    		"level2b" = {
    			// this will show "Level 2 message B" in the Broadcast Comms MFD
    			// when selected, it will display the "transmit" message.
    			// after 2 seconds it will display the "reply" message, and run the post_reply_function, passing the ship and messagekey ("level2b") to the function
    			// then it will hide "level2a" (it doesn't need to hide itself, as the "delete_on_transmit" is set to "yes")
    
    			state = "hidden";
    			display = "Level 2 message B"; // what to display in the MFD
    			delete_on_transmit = yes; // indicates whether message option should be removed from MFD after use
    			transmit = "This is the transmitted message for level 2B."; // what to transmit when the option is activated
    			delay = 2; // how long to delay after transmission
    			reply = "This is the response to level 2B."; // the message that will be sent back to the player.
    			post_reply_function = "$test_reply"; // function to call after sending the reply (optional)
    			post_reply_worldscript = "MyWorldScriptName";
    			post_reply_hide_messages = ("level2a");
    		};
    	};
    
    
    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/
    
    Discussion
    ==========
    This OXP is discussed at this forum link: http://bb.oolite.space/viewtopic.php?f=4&t=16826
    
    Version History
    ===============
    1.4
    - Moved all text into descriptions.plist for easier localisation.
    
    1.3.7
    - Better handling of unpurchased equipment. MFD will not be shown at all in this case.
    
    1.3.6
    - Better handling of damaged equipment - MFD will now show a "damaged" message.
    
    1.3.5
    - Really fixed JS error when targeting wormholes (or any entity that doesn't have a script object).
    
    1.3.4
    - Fixed JS error when targeting wormholes (or any entity that doesn't have a scriptInfo object).
    - Fixed JS error where a ship may not be in a group when attacking the player and a bribe is offered.
    
    1.3.3
    - Added check for existing message before trying to add custom messages via bcc_custom_messages.
    - Removed debug message.
    
    1.3.2
    - Fixed issue with not being able to replace core messages with custom ones.
    
    1.3.1
    - Fixed issue with some transmissions defaulting to a blank message.
    - Added checks for "bcc_disable_defaults" when validating new external messages, making it easier to replace core messages with custom ones.
    
    1.3
    - Added ability to control messages via script_info.
    - Added TestBed OXP (in Resources folder) for demonstrating how to use the script_info integrations.
    - Fixed issue where requesting docking clearance from a station with no docks would not be correctly interpreted.
    
    1.2.12
    - Improved integration with Bounty System OXP.
    - Improvements to the process of surrendering to police, as suggested by Milo.
    - Added method to allow message responses to be overridden by another OXP.
    
    1.2.11
    - Fix for issue where surrendering to police was not implemented correctly/completely.
    
    1.2.10
    - Tweaks to routine that stops timers.
    
    1.2.9
    - Fixed missing expansion in bribe message.
    
    1.2.8
    - Added missing ";" to role-categories.plist.
    
    1.2.7
    - Moved initialisations to startUp.
    
    1.2.6
    - Corrected errors in descriptions.plist.
    
    1.2.5
    - Moved "AccuracyAdj" out of the global namespace.
    - Improved methodology for NPC type identification.
    - Added some additional responses.
    - Code refactoring.
    
    1.2.4
    - Fix for "Send distress message" not sending the correct message.
    - Further updates for docking requests to handle further changes in Oolite 1.85/6.
    - Code refactoring.
    
    1.2.3
    - Updated method for determining player's next target system, to use new property "nextSystem". (Oolite 1.85/6 only - previous version will use old method).
    - Added "Request docking clearance" and "Withdraw docking clearance" messages, available when targeting a station. (Oolite 1.85/6 only - not available in previous versions).
    - Removed hard-coded scanner range value.
    - Code stability improvements.
    
    1.2.2
    - Sound effects added for mode/activate functions of MFD.
    - Determination of player's hyperspace destination now considers the presence of the ANA.
    
    1.2.1
    - Added the ability to sell the MFD.
    - Corrected determination of Elite rankings when used in chance calculations.
    - Changed "==" comparisons to "===" for performance improvements.
    - Adjustments to the $playerTargetSystem calculation.
    - Changed the logic for determining if a message already exists or not, making it purely based on the message name.
    - Restructured internal array to include messageName in list, making it easier to reference message.
    
    1.2.0
    - Added "hideOnConditionRed" option to custom messages, allowing messages to be hidden if the player is under attack.
    - Fixed issue where, after having targeted something which is then scooped (ie escape capsule), messages were staying available to transmit.
    - Internal code cleanup.
    
    1.1.2
    - Fixed issue with $checkMessageExists function, which was not checking for a null value in the message array.
    - Added check for hostile targets when switching to red alert mode, so the MFD understands the difference between an environment red alert and a situational red alert.
    - Fixed inconsistency in external function names. $checkMessageExists was missing the leading "$" symbol.
    - Code cleanup.
    
    1.1.1
    - Further small tweaks to the wormhole request responses.
    
    1.1.0
    - Added some exclusions for Generation ships, just to be sure.
    - Small tweaks to the wormhole request responses.
    - Made the equipment less likely to be damaged.
    - Turned off the "portable_between_ships", as this is equipment installed in a ship and logically would need to be installed in a new one.
    - Fixed manifest file so the category matches the expansion manager setting.
    
    1.0.19
    - Fixed issue with JavaScript error when in interstellar space.
    
    1.0.18
    - Fixed issue with the MFD being too chatty and sending unnecessary updates to the console.
    - Fixed issue with police responding to surrender like a pirate. If you're not a fugitive they will now wait for the player to disable their weapons systems, and if the player responds in time, they will (probably) accept the surrender.
    - You can now also attempt to bribe a police ship. Police may accept a bribe, if they're not in the station aegis, and the chance increases with lower government types. They might also fine the player.
    - Fixed issue with surrendering to thargoids. They will now curse you. And probably continue shooting.
    - Bug fixes.
    
    1.0.17
    - Correctly resets the target system after a hyperspace jump or a launch from a station, so the "Is anyone heading to ..?" question will be available immediately.
    
    1.0.16
    - Code improvements as suggested by Wildeblood.
    
    1.0.15
    - Updates for 1.82 compatibility, particularly around the player.ship.targetSystem not being the next jump point any more. New process implemented.
    - Made MFD equipment item portable between ships
    
    1.0.14
    - Changed specification of the createMessage and updateMessage functions to use an object rather than parameters.
    - Added two new message types, "(Target last comms message)" and "Keep away from my target".
    - Changed the default callback wait time for external messages to be 2 seconds.
    - If there are more than 9 messages available to the player, the box will now scroll, and indicators in the title will show whether there are more items either on the top or bottom of the list.
    - Lots of bug fixes
    
    1.0.13
    - Bug fixes and code refactoring
    - Removed the "Answer distress call" message, as it never gets triggered
    - Added the "Offer to rescue escape pod" message
    - Added facility where messages starting with a bracket (ie. "(", "{", "<" or "[") don't get a message transmitted, although the callback function will still be called. This allows the MFD to used like a menu system.
      For instance, an OXP could add an item called "(Switch to special comms mode)". When the player executes that option, the OXP could disable all the default messages, and display a new, custom list, with an extra item called "(Switch to normal comms mode)" which then re-enables all then standard comms messages.
    - Changed the initial bribe amount to 1 credit, based on the reasoning that 1 CR = 1/2 ton of food, therefore is a considerable amount
      Also, this means there are more chances the pirates will accept a lower amount of bribe, thus making it more useful.
    
    1.0.12
    - Added "Send bribe to closest target" and "Surrender to closest target" messages, for when the player doesn't have a target but is under attack. Thanks for Norby for the suggestions.
    - Added the $disableMessage, $enableMessage, and $isMessageEnabled external functions, so that OXP developers can disable the standard internal messages, either individually or all of them.
    

    Equipment

    Name Visible Cost [deci-credits] Tech-Level
    Broadcast Comms MFD yes 2000 1+
    Remove Broadcast Comms MFD no 500 3+

    Ships

    This expansion declares no ships.

    Models

    This expansion declares no models.

    Scripts

    Path
    Scripts/broadcastcomms_equip.js
    "use strict";
    this.name        = "BroadcastCommsMFD_Equipment";
    this.author      = "phkb and Zireael";
    this.copyright   = "2015 phkb and Zireael";
    this.description = "Controls the selection and transmission of messages through the Broadcast Comms multi-function display.";
    this.licence     = "CC BY-NC-SA 4.0";
    
    this.activated = function() {
    	var w = worldScripts.BroadcastCommsMFD;
    	// transmit selected message
    	if (w._selectedMsg >= 0) w.$transmit();
    	this.$playSound("activate");
    }
    
    this.mode = function() {
    	var w = worldScripts.BroadcastCommsMFD;
    	// change the selected message
    	if (w._selectedMsg < w.$maxMessages()) w._selectedMsg += 1;
    	// if we've gone past the end, go back to the beginning
    	if (w._selectedMsg == w.$maxMessages()) w._selectedMsg = 0;
    	// refresh the MFD
    	w.$updateView(true);
    	this.$playSound("mode");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // play the buy/sell sound effects
    this.$playSound = function(soundtype) {
    	var mySound = new SoundSource;
    
    	switch (soundtype) {
    		case "mode":
    			mySound.sound = "[@click]";
    			break;
    		case "activate":
    			mySound.sound = "[@switch-on]";
    			break;
    		case "stop":
    			mySound.sound = "[@boop]";
    			break;
    	}
    	mySound.loop = false;
    	mySound.play();
    }
    
    Scripts/broadcastcomms_mfd.js
    "use strict";
    this.name = "BroadcastCommsMFD";
    this.author = "phkb and Zireael";
    this.copyright = "2015 phkb and Zireael";
    this.description = "Displays available broadcast messages in a MFD.";
    this.licence = "CC BY-NC-SA 4.0";
    
    //TODO:
    //Investigate the possibility of making all internal functions into a separate JS, and loading them using the external access functions
    // check for "Aliens" ships, and make response comms gibberish
    
    // specific settings for this oxp
    this._debug = false; // change to false for less logging
    this._MFDMaxLines = 9; // maximum number of lines visible in the MFD (10, minus 1 for the header line)
    this._bribeInitial = 1; // initial bribe offering (credits)
    this._demandCargo = 5; // amount of cargo to demand (tons)
    this._tauntAccuracyLength = 10; // how long the accuracy adjustment will last (in seconds)
    this._surrenderWait = 15; // time to wait (in seconds) after a surrender before checking player has complied
    this._surrenderWait_Police = 5; // time to wait (in seconds) after surrendering to police before checking if the player has complied (repeats until player reaches main station, jumps out of system, or is declared non-compliant)
    this._surrenderAccuracyLength = 20; // how long (in seconds) the extra accuracy lasts (given when player renegs surrender with lasers)
    this._outputToConsole = true; // indicates the selected comms message will be displayed as a console message
    // for cases where the MFD is not visible
    this._defaultResponseWait = 2; // default amount of time to wait between sending comms and reply
    this._internalMessages = 13; // number of internal messages
    this._lastSource = -1; // used to establish the start point for destination system calculations
    this._msgAlt = ""; // use to switch to a 3rd party OXP supplying new message responses
    this._idSource = 0;
    
    //=============================================================================================================
    // set up initial variables
    this.startUp = function () {
    	this._killedNames = []; // array containing list of ship names killed by the player in this outing
    	// used to determine how an escape pod will respond
    	this._escapepodShipNames = []; // array of links between escape pods and ships
    	this._scoopOffer = []; // keeps track of escape pods we've communicated with
    	this._selectedMsg = -1; // currently selected message (defaults to no message)
    	this._holdTarget = -1; // keeps track of the players hyperspace destination
    	this._lines = []; // available messages as lines on the MFD
    	this._greeted = []; // keeps track of ships we've greeted
    	this._bribed = []; // keeps track of ships we're bribed
    	this._demanded = []; // keeps track of ships we've demanded cargo from
    	this._alertCond = 1; // keeps track of the current alert condition
    	this._targetType = ""; // keeps track of the type of target (either npc, police, thargoid)
    	this._closestAttackerType = ""; // keeps track of the type of closest attacker
    	this._accuracyChanged = []; // keeps track of ships whose accuracy we've adjusted
    	this._bribeCurrent = []; // keeps track of the current bribe offered to different targets
    	this._bribeAmount = this._bribeInitial; // current bribe offering
    	this._piracyWorked = false; // flag to indicate that the players demand for cargo (piracy) worked
    	this._checkTimer = null; // timer to check for when the player target disappears (ie through a wormhole)
    	this._piracyNPCTimer = null; // timer for an NPC telling police about players act of piracy
    	this._piracyPoliceTimer = null; // timer for a police response to NPC telling them about players act of piracy
    	this._waitingForPiracyResponse = false; // flag to indicate the player is waiting for a target to respond to a demand for cargo
    	this._externalMessages = []; // any external messages
    	this._surrenderCurrent_Pirate = []; // keeps track of the current list of ships surrendered to
    	this._surrenderedTo = []; // keeps track of who we've surrendered to
    	this._surrenderTimer_Pirate = null; // timer that will wait for cargo being dumped after surrender
    	this._surrenderCheck_Pirate = false; // indicates that player has surrendered and that cargo should be getting dumped
    	this._surrenderCheck_Police = false; // indicates that player has sent a surrender request to police and has been told to disable weapons
    	this._surrenderTimer_Police = null; // timer to monitor player compliance after surrendering to police (will run until player docks or leaves system)
    	this._surrenderPoliceChances = 0; // if player is not complying with police instructions, they have this many timer updates remaining to comply
    	this._surrenderMainStationDist = 0; // keeps track of how close the player has come to the main station after surrender, in case they go somewhere else
    	this._surrenderBounty = 0; // "hidden" bounty after surrendering to police, will be restored when leaving system or docking at any station
    
    	this._dockingRequest = false;
    	this._targetStation = null;
    	this._targetLastComms = null; // holds reference to last transmission source
    	this._disableInternal = []; // array of flags to keep track of which internal messages have been disabled
    	for (var i = 0; i <= this._internalMessages; i++) {
    		this._disableInternal.push(false);
    	}
    	this.$updateView(false);
    	this._lastSource = system.ID;
    }
    
    // ideas: 	have a "sorry" message for when you accidentally fire on police or stations, which could then limit retaliation
    //				need to work out when the player has fired on GalCop's or system stations.
    
    //=============================================================================================================
    // external access
    
    //-------------------------------------------------------------------------------------------------------------
    // function used to create a custom message
    this.$createMessage = function (msgobj) {
    	var truetypes = ["yes", "1", "true", true, 1, -1];
    	var falsetypes = ["no", "0", "false", false, 0];
    	var transtypes = ["broadcast", "target"];
    
    	// check for some invalid conditions
    	if (msgobj.hasOwnProperty("messageName") === false || msgobj.messageName === "") {
    		throw "Invalid settings: messageName must not be null or blank.";
    	}
    	if (msgobj.hasOwnProperty("transmissionType") === true && transtypes.indexOf(msgobj.transmissionType) === -1) {
    		throw "Invalid settings: transmissiontype must be either 'broadcast' or 'target'.";
    	}
    	if (msgobj.hasOwnProperty("displayText") === false || msgobj.displayText === "") {
    		throw "Invalid settings: displayText must not be null or blank.";
    	}
    	if (msgobj.hasOwnProperty("callbackFunction") === false || msgobj.callbackFunction === "") {
    		throw "Invalid settings: callbackFunction not set.";
    	}
    	// if these params are null, default them
    	if (msgobj.hasOwnProperty("deleteOnTransmit") === false || falsetypes.indexOf(msgobj.deleteOnTransmit) === -1) msgobj.deleteOnTransmit = true;
    	if (msgobj.hasOwnProperty("delayCallback") === false) msgobj.delayCallback = this._defaultResponseWait;
    	if (msgobj.hasOwnProperty("messageText") === false || msgobj.messageText === "") msgobj.messageText = msgobj.displayText;
    	if (msgobj.hasOwnProperty("shipDisplayName") === false) msgobj.shipDisplayName = "";
    	if (msgobj.hasOwnProperty("hideOnConditionRed") === false || truetypes.indexOf(msgobj.hideOnConditionRed) === -1) msgobj.hideOnConditionRed = false;
    	// check that this message isn't already in the list
    	var found = false;
    	// check the external messages
    	if (!this._externalMessages) this._externalMessages = [];
    	if (this._externalMessages) {
    		for (var i = 0; i < this._externalMessages.length; i++) {
    			if (this._externalMessages[i] && this._externalMessages[i].messageName === msgobj.messageName) {
    				throw "Invalid settings: message already exists.";
    			}
    		}
    	}
    	var chkShip = null;
    	var disable_defaults = [];
    	if (msgobj.hasOwnProperty("ship")) chkShip = msgobj.ship;
    	if (chkShip && chkShip.scriptInfo.hasOwnProperty("bcc_disable_defaults") && Array.isArray(chkShip.scriptInfo.bcc_disable_defaults)) disable_defaults = chkShip.scriptInfo.bcc_disable_defaults
    	if (chkShip && chkShip.script.hasOwnProperty("bcc_disable_defaults") && Array.isArray(chkShip.script.bcc_disable_defaults)) disable_defaults = chkShip.script.bcc_disable_defaults
    
    	// then check the built in messages
    	if ((msgobj.displayText.indexOf(expandDescription("[bcc_anyone_heading]")) >= 0 && (!this._disableInternal[1] && disable_defaults.indexOf("1") == -1)) ||
    		(msgobj.displayText === expandDescription("[bcc_send_distress]") && (!this._disableInternal[2] && disable_defaults.indexOf("2") == -1)) ||
    		(msgobj.displayText === expandDescription("[bcc_send_greeting]") && (!this._disableInternal[3] && disable_defaults.indexOf("3") == -1)) ||
    		(msgobj.displayText === expandDescription("[bcc_send_taunt]") && (!this._disableInternal[4] && disable_defaults.indexOf("4") == -1)) ||
    		(msgobj.displayText === expandDescription("[bcc_send_threat]") && (!this._disableInternal[5] && disable_defaults.indexOf("5") == -1)) ||
    		(msgobj.displayText === expandDescription("[bcc_surrender]") && (!this._disableInternal[8] && disable_defaults.indexOf("8") == -1)) ||
    		(msgobj.displayText === expandDescription("[bcc_rescue_escape_pod]") && (!this._disableInternal[9] && disable_defaults.indexOf("9") == -1)) ||
    		(msgobj.displayText === expandDescription("[bcc_target_last]") && (!this._disableInternal[10] && disable_defaults.indexOf("10") == -1)) ||
    		(msgobj.displayText === expandDescription("[bcc_keep_away]") && (!this._disableInternal[11] && disable_defaults.indexOf("11") == -1)) ||
    		(msgobj.displayText === expandDescription("[bcc_request_dock]") && (!this._disableInternal[12] && disable_defaults.indexOf("12") == -1)) ||
    		(msgobj.displayText === expandDescription("[bcc_withdraw_dock]") && (!this._disableInternal[13] && disable_defaults.indexOf("13") == -1)) ||
    		(msgobj.displayText === expandDescription("[bcc_demand_cargo]", { amount: this._demandCargo }) && (!this._disableInternal[7] && disable_defaults.indexOf("7") == -1)) ||
    		(msgobj.displayText.indexOf(expandDescription("[bcc_offer_bribe]")) >= 0 && (!this._disableInternal[6]) && disable_defaults.indexOf("6") == -1)) {
    		throw "Invalid settings: Cannot use comm system message types.";
    	}
    	// if we didn't find it, add it now
    	this._externalMessages.push({
    		messageName: msgobj.messageName,
    		callbackFunction: msgobj.callbackFunction,
    		displayText: msgobj.displayText,
    		messageText: msgobj.messageText,
    		ship: msgobj.ship,
    		shipDisplayName: msgobj.shipDisplayName,
    		transmissionType: msgobj.transmissionType,
    		deleteOnTransmit: msgobj.deleteOnTransmit,
    		delayCallback: msgobj.delayCallback,
    		hideOnConditionRed: msgobj.hideOnConditionRed
    	});
    	/*var keys = Object.keys(msgobj);
    	for (var i = 0; i < keys.length; i++) {
    		log(this.name, keys[i] + " - " + msgobj[keys[i]]);
    	}*/
    	this.$buildMessageList();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // checks for the existence of a message in the current list. Returns true if found, otherwise false.
    this.$checkMessageExists = function (messagename) {
    	var found = false;
    	if (this._externalMessages != null && this._externalMessages.length > 0) {
    		for (var i = 0; i < this._externalMessages.length; i++) {
    			if (this._externalMessages[i] && this._externalMessages[i].messageName === messagename) {
    				found = true;
    				break;
    			}
    		}
    	}
    	return found;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // function used to update the message display text and transmission text of an existing message
    this.$updateMessage = function (msgobj) {
    	if (msgobj.hasOwnProperty("messageName") === false || msgobj.messageName === "") {
    		throw "Invalid setting: messageName must not be null or blank.";
    	}
    	if (msgobj.hasOwnProperty("displayText") === true && msgobj.displayText === "") {
    		throw "Invalid settings: displayText must not be blank.";
    	}
    	var found = false;
    	if (this._externalMessages) {
    		for (var i = 0; i < this._externalMessages.length; i++) {
    			if (this._externalMessages[i] && this._externalMessages[i].messageName != msgobj.messageName && this._externalMessages[i].displayText === msgobj.displayText) {
    				found = true;
    			}
    		}
    	}
    	if (found === true) {
    		throw "Invalid settings: displayText is already used in another another message.";
    	}
    	var rebuild = false;
    	// look through the list
    	if (this._externalMessages) {
    		for (var i = 0; i < this._externalMessages.length; i++) {
    			if (this._externalMessages[i] && this._externalMessages[i].messageName === msgobj.messageName) {
    				if (msgobj.hasOwnProperty("displayText") === true && this._externalMessages[i].displayText != msgobj.displayText) {
    					this._externalMessages[i].displayText = msgobj.displayText;
    					rebuild = true;
    				}
    				if (msgobj.hasOwnProperty("messageText") === true && msgobj.messageText != "") this._externalMessages[i].messageText = msgobj.messageText;
    				if (msgobj.hasOwnProperty("callbackFunction") === true) this._externalMessages[i].callbackFunction = msgobj.callbackFunction;
    				if (msgobj.hasOwnProperty("deleteOnTransmit") === true) this._externalMessages[i].deleteOnTransmit = msgobj.deleteOnTransmit;
    				if (msgobj.hasOwnProperty("delayCallback") === true) this._externalMessages[i].delayCallback = msgobj.delayCallback;
    				break;
    			}
    		}
    	}
    	// if we found a match, rebuild the message list
    	if (rebuild === true) this.$buildMessageList();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // function to disable internal messages
    // disable options
    // 0 - all
    // 1 - request for wormhole
    // 2 - send distress message
    // 3 - send greeting
    // 4 - send taunt
    // 5 - send threat
    // 6 - offer bribe
    // 7 - demand cargo
    // 8 - surrender to target
    // 9 - escape pod offer to rescue
    // 10 - target last comms message
    // 11 - keep away from my target
    // 12 - request docking clearance
    // 13 - withdraw docking request
    this.$disableMessage = function (msgid) {
    	if (msgid > this._disableInternal.length - 1) {
    		throw "Invalid settings: msgid must be less than " + (this._disableInternal.length - 1).toString() + ".";
    	}
    	if (msgid === 0) {
    		for (var i = 0; i < this._disableInternal.length; i++) {
    			this._disableInternal[i] = true;
    		}
    	} else {
    		this._disableInternal[msgid] = true;
    	}
    	this.$buildMessageList();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // function to enable internal messages
    // enable options
    // 0 - all
    // 1 - request for wormhole
    // 2 - send distress message
    // 3 - send greeting
    // 4 - send taunt
    // 5 - send threat
    // 6 - offer bribe
    // 7 - demand cargo
    // 8 - surrender to target
    // 9 - escape pod offer to rescue
    // 10 - target last comms message
    // 11 - keep away from my target
    this.$enableMessage = function (msgid) {
    	if (msgid > this._disableInternal.length - 1) {
    		throw "Invalid settings: msgid must be less than " + (this._disableInternal.length - 1).toString() + ".";
    	}
    	if (msgid === 0) {
    		for (var i = 0; i < this._disableInternal.length; i++) {
    			this._disableInternal[i] = false;
    		}
    	} else {
    		this._disableInternal[msgid] = false;
    	}
    	this.$buildMessageList();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // function to determine if an internal message is enabled or not
    // msgid = id of internal message.
    // returns true if message is enabled, otherwise false
    // passing msgid value of 0 will check if all messages are enabled. Only returns true if all messages are enabled. Otherwise false.
    this.$isMessageEnabled = function (msgid) {
    	if (msgid > this._disableInternal.length - 1) {
    		throw "Invalid settings: msgid must be less than " + (this._disableInternal.length - 1).toString() + ".";
    	}
    	if (msgid === 0) {
    		var count = 0;
    		for (var i = 0; i < this._disableInternal.length; i++) {
    			if (this._disableInternal[i] === false) count += 1;
    		}
    		if (count === this._disableInternal.length) {
    			return true;
    		} else {
    			return false;
    		}
    	} else {
    		return !this._disableInternal[msgid]
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // function used to remove a message from the external messages list
    this.$removeMessage = function (messagename) {
    	if (messagename == null || messagename === "") {
    		throw "Invalid setting: messagename must not be null or blank.";
    	}
    	var rebuild = false;
    	// look through the list
    	if (this._externalMessages) {
    		for (var i = 0; i < this._externalMessages.length; i++) {
    			if (this._externalMessages[i] && this._externalMessages[i].messageName === messagename) {
    				// found it, so set the array entry to null to remove it
    				this._externalMessages[i] = null;
    				rebuild = true;
    			}
    		}
    	}
    	// if we found a match, rebuild the message list
    	if (rebuild === true) this.$buildMessageList();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // the number of lines (messages) in our message box
    this.$maxMessages = function () {
    	return this._lines.length;
    }
    
    //=============================================================================================================
    // ship interfaces
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillDockWithStation = function _shipWillDockWithStation(station) {
    	this.$stopAllTimers();
    	this._dockingRequest = false;
    	var that = _shipWillDockWithStation; // pointer to this function
    	var bountysystem = (that.bountysystem = that.bountysystem || worldScripts.BountySystem_Core); // cache worldScript reference in a local property on this function
    	if (this._surrenderBounty > 0) { // hidden bounty from surrendering to police will be restored when docking with any station (not only the main station!)
    		if (bountysystem) bountysystem._changing = true; // tell Bounty System OXP to ignore the bounty change we are about to do (so it doesn't get recorded as a new offence)
    		player.bounty += this._surrenderBounty; // make hidden bounty visible again (add it to current bounty in case the player gained additional bounty meanwhile)
    		if (bountysystem) bountysystem._changing = false; // tell Bounty System OXP to pay attention to bounty changes again
    		this._surrenderBounty = 0; // clear our hidden bounty variable (need to do this regardless of whether Bounty System is installed)
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipLaunchedFromStation = function (station) {
    	this.$fullResetVariables();
    	this._holdTarget = this.$playerTargetSystem();
    	this.$buildMessageList();
    
    	this._lastSource = system.ID;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerBoughtEquipment = function (equipment) {
    	if (equipment === "EQ_BROADCASTCOMMSMFD_REMOVAL") {
    		var p = player.ship;
    		p.removeEquipment("EQ_BROADCASTCOMMSMFD_REMOVAL");
    		p.removeEquipment("EQ_BROADCASTCOMMSMFD");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.equipmentDamaged = function (equipment) {
    	if (equipment == "EQ_BROADCASTCOMMSMFD") {
    		this.$buildMessageList();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.equipmentRepaired = function (equipment) {
    	if (equipment == "EQ_BROADCASTCOMMSMFD") {
    		this.$buildMessageList();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.guiScreenChanged = function (to, from) {
    	if (!player.ship.isInSpace) return;
    	if (from === "GUI_SCREEN_SHORT_RANGE_CHART" || from === "GUI_SCREEN_LONG_RANGE_CHART") {
    		if (this._holdTarget != this.$playerTargetSystem()) {
    			this._holdTarget = this.$playerTargetSystem();
    			this.$buildMessageList();
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.alertConditionChanged = function (newCondition, oldCondition) {
    	if (!player.ship.isInSpace) return;
    	if (player.ship.isInSpace === true && this._alertCond != player.alertCondition) {
    		if (player.alertCondition != 3 || (player.alertCondition === 3 && player.alertHostiles === true)) this._alertCond = player.alertCondition;
    		this.$buildMessageList();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipTargetLost = function (target) {
    	this.$resetVariables();
    	this.$startTargetTimer();
    	if (target === this._targetLastComms) this._targetLastComms = null;
    	this._bribeAmount = this._bribeInitial;
    	this.$buildMessageList();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillEnterWitchspace = function _shipWillEnterWitchspace(station) {
    	var that = _shipWillEnterWitchspace; // pointer to this function
    	var bountysystem = (that.bountysystem = that.bountysystem || worldScripts.BountySystem_Core); // cache worldScript reference in a local property on this function
    	if (this._surrenderBounty > 0) { // hidden bounty from surrendering to police will be restored when jumping out of the system
    		if (!bountysystem) { // but do not do this if Bounty System is installed; it will take care of setting the player's bounty including the hidden amount
    			player.bounty += this._surrenderBounty; // make hidden bounty visible again (add it to current bounty in case the player gained additional bounty meanwhile)
    		}
    		this._surrenderBounty = 0; // clear our hidden bounty variable (need to do this regardless of whether Bounty System is installed)
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillExitWitchspace = function () {
    	this.$stopAllTimers();
    	this.$fullResetVariables();
    	this._holdTarget = this.$playerTargetSystem();
    	this.$buildMessageList();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipExitedWitchspace = function () {
    	// update the lastSource ID
    	if (system.ID != -1) this._lastSource = system.ID;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerEnteredNewGalaxy = function (galaxyNumber) {
    	this.$stopAllTimers();
    	this.$fullResetVariables();
    	this._holdTarget = -1;
    	this.$buildMessageList();
    	this._lastSource = system.ID;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipTargetAcquired = function (target) {
    	this._targetType = "";
    	this._closestAttackerType = "";
    	this._bribeAmount = this._bribeInitial;
    	if (target.isShip === true && target.isPiloted) {
    		if (target.scanClass != "CLASS_CARGO") {
    			this._targetType = "npc";
    			// not sure about OXP ships, whether analysis of all roles (not just the primary role) will be required
    			if (Ship.roleIsInCategory(target.primaryRole, "oolite-police") === true) this._targetType = "police";
    			if (target.scanClass === "CLASS_THARGOID") this._targetType = "thargoid";
    			if (Ship.roleIsInCategory(target.primaryRole, "oolite-bounty-hunter") === true) {
    				this._targetType = "hunter";
    				// work out what the bribe amount should be
    				this.$workOutCurrentBribeAmount(target);
    			}
    			if (Ship.roleIsInCategory(target.primaryRole, "oolite-pirate") === true) {
    				this._targetType = "pirate";
    				// work out what the bribe amount should be
    				this.$workOutCurrentBribeAmount(target);
    			}
    			if (target.isStation) this._targetType = "station";
    		}
    		if (target.scanClass === "CLASS_CARGO" && target.hasRole("escape-capsule") === true) this._targetType = "escapepod";
    	}
    	this.$buildMessageList();
    	this.$startTargetTimer();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = function (whom, why) {
    	if (this._debug === true) log(this.name, "shipDied whom = " + whom.displayName + " why = " + why);
    	this.$stopAllTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipTargetDestroyed = function (target) {
    	this.$resetVariables();
    	this.$startTargetTimer();
    	this._bribeAmount = this._bribeInitial;
    	if (target === this._targetLastComms) this._targetLastComms = null;
    	this.$buildMessageList();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.commsMessageReceived = function (message, sender) {
    	if (sender && sender != player.ship && sender.isStation === false && sender != player.ship.target) {
    		if (this._debug) log(this.name, "last comms: " + sender.displayName);
    		this._targetLastComms = sender;
    		this.$buildMessageList();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipKilledOther = function (whom, damageType) {
    	if (whom === this._targetLastComms) this._targetLastComms = null;
    	// keep track of the names of ship's crew we've killed, so if they end up in an escape pod, we can adjust the message responses
    	if (!whom.hasRole("escape-capsule") && (whom.isPiloted || whom.isDerelict || whom.hasRole("tharglet") || whom.hasRole("thargon"))) {
    		this._killedNames.push(whom.displayName);
    
    		// did the ship launch an escape pod? look for one within 1km of destroyed ship
    		var podsinRange = system.shipsWithRole("escape-capsule", whom, 1000);
    
    		if (podsinRange && podsinRange.length > 0) {
    			// we only get here if we've found at least 1 escape pod
    			podsinRange.forEach(function (pod) {
    				var bAdd = true;
    				// have we already added this pod to our array?
    				if (this._escapepodShipNames.length != 0) {
    					for (var i = 0; i < this._escapepodShipNames.length; i++) {
    						if (this._escapepodShipNames[i].escapepod === pod) bAdd = false;
    					}
    				}
    				if (bAdd === true) this._escapepodShipNames.push({
    					escapepod: pod,
    					shipname: whom.displayName
    				});
    			}, this);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipBeingAttacked = function (whom) {
    	// add this ship to the greeting list - we don't want to send a greeting to someone who's fired at us
    	this.$addShipToArray(whom, this._greeted);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipAttackedOther = function (other) {
    	if (this._debug === true) log(this.name, "shipAttackedOther - " + other.displayName);
    
    	// check if player is attacking a ship they surrendered to
    	if (this._surrenderCheck_Pirate === true) {
    		var scp = this._surrenderCurrent_Pirate;
    		if (scp != null && scp.length > 0) {
    			var found = false;
    			var group = "";
    			for (var i = 0; i < scp.length; i++) {
    				if (scp[i] === other) {
    					// we have a match - surrender is void
    					found = true;
    					group = scp[i].group;
    					break;
    				}
    			}
    			if (found === true) {
    				// increase their accuracy
    				for (var i = 0; i < scp.length; i++) {
    					scp[i].target = player.ship;
    					this.$addShipToAccuracyList(scp[i], 4, this._surrenderAccuracyLength);
    					if (scp[i].isVisible === true && ((group == null || group === "") || this.$rand(20) > 10)) {
    						// send a message from the group
    						this.$sendMessageToPlayer(scp[0], this.$getRandomItemDescription("response-break-surrender-pirate"));
    					}
    				}
    				// reset the surrender current values
    				this._surrenderCheck_Pirate = false;
    				this._surrenderCurrent_Pirate = [];
    				this._surrenderTimer_Pirate.stop();
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.cargoDumpedNearby = function (cargo, ship) {
    	// target dumped cargo after we asked
    	if (this._source === ship && this._waitingForPiracyResponse) {
    		if (this._debug === true) log(this.name, "cargo dumped from players piracy victim - piracy worked");
    
    		// is ship in range of a police vessel or station
    		if (this.$countLawVessels(ship) > 0 && this.$rand(50) > 40) {
    			if (this._debug === true) log(this.name, "police vessel was notified of act of piracy - player bounty increased");
    
    			this._piracyVictim = ship;
    			// start a timer to send a notification from npc to police
    			this._piracyNPCTimer = new Timer(this, this.$sendPiracyNPCResponse, 4, 0);
    		}
    		this._piracyWorked = true;
    		this._waitingForPiracyResponse = false;
    	}
    
    	// player dumped cargo after surrender
    	if (this._surrenderCheck_Pirate === true && ship === player.ship) {
    		// player has complied with surrender demands - turn off retaliation
    		this._surrenderTimer_Pirate.stop();
    		this._surrenderCheck_Pirate = false;
    		// set one of the pirates in the group to scoop cargo
    		if (this._surrenderCurrent_Pirate.length > 0) {
    			for (var i = 0; i < this._surrenderCurrent_Pirate.length; i++) {
    				if (this._surrenderCurrent_Pirate[i].cargoSpaceCapacity > 0) {
    					this._surrenderCurrent_Pirate[i] = cargo;
    					break;
    				}
    			}
    		}
    		this._surrenderCurrent_Pirate = [];
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerDockingClearanceExpired = function () {
    	this._dockingRequest = false;
    	if (oolite.compareVersion("1.85") <= 0) this.$buildMessageList();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerDockingClearanceGranted = function () {
    	this._dockingRequest = true;
    	if (oolite.compareVersion("1.85") <= 0) this.$buildMessageList();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerDockingClearanceCancelled = function () {
    	this._dockingRequest = false;
    	if (oolite.compareVersion("1.85") <= 0) this.$buildMessageList();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerRequestedDockingClearance = function (message) {
    	//log(this.name, "bcc " + message);
    	switch (message) {
    		case "DOCKING_CLEARANCE_EXTENDED":
    		case "DOCKING_CLEARANCE_DENIED_TRAFFIC_OUTBOUND":
    		case "DOCKING_CLEARANCE_DENIED_TRAFFIC_INBOUND":
    		case "DOCKING_CLEARANCE_GRANTED":
    			this._dockingRequest = true;
    			break;
    		case "DOCKING_CLEARANCE_DENIED_SHIP_FUGITIVE":
    		case "DOCKING_CLEARANCE_DENIED_NO_DOCKS":
    		case "DOCKING_CLEARANCE_NOT_REQUIRED":
    		case "DOCKING_CLEARANCE_CANCELLED":
    			this._dockingRequest = false;
    			break;
    	}
    	if (oolite.compareVersion("1.85") <= 0) this.$buildMessageList();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipSpawned = function (ship) {
    	var msgs = {};
    	if (ship.scriptInfo.hasOwnProperty("bcc_custom_messages")) msgs = ship.scriptInfo.bcc_custom_messages;
    	if (ship.script.hasOwnProperty("bcc_custom_messages")) msgs = ship.script.bcc_custom_messages;
    	if (Object.keys(msgs).length > 0) {
    		ship.script.bcc_id_source = this._idSource;
    		this._idSource += 1;
    		// put the dictionary on the ship script so we can track state (scriptInfo is readonly otherwise)
    		ship.script.bcc_current_messages = JSON.parse(JSON.stringify(msgs));
    		this.$setupCurrentCustomMessages(ship);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$setupCurrentCustomMessages = function (ship) {
    	var msgs = ship.script.bcc_current_messages;
    	if (Object.keys(msgs).length == 0) return;
    	var keys = Object.keys(msgs);
    	for (var i = 0; i < keys.length; i++) {
    		var msg = msgs[keys[i]];
    		if (msg.state == "visible") {
    			if (this.$checkMessageExists(this.name + "_" + ship.script.bcc_id_source + "_" + keys[i]) == false) {
    				this.$createMessage({
    					messageName: this.name + "_" + ship.script.bcc_id_source + "_" + keys[i],
    					transmissionType: "target",
    					ship: ship,
    					displayText: msg.display,
    					messageText: expandDescription(msg.transmit),
    					deleteOnTransmit: msg.delete_on_transmit,
    					delayCallback: msg.delay,
    					callbackFunction: this.$customMessageCallback.bind(this, ship, keys[i])
    				});
    			}
    		} else {
    			if (this.$checkMessageExists(this.name + "_" + ship.script.bcc_id_source + "_" + keys[i])) {
    				this.$removeMessage(this.name + "_" + ship.script.bcc_id_source + "_" + keys[i]);
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$customMessageCallback = function (ship, msg_key) {
    	var msgs = ship.script.bcc_current_messages;
    	var msg = msgs[msg_key];
    	msg.state = "hidden";
    	this.$sendMessageToPlayer(ship, expandDescription(msg.reply));
    	if (msg.hasOwnProperty("post_reply_hide_messages") && Array.isArray(msg.post_reply_hide_messages)) {
    		var replies = msg.post_reply_hide_messages;
    		for (var i = 0; i < replies.length; i++) {
    			msgs[replies[i]].state = "hidden";
    		}
    	}
    	if (msg.hasOwnProperty("post_reply_show_messages") && Array.isArray(msg.post_reply_show_messages)) {
    		var replies = msg.post_reply_show_messages;
    		for (var i = 0; i < replies.length; i++) {
    			msgs[replies[i]].state = "visible";
    		}
    	}
    	if (msg.hasOwnProperty("post_reply_function") && msg.post_reply_function != "" && msg.hasOwnProperty("post_reply_worldscript") && msg.post_reply_worldscript != "") {
    		worldScripts[msg.post_reply_worldscript][msg.post_reply_function](ship, msg_key);
    	}
    	// force the messages to be updated
    	this.$setupCurrentCustomMessages(ship);
    }
    
    //=============================================================================================================
    // starts the target watcher timer if it's appropriate to do so, otherwise stops the timer
    this.$startTargetTimer = function () {
    	// start/stop the check target timer
    	if (this._targetType != "" && this._targetType != "station" && (this._checkTimer == null || this._checkTimer.isRunning === false)) {
    		// if we have a non-station target, then start our check timer
    		this._checkTimer = new Timer(this, this.$checkForTargetInvalid, 1, 1);
    	} else if ((this._targetType === "" || this._targetType === "station") && this._checkTimer != null && this._checkTimer.isRunning === true) {
    		// stop our timer - we don't have a target to watch anymore
    		this._checkTimer.stop();
    	}
    }
    
    //=============================================================================================================
    // rebuilds the list of available messages, based on current state of play
    this.$buildMessageList = function () {
    	// store the currently selected message, so we can reselect it after the list is rebuilt
    	var stored = "";
    	if (this._selectedMsg >= 0 && this._selectedMsg < this._lines.length && this._lines[this._selectedMsg] != null) {
    		stored = this._lines[this._selectedMsg].id;
    	}
    
    	// reset comms log settings
    	this._lines = [];
    
    	// p = player ship, t = player target, tt = player's targets target
    	var p = player.ship,
    		t = p && p.target,
    		tt = t && t.target;
    
    	var disable_defaults = [];
    	if (t) {
    		if (t.scriptInfo && t.scriptInfo.hasOwnProperty("bcc_disable_defaults") && Array.isArray(t.scriptInfo.bcc_disable_defaults)) disable_defaults = t.scriptInfo.bcc_disable_defaults;
    		if (t.script && t.script.hasOwnProperty("bcc_disable_defaults") && Array.isArray(t.script.bcc_disable_defaults)) disable_defaults = t.script.bcc_disable_defaults;
    	}
    
    	if (p.equipmentStatus("EQ_BROADCASTCOMMSMFD") === "EQUIPMENT_OK") {
    
    		if (this._targetLastComms && this._disableInternal[10] === false && disable_defaults.indexOf("10") == -1) this._lines.push({
    			id: "core_10",
    			text: expandDescription("[bcc_target_last]")
    		});
    
    		// add default messages (ie "broadcast" messages)
    		// if there are hostiles present
    		if (this._alertCond === 3 && this._disableInternal[2] === false && disable_defaults.indexOf("2") == -1) this._lines.push({
    			id: "core_2",
    			text: expandDescription("[bcc_send_distress]")
    		});
    
    		// load up any external messages
    		if (this._externalMessages !== null && this._externalMessages.length > 0) {
    			var cMsg = {};
    			var bAdd = false;
    			for (var i = 0; i < this._externalMessages.length; i++) {
    				bAdd = false;
    				cMsg = this._externalMessages[i];
    				if (cMsg) {
    					// are we going to display this message?
    					if (cMsg.transmissionType === "broadcast" ||
    						(cMsg.transmissionType === "target" && cMsg.ship === null && cMsg.shipDisplayName === "" && t !== null) ||
    						(cMsg.transmissionType === "target" && cMsg.ship && t === cMsg.ship) ||
    						(cMsg.transmissionType === "target" && cMsg.shipDisplayName && t.displayName === cMsg.shipDisplayName)) {
    						bAdd = true;
    					}
    					if (this._alertCond === 3 && cMsg.hideOnConditionRed === true) bAdd = false;
    					if (bAdd === true) this._lines.push({
    						id: cMsg.messageName,
    						text: cMsg.displayText
    					});
    				}
    			}
    		}
    
    		// do we have a non-hostile target? add general target messages
    		// greeting, taunt, drop your cargo,
    		if (this._alertCond !== 3 && t && t.isPiloted && this._targetType !== "station" && this._targetType !== "escapepod") {
    			// check who we've greeted before
    			// only allow greeting to non-greeted ships
    			if (this.$itemIsInArray(t, this._greeted) === false && this._disableInternal[3] === false && disable_defaults.indexOf("3") == -1) {
    				this._lines.push({
    					id: "core_3",
    					text: expandDescription("[bcc_send_greeting]")
    				});
    			}
    			if (this._disableInternal[4] === false && disable_defaults.indexOf("4") == -1) this._lines.push({
    				id: "core_4",
    				text: expandDescription("[bcc_send_taunt]")
    			});
    
    			// only add add a demand for cargo if the ship has cargo capacity
    			if (t.cargoSpaceCapacity > 0 && this._disableInternal[7] === false && disable_defaults.indexOf("7") == -1) {
    				// check who we've demanded cargo from before
    				// only allow demands to ships once
    				if (this.$itemIsInArray(t, this._demanded) === false) {
    					this._lines.push({
    						id: "core_7",
    						text: expandDescription("[bcc_demand_cargo]", { amount: this._demandCargo })
    					});
    				}
    			}
    		}
    		if (t && t.isPiloted && this._targetType === "escapepod") {
    			if (this.$itemIsInArray(t, this._scoopOffer) === false && this._disableInternal[9] === false && disable_defaults.indexOf("9") == -1 && p.equipmentStatus("EQ_FUEL_SCOOPS") === "EQUIPMENT_OK") {
    				this._lines.push({
    					id: "core_9",
    					text: expandDescription("[bcc_rescue_escape_pod]")
    				});
    			}
    		}
    
    		// no target set (or target is not attacking player), but red alert condition
    		if (this._alertCond === 3 && (!t || tt !== p)) {
    			if (this._disableInternal[8] === false && disable_defaults.indexOf("8") == -1) this._lines.push({
    				id: "core_8",
    				text: expandDescription("[bcc_surrender_nearest]")
    			});
    			if (this._disableInternal[6] === false && disable_defaults.indexOf("6") == -1) this._lines.push({
    				id: "core_6",
    				text: expandDescription("[bcc_offer_bribe_nearest]")
    			});
    		}
    
    		// do we have a hostile target? add hostile target messages
    		// i surrender, taunt, threat, bribe
    		if (this._alertCond === 3 && t && t.isPiloted && this._targetType !== "station" && this._targetType !== "escapepod") {
    			// only allow surrender to ships we haven't surrendered to before ... unless they are police attacking us!
    			if ((t.isPolice && t.hasHostileTarget && tt == p) || (this._disableInternal[8] === false && disable_defaults.indexOf("8") == -1 && this.$itemIsInArray(t, this._surrenderedTo) === false)) {
    				this._lines.push({
    					id: "core_8",
    					text: expandDescription("[bcc_surrender]")
    				});
    			}
    
    			// only offer bribe option to ship targeting the player
    			if (tt === p && player.credits >= this._bribeAmount && (this._targetType === "npc" || this._targetType === "pirate" || this._targetType === "hunter" || this._targetType === "police")) {
    				// check who we've bribed before
    				// only allow bribing to non-bribed ships
    				if (this.$itemIsInArray(t, this._bribed) === false && this._disableInternal[6] === false && disable_defaults.indexOf("6") == -1) {
    					this._lines.push({
    						id: "core_6",
    						text: expandDescription("[bcc_offer_bribe_target]", { amount: formatCredits(this._bribeAmount, false, true) })
    					});
    				}
    			}
    			if (this._disableInternal[4] === false && disable_defaults.indexOf("4") == -1) this._lines.push({
    				id: "core_4",
    				text: expandDescription("[bcc_send_taunt]")
    			});
    			if (this._disableInternal[5] === false && disable_defaults.indexOf("5") == -1) this._lines.push({
    				id: "core_5",
    				text: expandDescription("[bcc_send_threat]")
    			});
    			if (this._disableInternal[11] === false && disable_defaults.indexOf("11") == -1) this._lines.push({
    				id: "core_11",
    				text: expandDescription("[bcc_keep_away]")
    			});
    		}
    
    		// if a target system is set
    		if (this._alertCond !== 3 && this._holdTarget !== global.system.ID && this._holdTarget >= 0 && this._disableInternal[1] === false && disable_defaults.indexOf("1") == -1) {
    			this._lines.push({
    				id: "core_1",
    				text: expandDescription("[bcc_anyone_heading_system]", { sysname: System.systemNameForID(this._holdTarget) })
    			});
    		}
    		// docking clearance
    		if (typeof p.requestDockingClearance === "function") {
    			if (t && t.isValid && t.isStation && t.canDockShip(p) && this._dockingRequest === false && this._disableInternal[12] === false && disable_defaults.indexOf("12") == -1) {
    				this._lines.push({
    					id: "core_12",
    					text: expandDescription("[bcc_request_dock]")
    				});
    			}
    			if (t && t.isValid && t.isStation && t.canDockShip(p) && this._dockingRequest === true && this._disableInternal[13] === false && disable_defaults.indexOf("13") == -1) {
    				this._lines.push({
    					id: "core_13",
    					text: expandDescription("[bcc_withdraw_dock]")
    				});
    			}
    		}
    	}
    
    	if (stored !== null && stored !== "") {
    		// in our new array, check to see that our stored message still exists
    		var found = false;
    		for (var i = 0; i < this._lines.length; i++) {
    			if (this._lines[i].id === stored) {
    				// if it does, set the selected message pointer to it
    				this._selectedMsg = i;
    				found = true;
    			}
    		}
    		if (found === false) {
    			// if the selected message isn't there anymore, reset the selected message pointer
    			this._selectedMsg = -1;
    		}
    	}
    
    	this.$updateView(false);
    }
    
    //=============================================================================================================
    // displays the available messages in the MFD
    this.$updateView = function (sendConsole) {
    
    	var p = player.ship;
    	if (p.equipmentStatus("EQ_BROADCASTCOMMSMFD") === "EQUIPMENT_OK") {
    
    		var output = "";
    		var glyph = ">";
    
    		// if we have lines, work out what to display, based on the current scroll position
    		if (this._lines != null && (this._lines.length != 1 || this._lines[0].text != "")) {
    
    			var start = 0;
    			if (this._selectedMsg > this._MFDMaxLines - 1) start = this._selectedMsg - 8;
    
    			// build the header
    			output = expandDescription("[bcc_mfd_header]");
    			if (start > 0) output += " << ";
    			if (this._lines.length > this._MFDMaxLines) {
    				if (this._selectedMsg < this._lines.length - 1) {
    					output += " >>";
    				}
    			}
    			output += "\n";
    
    			for (var i = start; i < this._lines.length; i++) {
    				if (this._selectedMsg === i) {
    					output += glyph + " ";
    					// only send an update to the console if the option is on and the request came from user input
    					if (this._outputToConsole === true && sendConsole === true) {
    						player.consoleMessage(expandDescription("[bcc_item_selected]", { item: this._lines[i].text }));
    					}
    				}
    				output += this._lines[i].text + "\n";
    			}
    		} else {
    			output = expandDescription("[bcc_mfd_header]");
    		}
    
    		// send output to MFD
    		p.setMultiFunctionText("BroadcastCommsMFD", output, false);
    
    	} else {
    		if (p.equipmentStatus("EQ_BROADCASTCOMMSMFD") == "EQUIPMENT_DAMAGED") {
    			p.setMultiFunctionText("BroadcastCommsMFD", expandDescription("[bcc_mfd_header]") + expandDescription("[bcc_damaged]"), false);
    		}
    	}
    }
    
    //=============================================================================================================
    // transmit selected message
    this.$transmit = function () {
    
    	var p = player.ship;
    	var msg = "";
    	var id = "";
    	var transmission = "";
    	var params = {};
    
    	// only allow a transmit if we're not currently waiting for a response to another message
    	if (this._delay == null || this._delay.isRunning === false) {
    		// do we have a selected message?
    		if (this._selectedMsg >= 0 && this._selectedMsg < this._lines.length && this._lines[this._selectedMsg] != null) {
    			id = this._lines[this._selectedMsg].id;
    			msg = this._lines[this._selectedMsg].text;
    			// perform tranmission
    			switch (id) {
    				case "core_1": //"Is anyone heading to somewhere?"
    					this.$sendBroadcastMessage(expandDescription("[bcc_anyone_heading_system]", { sysname: System.systemNameForID(this._holdTarget) }));
    					if (this._delay && this._delay.isRunning) this._delay.stop();
    					this._delay = new Timer(this, this.$respondToRequestForWormhole, this._defaultResponseWait, 0);
    					break;
    				case "core_2": //"Send distress message":
    					this.$sendBroadcastMessage(this.$getRandomItemDescription("transmit-help"));
    					p.broadcastDistressMessage();
    					break;
    				case "core_3": //"Send greeting to target":
    					if (this.$checkTarget() === false) return;
    					this._source = p.target;
    					transmission = this.$getCustomTransmission(this._source, 3);
    					if (transmission == "") transmission = this.$getRandomItemDescription("transmit-greeting-" + this._targetType);
    					this.$sendMessageToTarget(transmission);
    					if (this._delay && this._delay.isRunning) this._delay.stop();
    					this._delay = new Timer(this, this.$respondToGreeting.bind(this, this._source, this._targetType), this._defaultResponseWait, 0);
    					break;
    				case "core_4": //"Send taunt to target":
    					if (this.$checkTarget() === false) return;
    					this._source = p.target;
    					transmission = this.$getCustomTransmission(this._source, 4);
    					if (transmission == "") transmission = this.$getRandomItemDescription("transmit-taunt-" + this._targetType);
    					this.$sendMessageToTarget(transmission);
    					if (this._delay && this._delay.isRunning) this._delay.stop();
    					this._delay = new Timer(this, this.$respondToTaunt.bind(this, this._source, this._targetType), this._defaultResponseWait, 0);
    					// add this target to the greeted array - illogical to say Hi after a taunt
    					this.$addShipToArray(this._source, this._greeted);
    					// update the MFD straight away, because we might do this in yellow alert status
    					this.$buildMessageList();
    					break;
    				case "core_5": //"Issue threat to target":
    					if (this.$checkTarget() === false) return;
    					this._source = p.target;
    					transmission = this.$getCustomTransmission(this._source, 5);
    					if (transmission == "") transmission = this.$getRandomItemDescription("transmit-threat-" + this._targetType);
    					this.$sendMessageToTarget(transmission);
    					if (this._delay && this._delay.isRunning) this._delay.stop();
    					this._delay = new Timer(this, this.$respondToThreat, this._defaultResponseWait, 0);
    					// add this target to the greeted array - illogical to say Hi after a threat
    					this.$addShipToArray(this._source, this._greeted);
    					break;
    				case "core_6": //"Offer bribe to target":
    					if (msg.indexOf("nearest") === -1) {
    						if (this.$checkTarget() === false) return;
    						this._source = p.target;
    						var crType = "credits";
    						if (this._bribeAmount === 1) crType = "credit";
    						params = { amount: this._bribeAmount.toString(), credittype: crType };
    
    						transmission = this.$getCustomTransmission(this._source, 6, params);
    						if (transmission == "") transmission = this.$getRandomItemDescription("transmit-bribe", params);
    
    						this.$sendMessageToTarget(transmission);
    						if (this._delay && this._delay.isRunning) this._delay.stop();
    						this._delay = new Timer(this, this.$respondToBribe, this._defaultResponseWait, 0);
    
    						// add this target to the greeted array - illogical to say Hi after a bribe
    						this.$addShipToArray(this._source, this._greeted);
    					} else {
    						this._source = this.$findNearestAttacker();
    						if (this.$itemIsInArray(this._source, this._bribed) === false) {
    							// add this target to the greeted array - illogical to say Hi after a bribe
    							this.$addShipToArray(this._source, this._greeted);
    
    							this.$workOutCurrentBribeAmount(this._source);
    							if (this._bribeAmount < player.credits) {
    								params = { amount: this._bribeAmount.toString(), credittype: crType };
    								transmission = this.$getCustomTransmission(this._source, 6, params);
    								if (transmission == "") transmission = this.$getRandomItemDescription("transmit-bribe", params);
    
    								this.$sendMessageToTarget(transmission);
    								if (this._delay && this._delay.isRunning) this._delay.stop();
    								this._delay = new Timer(this, this.$respondToBribe, this._defaultResponseWait, 0);
    							} else {
    								// we shouldn't ever get here, because once you reach beyond your credit balance you are automatically
    								// added to the bribed array, but still. Just in case.
    								player.consoleMessage(expandDescription("[bcc_no_cash]"));
    							}
    						} else {
    							player.consoleMessage(expandDescription("[bcc_closest_bribed]"));
    						}
    					}
    					break;
    				case "core_7": //"Demand cargo":
    					if (this.$checkTarget() === false) return;
    					this._source = p.target;
    					var tons = this._demandCargo.toString() + "t";
    
    					params = { "tons": tons };
    					transmission = this.$getCustomTransmission(this._source, 7, params);
    					if (transmission == "") transmission = this.$getRandomItemDescription("transmit-piracy", params);
    
    					this.$sendMessageToTarget(transmission);
    					this._waitingForPiracyResponse = true;
    
    					if (this._targetType === "npc" || this._targetType === "pirate") {
    						if (this._debug === true) log(this.name, "ship '" + this._source.displayName + "' might be responding");
    
    						if (this._source.AIScript.oolite_priorityai) {
    							if (this._debug === true) log(this.name, "ai found!");
    
    							var hold = this._source.target;
    							this._source.AIScript.oolite_intership.cargodemand = this._demandCargo;
    							this._source.target = p;
    							this._source.performAttack();
    							this._source.AIScript.oolite_priorityai.reconsiderNow();
    							// de-target the player so any red-alert is removed
    							this._source.target = hold;
    						} else {
    							if (this._debug === true) log(this.name, "no priority AI found!");
    
    						}
    					}
    					// we can only demand once - after that, if you want cargo, use your lasers!
    					this.$addShipToArray(this._source, this._demanded);
    					this.$addShipToArray(this._source, this._greeted);
    					if (this._delay && this._delay.isRunning) this._delay.stop();
    					this._delay = new Timer(this, this.$respondToDemandForCargo, this._defaultResponseWait, 0);
    					this.$buildMessageList();
    					break;
    				case "core_8": //"Surrender to target":
    					if (msg.indexOf("nearest") === -1) {
    						if (this.$checkTarget() === false) return;
    						this._source = p.target;
    						transmission = this.$getCustomTransmission(this._source, 8);
    						if (transmission == "") transmission = this.$getRandomItemDescription("transmit-surrender");
    						this.$sendMessageToTarget(transmission);
    						if (this._delay && this._delay.isRunning) this._delay.stop();
    						this._delay = new Timer(this, this.$respondToSurrender, this._defaultResponseWait, 0);
    						// add this target to the greeted array - illogical to say Hi after a surrender
    						this.$addShipToArray(this._source, this._greeted);
    					} else {
    						this._source = this.$findNearestAttacker();
    						if ((this._source && this._source.isPolice) || this.$itemIsInArray(this._source, this._surrenderedTo) === false) {
    							transmission = this.$getCustomTransmission(this._source, 8);
    							if (transmission == "") transmission = this.$getRandomItemDescription("transmit-surrender");
    							this.$sendMessageToTarget(transmission);
    							if (this._delay && this._delay.isRunning) this._delay.stop();
    							this._delay = new Timer(this, this.$respondToSurrender, this._defaultResponseWait, 0);
    							// add this target to the greeted array - illogical to say Hi after a surrender
    							this.$addShipToArray(this._source, this._greeted);
    						} else {
    							player.consoleMessage(expandDescription("[bcc_closest_surrender]"));
    						}
    					}
    					break;
    				case "core_9": //"Offer to rescue escape pod":
    					if (this.$checkTarget() === false) return;
    					this._source = p.target;
    					transmission = this.$getCustomTransmission(this._source, 9);
    					if (transmission == "") transmission = this.$getRandomItemDescription("transmit-escapepod");
    					this.$sendMessageToTarget(transmission);
    					if (this._delay && this._delay.isRunning) this._delay.stop();
    					this._delay = new Timer(this, this.$respondToEscapePodOffer, this._defaultResponseWait, 0);
    					break;
    				case "core_10": //"(Target last comms message)":
    					if (this._targetLastComms) {
    						// still in range?
    						var found = false;
    						var tlc = p.checkScanner(true);
    						for (var i = 0; i < tlc.length; i++) {
    							if (tlc[i] === this._targetLastComms) found = true;
    						}
    						if (found === false) {
    							player.consoleMessage(expandDescription("[bcc_target_gone]"));
    						} else {
    							p.target = this._targetLastComms;
    						}
    					}
    					this._targetLastComms = null;
    					this.$buildMessageList();
    					break;
    				case "core_11": //"Keep away from my target":
    					if (this.$checkTarget() === false) return;
    					this._source = p.target;
    					this.$sendBroadcastMessage(this.$getRandomItemDescription("transmit-keep-away-from-target"));
    					if (this._delay && this._delay.isRunning) this._delay.stop();
    					this._delay = new Timer(this, this.$respondToKeepAwayFromTarget, this._defaultResponseWait, 0);
    					break;
    				case "core_12": // request docking clearance
    				case "core_13": // withdraw docking request
    					if (this.$checkTarget() === false) return;
    					if (p.target.isStation && p.target.canDockShip(p)) {
    						if (this._dockingRequest === false) {
    							this._dockingRequest = true;
    							this.$sendBroadcastMessage(this.$getRandomItemDescription("request-docking-clearance"));
    						} else {
    							this.$sendBroadcastMessage(this.$getRandomItemDescription("withdraw-docking-request"));
    							this._dockingRequest = false;
    						}
    						if (this._delay && this._delay.isRunning) this._delay.stop();
    						this._delay = new Timer(this, this.$respondToDockingRequest, this._defaultResponseWait, 0);
    						this._targetStation = p.target;
    					} else {
    						player.consoleMessage(expandDescription("[bcc_no_dock]"));
    					}
    					break;
    				default:
    					// this should be any external messages
    					var cMsg = {};
    					// find the message to call
    					for (var i = 0; i < this._externalMessages.length; i++) {
    						cMsg = this._externalMessages[i];
    						if (cMsg) {
    							if (cMsg.messageName === id) {
    								// we have a hit
    								// but only transmit the message if it doesn't start with bracket characters
    								if (msg.substring(0, 1) != "{" && msg.substring(0, 1) != "(" && msg.substring(0, 1) != "[" && msg.substring(0, 1) != "<") {
    									this.$sendMessageToTarget(expandDescription(cMsg.messageText));
    								}
    								if (this._delay && this._delay.isRunning) this._delay.stop();
    								this._delay = new Timer(this, cMsg.callbackFunction, cMsg.delayCallback, 0);
    								if (cMsg.deleteOnTransmit === true) {
    									this._externalMessages[i] = null;
    									this.$buildMessageList();
    								}
    								// break out of the loop once we've found a hit.
    								break;
    							}
    						}
    					}
    					break;
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$getCustomTransmission = function (ship, typeID, params) {
    	var msg = "";
    
    	var custom_defaults = {};
    	if (ship.scriptInfo.hasOwnProperty("bcc_custom_defaults")) {
    		custom_defaults = ship.scriptInfo.bcc_custom_defaults;
    	}
    	if (ship.script.hasOwnProperty("bcc_custom_defaults")) {
    		custom_defaults = ship.script.bcc_custom_defaults;
    	}
    
    	if (Object.keys(custom_defaults).length > 0) {
    		if (custom_defaults.hasOwnProperty(typeID) && custom_defaults[typeID].hasOwnProperty("transmit")) {
    			if (params) {
    				msg = expandDescription(custom_defaults[typeID].transmit, params);
    			} else {
    				msg = expandDescription(custom_defaults[typeID].transmit);
    			}
    		}
    	}
    	return msg;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$getCustomReply = function (ship, typeID, customProperty, params) {
    	var msg = "";
    	var property = "reply";
    	if (customProperty && customProperty != "") property = customProperty;
    
    	var custom_defaults = {};
    	if (ship.scriptInfo.hasOwnProperty("bcc_custom_defaults")) {
    		custom_defaults = ship.scriptInfo.bcc_custom_defaults;
    	}
    	if (ship.script.hasOwnProperty("bcc_custom_defaults")) {
    		custom_defaults = ship.script.bcc_custom_defaults;
    	}
    
    	if (Object.keys(custom_defaults).length > 0) {
    		if (custom_defaults.hasOwnProperty(typeID) && custom_defaults[typeID].hasOwnProperty(property)) {
    			if (params) {
    				msg = expandDescription(custom_defaults[typeID][property], params);
    			} else {
    				msg = expandDescription(custom_defaults[typeID][property]);
    			}
    		}
    	}
    	return msg;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // send message out to target
    this.$sendMessageToTarget = function (msg) {
    	// second parameter causes function to either fail or happen invisibly
    	player.ship.commsMessage(msg); //player.ship.target
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // send message out to everyone
    this.$sendBroadcastMessage = function (msg) {
    	player.ship.commsMessage(msg);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // send message to player
    this.$sendMessageToPlayer = function (source, msg) {
    	if (source.isValid === true) source.commsMessage(msg, player.ship);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // checks that the player has a valid target
    this.$checkTarget = function () {
    	if (player.ship.target == null) {
    		player.consoleMessage(expandDescription("[bcc_target_gone]"));
    		this.$buildMessageList();
    		return false;
    	} else {
    		return true;
    	}
    }
    
    //=============================================================================================================
    // response routines
    
    //=============================================================================================================
    // wormhole request - 1
    this.$respondToRequestForWormhole = function $respondToRequestForWormhole() {
    	// find all NPC ships in range who are about to go to destination
    	var npc = this.$findNPCShips();
    	var rsp = false;
    
    	npc.forEach(function (ship) {
    		// if the ships destination system is the same as the players, and a random chance passes, display a "come along" message
    		if (ship.destinationSystem === this._holdTarget && this.$rand(10) > 1) {
    			var respType = "std";
    			// there is a 70% chance a pirate will give an unfriendly response, and a 30% chance a trader will do it as well
    			if (((ship.hasRole("pirate") || ship.hasRole("assassin-light") || ship.hasRole("assassin-medium") || ship.hasRole("assassin-heavy")) && Math.random() > 0.3) || Math.random() > 0.7) {
    				respType = "unfriendly";
    			}
    			var reply = "";
    			reply = this.$getCustomReply(ship, 1, "reply_confirm");
    			if (reply == "") reply = this.$getRandomItemDescription("response-wormhole-" + respType);
    			this.$sendMessageToPlayer(ship, reply);
    		}
    		// if the ships destination system is not the players destination, but the ships destination is outbound, and a random chance passes, display a "I'm going somewhere else" message
    		if (ship.destinationSystem != this._holdTarget && ship.destinationSystem != global.system.ID && ship.hasRole("generationship") === false && this.$rand(10) > 2) {
    			if (ship.hasRole("pirate") || ship.hasRole("assassin-light") || ship.hasRole("assassin-medium") || ship.hasRole("assassin-heavy")) {
    				// unfriendly ships won't respond
    				return;
    			}
    			var reply = "";
    			var params = { systemname: System.systemNameForID(ship.destinationSystem) };
    			reply = this.$getCustomReply(ship, 1, "reply_decline", params);
    			if (reply == "") reply = this.$getRandomItemDescription("response-wormhole-decline", params);
    			this.$sendMessageToPlayer(ship, reply);
    		}
    	}, this);
    }
    
    //=============================================================================================================
    // respond to comms message -- ???
    this.$respondToCommsMessage = function $respondToCommsMessage() {
    	// is target hostile?
    	if (this._source.hasRole("generationship")) return;
    	if (this._source.hasRole("thargoid")) {
    		this.$sendMessageToPlayer(this._source, expandDescription("[thargoid_curses]"));
    	} else {
    		if (this._alertCond === 3) {
    			// is target actually targeting the player, or is the target part of a group that is targeting the player?
    			var friend = true;
    			if (this._source.target === player.ship) {
    				friend = false
    			} else {
    				if (this._source.group.ships && this._source.group.ships.length > 0) {
    					var ships = this._source.group.ships;
    					ships.forEach(function (ship) {
    						if (ship.target === player.ship) friend = false;
    					}, this);
    				}
    			}
    
    			if (friend === false) {
    				this.$sendMessageToPlayer(this._source, this.$getRandomItemDescription("response-comms-unfriendly"));
    			} else {
    				this.$sendMessageToPlayer(this._source, this.$getRandomItemDescription("response-comms-friendly"));
    			}
    		} else {
    			// is this ship under attack?
    			if (this.$countShipsTargetingNPC(this._source) > 0) {
    				this.$sendMessageToPlayer(this._source, this.$getRandomItemDescription("response-comms-friendly"));
    			} else {
    				this.$sendMessageToPlayer(this._source, this.$getRandomItemDescription("response-comms-nobattle"));
    			}
    		}
    	}
    }
    
    //=============================================================================================================
    // escape pod - 9
    this.$respondToEscapePodOffer = function $respondToEscapePodOffer() {
    	// did we shoot his ship down?
    	var podshipname = "";
    	if (this._escapepodShipNames.length > 0) {
    		for (var i = 0; i < this._escapepodShipNames.length; i++) {
    			if (this._escapepodShipNames[i].escapepod === this._source) podshipname = this._escapepodShipNames[i].shipname;
    		}
    	}
    
    	var reply = "";
    	if (podshipname != "" && this.$itemIsInArray(podshipname, this._killedNames) === true) {
    		reply = this.$getCustomReply(this._source, 9, "reply_unfriendly");
    		if (reply == "") reply = this.$getRandomItemDescription("response-escapepod-unfriendly");
    		this.$sendMessageToPlayer(this._source, reply);
    	} else {
    		reply = this.$getCustomReply(this._source, 9, "reply_friendly");
    		if (reply == "") reply = this.$getRandomItemDescription("response-escapepod-friendly");
    		this.$sendMessageToPlayer(this._source, reply);
    	}
    
    	this.$addShipToArray(this._source, this._scoopOffer);
    	// rebuild the messagelist (so that offer to scoop is removed)
    	this.$buildMessageList();
    }
    
    //=============================================================================================================
    // keep away from target - 11
    this.$respondToKeepAwayFromTarget = function $respondToKeepAwayFromTarget() {
    
    	var ships = checkScanner(true);
    	// look for any ships that are currently targeting the players target
    	ships.forEach(function (ship) {
    		if (ship.target === this._source) {
    			// is this ship friendly towards player
    			var friend = true;
    			if (ship.group.ships && ship.group.ships.length > 0) {
    				var shipgroup = ship.group.ships;
    				shipgroup.forEach(function (g_ship) {
    					if (g_ship.target === player.ship) friend = false;
    				}, this);
    			}
    			// only respond if ship is friendly
    			if (friend) {
    				ship.target = null;
    				ship.removeDefenseTarget(this._source);
    				var reply = this.$getCustomReply(this._source, 11);
    				if (reply == "") reply = this.$getRandomItemDescription("response-keep-away-from-target");
    				this.$sendMessageToPlayer(this._source, reply);
    			}
    		}
    	}, this);
    }
    
    //=============================================================================================================
    // respond to "request docking clearance" - 12
    this.$respondToDockingRequest = function $respondToDockingRequest() {
    	if (this._targetStation.isValid && this._targetStation.isStation && this._targetStation.canDockShip(player.ship)) {
    		if (this._dockingRequest === true) {
    			player.ship.requestDockingClearance(this._targetStation);
    		} else {
    			player.ship.cancelDockingRequest(this._targetStation);
    		}
    		this.$buildMessageList();
    	}
    }
    
    //=============================================================================================================
    // greeting - 3
    this.$respondToGreeting = function $respondToGreeting(ship, targetType) {
    
    	if (ship != null && ship.hasRole("generationship")) return;
    
    	// get one of predefined list of greetings and transmit
    	var rsp = 0; // 0 = no response, 1 = pick a response
    	if (this.$rand(10) > 2) rsp = 1;
    
    	// always give a response for thargoids
    	if (targetType === "thargoid") rsp = 1;
    
    	// todo: could this make the target friendlier towards the player?
    	// any changes to NPC behaviour will happen regardless of whether they have decided to send a response or not
    
    	if (rsp === 1 && ship != null && targetType != "" && ship.isPiloted) {
    		var reply = "";
    		reply = this.$getCustomReply(ship, 3);
    		if (reply == "") reply = this.$getRandomItemDescription("response-greeting-" + this._targetType);
    		this.$sendMessageToPlayer(ship, reply);
    		// add the target to the greeted list
    		this.$addShipToArray(ship, this._greeted);
    		// rebuild the messagelist (so that greeting is removed)
    		this.$buildMessageList();
    	}
    }
    
    //=============================================================================================================
    // bribe - 6
    this.$respondToBribe = function $respondToBribe() {
    	// there will always be a response to this message
    	// based on the amount of bribe, the chances of accepting grow steadily greater
    	// i.e. low chance of accepting with low amounts, greater chance with higher amounts
    	// initial chance of 2.5% of accepting (slightly higher chance of 12.5% when player is in the early game), increases as digits are added to bribe amount
    	var shiptype = "";
    	var chance = 0.025 + ((player.score < 100) ? 0.1 : 0.0) + (0.1 * (this._bribeAmount.toString().length - this._bribeInitial.toString().length))
    
    	// set the shiptype - initially the targeted shiptype
    	shiptype = this._targetType;
    	// however, if the closestAttacker is set, use that
    	if (this._closestAttackerType != "") shiptype = this._closestAttackerType;
    
    	if (shiptype === "police") {
    		// no chance of bribing a police vessel in the aegis
    		if (this._source.position.distanceTo(system.mainStation) < 51200) chance = 0;
    
    		// chance decreases based on system government level
    		if (chance > 0) {
    			chance -= system.info.government;
    			if (chance < 0) chance = 0;
    		}
    	}
    
    	if (this._debug === true) log(this.name, "chance of accepting bribe = " + chance.toString());
    
    	if (this._source != null && shiptype != "") {
    		var reply = "";
    		// if the pirates are demanding cargo, they might be susceptible to a bribe.
    		// if they're a bounty hunter, they probably want to kill the player, so they won't be
    		if (((shiptype === "police" || shiptype === "pirate") && this.$rand(50) > (50 * chance)) || shiptype === "hunter") {
    			// decline
    			if (this._debug === true) log(this.name, shiptype + " '" + this._source.displayName + "' rejected bribe of " + this._bribeAmount.toString());
    
    			// after a few bribes, the police will eventually fine the player
    			if (shiptype === "police") {
    				if (this._bribeAmount > 5 && Math.random() < 0.5) {
    					player.ship.setBounty(player.ship.bounty + this.$rand(20), "attempted bribe");
    					this.$addShipToBribedArray(this._source);
    					this._bribeAmount = this._bribeInitial;
    					reply = this.$getCustomReply(this._source, 6, "reply_fine");
    					if (reply == "") reply = this.$getRandomItemDescription("response-bribe-police-fine");
    					this.$sendMessageToPlayer(this._source, reply);
    					this.$buildMessageList();
    					return;
    				}
    			}
    
    			reply = this.$getCustomReply(this._source, 6, "reply_decline");
    			if (reply == "") reply = this.$getRandomItemDescription("response-bribe-decline-" + shiptype);
    			this.$sendMessageToPlayer(this._source, reply);
    			if (this._bribeAmount.toString().substring(0, 1) === "2") {
    				// use 2.5 when then current amount starts with "2", so from 200 we end up on 500, from 2000 to 5000, 20000 to 50000 etc
    				this._bribeAmount *= 2.5;
    			} else {
    				// otherwise use the default multiple of 2, so we go from 100 to 200, 500 to 1000 etc
    				this._bribeAmount *= 2;
    			}
    			if (this._bribeAmount > player.credits) {
    				// can't be bribed
    				// add them to the bribed list so they can't be offered a bribe and reset bribe amount
    				this.$addShipToBribedArray(this._source);
    				this._bribeAmount = this._bribeInitial;
    			} else {
    				// update the current bribe array with the new bribe amount
    				// if the player detargets the ship and retargets them, the same amount will then be shown
    				this.$updateShipInCurrentBribeArray(this._source, true);
    			}
    			this.$buildMessageList();
    		} else {
    			// accept
    			if (this._debug === true) log(this.name, shiptype + " '" + this._source.displayName + "' accepted bribe of " + this._bribeAmount.toString());
    
    			reply = this.$getCustomReply(this._source, 6, "reply_accept");
    			if (reply == "") reply = this.$getRandomItemDescription("response-bribe-accept-" + (shiptype === "police" ? "police" : "npc"));
    			this.$sendMessageToPlayer(this._source, reply);
    
    			player.credits -= this._bribeAmount;
    			// detarget the player from the group
    			if (this._source.group != "") {
    				this.$removePlayerTargetFromPirateGroup(this._source);
    			} else {
    				if (this._debug === true) log(this.name, "player de-targeted from ship " + this._source.displayName);
    
    				this._source.target = null;
    			}
    			// add the ship(s) to the bribed array so we don't offer that option again
    			this.$addShipToBribedArray(this._source);
    			this._bribeAmount = this._bribeInitial;
    			this.$buildMessageList();
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // add a ship (or a group of ships) to the bribeCurrent array
    this.$updateShipInCurrentBribeArray = function (ship, recursive) {
    
    	// keep track of what we've offered different ships
    	if (this._bribeCurrent != null && this._bribeCurrent.length > 0) {
    		var found = false;
    		for (var i = 0; i < this._bribeCurrent.length; i++) {
    			if (this._bribeCurrent[i].ship === ship) {
    				this._bribeCurrent[i].amount = this._bribeAmount;
    				found = true;
    			}
    		}
    		if (found === false) {
    			this._bribeCurrent.push({
    				ship: ship,
    				amount: this._bribeAmount
    			});
    		}
    	} else {
    		this._bribeCurrent.push({
    			ship: ship,
    			amount: this._bribeAmount
    		});
    	}
    
    	// add in all the pirates in the group, so we can't keep trying to bribe individual members
    	if (recursive === true) {
    		if (ship.group == null || ship.group === "") {
    			// single
    			// do nothing
    		} else {
    			// group
    			var shipGroup = ship.group.ships;
    			shipGroup.forEach(function (groupship) {
    				this.$updateShipInCurrentBribeArray(groupship, false);
    			}, this);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // adds a ship (or a group of ships) to the bribed array
    this.$addShipToBribedArray = function (ship) {
    	if (ship.group == null || ship.group === "") {
    		// single
    		this.$addShipToArray(ship, this._bribed);
    	} else {
    		// group
    		var shipgroup = ship.group.ships;
    		shipgroup.forEach(function (groupship) {
    			this.$addShipToArray(groupship, this._bribed);
    		}, this);
    	}
    }
    
    //=============================================================================================================
    // taunt - 4
    this.$respondToTaunt = function $respondToTaunt(ship, targetType) {
    	if (ship != null && ship.hasRole("generationship")) return;
    
    	var reply = "";
    
    	// get one of predefined list of taunts and transmit
    	var rsp = 0; // 0 = no response, 1 = pick a response
    	// hight chance of a response
    	if (this.$rand(10) > 2) rsp = 1;
    	// always give a response for thargoids
    	if (targetType === "thargoid") rsp = 1;
    
    	var p = player;
    
    	// any changes to NPC behaviour will happen regardless of whether they have decided to send a response or not
    	// todo: could this make the target angrier towards the player?
    	// possibilities: for enemy: briefly makes their accuracy drop
    	// 				  			 briefly makes their accuracy increase
    	if (ship != null && ship.target === p.ship && this._alertCond === 3 && targetType != "" && targetType != "thargoid") {
    		if (this._debug === true) log(this.name, "we'll probably be changing the accuracy now...");
    
    		// settings to configure randomness
    		var chanceOfChange = 0.5; // likelihood of change as decimal, where 0.5 = 50%, .75 = 75%, etc
    		var maxChange = 3; // maximum amount of change to accuracy
    
    		// this npc is targeting the player
    		// decide if this taunt is going to have an impact
    		if (this.$rand(50) >= (50 * chanceOfChange)) {
    			if (this._debug === true) log(this.name, "yep - changing");
    
    			// decide how much better/worse their accuracy will be, either 1, 2 or 3 points
    			var diff = this.$rand(maxChange);
    			// decide if the npc will get become more or less accurate, although more likely it will be less accurate
    			// 33% chance of more accurate, 66% chance of less accurate
    			if (this.$rand(3) > 1) diff *= -1; // less accurate
    
    			// add the ship to the accuracyChanged array
    			this.$addShipToAccuracyList(ship, diff, this._tauntAccuracyLength);
    		}
    	}
    	// if the players target is not targeting the player, then potentially switch their target to the player
    	// this could be used to draw someone's fire away from another ship
    	if (ship != null && ship.target != p.ship && targetType != "") {
    		var chance = 45;
    		// adjust the chance by player score - the higher the score the less likely they will attack
    		// elite ranking
    		if (p.score >= 32 && p.score < 64) chance -= 5; // average
    		if (p.score >= 64 && p.score < 128) chance -= 10; // above average
    		if (p.score >= 128 && p.score < 512) chance -= 15; // competant
    		if (p.score >= 512 && p.score < 2560) chance -= 20; // dangerous
    		if (p.score >= 2560 && p.score < 6400) chance -= 25; // deadly
    		if (p.score >= 6400) chance -= 30; // elite
    		this.$willPlayerTargetTurnOnPlayer(chance);
    	}
    
    	// for nonenemy: make them more likely to start firing at you
    
    	// send out the response
    	if (rsp === 1 && ship != null && ship.isPiloted) {
    		reply = this.$getCustomReply(ship, 4);
    		if (reply == "") reply = this.$getRandomItemDescription("response-taunt-" + targetType);
    		this.$sendMessageToPlayer(ship, reply);
    	}
    }
    
    //=============================================================================================================
    // piracy (demand for cargo) - 7
    this.$respondToDemandForCargo = function $respondToDemandForCargo() {
    
    	if (this._source != null && this._source.hasRole("generationship")) return;
    
    	// only need to send a response if the ships refuses to comply
    	if (this._source != null && this._targetType != "" && this._piracyWorked === false) {
    		var reply = this.$getCustomReply(this._source, 7, "reply_refuse");
    		if (reply == "") reply = this.$getRandomItemDescription("response-piracy-" + this._targetType);
    		this.$sendMessageToPlayer(this._source, reply);
    		// rebuild the messagelist (so that greeting is removed)
    		this._waitingForPiracyResponse = false;
    		this.$buildMessageList();
    	}
    
    	this._piracyWorked === false;
    
    }
    
    //=============================================================================================================
    // surrender - 8
    this.$respondToSurrender = function $respondToSurrender() {
    	// todo: has pirate asked for cargo?
    
    	//note: real check will be 48
    	if (this._debug === true) log(this.name, "responding to surrender");
    	var reply = "";
    
    	// pirate have a chance of accepting the surrender, police will always accept it
    	if (this._source != null && (this._source.isPolice || this.$rand(50) > 45)) {
    		// remove the player as the primary target from any NPC's targeting the player
    		if (this._source.isPolice === false) {
    			this._surrenderCurrent_Pirate = [];
    			var isGroup = false;
    			if (this._source.group && this._source.group != "") {
    				isGroup = true;
    				var group = this._source.group.ships;
    				group.forEach(function (ship) {
    					this.$addShipToArray(ship, this._surrenderedTo);
    					this.$addShipToArray(ship, this._surrenderCurrent_Pirate);
    				}, this);
    			} else {
    				this.$addShipToArray(this._source, this._surrenderedTo);
    				this.$addShipToArray(this._source, this._surrenderCurrent_Pirate);
    			}
    			this.$buildMessageList();
    
    			if (this._debug === true) log(this.name, "pirates accepted surrender");
    
    			if (this._source.group != "") {
    				this.$removePlayerTargetFromPirateGroup(this._source);
    			} else {
    				this._source.target = null;
    			}
    			if (!this._surrenderTimer_Pirate || this._surrenderTimer_Pirate.isRunning === false) {
    				this._surrenderTimer_Pirate = new Timer(this, this.$surrenderCheckState_Pirate, this._surrenderWait, 0);
    			}
    			this._surrenderCheck_Pirate = true;
    			// reply should be "Hurry up and dump some cargo, scum"
    			var pnCap = "We";
    			var pnLw = "we";
    			if (isGroup === false) {
    				pnCap = "I";
    				pnLw = "I";
    			}
    			reply = this.$getCustomReply(this._source, 8, "reply_impatient");
    			if (reply == "") reply = this.$getRandomItemDescription("response-surrender-pirate", { pronouncap: pnCap, pronoun: pnLw });
    			this.$sendMessageToPlayer(this._source, reply);
    
    		} else {
    			// police surrender
    			// only if bounty is less than 50 ie offender status only. Fugitive will not be allowed to surrender
    			if (player.bounty < 50) {
    				if (player.bounty > 30 && Math.random() < 0.5) {
    					if (this._debug === true) log(this.name, "police ignored surrender request from player with high bounty (50% chance)");
    					return; // no response - player can try sending another surrender
    				}
    				reply = this.$getCustomReply(this._source, 8, "reply_police_accept");
    				if (reply == "") reply = this.$getRandomItemDescription("response-surrender-police");
    				this.$sendMessageToPlayer(this._source, reply);
    				this.$addShipToArray(this._source, this._surrenderedTo); // add the police ship that responded to list that player can't surrender to again (we also use this to identify who should send rejection if the player doesn't comply)
    				this._surrenderCheck_Police = true; // a separate variable is needed because the timer keeps running and we want to know if the player surrenders again to new police after gaining additional bounty
    				if (this._surrenderPoliceChances === 0) this._surrenderPoliceChances = 5; // number of timer updates to tolerate non-compliance (but no extra chances if they send multiple surrender messages)
    				// start a timer, if it's not already started
    				if (!this._surrenderTimer_Police || this._surrenderTimer_Police.isRunning === false) {
    					this._surrenderTimer_Police = new Timer(this, this.$surrenderCheckState_Police, this._surrenderWait_Police, this._surrenderWait_Police); // repeats until stopped
    				}
    			} else {
    				reply = this.$getCustomReply(this._source, 8, "reply_police_reject");
    				if (reply == "") reply = this.$getRandomItemDescription("response-deny-surrender-police");
    				this.$sendMessageToPlayer(this._source, reply); // fugitive rejection
    			}
    		}
    	} else {
    		if (this._debug === true) log(this.name, "declined");
    		if (this._source.isThargoid) {
    			this.$sendMessageToPlayer(this._source, expandDescription("[thargoid_curses]"));
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // check that the player has surrendered to the pirate
    // if we get here, it means the player hasn't dumped cargo (which would stop the timer)
    this.$surrenderCheckState_Pirate = function $surrenderCheckState_Pirate() {
    
    	this._surrenderCheck_Pirate = false;
    	var scp = this._surrenderCurrent_Pirate;
    	if (scp != null && scp.length > 0) {
    		for (var i = 0; i < scp.length; i++) {
    			// retarget the player
    			if (scp[i] && scp[i].isValid === true) {
    				scp[i].target = player.ship;
    				if (this._debug === true) log(this.name, "player re-targeted on ship " + scp[i].displayName + " in group " + scp[i].group);
    
    				// send message, from random members of the group, or from the single pirate otherwise
    				if (scp[i].isVisible === true && ((scp[i].group == null || scp[i].group === "") || this.$rand(20) > 10)) {
    					// send a message from the group
    					this.$sendMessageToPlayer(scp[0], this.$getRandomItemDescription("response-break-surrender-pirate"));
    				}
    				scp[i].performAttack();
    			}
    		}
    	}
    	// clear the array
    	this._surrenderCurrent_Pirate = [];
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // check that the player has surrendered to the police
    // player has to disable their weapons system ("_" key)
    /*this.$surrenderCheckState_Police = function $surrenderCheckState_Police() {
    
    	var law = this.$findLawVessels(player.ship);
    	var sent = false;
    
    	// if the player hasn't turned their weapons off transmit message
    	if (player.ship.weaponsOnline) {
    		if (this._debug === true) log(this.name, "Police rejected surrender");
    		// send a message from the group
    		if (law.length > 0) {
    			for (var i = 0; i < law.length; i++) {
    				if (law[i].isStation === false && law[i].target != player.ship) {
    					if (sent === false) {
    						this.$sendMessageToPlayer(law[i], this.$getRandomItemDescription("response-break-surrender-police"));
    						sent = true;
    					}
    				}
    			}
    		}
    	} else {
    		if (this._debug === true) log(this.name, "Police accepted surrender");
    		for (var i = 0; i < law.length; i++) {
    			if (law[i].isStation === false && law[i].target === player.ship) {
    				this.$addShipToArray(law[i], this._surrenderedTo);
    				if (sent === false) {
    					this.$sendMessageToPlayer(law[i], this.$getRandomItemDescription("response-accept-surrender-police"));
    					sent = true;
    					// mark the player for fines
    					if (player.bounty > 0) {
    						law[i].markTargetForFines();
    					}
    				}
    				law[i].target = null;
    				if (this._debug === true) log(this.name, law[i].displayName + " de-targeting player");
    			}
    		}
    	}
    }*/
    //-------------------------------------------------------------------------------------------------------------
    // check if the player is complying with instructions after surrendering to police, and make police stop hostilities if they do
    // player has to disable their weapons system ("_" key) initially but can switch them on again after surrender is accepted
    // player has to approach the main station without deviating more than 50km (player can take all day to travel the distance, but must not go somewhere else)
    // this timer will continue running until the player is declared non-compliant, or docks and pays their fine, or jumps out of the system
    this.$surrenderCheckState_Police = function $surrenderCheckState_Police() {
    	var that = $surrenderCheckState_Police; // pointer to this function
    	var bountysystem = (that.bountysystem = that.bountysystem || worldScripts.BountySystem_Core); // cache worldScript reference in a local property on this function
    	var p = player.ship,
    		ms = system.mainStation;
    	var dist = (ms && ms.isValid) ? p.position.distanceTo(ms) : 0; // player current distance to main station
    	var hasSurrendered = this._surrenderMainStationDist !== 0, // this._surrenderMainStationDist > 0 implies surrender was accepted and we are tracking their progress
    		isThreat = p.weaponsOnline && this._surrenderCheck_Police, // we require weapons off when a surrender request is pending, after surrender is accepted they can be re-enabled
    		isFar = dist > this._surrenderMainStationDist + 50000, // after surrendering, did distance from main station increase by more than 50km since the last time seen by police?
    		isFugitive = player.bounty > 50; // gained after surrender, otherwise the surrender would be rejected immediately
    	// begin nearby police/main-station loop
    	var law = this.$findLawVessels(p);
    	for (var i = 0; i < law.length; i++) {
    		// is the player seen?
    		if (law[i].hasHostileTarget && law[i].target !== p) {
    			continue; // skip any police that are occupied with fights against someone other than the player
    		} else if (dist < this._surrenderMainStationDist) { // for players that have surrendered, if they are near any police that aren't too busy to notice, update distance tracking
    			this._surrenderMainStationDist = dist; // this is the closest the player has been to the main station since they surrendered (to be used in the next timer update)
    		}
    		// check compliance
    		if (isFugitive) { // player became a fugitive after surrendering earlier and police nearby aren't too busy to notice
    			hasSurrendered = false; // police AI will handle comms to fugitives...
    			break; // fall through to restore hidden bounty and stop timer
    		} else if (!isThreat && !isFar) { // player is compliant (handle this case first so later checks can assume non-compliance)
    			// check if player has a pending surrender request	
    			if (this._surrenderCheck_Police) {
    				if (hasSurrendered === false && law[i].target == p) { // must be targeting the player because markTargetForFines uses the ship's target
    					law[i].markTargetForFines(); // flag player to be fined when they dock at the main station
    					this._surrenderMainStationDist = dist; // begin tracking player distance to station (and make hasSurrendered true for the next timer update)
    					hasSurrendered = true; // make it true for the current timer update to skip bounty restoration code after the loop
    				}
    				if (hasSurrendered) { // things to do once each time the player surrenders (not just the first time)
    					this.$sendMessageToPlayer(law[i], this.$getRandomItemDescription("response-accept-surrender-police")); // police ships or main station can send this message
    					if (this._debug === true) log(this.name, "Police accepted surrender");
    					// hide most of the player's current bounty (police attack offenders with bounty above a threshold, which is lower in higher-government systems)
    					if (player.bounty > 0) {
    						this._surrenderBounty += player.bounty - 1; // add current bounty to hidden bounty (player may gain additional bounty after surrender and surrender to a different group of police)
    						if (bountysystem) bountysystem._changing = true; // tell Bounty System OXP to ignore the bounty change we are about to do (so it doesn't clear local offences)
    						player.bounty = 1; // leave a token 1 cr visible bounty so player is offender but police won't attack (bounty hunters probably won't either, but who knows?)
    						if (bountysystem) bountysystem._changing = false; // tell Bounty System OXP to pay attention to bounty changes again
    					}
    					// so if the player gets a new bounty on the way to the station, they can try to surrender again to police they encounter, stopping hostilities
    					// but if they go off course and are seen by police, their hidden bounty will be restored (code below) and the police probably will attack them
    					var nearbyShips = p.checkScanner(true); // poweredOnly : true (includes stations) - note this only returns up to 32 results even if more are in scanner range
    					for (var k = 0; k < nearbyShips.length; k++) // inner loop to record surrender and stop hostilities with all nearby clean-legal-status ships/stations that are hostile to the player
    					{
    						if (nearbyShips[k].bounty === 0 && nearbyShips[k].target === p && nearbyShips[k].hasHostileTarget) { // nearby ship or station that is hostile and targeting player (stations also have bounty === 0 usually)
    							this.$addShipToArray(nearbyShips[k], this._surrenderedTo); // make sure player can't surrender to them again
    							nearbyShips[k].target = null; // de-target the player so they stop attacking and their AI can find something else to do
    							nearbyShips[k].removeDefenseTarget(p); // reduce likelihood of re-acquiring the player as a target if ships are in combat with someone else
    							if (nearbyShips[k].AIScript && nearbyShips[k].AIScript.oolite_priorityai.getParameter("oolite_distressAggressor") === p)
    								nearbyShips[k].AIScript.oolite_priorityai.setParameter("oolite_distressAggressor", null); // clear a recent distress call made against the player
    							if (this._debug === true) log(this.name, nearbyShips[k].displayName + " de-targeting player");
    						}
    					}
    					break; // fall through to clear this._surrenderCheck_Police (using break as a do-once, but we have to do it inside the hasSurrendered check in case the first surrender needs to keep looking for police targeting the player)
    				}
    			} else { // this._surrenderCheck_Police === false
    				return; // the player hasn't sent another surrender again, and is compliant, so we don't need to do anything else until the next timer update
    			}
    			continue; // if execution reaches this line, this._surrenderCheck_Police === true but hasSurrendered === false && law[i].target !== p so we are still looking for a law[i].target === p
    		} else if (law[i].isStation === false) { // player is non-compliant (implied by previous check), and stations don't interact with non-compliant players, so we are a nearby police ship
    			if (isThreat && this._surrenderPoliceChances--) { // player has not yet disabled weapons (pre-surrender) and we haven't exhausted our patience yet
    				this.$sendMessageToPlayer(law[i], this.$getRandomItemDescription("response-surrender-police")); // one police sends another instructions message
    				return; // take no further action until the next timer update (using return as a do-once and to skip the code that clears this._surrenderCheck_Police after the loop)
    			} else { // either isThreat with no chances left, or isFar (going off course doesn't receive any tolerance (or warning), but the player may be able to surrender to a new group of police)
    				hasSurrendered = false; // make sure this is set before the break in the block below so we restore their hidden bounty even if we don't send a message
    				if (this.$itemIsInArray(law[i], this._surrenderedTo)) { // only police that witnessed the original surrender can send a message
    					this.$sendMessageToPlayer(law[i], this.$getRandomItemDescription("response-break-surrender-police"));
    					if (this._debug === true) log(this.name, "Police rejected surrender");
    					break; // only send one message
    				}
    				// fall through to restore hidden bounty and stop timer
    				// the police AI probably will attack them and send its own messages, since they were attacked by police before (why else did the player surrender?)
    			}
    		}
    	}
    	// end nearby police/main-station loop
    	if (hasSurrendered === false) { // surrender was rejected because the player didn't disable weapons in time, or has been broken because player went off course or became a fugitive after surrender was accepted
    		this._surrenderMainStationDist = 0; // reset distance tracking for future surrender attempts (would have to be addressed to other police than the ones to which the player originally surrendered)
    		if (this._surrenderBounty > 0) { // player has a hidden bounty
    			if (bountysystem) bountysystem._changing = true; // tell Bounty System OXP to ignore the bounty change we are about to do (so it doesn't get recorded as a new offence)
    			player.bounty += this._surrenderBounty; // make hidden bounty visible again (add it to current bounty in case the player gained additional bounty meanwhile)
    			if (bountysystem) bountysystem._changing = false; // tell Bounty System OXP to pay attention to bounty changes again
    			this._surrenderBounty = 0; // clear hidden bounty
    		}
    		this._surrenderTimer_Police.stop();
    	} else { // hasSurrendered is true
    		this._surrenderCheck_Police = false; // any pending request has been handled (we use return to bypass this when needed)
    	}
    }
    
    //=============================================================================================================
    // threat - 5
    this.$respondToThreat = function $respondToThreat() {
    
    	if (this._source != null && this._source.hasRole("generationship")) return;
    
    	// get one of a predefined list of threats and transmit
    	var rsp = 0; // 0 = no response, 1 = pick a response
    	if (this.$rand(10) > 2) rsp = 1;
    
    	// always give a response for thargoids
    	if (this._targetType === "thargoid") rsp = 1;
    
    	var p = player;
    	var altResponse = 0; // whether we're using the alternate responses because the npc is fleeing (0 = normal, 1 = fleeing)
    
    	// for thargoid - no change in behaviour
    	// any changes to NPC behaviour will happen regardless of whether they have decided to send a response or not
    
    	// for police -- make it an offence
    	if (this._source != null && this._targetType != "" && this._targetType === "police") {
    		// threatening police results in a fine
    		player.bounty += 1;
    		this._source.target = player.ship;
    		this._source.markTargetForFines();
    	}
    
    	// for npc/pirate/hunter - small chance of enemy giving up if player kill count is high and/or player status is offender or greater
    	// if npc decides to flee, get different message
    	// check: target is still there, they're not thargoids or police, and they are targeting the ship
    	if (this._source != null && this._targetType != "" && this._targetType != "thargoid" && this._targetType != "police" && this._source.target === p.ship) {
    		var chance = 0.0; // starting chance is 0% ie. no chance the target will flee
    		// weight up the chances of this enemy fleeing
    		// 1. how many ships are targeting the player? If lots, then less likely to flee
    		// 		if >= 5, no change
    		var shipcount = this.$countShipsTargetingPlayer();
    		// if there are lots of ships targeting the player this function could make our value go negative, which we'll check for later
    		if (shipcount < 5) chance += (0.3 - (shipcount * 0.05));
    
    		// 2. how strong is the player
    		// based on status (fugitive = higher chance, offender = not so high, clean = not high at all)
    		if (p.bounty > 0 && p.bounty <= 50) chance += 0.1; // offender status
    		if (p.bounty > 50) chance += 0.2; // fugitive status
    
    		// elite ranking
    		if (p.score >= 32 && p.score < 64) chance += 0.05; // average
    		if (p.score >= 64 && p.score < 128) chance += 0.1; // above average
    		if (p.score >= 128 && p.score < 512) chance += 0.15; // competant
    		if (p.score >= 512 && p.score < 2560) chance += 0.2; // dangerous
    		if (p.score >= 2560 && p.score < 6400) chance += 0.25; // deadly
    		if (p.score >= 6400) chance += 0.3; // elite
    
    		// threat assessment levels
    		if (this._source.threatAssessment() < p.ship.threatAssessment()) chance += 0.1;
    
    		// 3. how many other ships are targeting it
    		var shipcount = this.$countShipsTargetingPlayerTarget();
    		if (shipcount > 2) chance += (shipcount * 0.05);
    
    		// make sure we're positive
    		if (chance < 0) chance = 0.0;
    
    		// not greater than a 50% chance
    		if (chance > 0.5) chance = 0.5
    
    		if (this._debug === true) log(this.name, "final chance percentage = " + chance.toString());
    
    		// will the ship flee?
    		if (this.$rand(70) < (50 * chance)) {
    			if (this._debug === true) log(this.name, "ship '" + this._source.displayName + "' is fleeing");
    
    			// force a response for a fleeing ship
    			rsp = 1;
    			// change response type to fleeing
    			altResponse = 1;
    			// remove the player as a target
    			this._source.target = null;
    			// make ship flee
    			this._source.performFlee();
    		}
    	} else if (this._source != null && this._targetType != "" && this._targetType != "police" && this._source.target != p.ship) {
    		// check: target is still there, they're not thargoids or police, and they are *not* targeting the ship
    		// basically, if the other ship is not targeting the player, make them do so now
    		var chance = 35;
    		// adjust the chance by player score - the higher the score the less likely they will attack
    		// elite ranking
    		if (p.score >= 32 && p.score < 64) chance -= 5; // average
    		if (p.score >= 64 && p.score < 128) chance -= 10; // above average
    		if (p.score >= 128 && p.score < 512) chance -= 15; // competant
    		if (p.score >= 512 && p.score < 2560) chance -= 20; // dangerous
    		if (p.score >= 2560 && p.score < 6400) chance -= 25; // deadly
    		if (p.score >= 6400) chance -= 30; // elite
    
    		this.$willPlayerTargetTurnOnPlayer(chance);
    		// force a response
    		rsp = 1;
    	}
    
    	if (rsp === 1 && this._source != null && this._targetType != "" && this._source.isPiloted) {
    		// get a standard "I'm not scared of you" threat response
    		var reply = this.$getCustomReply(this._source, 5, (altResponse == 0 ? "reply_normal" : "reply_fleeing"));
    		if (reply == "") reply = this.$getRandomItemDescription("response-threat-" + this._targetType + "-" + altResponse.toString());
    		this.$sendMessageToPlayer(this._source, reply);
    		// rebuild the messagelist (so that greeting is removed)
    		this.$buildMessageList();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // handles the response of an NPC to piracy, where they have decided to dob the player in
    this.$sendPiracyNPCResponse = function $sendPiracyNPCResponse() {
    	// only send the response if the victim is still valid and is visible to player
    	if (this._piracyVictim.isValid === true && this._piracyVictim.isVisible === true) {
    		var listtype = "response-piracy-npc-police";
    		if (this._piracyPolice.isPolice === false) listtype = "response-piracy-npc-station";
    
    		this.$sendMessageToPlayer(this._piracyVictim, this.$getRandomItemDescription(listtype));
    	}
    	// if the victim has died before they get a chance to tell, don't send a response
    	if (this._piracyVictim.isValid === true) this._piracyPoliceTimer = new Timer(this, this.$sendPiracyPoliceResponse, 4, 0);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // handles the response of a police vessel to an NPC's notification of the players piracy
    this.$sendPiracyPoliceResponse = function $sendPiracyPoliceResponse() {
    	if (this._piracyPolice.isValid === true && this._piracyPolice.isVisible === true) {
    		this.$sendMessageToPlayer(this._piracyPolice, this.$getRandomItemDescription("response-piracy-police-npc"));
    	}
    	// if the police vessel has died before they get a chance to set the players bounty, don't change it.
    	if (this._piracyPolice.isValid === true) {
    		player.ship.setBounty(player.ship.bounty + this.$rand(10), "act of piracy");
    		if (this._debug === true) log(this.name, "player bounty now " + player.bounty.toString());
    
    	}
    }
    
    //=============================================================================================================
    // helper functions
    
    //-------------------------------------------------------------------------------------------------------------
    this.$resetVariables = function () {
    	this._alertCond = player.alertCondition;
    	this._targetType = "";
    	this._closestAttackerType = "";
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$fullResetVariables = function () {
    	this._escapepodShipNames = [];
    	this._scoopOffer = [];
    	this._greeted = [];
    	this._bribed = [];
    	this._demanded = [];
    	this._bribeCurrent = [];
    	this._surrenderCurrent_Pirate = [];
    	this._surrenderedTo = [];
    	this._surrenderCheck_Pirate = false;
    	this._surrenderCheck_Police = false;
    	this._surrenderPoliceChances = 0;
    	this._surrenderMainStationDist = 0;
    	this._surrenderBounty = 0;
    	this._bribeAmount = this._bribeInitial;
    	this._piracyWorked = false;
    	this._waitingForPiracyResponse = false;
    	this._killedNames = [];
    	this._targetLastComms = null;
    	this.$resetVariables();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // stop any timers we have running
    this.$stopAllTimers = function () {
    	// stop the check timer
    	if (this._checkTimer && this._checkTimer.isRunning === true) this._checkTimer.stop();
    	if (this._piracyNPCTimer && this._piracyNPCTimer.isRunning === true) this._piracyNPCTimer.stop();
    	if (this._piracyPoliceTimer && this._piracyPoliceTimer.isRunning === true) this._piracyPoliceTimer.stop();
    
    	// stop the response message delay timer
    	if (this._delay && this._delay.isRunning === true) this._delay.stop();
    
    	// stop the surrender timer
    	if (this._surrenderTimer_Pirate && this._surrenderTimer_Pirate.isRunning === true) this._surrenderTimer_Pirate.stop();
    	if (this._surrenderTimer_Police && this._surrenderTimer_Police.isRunning === true) this._surrenderTimer_Police.stop();
    
    	// stop any accuracy changes
    	if (this._accuracyChanged && this._accuracyChanged.length > 0) {
    		for (var i = 0; i < this._accuracyChanged.length; i++) {
    			if (this._accuracyChanged[i].IsActive === true) this._accuracyChanged[i].StopChange();
    		}
    	}
    	// reset the arrays
    	this._accuracyChanged = [];
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // return a random number between 1 and max
    this.$rand = function (max) {
    	return Math.floor((Math.random() * max) + 1)
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // adds a ship to an array, but only once
    this.$addShipToArray = function (ship, array) {
    	var found = false;
    	if (array != null && array.length > 0) {
    		for (var i = 0; i < array.length; i++) {
    			if (array[i] === ship) {
    				found = true;
    				break;
    			}
    		}
    	}
    	if (found === false) array.push(ship);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns a description item
    this.$getRandomItemDescription = function (listitem, dict) {
    	// we need to distinguish hunters from pirates in the above code, but in the transmissions and responses they're the same thing
    	var listname = "[" + listitem.replace("-hunter", "-pirate") + this._msgAlt + "]";
    	// fail safe, incase there's a 
    	if (listname.indexOf("-]") >= 0) {
    		listname = listname.replace("-" + this._msgAlt + "]", "-npc" + this._msgAlt + "]");
    	}
    	if (dict) {
    		var item = expandDescription(listname, dict);
    	} else {
    		var item = expandDescription(listname);
    	}
    	return item;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // find all npc ships in range
    this.$findNPCShips = function () {
    	function _isNPC(entity) {
    		return entity.isShip && !(entity.isPlayer) && !(entity.isMissile) && !(entity.isBeacon) && !(entity.isBoulder) && !(entity.isCargo) && !(entity.isRock) && !(entity.isWeapon) && !(entity.isTurret);
    	}
    	return system.filteredEntities(this, _isNPC, player.ship, player.ship.scannerRange);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // 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);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // return the count of law vesses in range of ship
    this.$countLawVessels = function (npc) {
    	var ships = this.$findLawVessels(npc);
    	this._piracyPolice = null;
    	ships.forEach(function (ship) {
    		// find the first police vessel in the list and set our piracy police variable to that one
    		if (ship.isPolice === true && this._piracyPolice == null) this._piracyPolice = ship;
    	}, this);
    	// if there was no police vessel in the list, it must be a main station, so set it instead
    	if (this._piracyPolice == null && ships.length > 0) this._piracyPolice = ships[0];
    	return ships.length;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // find all npc ships in range that are targeting the player
    this.$countShipsTargetingPlayer = function () {
    	function _isShipTargetingPlayer(entity) {
    		return entity.isShip && !(entity.isPlayer) && !(entity.isMissile) && !(entity.isBeacon) && !(entity.isBoulder) && !(entity.isCargo) && !(entity.isRock) && !(entity.isWeapon) && !(entity.isTurret) && entity.target === player.ship;
    	}
    	var ships = system.filteredEntities(this, _isShipTargetingPlayer, player.ship, player.ship.scannerRange);
    	return ships.length;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // find all npc ships in range that are targeting the player
    this.$countShipsTargetingNPC = function (npc) {
    	function _isShipTargetingNPC(entity) {
    		return entity.isShip && !(entity.isPlayer) && !(entity.isMissile) && !(entity.isBeacon) && !(entity.isBoulder) && !(entity.isCargo) && !(entity.isRock) && !(entity.isWeapon) && !(entity.isTurret) && entity.target === npc;
    	}
    	var ships = system.filteredEntities(this, _isShipTargetingNPC, npc, npc.scannerRange);
    	return ships.length;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // find all npc ships in range that are targeting the player
    this.$countShipsTargetingPlayerTarget = function () {
    	function _isShipTargetingTarget(entity) {
    		return entity.isShip && !(entity.isPlayer) && !(entity.isMissile) && !(entity.isBeacon) && !(entity.isBoulder) && !(entity.isCargo) && !(entity.isRock) && !(entity.isWeapon) && !(entity.isTurret) && entity.target === this._source;
    	}
    	var ships = system.filteredEntities(this, _isShipTargetingTarget, player.ship, player.ship.scannerRange);
    	return ships.length;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // remove the player as the target of a pirate group
    this.$removePlayerTargetFromPirateGroup = function (pirateship) {
    	if (pirateship.group) {
    		var ships = pirateship.group.ships;
    		ships.forEach(function (ship) {
    			if (this._debug === true) log(this.name, "player de-targeted from ship " + ship.displayName + " in group " + pirateship.group);
    
    			ship.target = null;
    		}, this);
    	} else {
    		pirateship.target = null;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // adds a ship to the accuracy changes array and starts the process
    this.$addShipToAccuracyList = function (ship, addvalue, seconds) {
    	//-------------------------------------------------------------------------------------------------------------
    	// internal class used for adjusting the accuracy of a ship for a given number of seconds
    	function AccuracyAdj(ship, addvalue, seconds) {
    		this.ship = ship;
    		this.oldvalue = ship.accuracy;
    		this.addvalue = CheckNewAccuracyLevel(this.oldvalue, addvalue);
    		this.seconds = seconds;
    		this.delay = null;
    
    		this.DoChange = function () {
    			if (this.addvalue != 0) {
    				if (worldScripts.BroadcastCommsMFD._debug === true)
    					log(this.name, "changing " + this.ship.displayName + " accuracy from " + this.oldvalue.toString() + " to " + (this.oldvalue + this.addvalue).toString());
    
    				this.ship.accuracy += this.addvalue;
    				this.delay = new Timer(this, this.RevertChange.bind(this), this.seconds, 0);
    			}
    		}
    
    		this.IsActive = function () {
    			if (this.delay && this.delay.isRunning) {
    				return true;
    			} else {
    				return false;
    			}
    		}
    
    		this.StopChange = function () {
    			this.delay.stop();
    			// reset the accuracy of the ship
    			if (this.ship && this.ship.isValid) {
    				this.ship.accuracy = this.oldvalue;
    				if (worldScripts.BroadcastCommsMFD._debug === true)
    					log(this.name, "reverting " + this.ship.displayName + " accuracy back to " + this.oldvalue.toString() + " from " + (this.oldvalue + this.addvalue).toString());
    			}
    		}
    
    		this.RevertChange = function RevertChange() {
    			if (this.ship && this.ship.isValid) {
    				this.ship.accuracy = this.oldvalue;
    				if (worldScripts.BroadcastCommsMFD._debug === true)
    					log(this.name, "reverting " + this.ship.displayName + " accuracy back to " + this.oldvalue.toString() + " from " + (this.oldvalue + this.addvalue).toString());
    			}
    		}
    
    		function CheckNewAccuracyLevel(curr, diff) {
    			var newvalue = diff;
    			if ((curr + diff) > 10 || (curr + diff) < -5) {
    				if (diff < 0) {
    					newvalue = -5 - curr;
    				} else {
    					newvalue = 10 - curr;
    				}
    			}
    			return newvalue;
    		}
    	}
    
    	var item = -1;
    	// first, make sure we aren't already adjusting a ship's accuracy
    	if (this._accuracyChanged != null && this._accuracyChanged.length > 0) {
    		for (var i = 0; i < this._accuracyChanged.length; i++) {
    			if (this._accuracyChanged[i] && this._accuracyChanged[i].ship === ship) {
    				item = i;
    				break;
    			}
    		}
    	}
    	// if we found an item, check to see if it's running.
    	if (item != -1) {
    		if (this._accuracyChanged[item].IsActive === false) {
    			// not running, so remove it and add it as a new item
    			this._accuracyChanged[item] = null;
    			item = -1;
    		}
    		// if it is running, don't make any changes.
    	}
    	if (item === -1) {
    		// not found, so add the ship
    		var newItem = new AccuracyAdj(ship, addvalue, seconds);
    		newItem.DoChange();
    		this._accuracyChanged.push(newItem);
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // check timer essentially looks to see if our target tas become invalid (by going through a wormhole)
    // for some reason the "shipTargetLost" is not firing in this scenario
    this.$checkForTargetInvalid = function $checkForTargetInvalid() {
    	var p = player.ship;
    	var found = false;
    	if (this._targetType != "" && (!(p.target) || p.isValid === false)) {
    		this._targetType = "";
    		found = true;
    		this.$startTargetTimer();
    	}
    	if (found === true) this.$buildMessageList();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // determines if the players target will turn on the player
    // used by the taunt and threat functions
    this.$willPlayerTargetTurnOnPlayer = function (checkAmount) {
    	if (this._debug === true) log(this.name, "Chance target will turn on player = " + checkAmount.toString());
    
    	if (player.ship.target && player.ship.target.target != player.ship && this.$rand(50) > checkAmount) {
    		player.ship.target.target = player.ship;
    		if (this._debug === true) log(this.name, "target " + player.ship.target + " is now targeting player");
    
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // sets the current bribe amount to the amount set for a particular ship
    this.$workOutCurrentBribeAmount = function (ship) {
    	if (this._bribeCurrent != null && this._bribeCurrent.length > 0) {
    		for (var i = 0; i < this._bribeCurrent.length; i++) {
    			if (this._bribeCurrent[i].ship === ship) this._bribeAmount = this._bribeCurrent[i].amount;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // finds and returns the nearest ship targeting the player. If none found, returns null
    this.$findNearestAttacker = function () {
    	var ships = player.ship.checkScanner(true);
    	var min = player.ship.scannerRange;
    	var calc = 0;
    	var selected = null;
    	for (var i = 0; i < ships.length; i++) {
    		if (ships[i].target === player.ship) {
    			calc = player.ship.position.distanceTo(ships[i]);
    			if (calc < min) {
    				min = calc;
    				selected = ships[i];
    			}
    		}
    	}
    	if (selected != null) {
    		this._closestAttackerType = "npc";
    		if (selected.hasRole("police") === true) this._closestAttackerType = "police";
    		if (selected.hasRole("thargoid") === true) this._closestAttackerType = "thargoid";
    		if (selected.hasRole("hunter") === true) this._closestAttackerType = "hunter";
    		if (selected.hasRole("pirate") === true) this._closestAttackerType = "pirate";
    	}
    	return selected;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // returns the player's target system (1.80) or the next jump to their target system (1.82)
    this.$playerTargetSystem = function () {
    	if (player.ship.hasOwnProperty("nextSystem")) return player.ship.nextSystem;
    
    	var target = player.ship.targetSystem;
    	if (oolite.compareVersion("1.81") < 0 && player.ship.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY") === true) {
    		// in 1.81 or greater, the target system could be more than 7 ly away. It becomes, essentially, the final destination.
    		// there could be multiple interim stop points between the current system and the target system.
    		// the only way to get this info is to recreate a route using the same logic as entered on the ANA, and pick item 1
    		// from the list. That should be the next destination in the list.
    		if (system.ID === -1) {
    			var myRoute = System.infoForSystem(galaxyNumber, this._lastSource).routeToSystem(System.infoForSystem(galaxyNumber, target), player.ship.routeMode);
    		} else {
    			var myRoute = System.infoForSystem(galaxyNumber, system.ID).routeToSystem(System.infoForSystem(galaxyNumber, target), player.ship.routeMode);
    		}
    		if (myRoute && myRoute.route.length >= 1) {
    			target = myRoute.route[1];
    		}
    	} else {
    		if (target >= 0 && system.info.distanceToSystem(System.infoForSystem(galaxyNumber, target)) > 7) target = -1;
    	}
    	return target;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$itemIsInArray = function (element, array) {
    	var found = false;
    	if (array != null && array.length > 0) {
    		for (var i = 0; i < array.length; i++) {
    			if (array[i] === element) found = true;
    		}
    	}
    	return found;
    }