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

Expansion Defence Rider Drones

Content

Warnings

  1. http://wiki.alioth.net/index.php/Defence%20Rider%20Drones -> 404 Not Found
  2. Low hanging fuit: Information URL exists...

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Galcop authorizes you to carry Mambita Defence Rider Drones, these weapons to be deployed from hardpoints on your ship's hull. A Rider's single purpose is to eliminate your attackers or any other Offender you target or fire upon. Galcop authorizes you to carry Mambita Defence Rider Drones, these weapons to be deployed from hardpoints on your ship's hull. A Rider's single purpose is to eliminate your attackers or any other Offender you target or fire upon.
Identifier oolite.oxp.Reval.Defence_Rider_Drones oolite.oxp.Reval.Defence_Rider_Drones
Title Defence Rider Drones Defence Rider Drones
Category Weapons Weapons
Author Reval Reval
Version 1.9 1.9
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Information URL https://wiki.alioth.net/index.php/Defence_Rider_Drones_OXZ n/a
Download URL https://wiki.alioth.net/img_auth.php/3/3e/Defence_Rider_Drones.oxz https://wiki.alioth.net/img_auth.php/3/3e/Defence_Rider_Drones.oxz
License CC-BY-NC-SA 4.0 CC-BY-NC-SA 4.0
File Size n/a
Upload date 1780640449

Relationships Diagram

Documentation

ReadMe.txt

== Downsizing ==

Galcop, in a final drive to economize on personnel and vehicles (particularly its costly Police Viper and Viper Interceptor models), sanctioned the galaxy-wide production and adoption of a smaller, cheaper enforcement platform.

The result, under consultation with several manufacturers, primarily FE Shipyards, is the Mambita - a fully autonomous reduced-scale defence craft, dubbed Ship Defence Drone (SDD).

For the project, FE Shipyards took its proven Mamba II design, scaled down its hull to less than half the size of the original and dispensed with the need for an organic pilot, integrating instead advanced robotic AI systems, all of which can be mass-produced. The SDD Mambita's internals are almost wholly taken up by a powerful - for an organic, 'too powerful' would be understating the case - non-Hyperspace engine. What remains houses a laser mounting of military specification.

Mambita SDD is a fire-and-forget weapon, in effect, and belongs officially to the Mine-class, being too light in mass to lock or hinder any vessel.

Police forces have begun a gradual phase-out of their manned Viper patrol vehicles, and are introducing artfully programmed Mambitas to take their place. Remaining Vipers are being modified to carry Mambita SDDs as Rider drones, couched along the mothership's underside, to be released and recalled when occasion demands. 

Trials have proven an outstanding success. The Mambita's speed and acceleration, turn rate, and reaction-time, outstrip those of opponent ships, and their miniaturized electromagnetic and visual profile renders the drones hard to detect and hit. Loss rates are low, far lower than those of manned Vipers in dangerous systems. The SDD's future looks bright.

'Drop a drone' is becoming a popular phrase not only among the shrinking Galcop Police Corps, but increasingly among non-uniformed crews. And thereby hangs another tale...


== Civilian SDD ==

At some stage in the project, an unknown Galcop nabob had a brainwave: A de facto space police force might soon become unnecessary. All that would be required was licensing - a stringent, Galcop controlled form of licensing, granted; a licensing of SDD for the use of approved civilian vessels. A trader vessel, for example, sporting its own Rider drone hardpoints and Mambita complement, could effectively carry its own police enforcers along with it. 

Plans were quickly realized for the formation of a new Civilian SDD Authority, whose responsibilities include approval and licensing of applicants, along with outsourcing of Mambita production for the general market. All accompanied by the close and seamless integration of the 'product' not only with individual ship systems, but with the Galcop communications network itself, every aspect of SDD deployment to be monitored and recorded.

SDD capability requires one thing and one thing only of any applicant: a clean and spotless Galcop record and a history of compliance with galactic laws.

The SDD Authority, once satisfied, will grant approval and a duly legitimized licence for the carrying of Rider drones. There is little room for abuse of the system as it has been put in place: Special codes are issued and hard-coded in to every drone alotted; no drone can function without them. Hardpoints are mated with a unique coded link; an SDD will not attach itself without such a code. Every 'drop' is recorded, every retrieval, every kill, every loss.

The SDD, as a weapon of the ship on which it is installed, and certainly not accounted a vessel or craft in its own right, but a Mine, carries out its allotted task of targetting offending vessels of its own selection or that of its mothership, and on their elimination registers a 'kill' with the on-ship and Galcop systems simultaneously. Galcop credits the launching vessel with the kill, awarding the ship its bounty plus 100 credits. Galcop regards the bonus as 'incentive to policing', but charges the licensee 100 credits for every drone lost. The dropping of a drone is accompanied by a (negligible) tax.

Some crews noticed that there can be profit in this. Some, reputedly, have become hunters using SDDs only. The Galcop Administration must be smiling. They have a free Police Force that does not require wages or great expenditure, merely ongoing production and licensing. Licensing fees swell Galcop's coffers and the spacelanes become safe for all (except offenders). Win-win.


== Onboard SDD Mechanisms ==

An automated comm-link is established with all attached SDDs on station launch.

The deployment mechanism is mated with the WEAPONs (online/offline) switch. An organic commander must toggle ship weapons ON to drop an SDD. Toggle OFF, then ON again to deploy another Mambita.

There are various ways and situations in which deployed SDDs are recalled and retrieved:

1) Manually. In flight, from the F5 ship status screen, press [x].
2) On Torus-drive engagement, all deployed drones are automatically recalled and retrieved.
3) On requesting docking clearance, any deployed SDDs are automatically recalled and retrieved.
4) On engagement of the hyperspace motor (witchdrive), all deployed drones are recalled.


== Hostile encounters ==

Frequently, on encountering a group of Offender ships, deployed drones will select appropriate targets and initiate their own attack. Whether they do will depend on the gravity of the offences the scanned group has committed or is in the process of committing.

Otherwise, when their mothership fires on an identified Offender, every deployed drone will join in attacking this target. In fact, any ship their mothership fires on will become a target, regardless of legal status. The only class of ship an SDD is prevented from attacking is a vessel of CLASS_POLICE or CLASS_PLAYER.

When the mothership receives hostile fire, drones will be assigned to attack the attacker.

When the alert condition changes to RED, any currently unengaged (idle) drones will be assigned the nearest hostile to attack.


== General SDD status and other observations ==

As already observed, the SDD Mambita is officially classed as a Mine. As a Mine it cannot mass-lock other ships.

All SDD Mambitas are identified as CLASS_POLICE, with role 'police', as they are in every sense Galcop property. Additionally, as mentioned, Galcop Police Corps increasingly assigns SDDs to its own patrols, replacing organic crew and expensive Vipers. 

On the scanner, a Police SDD can be recognised by its lollipop colours flashing purple and blue. Civilian-licensed SDDs when dropped are distinguished by their red and blue flashing blips.


== SDD Charges and Credits ==

The nominal cost of a Galcop SDD Licence is 1000 credits, renewable every LY (Lave Year).

As mentioned already, each drone deployment or 'drop' carries a small tax.

Loss of a drone through destruction incurs a Galcop penalty of 100 credits.

On registration of a kill, each kill by an SDD is credited to the mothership pilot's Elite score (the SDD is a weapon fired by the mothership, just like any other missile or mine.) The mothership pilot's account is credited with the bounty on the destroyed vessel plus Galcop's 'policing incentive' bonus of 100 credits. All accounting occurs in realtime.

Thus, with the aid of SDDs, a reasonable living can be eked out through hunting Offenders insystem - a practice Galcop tacitly if not overtly encourages via its 'policing incentive'.

Equipment

Name Visible Cost [deci-credits] Tech-Level
Defence Rider Drones (Mambita) yes 10000 1+
Terminate Mambita SDD Licence yes 0 4+

Ships

Name
SDD Mambita

Models

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

Scripts

Path
Scripts/SDD_Rider.js
"use strict";
this.name = "SDD-Rider";
this.author = "Reval";
this.license = "CC-BY-NC-SA 4.0";
this.version = "1.9";
this.description = "This is the ship-script for individual SDD Riders.";

this.$log = true; // logging ship events?


this.shipSpawned = function() {
	var s = this.ship;
	s.displayName = this._generateSDDName();
	// CHECK: who spawned me?
	if (s.$spawnedByMother) {
		s.displayName += " Rider";
		s.commsMessage("Dropped. Pursuing.", player.ship);
		if (this.$log) this._log(s.displayName+" spawned.");
		s.$myKills = 0;
		s.$creditsAwarded = 0;
		// Start Comms Timer
		this._startCommsTimer();
	} else {
	// New-christened Police SDD logs in with attached or deployed Riders
		s.displayName = "Police "+s.displayName;
		var pname = s.displayName;
		var ps = player.ship;
		// proceed only if player is licensed to carry SDDs
		var ws = worldScripts["Defence Rider Drones"];
		if (ws && ws.$sddHasEQ) {
			var psn = ps.displayName;
			var pc = player.consoleMessage;
			var oc = ps.messageGuiTextColor;
			ps.messageGuiTextColor = "0.7 0.3 0.8";
			pc(pname+" <--> "+psn+" SDD-Net.",9);
			ps.messageGuiTextColor = oc;
			if (this.$log) this._log("I am "+pname);
		}
	}
}


this.shipRemoved = function(suppressDeathEvent) {
    var s = this.ship;
	if (suppressDeathEvent) {
        // Ship was removed by player-script (e.g. ship.remove(true))
		s.commsMessage("Rejoining. Re-attached.", player.ship);
		if (this.$log) this._log(s.displayName+" removed (script).");
		if (this._Offscan())
			player.consoleMessage(s.displayName+": rejoining. Re-attached.", 5);	
    } else {
        // Ship was destroyed in combat (normal death), final machine incoherence
		s.commsMessage(this._generateJumbledString(24), player.ship);
		if (this.$log) this._log(s.displayName+" removed (died).");
    }
	// cancel comms
	if (this._commsTimer) {
        this._commsTimer.stop();
        this._commsTimer = null;
    }
};   


this.shipDied = function(why) {
	var ddm = this._generateDyingDroneMessage();
	this.ship.commsMessage(ddm, player.ship);	
	if (this._Offscan())
		player.consoleMessage(ddm, 5);
	if (this.$log) this._log(this.ship.displayName+" dies.");
	// cancel comms
	if (this._commsTimer) {
        this._commsTimer.stop();
        this._commsTimer = null;
    }
	if (why !== "removed") {
		// charge player 100 cr for a destroyed drone
		player.credits -= 100;
		// keep a cumulative record
		var ws = worldScripts["Defence Rider Drones"];
		if (ws && ws.$sddHasEQ) {
			ws.$sddRTax += 100;
			ws.$sddRLosses ++;
		}
	}
}


this.shipKilledOther = function(whom, damageType) {
    // CHECK if the killed ship was W.S. primary target
	var ws = worldScripts["Defence Rider Drones"];
	if (ws && ws.$sddHasEQ)
		if (ws.$sddTargetShip === whom) {
			ws.$sddTargetShip = null; // reset
			// clean up H.O. list
			ws.$sddHighOffenders = ws.$sddHighOffenders.filter(function(ship) {
				return ship && ship.isValid;
			});   
			var ps = player.ship;
			// Re-sort by distance to player (nearest first)
			ws.$sddHighOffenders.sort(function(a, b) {
				return a.position.distanceTo(ps) - b.position.distanceTo(ps);
			});
			// Assign new top offender as target
			if ((ws.$sddHighOffenders.length > 0) && (ws.$sddAutoT)) {
				var hf = ws.$sddHighOffenders[0];
				if (hf.isValid) {
					ws.$sddTargetShip = hf;
					// update HUD
					ws._updateTargetInfo();
					if (this.$log) this._log("H.O. Target Ship changed to "+hf+".");
				}
			}
		}
};   


this.shipTargetDestroyed = function(whom) {
	var miss = whom.isMissile, s = this.ship;
	// Galcop 'policing incentive' for drone's kill
	var bounty = 100;
	// update drone's kill tally
	s.$myKills++;
	// award this drone's kill to player
	player.score++;
	// plus the killed ship's total bounty
	bounty += whom.bounty;
	// Award 10 credits if the destroyed target is a missile
	if (miss) bounty = 10;
	player.credits += bounty;
	s.$creditsAwarded += bounty;
	// communicate kill to player on comm
	s.commsMessage("Killed "+whom.name+". Bounty "+bounty+".", player.ship);
	if (this._Offscan())
		player.consoleMessage(s.displayName+": killed "+whom.name+". Bounty "+bounty+".", 5);
	if (this.$log) 
		this._log(s.displayName+" kills "+whom.name+" (bounty "+whom.bounty+").");
	// update world script tallies
	var ws = worldScripts["Defence Rider Drones"];
	if (ws && ws.$sddHasEQ) {
		ws.$sddRBounty += bounty;
		ws.$sddRKills ++;
	}
}


this.shipTargetAcquired = function(target) {
	 // only report a viable target that has changed
	 if ((target != player.ship) && (target.dataKey != "sdd-mambita")) {
			if (target != (this.$lastTarget)) {
				var s = this.ship;
				this.$lastTarget = target;
				var pcm = player.consoleMessage;
				s.commsMessage("Targetting "+target.name+"...", player.ship);
				if (this._Offscan())
					pcm(s.displayName+": Targetting "+target.name+"...", 5);
				if (this.$log) this._log(s.displayName+" targets "+target.name+".");
			}
	 }
}


this.shipCollided = function(other) {
	if (this.$log) this._log(this.ship.displayName+" collides with "+other.name+".");
}


// About-to-die procedures for this drone
this.shipTakingDamage = function(damageAmount, damageType, damageSource) {
// Damage must be serious
if (this._hullStatus() <= 0.1) {
	var s = this.ship;
	var leaderAssigned = false;
    var potentialLeader = null;
	// 1. last order: Group, attack my attacker!
	   if (s.group) {
        // Iterate through all ships in the group
        for (var i = 0; i < s.group.ships.length; i++) {
            var member = s.group.ships[i];
            if (member.isValid && member !== s) {
                // Directly command each member to attack
                member.setTarget(damageSource);
                member.performAttack();
            }
        }
    }
	// 2. Assign a new group leader before destruction
	// Check if this ship is the group leader and the group exists
    if (s.group && s.group.leader === s) {
        // Find the next valid ship in the group to be the new leader
        for (var i = 0; i < s.group.ships.length; i++) {
            potentialLeader = s.group.ships[i];
            // Skip self and invalid ships
            if (potentialLeader !== s && potentialLeader.isValid) {
                s.group.leader = potentialLeader;
				leaderAssigned = true;
				break; // Assign and exit loop
            }
        }
    }
	// 4. Take evasive action
	s.performFlee();
	
	// 5. Comms and logging come last
	var msg = "Taking damage from "+damageSource.name;
	s.commsMessage(msg);
	if (this.$log) this._log(s.displayName+" "+msg);
    // report group defence
	msg = "Group: Attack "+damageSource.name;
	if (this.$log) this._log(s.displayName+" orders "+msg);
	// report leader change
	if (leaderAssigned) {
		msg = "Assigning "+potentialLeader.displayName+" Group Leader."
		s.commsMessage(msg);
		if (this.$log) _log(s.displayName+" "+msg);
	}
	msg = "Fleeing...";
	s.commsMessage(msg);
	if (this.$log) _log(s.displayName+" "+msg);
}}   



this._hullStatus = function() {
    // fraction of maximum energy remaining
	var energyStatus = this.ship.energy / this.ship.maxEnergy;
    // a 'hullStatus' property is likely undefined in Oolite
	if (('hullStatus' in this.ship) && (this.ship.hullStatus != null))
        return this.ship.hullStatus;
	else return energyStatus;
}   


// is this drone within player's scanner range?
this._Offscan = function() {
    // Get the distance to the player's ship
    var distance = this.ship.position.distanceTo(player.ship);
    // Get the player's scanner range
    var scannerRange = player.ship.scannerRange;
    // Return true if the ship is beyond scanner range
    if (distance > scannerRange) {
		return true;
	} else return false;
};   


// distance to Mother ship in km - call with (this.ship)
this._DistanceToMother = function(ship) {
    var distanceInMeters = ship.position.distanceTo(player.ship.position);
    return (distanceInMeters / 1000).toFixed(2);
};   
  

// this Rider's SDD designation
this._generateSDDName = function() {
    // Generates a 4-digit number (1000-9999)
	var randomNumber = Math.floor(Math.random() * 9000) + 1000; 
    return "SDD" + randomNumber/* + " Rider"*/;
}   


// 'dying drone' message #1
this._generateJumbledString = function(length) {
    var result = '';
    var characters = ' !"#$&\'*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ\\^_`abcdefghijklmnopqrstuvwxyz|~';
    var charactersLength = characters.length;
    for (var i = 0; i < length; i++) {
        result += characters.charAt(Math.floor(Math.random() * charactersLength));
    }
    return result;
}


// 'dying drone' message #2
this._generateDyingDroneMessage = function() {
    var chars = '01OIl!@#$&*?+=';
    var segments = [];
    for (var i = 0; i < 4; i++) {
        var segment = '';
        for (var j = 0; j < 6; j++) {
            segment += chars.charAt(Math.floor(Math.random() * chars.length));
        }
        segments.push(segment);
    }
    return segments.join(' ** ');
}  


// echo to Oolite log for this script only
this._log = function(msg) {
	log(this.name+".debug", msg);
}


// Comms Check Timer functions
// ---------------------------

// Drones send periodic status reports at random intervals
this._startCommsTimer = function() {
    // Calculate a random delay between 120 and 180 seconds
    var randomDelay = 120 + Math.random() * 60;
    // Create a one-shot timer
    this._commsTimer = new Timer(this, this._sendCommsCheck, randomDelay);
}


this._sendCommsCheck = function() {
    var s = this.ship;
	// Only send the message if the drone is in the FOLLOW_MOTHER state
    if ((s.AIState==="FOLLOW_MOTHER") || (s.target==player.ship)) {
        var distance = s.position.distanceTo(player.ship.position);
		var km = (distance / 1000).toFixed(2);
		s.commsMessage("Comms check. Systems optimal. Distance: " + km + " Lk.");
		if (this._Offscan()) 
			player.consoleMessage(s.displayName+" (subspace): Distance: " + km + " Lk.", 5);
		// Restart the timer
		this._startCommsTimer();
    }
}
Scripts/autodock_conditions.js
"use strict";

this.allowAwardEquipment = function(equipment, ship, context) {
	if (context != "scripted") return false;
	return true
}
Scripts/police_call_AI.js
"use strict";

this.name = "Police SDD Call AI";
this.descripton = "This is the amended default Police js script, with the new highest priority of rendezvous with player inserted at the top (when script is explicity changed).";


this.aiStarted = function() {
	var ai = new worldScripts["oolite-libPriorityAI"].PriorityAIController(this.ship);

	ai.setParameter("oolite_flag_listenForDistressCall",true);
	ai.setParameter("oolite_flag_markOffenders",true);
	ai.setParameter("oolite_flag_fightsNearHostileStations",true);
	ai.setParameter("oolite_flag_selfDestructAbandonedShip",true);

	if (this.ship.primaryRole == "police-station-patrol") 
	{
		ai.setParameter("oolite_leaderRole","police-station-patrol");
		ai.setWaypointGenerator(ai.waypointsStationPatrol);
		ai.setParameter("oolite_flag_patrolStation",true);
	}
	else if (this.ship.primaryRole == "police-witchpoint-patrol") 
	{
		ai.setParameter("oolite_leaderRole","police-witchpoint-patrol");
		ai.setWaypointGenerator(ai.waypointsWitchpointPatrol);
	}
	else
	{
		// chasing a bandit well off the spacelane is almost as good
		// as destroying them
		ai.setParameter("oolite_leaderRole","police");
		ai.setWaypointGenerator(ai.waypointsSpacelanePatrol);
	}

	ai.setParameter("oolite_escortRole","wingman");

	ai.setParameter("oolite_friendlyRoles",["oolite-trader","oolite-bounty-hunter","oolite-scavenger","oolite-shuttle"]);

	ai.setParameter("oolite_personalityMatchesLeader",0.5);
	ai.setCommunicationsRole("police");

   // Define the priority list
    var priorities = [
        // NEW HIGHEST PRIORITY: Rendezvous with player
        {
            condition: function() { 
                // Return true if NOT close to player, triggering the rendezvous
                return !ai.isInRangeOfPlayer(5000); // Adjust range as needed
            },
            configuration: ai.configurationSetDestinationToPlayer,
            behaviour: ai.behaviourApproachDestination,
            reconsider: 10
        },
        // ... (rest of the original police priorities follow here)	
		
		/* Fight */
		{
			preconfiguration: ai.configurationLightsOn,
			condition: ai.conditionLosingCombat,
			behaviour: ai.behaviourFleeCombat,
			reconsider: 5
		},
		{
			condition: ai.conditionInCombat,
			configuration: ai.configurationAcquireCombatTarget,
			behaviour: ai.behaviourDestroyCurrentTarget,
			reconsider: 5
		},
		/* Check for distress calls */
		{
			condition: ai.conditionHasReceivedDistressCall,
			behaviour: ai.behaviourRespondToDistressCall,
			reconsider: 20
		},
		/* Check for offenders */
		{
			preconfiguration: ai.configurationCheckScanner,
			condition: ai.conditionScannerContainsFugitive,
			configuration: ai.configurationAcquireScannedTarget,
			behaviour: ai.behaviourCommenceAttackOnCurrentTarget,
			reconsider: 1
		},
		{
			condition: ai.conditionScannerContainsSeriousOffender,
			configuration: ai.configurationAcquireScannedTarget,
			behaviour: ai.behaviourCommenceAttackOnCurrentTarget,
			reconsider: 1
		},
		{
			preconfiguration: ai.configurationLightsOff,
			condition: ai.conditionScannerContainsFineableOffender,
			configuration: ai.configurationAcquireScannedTarget,
			behaviour: ai.behaviourFineCurrentTarget,
			reconsider: 10
		},
		/* What about escape pods? */
		{
			condition: ai.conditionScannerContainsEscapePods,
			configuration: ai.configurationAcquireScannedTarget,
			behaviour: ai.behaviourCollectSalvage,
			reconsider: 20
		},
		/* Regroup if necessary */
		{
			preconfiguration: ai.configurationAppointGroupLeader,
			condition: ai.conditionGroupIsSeparated,
			configuration: ai.configurationSetDestinationToGroupLeader,
			behaviour: ai.behaviourApproachDestination,
			reconsider: 15
		},
		{
			condition: ai.conditionGroupLeaderIsStation,
			/* Group leader is the station: a short-range patrol or
			 * defense ship */
			truebranch: [
				{
					condition: ai.conditionHasWaypoint,
					configuration: ai.configurationSetDestinationToWaypoint,
					behaviour: ai.behaviourApproachDestination,
					reconsider: 30
				},
				{
					condition: ai.conditionPatrolIsOver,
					truebranch: ai.templateReturnToBase()
				},
				/* No patrol route set up. Make one */
				{
					configuration: ai.configurationSetWaypoint,
					behaviour: ai.behaviourApproachDestination,
					reconsider: 30
				}
			],
			/* Group leader is not station: i.e. this is a long-range
			 * patrol unit */
			falsebranch: [
				{
					/* The group leader leads the patrol */
					condition: ai.conditionIsGroupLeader,
					truebranch: [
						{
							/* Sometimes follow, sometimes not */
							label: "Consider following suspicious?",
							condition: ai.conditionCoinFlip,
							truebranch: [
								/* Suspicious characters */
								{
									condition: ai.conditionScannerContainsSuspiciousShip,
									configuration: ai.configurationSetDestinationToScannedTarget,
									behaviour: ai.behaviourApproachDestination,
									reconsider: 20
								}
							]
						},
						/* Nothing interesting here. Patrol for a bit */
						{
							condition: ai.conditionHasWaypoint,
							configuration: ai.configurationSetDestinationToWaypoint,
							behaviour: ai.behaviourApproachDestination,
							reconsider: 30
						},
						{
							condition: ai.conditionPatrolIsOver,
							truebranch: [
								{
									condition: ai.conditionMainPlanetNearby,
									truebranch: ai.templateReturnToBase()
								}
							]
						},
						/* No patrol route set up. Make one */
						{
							configuration: ai.configurationSetWaypoint,
							behaviour: ai.behaviourApproachDestination,
							reconsider: 30
						}
					],
					/* Other ships in the group will set themselves up
					 * as escorts if possible, or looser followers if
					 * not */
					falsebranch: [
						{
							preconfiguration: ai.configurationEscortGroupLeader,
							condition: ai.conditionIsEscorting,
							behaviour: ai.behaviourEscortMothership,
							reconsider: 30
						},
						/* if we can't set up as an escort */
						{
							behaviour: ai.behaviourFollowGroupLeader,
							reconsider: 15
						}
					]
				}
			]
		}
	]);
	// apply the amended priority array
	ai.setPriorities(priorities);

}
Scripts/sdd-police-fallback.js
"use strict";
this.name = "SDD-Police";
this.author = "Reval";
this.license = "CC-BY-NC-SA 4.0";
this.version = "1.9";
this.description = "This is the fallback script for Oolite populator-spawned SDD Police Mambitas.";

this.$log = true;

this.shipTargetDestroyed = function(whom) {
	var miss = whom.isMissile, s = this.ship;
	// Licensee boon for Police SDD kill is 25-99% of bounty
	var boon = Math.round((Math.random() * (1.0 - 0.25) + 0.25) * whom.bounty);
	// proceed only if player is licensed to carry SDDs
	var ws = worldScripts["Defence Rider Drones"];
	if (ws && ws.$sddHasEQ) {
		var ps = player.ship;
		if (ws.$sddTargetShip === whom) {
			ws.$sddTargetShip = null; // reset
			// clean up H.O. list
			ws.$sddHighOffenders = ws.$sddHighOffenders.filter(function(ship) {
			return ship && ship.isValid;
			});   
			// Re-sort by distance to player (nearest first)
			ws.$sddHighOffenders.sort(function(a, b) {
				return a.position.distanceTo(ps) - b.position.distanceTo(ps);
			});
			// Assign new top offender as target (if auto-targeting)
			if ((ws.$sddHighOffenders.length > 0) && (ws.$sddAutoT)) {
				var hf = ws.$sddHighOffenders[0];
				if (hf.isValid) {
					ws.$sddTargetShip = hf;
				// update HUD
				ws._updateTargetInfo();
				}
			}
		}
		var pc = player.consoleMessage;
		var oc = ps.messageGuiTextColor;
		// Award 10 credits if the destroyed target is a missile
		if (miss) boon = 10;
		// set the 'police comms' colour - a light purple
		ps.messageGuiTextColor = "0.7 0.3 0.8";
		// communicate kill to SDD network on subspace comm
		pc(s.displayName+" terminated a "+whom.name+" (+"+boon+").", 9);
		ps.messageGuiTextColor = oc;
		// player receives some bounty (but no score)
		player.credits += boon;
		// update world script tallies
		ws.$sddPBoons += boon;
		ws.$sddPKills ++;
	}
	if (this.$log) 
		this._log(s.displayName+" terminates a "+whom.name+" (+"+boon+").");
}


this.shipTargetAcquired = function(target) {
	// proceed only if player is licensed to carry SDDs
	var ws = worldScripts["Defence Rider Drones"];
	if ((ws && ws.$sddHasEQ) && (target.bounty>0)) {
		// report NEW offender targets only
		if (target != this.$lastTarget) {
			this.$lastTarget = target;
			var ps = player.ship;
			var pc = player.consoleMessage;
			var oc = ps.messageGuiTextColor;
			ps.messageGuiTextColor = "0.7 0.3 0.8";
			// communicate offender target to SDD network on subspace comm
			pc(this.ship.displayName+" targetted "+target.name+".", 9);
			ps.messageGuiTextColor = oc;
		}
	}
}


// echo to Oolite log for this script only
this._log = function(msg) {
	log(this.name+".debug", msg);
}

Scripts/sdd_equipment_conditions.js
"use strict";

this.allowAwardEquipment = function(equipment, ship, context) {
    // Disallow license if legal status is not clean
    if (context == "purchase" && player.legalStatus != "Clean") {
        player.consoleMessage("SDD Licensing disallowed due to legal status. You must have a clean record.",9);
        return false;
    }    
    // this allows other purchases
    return true;
}   
Scripts/sdd_removal_conditions.js
"use strict";


this.allowAwardEquipment = function(equipment, ship, context) {
    // Only allow the removal equipment to be purchased if the main equipment is installed
    // ensuring the disappearance from the menu of the removal option, once 'bought'.
	if (equipment == "EQ_SDD_MAMBITA_REM") {
        return (player.ship.equipmentStatus("EQ_SDD_MAMBITA") === "EQUIPMENT_OK");
    }
    return true;
};   
Scripts/sdd_script.js
"use strict";
this.name = "Defence Rider Drones";
this.author = "Reval";
this.license = "CC-BY-NC-SA 4.0";
this.version = "1.9";
this.description = "FE Shipyards, under Galcop sanction, undertakes to furnish you with Mambita Ship Defence Rider Drones, known as SDDs, to be mounted at dedicated hardpoints on your ship's hull. An SDD's single purpose is to eliminate hostiles, either automatically or on your orders. Charges and fees apply.";

/*
	Version 1.9
		Suppressed broadcast of spawned High Offenders (use F5->[f] to list, [t] to target).
		Low offender scanner colour switched to 'peach' (improved contrast w/ other types).
		Code cleanup.
		
	Version 1.8
		High Offenders (often 'Fugitives') coloured magenta on scan.
		Low Offenders coloured orange on scan (v.1.9 peach).
		Added shipDied w.s. event handler for final cleanup.
		Nothing displayed on F5F5 if no data, ie. no kills by Riders or Police.
		Sundry additional checks implemented.
		Tightened some existing checks and conditions.
	
	Version 1.7
		F5->[t] Cycle H.O. targets: selection will be tracked in realtime on GETter HUD.
		Auto-targeting automatically cancelled when pilot cycles using [t].
		Prefix listed High Offenders with their index number a la PT-BVRM.
		'Licensee:' line now heads the F5F5 SDD-Net display. Shows commander and ship name.

	Version 1.6
		OPTION: SDD Rider made CLASS_POLICE if masslock is desired. It will assume game-coded Police - coupled with scripted - behaviour. Careful not to hit one!
		High Offenders automatically re-sorted by 'nearest first' in all conceivable cases.
		Covered case of target being a victim of the NPB Neutralizer or in a disabled state.
		Invalid or disabled victims automatically removed from H.O. list.
		Adding a new High Offender to the list (F5F5->[f]) automatically re-sorts it.
		Docking with Station clears the current Top Target from GETter HUD display.
	
	Version 1.5
		Top High Offender automatically re-tracked in GETter HUD v1.6, on acquisition.
		High Offenders instantly updated on elimination of a Top target (by SDD or Mother).

	Version 1.4
		+10 cr for missile elimination (by Police SDD or SDD Rider)
		Incremental 'drop tax' for SDD deployments introduced.
		F5F5 Mission screen 'SDD-Network' shows cumulative SDD Rider & Police SDD statistics.
		SDD Rider & Police SDD gains/losses tracked in-game and via missionVariables.
		Various optimizations and refactorings in all three scripts.

	Version 1.3
		SDD Mambita's default forward weapon changed to Military Laser: SDD kills are now quicker and from greater distances.
		Fully implemented Galcop License-granting conditions.
	
	Version 1.2
		SDD Licensees get random percentage of the bounty on Police SDD kills.
		Police Mambitas report kills on system-wide SDD Net.
		New Police SDDs establish link with player ship's SDD network.
		Idle drone(s) vectored to attack when Mother targets an Offender.
		SDD Mambita drones now know who spawned them (player or system).
		Fix: tightened condition inside the frame callback for close attacks.
		Code re-arrangement for shipSpawned() events between world- and ship-scripts.
		Tidied and deleted sundry debug log entries.
		
	Version 1.1
		Subspace (console message) comms from SDDs out of scan-range.
		Top High Offender made SDD target on close range approach by Mother.
		Highest offender realtime viewscreen tracking integrated with GETter HUD v.1.6
		Highest bounty Offenders in system updated and listed on keypress [f] from F5 screen.
		Advisory broadcast to SDDs when Mother targets an Offender.
		Galcop prohibition on drone deployment from an offender ship.
		SDD Mambita entry and description added to the Ship Library.
		Equipment text changed.
*/

/* To Do
	
	Drone deployment given a very small chance of FAILURE, resulting in WARNING and downgrading. (see if (!drone) in deployDrone).
	[n] keypress nulls (removes) current HUD-tracked target (makes it "").
	shipDied and shipRemoved for Police SDD script (analogous to that in SDD Rider script)
	[?] Keypress on F5 screen provides information on Police SDDs in system
	Mother can call any unengaged in-system Police SDDs to aid her (via a replacement  policeAI.js script, with rendezvous parameters appended as high priority).
	Functions for selecting and calling Police SDDs (nearest first etc)
*/


// OPTION: are we logging ALL SDD and Police activities?
this.$log = true;
// OPTION: Do deployed Riders masslock other vessels? ('true' is 'yes')
this.$sddMassLock = false; // default 'no' (they are MINES)


this.shipSpawned = function(ship) {
    // check whose drone by scanner colour
	if ((ship.shipClassName === "SDD Mambita") && ( 
    (ship.scannerDisplayColor1[0] === 0.5) && 
    (ship.scannerDisplayColor1[1] === 0.0) && 
    (ship.scannerDisplayColor1[2] === 0.5))) {
        // This is a populator-spawned NPC Police Mambita
		// give the populator-spawned drone Police AI
		ship.setAI("oolite-policeAI.js");
		ship.setScript("sdd-police-fallback.js");   
    } else {
		// This is a system-spawned NPC ship,
		// processed here only if an offender.
		if  (this._sddIsOffender(ship)) {
			var b = ship.bounty;
			var d = this._sddDistanceKm(ship);
			var c = this._sddCompassDirection(ship);
			var n = ship.shipClassName;
			if (this.$log) this._log("Spawned: "+n+" ("+b+") "+d+" km ");
			// colour low offenders 'soft coral peach' on scanner
			if (b<40) {
				ship.scannerDisplayColor1 = [ 1.0, 0.65, 0.55 ]; 
				return;
			}
			// add high offenders to recallable list
			if (b>=40) {
				// colour High Offenders magenta on scanner
				ship.scannerDisplayColor1 = "magentaColor";
				// only add NEW potential targets
				var oTarget = this.$sddTargetShip;
				var newT = (oTarget !== ship);
				// add the new high offender...
				this._sddAddHighOffender(ship);
				// clean up H.O. list
				this.$sddHighOffenders = this.$sddHighOffenders.filter(function(ship) {
					return ship && ship.isValid;
				});   
				var ps = player.ship;
				// Re-sort by distance to player (nearest first)
				this.$sddHighOffenders.sort(function(a, b) {
					return a.position.distanceTo(ps) - b.position.distanceTo(ps);
				});
				// player.commsMessage("High Offender "+n+" at "+d+ " Lk "+c,9);
				// Assign new top offender as target
				if (this.$sddHighOffenders.length > 0) {
					var hf = this.$sddHighOffenders[0];
					// change primary target if new
					if (hf.isValid && newT) {
						this.$sddTargetShip = hf;
						// update HUD
						this._updateTargetInfo();
				}	
			}
		}
	}
}};


this.shipWillLaunchFromStation = function() {
	var ps = player.ship;
	this.$sddHasEQ = (ps.equipmentStatus ("EQ_SDD_MAMBITA") === "EQUIPMENT_OK");
	this._sddReset();
	// create callback frame for detecting Torus
	this.$fcb = addFrameCallback(function(delta) {
		// Check Torus status every frame
		if (ps.torusEngaged && !this.$torusActive) {
			// Torus drive engaged
			this._sddReset();
		}
		this.$torusActive = ps.torusEngaged;
		// idle SDDs target highest offender if in range
		if ((this.$sddHens>0) && (this.$sddHighOffenders.length>0)) {
			var hf = this.$sddHighOffenders[0];
			if ((hf.isValid) && (this._sddDistanceKm(hf)<=10.0)) {
				this._sddIdleAttack(hf);
			}
		}	
    }.bind(this));
}

 
// establish drone complement by ship-type
this.shipLaunchedFromStation = function(station) {
    var pc = player.consoleMessage;
	if (this.$sddHasEQ) {
		pc("Initializing comm-link to Riders...", 9);
		var d = this._sddMaxDrones();
		this.$sddMaxHens = (d > 0) ? d : 7;
		pc(this.$sddMaxHens+" SDD Rider drones listening.", 9);
		pc("Re-toggle WEAPONS to launch a rider.",9);
    } else 
		pc("Mount SDD Rider drones in shipyard.", 9);
	// start realtime Target info update
    // Use a Timer to update offender ship's distance and direction
    // ONLY IF GETter HUD is loaded.
	if (worldScripts["GETter HUD"]) {
		this._updateTimer = new Timer(this, this._updateTargetInfo.bind(this), 0.5, 0.5);
		this._updateTargetInfo();
	}
};



this.shipWillDockWithStation = function(station) {
    // Remove callback for Torus detection
	if (this.$fcb) {
        removeFrameCallback(this.$fcb);
        this.$fcb = null;
    }
	// Stop the HUD target info timer
    if (this._updateTimer) {
        player.ship.setCustomHUDDial("sddTargetInfo", "");
        this._updateTimer.stop();
        this._updateTimer = null;
    }
	// clear and reset H.O. list
	this.$sddHighOffenders = [];
	this.$sddTargetShip = null;
	// update F5F5 Mission data
	this._updateSDDNetworkDisplay();
}


this.playerStartedJumpCountdown = function(type, seconds) {
	// clean slate on H-jump
	this._sddReset();
	// clear and reset H.O. list
	this.$sddHighOffenders = [];
	this.$sddTargetShip = null;
}


this.shipEnteredStationAegis = function(station) {
	this._sddAccounting();
}


// retrieve drones on receipt of docking clearance
this.playerRequestedDockingClearance = function() {
	// unset tracker target on docking
	this.$sddTargetShip = null;
	this._sddReset();
}


this.playerBoughtEquipment = function(equipment, paid) {
	var pc = player.consoleMessage;
	if (equipment=="EQ_SDD_MAMBITA") {		
		if (player.legalStatus=="Clean")
			pc("Licence issued: SDD Riders mounted.",9);
		else pc("Licence denied. No SDD Riders mounted.",9);
	} else
	// Licence termination, removal and refund
	if (equipment == "EQ_SDD_MAMBITA_REM") {
		player.ship.removeEquipment("EQ_SDD_MAMBITA");
		player.ship.removeEquipment("EQ_SDD_MAMBITA_REM");
		pc("SDD hardpoints have been removed. Half refund.",9);
		player.credits += 500;
	}
}


this.shipTargetAcquired = function(target) {
    if (target.bounty > 10) {
		var pc = player.commsMessage;
        // The targeted vessel is an offender
        pc("Alert SDD: Mother's target is "+target.name+"(+"+target.bounty+")", 9);
		this._sddIdleAttack(target);
    }
};


// 'put affairs in order'
this.shipDied = function(whom, why) {
    // Remove callback for Torus detection
	if (this.$fcb) {
        removeFrameCallback(this.$fcb);
        this.$fcb = null;
    }
	// Stop the HUD target info timer
    if (this._updateTimer) {
        player.ship.setCustomHUDDial("sddTargetInfo", "");
        this._updateTimer.stop();
        this._updateTimer = null;
    }
	// clear and reset H.O. list
	this.$sddHighOffenders = [];
	this.$sddTargetShip = null;
}


// mother is under attack: assign nearest drone to defend
this.shipBeingAttacked = function(whom) {
    if ((this.$sddHasEQ) && (this.$sddShips.length > 0)) {
        this.$sddAttacker = whom;
        
        // Clean up invalid ships
		this.$sddShips = this.$sddShips.filter(function(ship) { return ship && ship.isValid; });
        
		// Now assign to nearest valid ship in the same array
        let nearestIndex = this._getNearestShipIndex();
        if (nearestIndex !== -1) {
            let assignedShip = this.$sddShips[nearestIndex];
            assignedShip.target = whom;
            assignedShip.performAttack();
        }       
    }
}


this.shipAttackedWithMissile = function(missile, whom) {
	if ((this.$sddHasEQ) && (this.$sddShips.length>1)) {
		this.$sddAttacker = whom;
		// assign idle drone to attack
		this._sddIdleAttack(whom);
	}
}


// re-toggling WEAPONS ON deploys a drone
this.weaponsSystemsToggled = function(state) {
    if (player.legalStatus=="Clean") {
		if (this.$sddHasEQ)
			if (state) this._sddDeployDrone(1);
	} else if (state) 
		player.consoleMessage("GALCOP WARNING: SDD inactive due to ship's legal status.",9);
}


this.alertConditionChanged = function(newCondition, oldCondition) {
	if (this.$sddHasEQ) {	
		var psa = newCondition;
		// condition Red-alert: advise to deploy drone...
		if (psa==3)  {
			this._sddDistress(this.$sddAttackMsg);
			// assign unengaged SDD to target
			// (detecting which attacker within a frame callback)
			if (!this.$afcb) {
				this.$afcb = addFrameCallback(function(delta) {
					var attackers = system.filteredEntities(this, function(e) {
					return e.isShip && e.target === player.ship;
					}, player.ship);
				if (attackers.length > 0) {
					for (var i = 0; i < attackers.length; i++) {
						if (attackers[i].bounty > 0) {
							this._sddIdleAttack(attackers[i]);
							break;
						}
					}
   					if (this.$afcb) {
						removeFrameCallback(this.$afcb);
						delete this.$afcb;
					}
				}
			}.bind(this));
		}
	}
}
}


// instruct all drones to attack <target>
this._sddGroupAttack = function(target) {
    // Check if the target is a potential hostile
    if (target && (target.scanClass!="CLASS_POLICE") && (target.scanClass!="CLASS_PLAYER") && (target.scanClass!="CLASS_MINE")) {
        for (var i = 0; i < this.$sddShips.length; i++) {
            var drone = this.$sddShips[i];
			drone.target = target;
			drone.performAttack();
        }
    }
}


// instruct an 'idle' drone to attack <target>
this._sddIdleAttack = function(target) {
    // Check if the target is a potential hostile
    if (target && (target.scanClass!="CLASS_POLICE") && (target.scanClass!="CLASS_PLAYER") && (target.scanClass!="CLASS_MINE")) {
		for (var i = 0; i < this.$sddShips.length; i++) {
			var drone = this.$sddShips[i];
			// if drone is idle, ie. following Mother
			if (drone && drone.target === player.ship) {
				drone.target = target;
				drone.performAttack();
				break; // Assign only one drone
			}
		}
	}
}


// deploy a drone or drones
this._sddDeployDrone = function(num) {
    var pc = player.consoleMessage;
    var ps = player.ship;
    if ((this.$sddHasEQ) && (this.$sddHens<this.$sddMaxHens)) {	
        		
		// spawn a new drone or drones
        var drone = system.addShips("[sdd-mambita]", num, ps.position);
        
		// Belt'n'braces check for drone presence and validity
		if (!drone) {
			var psh = player.ship, pcm = ps.consoleMessage;
			var ocolor = psh.messageGuiTextColor;
			psh.messageGuiTextColor = "redColor";
			pcm("SDD DEPLOYMENT FAILURE:",9);
			pcm("Report to CSDDA soonest.",9);
			pcm("Until such time, your status: Downgraded.",9);
			psh.messageGuiTextColor = ocolor;
			// make the possibly defaulting Licensee a small offender
			psh.bounty = 5; 
			if (this.$log) this._log("SDD DEPLOYMENT FAILURE.");
			return;
		}
		
		// put masslock/no masslock option into effect
		for (var d = 0; d < drone.length; d++)
			if (this.$sddMassLock) drone[d].scanClass = "CLASS_POLICE";
		
		// set custom property on player-spawned drone(s)
		for (var d = 0; d < drone.length; d++)
			drone[d].$spawnedByMother = true;
		
		// set scanner lollipop colour for player-spawned drones
		for (var d = 0; d < drone.length; d++) {
			drone[d].scannerDisplayColor1 = "blueColor";
			drone[d].scannerDisplayColor2 = "redColor";
		}
		
		// prevent collisions between new drone, player, and other drones
		for (var d = 0; d < drone.length; d++)
			this._collisionAvoidance(drone[d]);
		
        // Create a group with the first drone as leader
        if (!this.$sddGroup) {
            this.$sddGroup = new ShipGroup();
			this.$sddGroup.leader = drone[0];
		}

        // Add new drone(s) to the group
        for (var d = 0; d < drone.length; d++) {
            this.$sddGroup.addShip(drone[d]);
        }
		
		// concatenate new drone(s) to existing drone-array
		this.$sddShips = this.$sddShips.concat(drone);
		
		// final leader validity check
		// using the group's ships array
		if (!this.$sddGroup.leader || !this.$sddGroup.leader.isValid) {
			// Find a new valid leader from the group
			for (var s = 0; s < this.$sddGroup.ships.length; s++) {
				if (this.$sddGroup.ships[s].isValid) {
					this.$sddGroup.leader = this.$sddGroup.ships[s];
					break;
				}
			}
		}
        // update drone count
		this.$sddHens += num;
		// apply per-deployment maintenance tax w/ level-increment
		this.$sddDeployTax += this.$sddTaxLevel; // +0.01 cr nominal
		// keep a record (total taxes)
		this.$sddRTax += this.$sddDeployTax;
		// refresh the F5F5 mission screen as the debit happens
		this._updateSDDNetworkDisplay();   
		// debit account for the tax
		player.credits -= this.$sddDeployTax;
		pc("Deployed SDD rider #"+this.$sddHens+".", 9);
    
	// no more drones to deploy
	} else {
		if (this.$sddHasEQ) pc("All "+this.$sddHens+" SDD Riders are deployed.",9);
	}
}   


this.shipAttackedOther = function(other) {
// Mother fires on any ship and expects all drones to engage it
	if (other && other.isValid)
		this._sddGroupAttack(other);
	
	// only process this victim once
	if (other.$sddAttacked) return;
		else other.$sddAttacked = true;
		
	// if firing on a neutralized or disabled ship
	if (((other.$neutralized) || (other.isDisabled)) 
		&& (other == this.$sddTargetShip)) {
        this.$sddTargetShip = null; // reset
		// clean up H.O. list
		this.$sddHighOffenders = this.$sddHighOffenders.filter(function(ship) {
			return ship && ship.isValid;
		});   
		var ps = player.ship;
		// Re-sort by distance to player (nearest first)
		this.$sddHighOffenders.sort(function(a, b) {
			return a.position.distanceTo(ps) - b.position.distanceTo(ps);
		});
		// Assign new top offender as target
        if ((this.$sddHighOffenders.length > 0) && (this.$sddAutoT)) {
            var hf = this.$sddHighOffenders[0];
            if (hf.isValid) {
                this.$sddTargetShip = hf;
				// update HUD
				this._updateTargetInfo();
				if (this.$log) this._log("H.O. Target Ship changed to "+hf+".");
            }
        }
	}
}   


this.shipKilledOther = function(whom, damageType) {
    // triggered whenever this ship destroys another
    if (this.$sddTargetShip === whom) {
        this.$sddTargetShip = null; // reset
		// clean up H.O. list
		this.$sddHighOffenders = this.$sddHighOffenders.filter(function(ship) {
			return ship && ship.isValid;
		});   
		var ps = player.ship;
		// Re-sort by distance to player (nearest first)
		this.$sddHighOffenders.sort(function(a, b) {
			return a.position.distanceTo(ps) - b.position.distanceTo(ps);
		});
		// Assign new top offender as target
        if ((this.$sddHighOffenders.length > 0) && (this.$sddAutoT)) {
            var hf = this.$sddHighOffenders[0];
            if (hf.isValid) {
                this.$sddTargetShip = hf;
			// update HUD
			this._updateTargetInfo();
			if (this.$log) this._log("H.O. Target Ship changed to "+hf+".");
            }
        }
	}
};   


// prevent potential collisions within group
this._collisionAvoidance = function(ship) {
    if (!ship || !ship.isValid) return;
    // Prevent collision with player
    ship.addCollisionException(player.ship);
    // Prevent collision with other drones
    for (let i = 0; i < this.$sddShips.length; i++) {
        let other = this.$sddShips[i];
        if (other && other.isValid && other !== ship) {
            ship.addCollisionException(other);
        }
    }
}

   
// report condition status
this._sddDoCond = function(cond) {
	var ps = player.ship;
	var pc  = player.consoleMessage;
	var c = ps.messageGuiTextColor;
	if (cond==0) ps.messageGuiTextColor = "blueColor"; else
	if (cond==1) ps.messageGuiTextColor = "greenColor"; else
	if (cond==2) ps.messageGuiTextColor = "yellowColor"; else
	if (cond==3) ps.messageGuiTextColor = "redColor";
	pc("Condition "+this.$sddConds[cond],3);
	ps.messageGuiTextColor = c;
}


// put out the distress call
this._sddDistress = function(msg) {
	if (this.$sddHasEQ) {
		var ps = player.ship;
		var pc = player.consoleMessage;
		var c = ps.messageGuiTextColor;
		ps.messageGuiTextColor = "redColor";
		pc(msg,9);
		ps.broadcastDistressMessage();	
		if (this.$sddHens <= this.$sddMaxHens)
			pc("Toggle WEAPONS to launch Riders!",9);
		ps.messageGuiTextColor = c;
	}
}	


// determine if a given drone is already engaged
this._isShipAttacking = function(ship) {
    if (!ship.isValid) return false;
    // Check if in an attack-related AI state
    const attackingStates = ["ATTACK_SHIP", "ATTACK_PIRATE"];
    return attackingStates.includes(ship.AIState) && 
           ship.target && 
           ship.target.isValid;
}


// index of nearest drone to player
this._getNearestShipIndex = function() {
    let nearestIndex = -1;
    let shortestDistance = Infinity;

    for (let i = 0; i < this.$sddShips.length; i++) {
        const ship = this.$sddShips[i];
        if (ship && ship.isValid) {
            const dist = player.ship.position.distanceTo(ship.position);
            if (dist < shortestDistance) {
                shortestDistance = dist;
                nearestIndex = i;
            }
        }
    }
    return nearestIndex;
};   


this._sddCountShips = function() {
	var count = 0;
	for (var i = 0; i < this.$sddShips.length; i++)
		if (this.$sddShips[i].isValid) count++;
	return count;
}


// return an array with only the single highest-bounty ship within scanner range (25.6 km). To get the top N, use .slice(0, N).
this._highestBountyShip = function() {
    var ships = system.filteredEntities(
        this,
        function(e) {
            return e.isShip && e.bounty > 0;
        },
        player.ship, // relative to player
        25600  // Scanner range in meters
    );
    ships.sort(function(a, b) {
        return b.bounty - a.bounty;
    });
    return ships.length ? [ships[0]] : [];
};   


// recall and re-attach riders (remove from system space)
this._sddReset = function() {
	for (var i = 0; i < this.$sddShips.length; i++)
		this.$sddShips[i].remove(true);
	// Perform other housekeeping (clear the array, etc)
	this.$sddShips.length = 0;
	this.$sddHens = 0;
	this.$sddCharges = 0;
}


// accounting info: all real debits and credits
// are made realtime in the ship-scripts
this._sddAccounting = function() {
	var pc = player.commsMessage;
	if ((this.$sddHasEQ) && (this.$sddHens>0)){
		// count # of surviving drones
		var surv = this._sddCountShips();
		pc(surv+" SDD drone riders retrieved.",9);
		var dead = this.$sddHens-surv;
		pc(dead+" SDD drone riders lost.",9);
		// nominal charge for deployment (survivors rejoin)
		var depcr = (this.$sddHens * 10);
		pc("Charges for drones deployed ("+this.$sddHens+"): "+depcr+" cr",9);
		var losscr = (dead * 100);
		pc("Charges for drones lost ("+dead+"): "+losscr+" cr",9);
		this.$sddCharges = (depcr+losscr);
	}
}


// detect an offending ship by its bounty
this._sddIsOffender = function(ship) {
	if ((ship.isShip) && (ship.shipClassName!=='Asteroid')) 
		return (ship.bounty > 0);
	else return false;
}


this._sddAddHighOffender = function(ship) {
    // clear the array of invalid ships
    this.$sddHighOffenders = this.$sddHighOffenders.filter(function(s) {
        return s && s.isValid;
    });
    // Add the new ship if valid and not already in the list
    if (ship && ship.isValid && this.$sddHighOffenders.indexOf(ship) === -1) {
        this.$sddHighOffenders.push(ship);
    }
	// Re-sort by distance to player (nearest first)
	var ps = player.ship;
	this.$sddHighOffenders.sort(function(a, b) {
		return a.position.distanceTo(ps) - b.position.distanceTo(ps);
	});
	// Explicitly set the Top target to the first element after sorting
	if (this.$sddHighOffenders.length > 0)
		this.$sddTargetShip = this.$sddHighOffenders[0];
};


// list the system's high offenders by distance via comm
this.$sddListHighOffenders = function() {
	var hof = this.$sddHighOffenders;
	// 1. clean existing offender list
	hof = hof.filter(function(s) { return s && s.isValid; });    
    // offenders list is empty
	if (hof.length === 0) {
        player.commsMessage("No high offenders detected in system.", 5);
        return;
    }
	// 2. SORT the cleaned list by distance from the player's ship (closest first)
	hof.sort(function(a, b) {
		return player.ship.position.distanceTo(a.position) - player.ship.position.distanceTo(b.position);
	});   
    // 3. send comms messages to report high offenders in system
	// (recallable via log & updatable by pressing [f] on F5 screen)
    var pc = player.consoleMessage;
	pc("High Offenders:", 5);
    for (var i = 0; i < hof.length; i++) {
        var ship = hof[i];
		var b = ship.bounty;
		var d = this._sddDistanceKm(ship);
		var c = this._sddCompassDirection(ship);
		var n = ship.displayName;
		pc("["+i+"] "+n+" ("+b+") at "+d+ " Lk "+c,9);
    }
};   


// Compass direction of the given ship, 'N' = top of scanner, 'S' = bottom.
this._sddCompassDirection = function(ship) {
    var ps = player.ship;
	// Get the vector from the player to the target ship
    var vectorToTarget = ship.position.subtract(ps.position);
    
    // Get the player's forward and right vectors (defining the horizontal plane)
    var playerForward = ps.vectorForward;
    var playerRight = ps.vectorRight;
    
    // Project the target vector onto the player's horizontal plane (XZ plane)
    var dotForward = vectorToTarget.dot(playerForward);
    var dotRight = vectorToTarget.dot(playerRight);
    
    // Calculate the angle in radians from the player's forward direction
    var angle = Math.atan2(dotRight, dotForward);
    
    // Convert angle from radians to degrees and normalize to 0-360
    var degrees = (angle * 180 / Math.PI + 360) % 360;
    
    // Determine the compass direction based on the angle
    if (degrees >= 337.5 || degrees < 22.5) return 'N';
    else if (degrees < 67.5) return 'NE';
    else if (degrees < 112.5) return 'E';
    else if (degrees < 157.5) return 'SE';
    else if (degrees < 202.5) return 'S';
    else if (degrees < 247.5) return 'SW';
    else if (degrees < 292.5) return 'W';
    else return 'NW';
};   


// distance to any ship in km
this._sddDistanceKm = function(ship) {
    var distanceInMeters = ship.position.distanceTo(player.ship.position);
    return (distanceInMeters / 1000).toFixed(2);
};   


this._sddGetRandomInt = function(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}


// echo to Oolite log for this script only
this._log = function(msg) {
	log(this.name+".debug", msg);
}


this.playerWillSaveGame = function(message) {
	var mv = missionVariables;
	mv.sddRKills = this.$sddRKills; // SDD Rider kills
	mv.sddRBounty = this.$sddRBounty; // SDD Earnings (incl. 100 cr 'incentives')
	mv.sddRLosses = this.$sddRLosses; // count: SDDs killed or collided
	mv.sddRTax    = this.$sddRTax;    // debit: SDD deployment taxes
	mv.sddPKills = this.$sddPKills; // Police 'terminations'
	mv.sddPBoons = this.$sddPBoons; // Police 'boons'
	// SDD Rider deployment taxation
	// current tax level, incremented per 'drop' (nominally +0.01 cr)
	mv.sddDeployTax= this.$sddDeployTax; 
}


this.startUp = function() {
	this.$sddConds = ["DOCKED","GREEN","YELLOW","RED"];
	this.$sddTesting = false;
	this.$sddHasEQ = false;
	this.$sddMaxHens = 9; // arbitrary initialization
	this.$sddHens = 0;
	this.$sddAttackMsg = "SDD set condition RED.";
	this.$sddAlertMsg = "RED alert. RED alert.";
	this.$sddCharges = 0;
	this.$sddTargetShip = null; // Top target on High Offenders list
	this.$sddDeployTax = 0.0; // increases by <tax-level> with each 'drop'
	this.$sddTaxLevel = 0.01; // per 'drop' increment for deployment tax
	// tallies to be passed from ship-scripts
	// and saved/loaded via missionVariables.
	this.$sddRKills = 0; // SDD Rider kills
	this.$sddPKills = 0; // Police 'terminations'
	this.$sddPBoons = 0; // Police 'boons'
	this.$sddRBounty= 0; // SDD Earnings (incl. 100 cr 'incentives')
	this.$sddRTax   = 0; // total SDD deployment taxes
	this.$sddRLosses= 0; // count of SDDs killed or collided
	this.$sddMKills = 0; // Mother's kills
	this.$sddMBounty= 0; // Mother's own bounties
	this.$sddIndex  = 0; // index into H.O. cycles target
	this.$sddAutoT  = true; // auto-selecting targets?
	// Register the 'x' key for the F5 screen
    // for manually recalling drones (v.1.92 +)
	// and the 'f' key for listing offenders...
	if (oolite.compareVersion("1.92") <= 0) {
		setExtraGuiScreenKeys(this.name, {
			guiScreen: "GUI_SCREEN_STATUS",
			registerKeys: {
				"recall-drones": [{key: "x"}],
				"list-offenders": [{key: "f"}],
				"cycle-hof": [{key: "t"}]
			},
			callback: this._sddKeyHandler.bind(this)
		});
	}
	// create and declare rider array once here
	this.$sddShips = [];
	// create and declare high-offender shiplist
	this.$sddHighOffenders = [];

	// Load persistent incremented variables
	// -------------------------------------
	var mv = missionVariables;
    if (mv.sddRKills !== undefined) this.$sddRKills = mv.sddRKills;  // SDD Rider kills
	if (mv.sddRBounty!== undefined) this.$sddRBounty= mv.sddRBounty; // SDD Rider Earnings
	if (mv.sddRLosses!== undefined) this.$sddRLosses= mv.sddRLosses; // SDD destroyed debit
    if (mv.sddRTax   !== undefined) this.$sddRTax   = mv.sddRTax;    // SDD deployment taxes
	if (mv.sddPKills !== undefined) this.$sddPKills = mv.sddPKills;  // Police SDD kills
	if (mv.sddPBoons !== undefined) this.$sddPBoons = mv.sddPBoons;  // Police SDD 'boons'
	// SDD Rider deployment taxation (floats)
	// if undefined, defaults to 0.0 when loading
    this.$sddDeployTax = (mv.sddDeployTax !== undefined) ? parseFloat(mv.sddDeployTax) : 0.0;
}


// 'recall-drones' with [x] key - ONLY from F5 status screen,
// 'list-offenders' with [f] key - ditto
// 'cycle-hof' with [t] key - ditto
this._sddKeyHandler = function(keyId) {
    if (keyId === "recall-drones") {
        // Remove all drones from the system
		this._sddReset();
        return true; // Consume the keypress
    }
    if (keyId === "list-offenders") {
        // list system's high-offenders
		var hof = this.$sddHighOffenders;
		this.$sddListHighOffenders();
		if ((this.$sddAutoT) && (hof.length>0) && (hof[0].isValid))
			this.$sddTargetShip = hof[0]; 
        return true; // Consume the keypress
    }
    // Cycle primary Target
	var hof = this.$sddHighOffenders;
    if ((hof) && (hof.length>0)) { 
		if (keyId === "cycle-hof") {
			// Increment index and wrap around if it reaches the end
			this.$sddIndex = (this.$sddIndex + 1) % hof.length;
			var idx = this.$sddIndex;
			// assign this target
			this.$sddTargetShip = hof[idx]; 
			this._updateTargetInfo();
			// cancel auto-targetting
			this.$sddAutoT = false;
			let ps = player.ship, pc = player.consoleMessage;
			let oc = ps.messageGuiTextColor; // old colour
			ps.messageGuiTextColor = "greenColor";
			pc("["+idx+"] Target: " + hof[idx].displayName, 7); 
			ps.messageGuiTextColor = oc; // old colour
			return true;
		}
    } else { 
		player.consoleMessage("No high-value targets in system.",7);
		return true;
	}
    return false;
}


// Helper method to update the F5F5 SDD-Net Mission Screen
this._updateSDDNetworkDisplay = function() {
    if (this.$sddHasEQ) {
        var ps = player.ship, dname = ps.displayName, pn = player.name;
        
        // Check if data exists
        var hasData = (this.$sddRKills > 0) || (this.$sddPKills > 0);

        if (hasData) {
            // Show stats
            mission.setInstructions([
                "SDD-Network",
                "Licensee: " + pn + ", " + dname,
                "SDD Rider Kills: " + (this.$sddRKills || 0),
                "SDD Rider Bounty: " + (this.$sddRBounty || 0) + " cr",
                "SDD Rider Taxes:  " + (Number(this.$sddRTax) || 0.0).toFixed(2) + " cr",
                "SDD 'Drop Tax':   " + (Number(this.$sddDeployTax) || 0.0).toFixed(2) + " cr",
                "Police SDD Kills: " + (this.$sddPKills || 0),
                "Police SDD Boons: " + (this.$sddPBoons || 0) + " cr"
            ], this.name);
        } else {
            // Essential: explicitly clear the section to hide it completely
            mission.setInstructions(null, this.name); 
        }
    }
};   


// Update the F5F5 SDD-Net Mission Screen on displaying it
this.guiScreenChanged = function(from, to) {
    if (this.$sddHasEQ) {
		if (guiScreen === "GUI_SCREEN_MANIFEST") {
			this._updateSDDNetworkDisplay();
		}
	}
};
  

// Drone capacities by ship class
this._sddMaxDrones = function() {
	var name = player.ship.shipClassName.toLowerCase();    
	var map = {
        "adder": 2,
		"anaconda": 20,
        "asp mk ii": 6,
		"asp mark ii": 6,
		"asp explorer": 6,   
        "boa": 15,
		"boa class cruiser": 15,
		"cobra mk i": 5,
		"cobra mark i": 5,
		"cobra mk ii": 6,
        "cobra mk iii": 8,
        "cobra mk 3": 8,
        "cobra mark iii": 8,
		"cobra mk iv": 10,
		"constrictor": 10,
        "fer-de-lance": 6,
        "gecko": 2,
        "krait": 5,
        "mamba": 3,
		"moray medical boat": 4,
		"moray star boat": 4,
        "python": 12,
        "sidewinder": 2,
		"sidewinder scout ship": 2,
		"training fighter": 4,
		"transporter": 2,
        "viper": 2,
		"galcop viper": 2,
		"galcop viper interceptor": 3,
		"worm": 2
    };
	return map[name] || 7;
};  


// nearest High Offender realtime tracking onscreen (via GETter HUD's hud.plist)
this._updateTargetInfo = function() {
    if (!player.ship.isValid) return;
	if (this.$sddTargetShip && this.$sddTargetShip.isValid) {
        var ps = player.ship, ts = this.$sddTargetShip;
		var distance = (ts.position.distanceTo(ps.position) / 1000).toFixed(2);
        var direction = this._sddCompassDirection(ts);
        
        // Get the vector from player to target
        var relPos = ts.position.subtract(ps.position);
        
        // Project the relative position onto the player's up and forward vectors
        var upComponent = relPos.dot(ps.orientation.vectorUp());
        var forwardComponent = relPos.dot(ps.heading); // heading is the forward vector
        
        // Determine elevation based on the ratio, avoiding division by zero
        var elevation = "";
        if (Math.abs(forwardComponent) > 1) { // Use a small threshold instead of zero
            var verticalAngle = Math.abs(upComponent / forwardComponent);
            elevation = (upComponent > 0) ? (verticalAngle > 0.1 ? "hi" : "") : (verticalAngle > 0.1 ? "lo" : "");
        }

        ps.setCustomHUDDial("sddTargetInfo", ts.name + " > " + direction + " " + elevation + " < " + distance + "km");
    } else {
        player.ship.setCustomHUDDial("sddTargetInfo", "");
    }
};