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

Expansion PT-BVR Missile

Content

Warnings

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

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Galcop's PT-BVRM 'Pteranodon' standoff missile engages prioritized targets at full-system ranges. Fires neutral particles on proximity, causing mine-like irradiative effects on ships inside sensor radius, crippling the victims. Mostly non-fatal to crew. Galcop's PT-BVRM 'Pteranodon' standoff missile engages prioritized targets at full-system ranges. Fires neutral particles on proximity, causing mine-like irradiative effects on ships inside sensor radius, crippling the victims. Mostly non-fatal to crew.
Identifier oolite.oxp.Reval.PT-BVR-Missile oolite.oxp.Reval.PT-BVR-Missile
Title PT-BVR Missile PT-BVR Missile
Category Weapons Weapons
Author Reval Reval
Version 1.4 1.4
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Information URL https://wiki.alioth.net/index.php/PT-BVRM_Pteranodon_OXZ n/a
Download URL https://wiki.alioth.net/img_auth.php/e/e5/PT-BVRM_Pteranodon.oxz https://wiki.alioth.net/img_auth.php/e/e5/PT-BVRM_Pteranodon.oxz
License CC-BY-NC-SA 4.0 CC-BY-NC-SA 4.0
File Size n/a
Upload date 1780639477

Relationships Diagram

Documentation

Equipment

Name Visible Cost [deci-credits] Tech-Level
PT-BVRM Pteranodon Launcher Package yes 50000 1+
Remove PT-BVRM Launcher yes 0 4+
PEW Laser yes 1000 5+

Ships

Name
PT-BVRM

Models

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

Scripts

Path
Scripts/PT-BVRM-equip.js
"use strict";
this.description = "Equipment script for priming PT-BVRM with [n] and launching with [N]";

this.activated = function() {
    // launch the missile -
    // call actual launch function in world script
    var ws = worldScripts["PT-BVRM Pteranodon"];
    if (!ws) return;
	ws._ptmLaunchBVRM();
};
 
Scripts/PT-BVRM.js
"use strict";
this.name = "PT-BVRM";
this.author = "Reval";
this.license = "CC-BY-NC-SA 4.0";
this.version = "1.4";
this.description = "This is the missile script for individual PT-BVRMs.";

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


this.shipSpawned = function() {
	var s = this.ship;
	// if BVRM is not SDD-designated, designate it
	if (!this._containsNumerals(s.displayName))
		s.displayName = this._generateBVRMName();
	// CHECK: who spawned me?
	if (s.$spawnedByMother) {
		s.displayName += "";
		s.commsMessage("Launched. Guidance active.", player.ship);
		if (this.$log) this._log(s.displayName+" spawned.");
		s.$myKills = 0;
		s.$creditsAwarded = 0;
		// set up Progress timer (fires once per second)
		if(!this.$ptmTimer) this.$ptmTimer=new Timer(this,this._ptmProgress.bind(this),0,1.0);
		// prepare for possible later target change
		this.$ptmLastTarget = s.target;
	}
}


// check if spawned BVRM is SDD-designated
this._containsNumerals = function(str) {
    return /\d/.test(str);
}

   
// PT-BVRM transmits periodic subspace updates to Mother
this._ptmProgress = function() {
    var s = this.ship, pc = player.consoleMessage;
	// invalid target: request change or retrieval
	if ((s.target==null) || (s.target==undefined) || (!s.target.isValid)) {
		// immediately change target to player (follow Mother)
		//  (avoid PriorityAI exceptions in the log). 
		s.target = player.ship;
		pc("PT Tracking switched to Mother. Request target or retrieval.",9);
		// flag our possible 'malfunction'
		s.$malfunction = true;
		return;
	}
	var t = s.target, ps = player.ship;
	if (!t || !t.isValid) return;

    var sn = s.displayName;
    var ws = worldScripts['PT-BVRM Pteranodon'];
    if (!ws) return;
    
	// Check for target change
	if (ws.$ptmTargetShip && ws.$ptmTargetShip.isValid && (ws.$ptmTargetShip !== this.$ptmLastTarget)) {
		let oc = ps.messageGuiTextColor; // old colour
		ps.messageGuiTextColor = [0.545, 0, 0.545]; // new magenta
		pc(sn + ": Target now " + ws.$ptmTargetShip.displayName, 9);
		ps.messageGuiTextColor = oc;
		this.$ptmLastTarget = ws.$ptmTargetShip;
		this.ship.target = ws.$ptmTargetShip;
		return;
	}
	var tn = t.displayName;

    // 10-second interval: tracking-status report
    this._ptmCounter = (this._ptmCounter || 0) + 1;
    
	if (this._ptmCounter % 10 === 0) {
		var oc = ps.messageGuiTextColor; // old colour
		ps.messageGuiTextColor = [0.545, 0, 0.545]; // new magenta
        var dt = this._ptmDistanceKm(t);
        var dm = this._ptmDistanceKm(player.ship);
        var mn = player.ship.displayName;
        if (ws.$ptmHasEQ) {
            pc(sn + " Tracking:", 9);
            pc("Target: " + tn + ", " + dt + " Lk", 9);
            pc("Mother: " + mn + ", " + dm + " Lk", 9);
            pc("Velocity " + s.speed.toFixed(1) + " Lm/s", 9);
        }
		ps.messageGuiTextColor = oc; // old colour
	}
    
	// 15-second interval: report within I-radius - then irradiate
    if (this._ptmCounter % 15 === 0) {
        dt = this._ptmDistanceKm(t);
        if ((ws.$ptmHasEQ) && (dt <= 13.0) && (!t.$irradiated)) {
			var oc = ps.messageGuiTextColor; // old colour
			ps.messageGuiTextColor = [0.545, 0, 0.545]; // magenta
			if (t !== ps) pc(sn + " within Irradiation range.", 9);
			// begin auto-irradiation and mark irradiated ships
			if (ws.$ptmAutoIrr) {
				// irradiate primary and 'collateral infractors'
				// (if Mother is not the target (ie. retrieval))
				if (t !== ps) {
					pc(sn + " irradiating...",9);
					this._ptmIrradiateAll();
				} else pc(sn+" within Retrieval range.",9);
				// 'courtesy boon' for primary (will total 2 cr)
				player.credits += 1.0;
				// add primary's boon to cumulative boons
				ws.$ptmRBounty += 1.0;
				// update F5F5 Mission screen display
				ws._updateBVRMNetworkDisplay();
			}
			ps.messageGuiTextColor = oc; // old color	
        } else if (t.$irradiated) pc(sn+" awaiting Target or Retrieval.",9);
    }
}   


// get a filtered array of offenders in scanner range
this._ptmOffendersInScanner = function() {
    // Use checkScanner for a fast scan
    var scanResults = this.ship.checkScanner(true); // 'true' for powered-only
    var offenders = [];
    
    for (var i = 0; i < scanResults.length; i++) {
        var entity = scanResults[i];
        // Filter for powered, non-cargo, non-ally offender ships
        if (entity.isShip && !entity.isCargo && (entity.bounty > 0)) {
            offenders.push(entity);
        }
    }
    return offenders;
};   


// Irradiate all offenders in scanner range
this._ptmIrradiateAll = function() {
	var infractors = this._ptmOffendersInScanner();
	
	if ((infractors) && (infractors.length>0)) {
		var ws = worldScripts['PT-BVRM Pteranodon'];
		if (!ws) return;
		var ps = player.ship;
		var pc = player.consoleMessage;
		var oc = ps.messageGuiTextColor; // old colour
		ps.messageGuiTextColor = [0.545, 0, 0.545]; // new magenta
		
		for (var i=0; i<infractors.length; i++) {
			// don't irradiate a victim more than once
			if (!infractors[i].$irradiated) { 
				this._ptmIrradiateOne(infractors[i]);
				pc(infractors[i].displayName,9);
				// Galcop pays the 'courtesy boon'
				player.credits += 1.0;
				// record cumulative boons
				ws.$ptmRKills ++;
				ws.$ptmRBounty += 1.0;
			}
		}	
		ps.messageGuiTextColor = oc; // old colour
	}
}


// Irradiate (cripple) a single ship
this._ptmIrradiateOne = function(victim) {
		// Don't irradiate an already irradiated ship
		if (!victim||victim.$irradiated||victim.$crippled||victim.$neutralized) return;
		// 50/50 chance of having lasers knocked out completely 
		//  (UNUSABLE due to known Oolite hard-code BUG.)
		//  (Here, REMOVING all lasers is the only practicable option.)
		//var fiftyfifty = (Math.random() < 0.5);
		//if (fiftyfifty) {
		/*	if (this._ptmRemoveLasers(victim)) 
				if (this.$log) this._log(victim.name+"'s Lasers removed."); */
		// 50/50 makes it a PEW Laser, simulating crippled weaponry
		/*} else {
			if (this._ptmGivePew(victim)) 
				if (this.$log) this._log(victim.name+" given PEW laser.");
		}*/
		// As the above removal methods are UNUSABLE due to Oolite BUG,
		//  PT's only recourse is to set all weapons' status to 'DAMAGED'...
		// *also* found UNUSABLE/INEFFECTUAL due to Oolite's core BUGGINESS!
		this._ptmCrippleShipWeapons(victim);
		// remove victim's missiles and mines to simulate disabling
		this._ptmRemoveMissilesMines(victim);
		// cripple speed and manoevrability
		victim.$crippled = this._ptmCripple(victim,0.25);
		// disable hyperdrive
		victim.hyperspaceSpinTime = -1;
		// disable fuel injector, if fitted, by removing it
		if (this._ptmShipHasEquipment(victim, "EQ_FUEL_INJECTION"))
			victim.removeEquipment("EQ_FUEL_INJECTION");
		// neutralize scan class
		victim.scanClass = "CLASS_NEUTRAL";   		
		// mark victim ship as Irradiated
		victim.$irradiated = true;
		// prefix display name with 'Irradiated '
		victim.displayName = "Irradiated "+victim.displayName;
		// flash 'Irradiated' scanner blip: original colour + grey 
		victim.scannerDisplayColor2 = "grayColor";
		// *TRULY* the only remaining no-weapons recourse: ALTER BEHAVIOUR...
		var ws = worldScripts["PT-BVRM Pteranodon"];
		if (!ws.$ptmWeaponsIntact)
			victim.setAI('PT-BVRM_CrippledOffenderAI.js');
}


// Reduce a ship to a desired level of performance
this._ptmCripple = function(other, level) {
	// reduce their speed, turn rate, acceleration
	other.maxSpeed = other.maxSpeed * level;
	other.maxThrust = other.maxThrust * level;
	other.maxPitch = other.maxPitch * level;
	other.maxRoll = other.maxRoll * level;
	other.maxYaw = other.maxYaw * level;
	// they are crippled
	return true;
}


// UNUSABLE: writing 'EQUIPMENT_DAMAGED' is _cosmetic_... another Oolite core BUG)
// Disable a target ship's weapons by setting them to EQUIPMENT_DAMAGED
this._ptmCrippleShipWeapons = function(targetShip) {
    // List of weapon equipment keys to target
    // (can add custom OXP weapon keys here as needed)
    var weaponKeys = [
        "EQ_WEAPON_PULSE_LASER",
        "EQ_WEAPON_BEAM_LASER",
        "EQ_WEAPON_MILITARY_LASER",
        "EQ_WEAPON_MINING_LASER"
    ];
   // Blindly attempt to damage each weapon type.
   // (if ship is without the weapon, setEquipmentStatus does nothing and throws no error.)
    for (var i = 0; i < weaponKeys.length; i++) {
        targetShip.setEquipmentStatus(weaponKeys[i], "EQUIPMENT_DAMAGED");
    }
}   
// Remove any and all lasers from NPC ship
// (UNUSABLE / INEFFECTUAL due to Oolite BUG)
this._ptmRemoveLasers = function(ship) {
    var laserKeys = [
        "EQ_WEAPON_PULSE_LASER",
		"EQ_WEAPON_BEAM_LASER", 
        "EQ_WEAPON_MINING_LASER", 
		"EQ_WEAPON_MILITARY_LASER"
    ];
    var lasersRemoved = false;
    
    for (var i = 0; i < laserKeys.length; i++) {
        var key = laserKeys[i];
        var eStatus = ship.equipmentStatus(key);
        
        if (eStatus === "EQUIPMENT_OK" || eStatus === "EQUIPMENT_DAMAGED") {
            ship.removeEquipment(key);
            lasersRemoved = true;
        }
    }
    if (lasersRemoved) {
        // Verify removal after a tiny delay (next tick) before logging
        // Or simply check status again immediately
        var stillArmed = false;
        for (var j = 0; j < laserKeys.length; j++) {
            if (ship.equipmentStatus(laserKeys[j]) !== undefined) {
                stillArmed = true; 
                break;
            }
        }
        if (!stillArmed) {
            if (this.$log) this._log(ship.name + " successfully disarmed.");
        } else {
            if (this.$log) this._log("WARNING: " + ship.name + " removal failed (Ghost equipment).");
        }
    }
    return lasersRemoved;
};   


   
// remove any and all missiles and mines from a NPC ship
this._ptmRemoveMissilesMines = function(ship) {
    var missileMineKeys = [
        "EQ_MISSILE", "EQ_HARDENED_MISSILE", "EQ_QC_MINE"
    ];
    var itemsRemoved = false;
    
    for (var i = 0; i < missileMineKeys.length; i++) {
        var key = missileMineKeys[i];
        // Check status once, then remove
        if (ship.equipmentStatus(key) === "EQUIPMENT_OK") {
            ship.removeEquipment(key);
            itemsRemoved = true;
        }
    }
    return itemsRemoved;
};   


// UNUSABLE due to a KNOWN OOLITE BUG (which effectively prevents the 
//  swapping or substitution of weapons in space.)
// Give victim a 'damaged' laser (EQ_WEAPON_PEW_LASER)
this._ptmGivePew = function(ship) {
    // 1. Remove standard lasers
    var otherWeapons = [
        "EQ_WEAPON_PULSE_LASER", "EQ_WEAPON_BEAM_LASER", 
        "EQ_WEAPON_MINING_LASER", "EQ_WEAPON_MILITARY_LASER"
    ];

    for (var i = 0; i < otherWeapons.length; i++) {
        var wKey = otherWeapons[i];
        // Remove repeatedly until status is undefined or unavailable
        for (var attempt = 0; attempt < 8; attempt++) {
            var eStatus = ship.equipmentStatus(wKey);
            if (eStatus === undefined) break;
            ship.removeEquipment(wKey);
        }
    }
    // 2. (failed) workaround: Force equipment dictionary refresh
    // Award and remove a dummy item to flush the "ghost" incompatibility memory
    ship.awardEquipment("EQ_FUEL"); 
    ship.removeEquipment("EQ_FUEL");

    // 3. Now try to award the PEW Laser
    if (ship.awardEquipment("EQ_WEAPON_PEW_LASER")) {
        if (this.$log) this._log(ship.name + " given PEW laser.");
        return true;
    } else {
        if (this.$log) this._log("ERROR: Could not award PEW laser to " + ship.name + " (Status: " + ship.equipmentStatus("EQ_WEAPON_PEW_LASER") + ")");
        return false;
    }
};   


// UNUSABLE due to a KNOWN OOLITE BUG (which effectively prevents the 
//  swapping or substitution of weapons in space.)
// Give victim a Pulse Laser as its only armament
this._ptmGivePulse = function(ship) {
    // already has a Pulse Laser?
    if (ship.equipmentStatus("EQ_WEAPON_PULSE_LASER") === "EQUIPMENT_OK") {
        return false;
    }
    // List of other lasers to remove
    var otherWeapons = [
        "EQ_WEAPON_BEAM_LASER",
        "EQ_WEAPON_MINING_LASER",
        "EQ_WEAPON_MILITARY_LASER"
    ];
    // Remove any other lasers
    for (var i = 0; i < otherWeapons.length; i++) {
        if (ship.equipmentStatus(otherWeapons[i]) === "EQUIPMENT_OK") {
            ship.removeEquipment(otherWeapons[i]);
        }
    }
    // Award the Pulse Laser
    ship.awardEquipment("EQ_WEAPON_PULSE_LASER");
    return true;
};   



this._ptmShipHasEquipment = function(ship, equip) {
    return (ship.equipmentStatus(equip) === "EQUIPMENT_OK");
};   

	
// distance from PT-BVRM to any ship, in Lave km
this._ptmDistanceKm = function(ship) {
    var distanceInOU = ship.position.distanceTo(this.ship.position);
    var distanceInKm = (distanceInOU / 1000);
    return distanceInKm.toFixed(2);
}; 


this.shipRemoved = function(suppressDeathEvent) {
    var s = this.ship, pc = player.commsMessage;
	var ws = worldScripts['PT-BVRM Pteranodon'];
	if (!ws) return;
	var recovered = ws.$ptmRetrieved;
    // BVRM was removed by player-script (e.g. ship.remove(true))
	if (suppressDeathEvent) {
		// successful recovery of BVRM - rewarded
		if (recovered) {
			// pay retrieval boon (handled in WS)
			//player.credits += 100.0;
			//ws.$ptmRBounty += 100.0;
			// display it
			ws._updateBVRMNetworkDisplay;
			s.commsMessage("Recovered.", player.ship);
			if (this.$log) this._log(s.displayName+" removed (recovered).");
			if (this._Offscan())
				player.consoleMessage(s.displayName+": recovered.", 5);	
		} else {
		// system removed us (not successfully recovered)
		// incurs Galcop negligence penalty (handled from WS)
		//player.credits -= 500.0;
		}
	} else {
        // Ship was destroyed in combat (normal death), final machine incoherence
		s.commsMessage(this._generateJumbledString(24), player.ship);
		if (s.$terminated) {
			if (this.$log) this._log(s.displayName+" removed (operator termination).");
		} else	
			if (this.$log) this._log(s.displayName+" removed (died).");
    }
	// cancel comms
	if (this._ptmTimer) {
        this._ptmTimer.stop();
        this._ptmTimer = 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._ptmTimer) {
        this._ptmTimer.stop();
        this._ptmTimer = null;
    }
	if (why !== "removed") {
		// charge player 500 cr for a destroyed BVRM
		player.credits -= 500.0;
		// keep a cumulative record
		var ws = worldScripts[""];
		if (ws && ws.$sddHasEQ) {
			ws.$ptmRTax += 500.0;
			ws.$ptmRLosses ++;
		}
	}
}


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



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;
}   


// are we 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 Lave km - call with (this.ship)
this._DistanceToMother = function(ship) {
    var distanceInMeters = ship.position.distanceTo(player.ship.position);
    return (distanceInMeters / 1000).toFixed(2);
};   
  
// distance to Target ship in Lave km - call with (this.ship)
this._DistanceToTarget = function(ship) {
    var distanceInMeters = ship.position.distanceTo(ship.target.position);
    return (distanceInMeters / 1000).toFixed(2);
};   
  

// this PT-BVRM's SDD-Network designation
this._generateBVRMName = function() {
    // Generates a 3-digit number (100-999)
	var randomNumber = Math.floor(Math.random() * 900) + 100; 
    return "PT-BVRM" + randomNumber;
}   


// '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);
}

Scripts/autodock_conditions.js
"use strict";

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

// pew_conditions.js - EQ_WEAPON_PEW_LASER

this.allowAwardEquipment = function(eqKey, ship, context) {
    // Block purchase attempts
    if (context === "purchase") return false;
    
    // Block new ship fitting (F3 F3 screen)
    if (context === "newShip") return false;

    // BLOCK random NPC assignment during ship setup
    // This prevents the game from giving it to random ships at spawn
    if (context === "npc") return false; 
    
    // ALLOW only PT-BVRM script to install it
    if (context === "scripted") return true;

    return true;
};   
Scripts/ptm-script.js
"use strict";
this.name = "PT-BVRM Pteranodon";
this.author = "Reval";
this.license = "CC-BY-NC-SA 4.0";
this.version = "1.4";
this.description = "Galcop's 'Pteranodon' PT-BVR missile tracks and engages prioritized targets from up to full-system distance. Actuates on proximity, firing neutral particles locally in rapid sequence. Effects are mine-like, irradiative, mostly non-fatal to crew. Ships proximal to target can be affected. ECM-immune. SDD-Network access essential for mounting and deployment.";

// OPTION: keep an irradiated ship's weapons intact (true='yes', false='no')
this.$ptmWeaponsIntact = false; // default: 'no'

/*
	Version 1.4
		OPTION: keep an irradiated ship's weapons intact (true='yes', false='no').
		True crippling ('no lasers') now achieved by switching the AI on irradiated vessels.
		Suppressed 'marginal energy' in the ship-crippling procedure.
		Low offender scanner colour changed to 'peach' (better contrast w/ other types).
	
	Version 1.3
		An Irradiated ship's scanner blip flashes original colour + grey. 
		High Offenders (often 'Fugitives') coloured magenta on scan.
		Low Offenders coloured orange on scan (peach v.1.4).
		In absence of data, ie. no irradiations or boons, show nothing on F5F5.
		Added shipDied w.s. event handler for final cleanup.
		In an attempt to facilitate 'damaged or disabled lasers?' logic,
		ran into the Oolite BUG (GitHub Issue #369) preventing effectual swapping in of equipment on-the-fly.
		Added _updateTargetInfo() + GETter HUD 1.6 timer integration, identical to SDD.
		Tightened checks for non-zero length of HighOffenders array.
		PT is now mode-of-death aware (killed, recovered, terminated, removed by system).
		Sundry additional checks and logs implemented.
		
	Version 1.2
		F5->[i] Order an area-irradiation, if PT is following Mother.
		F5->[d] BVRM 'destruct' button (if malfunction detected). Penalty 100 cr.
		F5->[m] call PT-BVRM back to Mother (for retrieval or escort).
	
	Version 1.1
		Prevent launch by an offender ship.
		Prevent launch while docked.
		Fixes, erasures, cleanups, checks, optimizations.
		
	Version 1.0
		PT-BVRM Operator Statistics on F5F5 Mission screen.
		Displayed statistics saved and loaded via missionVariables.
		Galcop 'retrieval boon' paid on successful recovery of PT-BVRM.
		Galcop 'couresy boon': 2 cr for primary, 1 cr each for collateral irradiations.
		Retrieval ([r] when in scan range) and 500 cr penalty for non-retrieval.
		PT-BVRM auto-irradiates multiple infractors including Primary, on proximity.
		PT-BVRM switches targets when operator cycles through them from the F5 screen.
		F5->[o] lists and re-sorts High Offenders, showing condition, bounty, distance.
		F5->[c] cycles long-range infractor targets.
		F5->[a] ARM launcher.
		F5->[l] LAUNCH PT-BVRM.
		F5->[r] RECOVER PT-BVRM.
		F5->[s] SAFE (disarm) Launcher.
		
	Version 0.0
		Proof of concept - functional, launchable, trackable, integrates with SDD.
*/

/*
	To Do
*/


// OPTION: are we logging ALL PT-BVRM activities?
this.$log = true;



// LAUNCH (spawn) the Pteranodon
this._ptmLaunchBVRM = function(num) {
	
	if (typeof num === "undefined") num = 1;       
	
	if ((this.$ptmHasEQ) && (this.$ptmHens<this.$ptmMaxHens)) {	
    	var ps = player.ship, pc = player.consoleMessage;		
		
		// spawn a new Ptera as a persistent WS array variable
        this.$ptmBVRM = system.addShips("[pt-missile]", num, ps.position);
		
		var ptera = this.$ptmBVRM; // abbreviate
		// Belt'n'braces check for Ptera presence and validity
		if (!ptera) {
			var psh = player.ship, pcm = player.consoleMessage;
			var ocolor = psh.messageGuiTextColor;
			psh.messageGuiTextColor = "redColor";
			pcm("PT-BVRM LAUNCH FAILURE:",9);
			pcm("Report to CSDDA soonest.",9);
			pcm("Until then, your status: Downgraded.",9);
			psh.messageGuiTextColor = ocolor;
			if (this.$log) this._log("PT-BVRM DEPLOYMENT FAILURE.");
			return;
		}
				
		// set custom property on player-spawned Ptera(s)
		for (var d = 0; d < ptera.length; d++)
			ptera[d].$spawnedByMother = true;
		
		// set scanner lollipop colour for player-spawned Pteras
		for (var d = 0; d < ptera.length; d++) {
			ptera[d].scannerDisplayColor1 = "blueColor";
			ptera[d].scannerDisplayColor2 = "magentaColor";
		}
		
		// prevent collisions between new Ptera, player, and other Pteras
		for (var d = 0; d < ptera.length; d++)
			this._collisionAvoidance(ptera[d]);
		
		// set the H.O. priority target
		if (ptera && (ptera.length > 0)) {
			for (var p=0; p<ptera.length; p++)
				ptera[p].target = this.$ptmTargetShip;
		}
        // update Ptera count
		this.$ptmHens += num;
		// apply Launch Tax w/ increment
		this._ptmLaunchTransactionDebits();
		// confirm BVRM launch
		pc("Launched PT-BVRM #"+this.$ptmHens+".", 9);
    
	// no more Pteras to deploy
	} else {
		if (this.$ptmHasEQ) pc("All "+this.$ptmHens+" PT-BVRMs are deployed.",9);
	}
}


this.shipSpawned = function(ship) {
	// This is a system-spawned NPC ship,
	// processed only if an offender.
		if  (this._ptmIsOffender(ship)) {
			var b = ship.bounty;
			var d = this._ptmDistanceKm(ship);
			var c = this._ptmCompassDirection(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.$ptmTargetShip;
				var newT = (oTarget !== ship);
				// add the new high offender...
				this._ptmAddHighOffender(ship);
				// clean up H.O. list
				this.$ptmHighOffenders = this.$ptmHighOffenders.filter(function(ship) {
					return ship && ship.isValid;
				});   
				var ps = player.ship;
				// Re-sort by distance to player (nearest first)
				this.$ptmHighOffenders.sort(function(a, b) {
					return a.position.distanceTo(ps) - b.position.distanceTo(ps);
				});
				//player.consoleMessage("High Offender "+n+" at "+d+ " Lk "+c,9);
				// Assign new top offender as target
				if (this.$ptmHighOffenders.length > 0) {
					var hf = this.$ptmHighOffenders[0];
					// change primary target if new
					if (hf.isValid && newT) {
						this.$ptmTargetShip = hf;
				}	
			}
		}
	}
};


// Establish whether ship has the PT-BVRM Launcher package
this.shipWillLaunchFromStation = function() {
	var ps = player.ship;
	this.$ptmHasEQ = (ps.equipmentStatus ("EQ_PT_BVRM_LAUNCHER") === "EQUIPMENT_OK");
	this._ptmReset();
}

 
// Establish BVRM complement by ship-type
this.shipLaunchedFromStation = function(station) {
    var pc = player.consoleMessage, ps = player.ship;
	if (this.$ptmHasEQ) {
		var oc = ps.messageGuiTextColor; // old colour
		ps.messageGuiTextColor = [0.545, 0, 0.545]; // new magenta
		pc("Initializing link to PT-BVRM...", 9);
		var d = this._ptmMaxDrones();
		this.$ptmMaxHens = (d > 0) ? d : 7;
		pc(this.$ptmMaxHens+" BVRM listening.", 9);
		pc("F5->[l] to launch BVRM",9);
		ps.messageGuiTextColor = oc; // old colour
		// make sure Launcher is primed (if HUD set up)
		ps.setPrimedEquipment("EQ_PT_BVRM_LAUNCHER", false);   	
    	// start realtime Target info update
		// Use a Timer to update offender ship's distance and direction
		// ONLY IF GETter HUD is loaded and SDD not.
		if ((worldScripts['GETter HUD']) && (!worldScripts['Defence Rider Drones'])) {
			this._updateTimer = new Timer(this, this._updateTargetInfo.bind(this), 0.5, 0.5);
			this._updateTargetInfo();
		}
	} else 
		pc("Install PT-BVRM launcher package in shipyard.", 9);
    // Reset Launch state
    this.$ptmBVRMLaunched = false;
};


this.shipDockedWithStation = function() {
	if (!this.$ptmHasEQ) return;
	// disarm the Launcher
	this.$ptmArmed = false;
	// reset retrieval flag
	this.$ptmRetrieved = false;
	// don't proceed if there's no PT-BVRM
	if ((!this.$ptmBVRM) || (this.$ptmBVRM[0]==undefined)) return;
	// penalty for non-retrieval of BVRM
	if ((this.$ptmBVRMLaunched) && (this.$ptmBVRM[0].isInSpace)) {
		var ps = player.ship, pc = player.consoleMessage;
		var oc = ps.messageGuiTextColor; // old colour
		ps.messageGuiTextColor = [0.545, 0, 0]; // dark red
		pc("PT-BVRM non-retrieval penalty: -500 cr",9);
		pc("Your Galcop status: Downgraded.",9);
		ps.messageGuiTextColor = oc; // old colour
		player.credits -= 500.0;
		this.$ptmRTax += 500.0
		player.ship.bounty = 5;
	}
}


this.shipWillDockWithStation = function(station) {
	// clear and reset H.O. list?
	//this.$ptmHighOffenders = [];
	this.$ptmTargetShip = null;
	// update F5F5 Mission data
	this._updateBVRMNetworkDisplay();
	// Stop the HUD target info timer, if set
    if (this._updateTimer) {
        // clear target tracker when docked
		player.ship.setCustomHUDDial("sddTargetInfo", "");
        this._updateTimer.stop();
        this._updateTimer = null;
    }
}


this.playerStartedJumpCountdown = function(type, seconds) {
	// disarm the Launcher
	this.$ptmArmed = false;
	// clear and reset H.O. list
	this.$ptmHighOffenders = [];
	this.$ptmTargetShip = null;
	
	if ((!this.$ptmHasEQ) || (!this.$ptmBVRM) || (this.$ptmBVRM[0]==undefined)) return; 
	
	// penalty for non-retrieval of BVRM
	if ((this.$ptmBVRMLaunched) && (this.$ptmBVRM[0].isInSpace)) {
		var ps = player.ship, pc = player.consoleMessage;
		var oc = ps.messageGuiTextColor; // old colour
		ps.messageGuiTextColor = [0.545, 0, 0]; // dark red
		pc("PT-BVRM non-retrieval penalty: -500 cr",9);
		pc("Your Galcop status: Downgraded.",9);
		ps.messageGuiTextColor = oc; // old colour
		player.credits -= 500;
		// penalties included under total taxes
		this.$ptmRTax += 500; 
		player.ship.bounty = 5;
	}
	// clean slate on H-jump
	this._ptmReset();
}


// retrieve BVRM on receipt of docking clearance
this.playerRequestedDockingClearance = function() {
	// unset tracker target on docking
	this.$ptmTargetShip = null;
	this._ptmReset();
}


this.playerBoughtEquipment = function(equipment, paid) {
	var pc = player.consoleMessage;
	if (equipment=="EQ_PT_BVRM_LAUNCHER") {		
		if (player.legalStatus=="Clean")
			pc("Licence issued: PT-BVRM Launchers installed.",9);
		else pc("Licence denied. No PT-BVRM Launchers installed.",9);
	} else
	// Licence termination, removal and refund
	if (equipment == "EQ_PT_BVRM_LAUNCHER_REM") {
		player.ship.removeEquipment("EQ_PT_BVRM_LAUNCHER");
		player.ship.removeEquipment("EQ_PT_BVRM_LAUNCHER_REM");
		pc("PT-BVRM launchers have been removed. Half refund.",9);
		player.credits += 2500;
	}
}


// put Mother's final affairs in order
this.shipDied = function(whom, why) { 	
	// Stop the HUD target info timer, if set
    if (this._updateTimer) {
        // clear target tracker
		player.ship.setCustomHUDDial("sddTargetInfo", "");
        this._updateTimer.stop();
        this._updateTimer = null;
    }
	// in case a BVRM is deployed...
	// remove it from system
	if ((this.$ptmBVRM!==undefined)&&(this.$ptmBVRM.length>0)) {
		this.$ptmBVRM[0].remove();
		this.$ptmBVRM = [];
		this.$ptmBVRMLaunched = false;
		this.$ptmArmed = false;
	}
}


// 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.$ptmBVRM.length; i++) {
        let other = this.$ptmBVRM[i];
        if (other && other.isValid && other !== ship) {
            ship.addCollisionException(other);
        }
    }
}


// recall and re-attach BVRMs (remove from system space)
this._ptmReset = function() {
	for (var i = 0; i < this.$ptmBVRM.length; i++)
		this.$ptmBVRM[i].remove(true);
	// Perform other housekeeping (clear the array, etc)
	this.$ptmBVRM.length = 0;
	this.$ptmHens = 0;
	this.$ptmCharges = 0;
}


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


this._ptmAddHighOffender = function(ship) {
    // clear the array of invalid ships
    this.$ptmHighOffenders = this.$ptmHighOffenders.filter(function(s) {
        return s && s.isValid;
    });
    // Add the new ship if valid and not already in the list
    if (ship && ship.isValid && this.$ptmHighOffenders.indexOf(ship) === -1) {
        this.$ptmHighOffenders.push(ship);
    }
	// Re-sort by distance to player (nearest first)
	var ps = player.ship;
	this.$ptmHighOffenders.sort(function(a, b) {
		return a.position.distanceTo(ps) - b.position.distanceTo(ps);
	});
};



// list the system's high offenders by distance via comm
this.$ptmListHighOffenders = function() {
	var ps = player.ship, pc = player.consoleMessage;
	var hof = this.$ptmHighOffenders;
	// 1. clean existing offender list
	hof = hof.filter(function(s) { return s && s.isValid; });    
    // offenders list is empty
	if (hof.length === 0) {
        pc("PT sensor detects no high offenders in system.", 5);
        return;
    }
	// 2. SORT the cleaned list by distance from the player's ship (closest first)
	hof.sort(function(a, b) {
		return ps.position.distanceTo(a.position) - ps.position.distanceTo(b.position);
	});   
    // 3. send message reporting high offenders in system
	// (recallable via log & updatable by pressing [o] on F5 screen)
	
	var oc = ps.messageGuiTextColor; // old colour
	ps.messageGuiTextColor = [0.545, 0, 0.545]; // new magenta
	pc("High Offenders:", 5);
    for (var i = 0; i < hof.length; i++) {
        var ship = hof[i];
		var b = ship.bounty;
		var d = this._ptmDistanceKm(ship);
		var c = this._ptmCompassDirection(ship);
		var n = ship.displayName;
		pc("["+i+"] "+n+" ("+b+") at "+d+ " Lk "+c,9);
    }
	ps.messageGuiTextColor = oc; // old colour 
};   


// Compass direction of the given ship, 'N' = top of scanner, 'S' = bottom.
this._ptmCompassDirection = 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._ptmDistanceKm = function(ship) {
    if ((!ship) || (!ship.isValid) || (ship==undefined)) return 1;
	var distanceInMeters = ship.position.distanceTo(player.ship.position);
    return (distanceInMeters / 1000).toFixed(2);
};   


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


// Galcop takes Launch Tax on every deployment
this._ptmLaunchTransactionDebits = function() {
	// Only deduct credits if the tax amount is a valid number
	if (!isNaN(this.$ptmDeployTax) && !isNaN(this.$ptmTaxLevel)) {
		this.$ptmDeployTax += this.$ptmTaxLevel; // +0.05 cr nominal
		// keep a record (total taxes)
		this.$ptmRTax += this.$ptmDeployTax;
		// refresh the F5F5 mission screen as the debit happens
		this._updateBVRMNetworkDisplay();   
		// debit operator's account for the tax
		player.credits -= this.$ptmDeployTax;
	} else {
		if (this.$log) this._log("Tax and debit transactions aborted (NaN).");
	}	
}


this.playerWillSaveGame = function(message) {
	var mv = missionVariables;
	mv.ptmRKills = this.$ptmRKills; // BVRM irradiations
	mv.ptmRBounty = this.$ptmRBounty; // BVRM 'courtesy boons'
	mv.ptmRLosses = this.$ptmRLosses; // count: BVRMs killed or collided
	mv.ptmRTax    = this.$ptmRTax;    // debit: BVRM deployment taxes
	// BVRM deployment taxation
	// current tax level, incremented per launch (nominally +0.05 cr)
	mv.ptmDeployTax= this.$ptmDeployTax; 
}


this.startUp = function() {
	this.$ptmDefaultMessageColor = 'cyanColor';
	this.$ptmConds = ["DOCKED","GREEN","YELLOW","RED"];
	this.$ptmTesting = false;
	this.$ptmHasEQ = false;
	this.$ptmMaxHens = 1; // arbitrary initialization
	this.$ptmHens = 0;
	this.$ptmAttackMsg = "SDD set condition RED.";
	this.$ptmAlertMsg = "RED alert. RED alert.";
	this.$ptmCharges = 0;
	// Register the Launcher keys for the F5 screen
    // a,s,o,r (v.1.92 +)
	if (oolite.compareVersion("1.92") <= 0) {
		setExtraGuiScreenKeys(this.name, {
			guiScreen: "GUI_SCREEN_STATUS",
			registerKeys: {
				"cycle-target": [{key: "c"}],
				"recall-pteras": [{key: "r"}],
				"list-infractors": [{key: "o"}],
				"arm-launcher": [{key: "a"}],
				"disarm-launcher": [{key: "s"}],
				"launch-ptera": [{key: "l"}],
				"call-ptera": [{key: "m"}],
				"destroy-ptera": [{key: "d"}],
				"irradiate-area": [{key: "i"}]
			},
			callback: this._ptmKeyHandler.bind(this)
		});
	}
	this.$ptmDeployTax = 25.0; // increases by <tax-level> with each 'drop'
	this.$ptmTaxLevel = 0.05; // per 'drop' increment for deployment tax
	// tallies to be passed from ship-scripts
	// and saved/loaded via missionVariables.
	this.$ptmRKills = 0; // BVRM irradiations
	this.$ptmRBounty= 0.0; // Galcop 'courtesy boons' (retrievals and irradiations)
	this.$ptmRTax   = 0.0; // total BVRM deployment taxes & penalties
	this.$ptmRLosses= 0; // count of BVRMs killed, lost or collided
	// create and declare missile array once here
	// (it will usually have a single element [0])
	this.$ptmBVRM = [];
	// create and declare high-offender shiplist
	this.$ptmHighOffenders = [];
	// index into H.O. array for target cycling
	this.$ptmIndex = 0;
	// Active target
	this.$ptmTargetShip = null; 
	// the PT-BVRM Launcher must be activated (armed)
	this.$ptmArmed = false;
	// preserved by S.S. before silent respawning
	this.$ptmPosition = null;
	this.$ptmVelocity = null;
	this.$ptmDesig = null;
	// PT-BVRM Irradiates without operator-intervention
	this.$ptmAutoIrr = true;
	// was this BVRM successfully retrieved by operator?
	this.$ptmRetrieved = false;

	// Load persistent incremented variables
	// -------------------------------------
	var mv = missionVariables;
    if (mv.ptmRKills !== undefined) this.$ptmRKills = mv.ptmRKills;  // BVRM irradiations
	if (mv.ptmRBounty!== undefined) this.$ptmRBounty= mv.ptmRBounty; // 'courtesy boons'
	if (mv.ptmRLosses!== undefined) this.$ptmRLosses= mv.ptmRLosses; // BVRM destroyed debit
    if (mv.ptmRTax   !== null) this.$ptmRTax   = mv.ptmRTax;    // BVRM deployment taxes
	// BVRM deployment taxation (floats)
	// if undefined or NaN, don't assign
    if (mv.ptmDeployTax!=null) this.$ptmDeployTax=mv.ptmDeployTax;
	
}


this._ptmKeyHandler = function(keyId) {
    if (keyId === "recall-pteras") {
        if (!this.$ptmBVRM) return;
		var dist = this._ptmDistanceKm(this.$ptmBVRM[0]);
		// Remove all PT-BVRMs from the system
		if ((this.$ptmHasEQ) && (this.$ptmBVRMLaunched))
			if (dist<=10.0) {
				// flag retrieval as successful
				this.$ptmRetrieved = true;
				// recover (remove) BVRM
				this._ptmReset();
				// pay retrieval boon
				player.credits += 100.0;
				// record for posterity
				this.$ptmRBounty += 100.0;
			} else player.consoleMessage("PT out of range. Not retrieved.",9);
        return true; // Consume the keypress
    }
    if (keyId === "call-ptera") {
        let pc = player.consoleMessage;
		// make Mother primary target
		this.$ptmTargetShip = player.ship;
		pc("PT-BVRM recalled. Target: Mother.",9);
        return true; // Consume the keypress
	}
    if (keyId === "destroy-ptera") {
        var pc = player.consoleMessage;
		// PT destruct, only if malfunction and exists
		if ((this.$ptmBVRM==undefined)||(this.$ptmBVRM.length==0)) return;
		if ((this.$ptmBVRM) && (this.$ptmBVRM[0].$malfunction)) {
			this.$ptmBVRM[0].$terminated = true;
			this.$ptmBVRM[0].remove();
			this.$ptmBVRM = [];
			this.$ptmBVRMLaunched = false;
			this.$ptmArmed = false;
			pc("PT-BVRM TERMINATED by Operator.",9);
			// Galcop destruction penalty
			player.credits -= 100.0;
			this.$ptmRTax += 100.0;
		} else pc("PT NOT DESTROYED. No error diagnosed.",9);
        return true; // Consume the keypress
	}
    if (keyId === "irradiate-area") {
        var pc = player.consoleMessage;
		// PT irradiates on operator's order, if following Mother
		if ((this.$ptmBVRM==undefined)||(this.$ptmBVRM.length==0)) return;
		if ((this.$ptmBVRM) && (this.$ptmBVRM[0].target==player.ship)) {
			this.$ptmBVRM[0].script._ptmIrradiateAll();
			pc("PT-BVRM IRRADIATE IMMEDIATE.",9);
		} else pc("Vessels NOT IRRADIATED. PT beyond range.",9);
        return true; // Consume the keypress
	}
    if (keyId === "list-infractors") {
        // list system's high-offenders
		var hof = this.$ptmHighOffenders;
		this.$ptmListHighOffenders();
        return true; // Consume the keypress
    }
    if (keyId === "arm-launcher") {
        let pc = player.consoleMessage;
		// arm the launcher
		pc("ARMED PT-BVRM Launcher.",9);
		this.$ptmArmed = true;
        return true; // Consume the keypress
    }
    if (keyId === "disarm-launcher") {
        let pc = player.consoleMessage;
		// disarm (safe) the Launcher
		pc("SAFED PT-BVRM Launcher.",9);
		this.$ptmArmed = false;
        return true; // Consume the keypress
    }
    if (keyId === "launch-ptera") {
        var pc = player.consoleMessage;
		// Launch a PT-BVRM (only in space & clean)
		if (player.ship.docked) {
			pc("PT cannot launch while docked.",9);
			return true;
		}
		if (player.ship.bounty>0) {
			pc("Launcher locked. Contact CSDDA.",9);
			return true;
		}
		if ((this.$ptmHasEQ) && (this.$ptmArmed)) {
			this._ptmLaunchBVRM(1);
			this.$ptmBVRMLaunched = true;
			this.$ptmArmed = false;
			pc("LAUNCHED PT-BVRM.",9);
		} else pc("PT NOT LAUNCHED. REQUIRES ARMING [a].",9);
        return true; // Consume the keypress
    }
    // Cycle primary Target
	var hof = this.$ptmHighOffenders;
    if ((hof) && (hof.length>0)) { 
		if (keyId === "cycle-target") {
			// Increment index and wrap around if it reaches the end
			this.$ptmIndex = (this.$ptmIndex + 1) % hof.length;
			var idx = this.$ptmIndex;
			// assign this target
			this.$ptmTargetShip = hof[idx];
			let ps = player.ship, pc = player.consoleMessage;
			let oc = ps.messageGuiTextColor; // old colour
			ps.messageGuiTextColor = [0.545, 0, 0.545]; // new magenta
			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._updateBVRMNetworkDisplay = function() {
    if (this.$ptmHasEQ) {
        var ps = player.ship, dname = ps.displayName, pn = player.name;
        // Check if any data exists
        var hasData = (this.$ptmRKills > 0) || (Number(this.$ptmRBounty) > 0.0);

        if (hasData) {
            // Show stats
            mission.setInstructions([
                "PT-BVRM (SDD-Net)",
                "Operator: " + pn + ", " + dname,
                "PT Irradiations:  " + (this.$ptmRKills || 0),
                "Taxes & Forfeits:  " + (Number(this.$ptmRTax) || 0.0).toFixed(2) + " cr",
                "BVRM Launch Tax:  " + (Number(this.$ptmDeployTax) || 0.0).toFixed(2) + " cr",
                "Galcop PT Boons:  " + (Number(this.$ptmRBounty) || 0.0).toFixed(2) + " cr"
            ], this.name);
        } else {
            // 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.$ptmHasEQ) {
		if (guiScreen === "GUI_SCREEN_MANIFEST") {
			this._updateBVRMNetworkDisplay();
		}
	}
};
  

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


// nearest High Offender realtime tracking onscreen (via GETter HUD's hud.plist)
this._updateTargetInfo = function() {
    if (!player.ship.isValid) return;
    if (this.$ptmTargetShip && this.$ptmTargetShip.isValid) {
        var ps = player.ship, ts = this.$ptmTargetShip;
		var distance = (ts.position.distanceTo(ps.position) / 1000).toFixed(2);
        var direction = this._ptmCompassDirection(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", "");
    }
};
  
Scripts/ptm_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("PT-BVRM Licensing disallowed due to legal status. You must have a clean record.",9);
        return false;
    }    
    // this allows other purchases
    return true;
}   
Scripts/ptm_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_PT_BVRM_LAUNCHER_REM") {
        return (player.ship.equipmentStatus("EQ_PT_BVRM_LAUNCHER") === "EQUIPMENT_OK");
    }
    return true;
};