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

Expansion Fast Target Selector

Content

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Primable equipment that allows quick targeting of other ships in scanner range. Primable equipment that allows quick targeting of other ships in scanner range.
Identifier oolite.oxp.phkb.FastTargetSelector oolite.oxp.phkb.FastTargetSelector
Title Fast Target Selector Fast Target Selector
Category Equipment Equipment
Author phkb phkb
Version 1.7 1.7
Tags equipment, scanner, targeting equipment, scanner, targeting
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Dependent Expansions
  • oolite.oxp.phkb.ModernStart:1.1
  • Information URL https://wiki.alioth.net/index.php/Fast_Target_Selector_OXP n/a
    Download URL https://wiki.alioth.net/img_auth.php/4/47/FastTargetSelector_1.7.oxz n/a
    License CC-BY-NC-SA 4.0 CC-BY-NC-SA 4.0
    File Size n/a
    Upload date 1749451329

    Relationships Diagram

    Documentation

    Also read http://wiki.alioth.net/index.php/Fast%20Target%20Selector

    readme.txt

    Fast Target Selector
    By Nick Rogers
    
    Overview
    ========
    This OXP aims to simplify the process of targeting ships. In the core game, a pilot must point their crosshairs at a ship in order to target it. However, at the edge of scanner range it is quite hard to move the reticule over the small dot of a ship in order to get a target lock. This seems counter-intuitive as the scanner can certainly identify the ship - why can't a lock be set without having to jiggle the crosshairs over the ship?
    
    This is where Fast Target Selector comes into play. Now you can do what NPC ships do - just cycle through all the available targets by priming the equipment and selecting "n". During red alert conditions when enemies are targeting you, the list of targets cycled through will be reduced to just those who are targeting you. 
    
    The Fast Target Selector is simple to install and use, costing a mere 10cr and available from all tech level 5 systems and above. The reason for the small cost is to make it a reasonable piece of equipment for new commanders to install even before they leave Lave. 
    
    Usage
    =====
    Ideally the Fast Target Selector will be set as the "Fast Defensive" equipment item, using the "0" key on the keyboard to switch to the next target. However, you can also prime the equipment using "Shift-N", and use the "n" key to go to the next target and "b" to go back to the previous target.
    
    When a ship or object is targeted it will be highlighted on the scanner with an alternating color, depending on the type of ship. 
    
    Ships are grouped together by type when you cycle through available targets (and you aren't in condition Red). The sorting priority is:
        Thargoids
        Fugitives
        Offenders
        "Neutral" ships (eg traders, non-aggressive ships, stations)
        Police
        Any others ships that aren't rocks or cargo
        Cargo containers
        Rocks (splinters, boulders, asteroids)
    
    If you find yourself in a situation where you are in Condition Red, but you need to target something other than enemy ships, prime the "FTS Mode Changer" equipment (this becomes available during flight), and press the "n" key to change the mode between "Standard target selection" (which is the default mode where only enemy ships are targeted in Condition Red) and "Include all targets" (which essentially ignores Condition Red and targets all entities in range).
    
    Note: Wormholes will not be targeted with this system. They must be targeted in the traditional way.
    Note: This equipment is incompatible with the "Targeter" and "Military Targeting System" equipment items. You will not be able to purchase this equipment with those items already installed.
    
    License
    =======
    This work is licensed under the Creative Commons Attribution-Noncommercial-Share Alike 4.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/
    
    Thanks for Thargoid for his "Target Autolock Plus" OXP, from which I borrowed the scanner highlighting code, and for his "sort by distance" calculation (although I can't remember where I sourced this from).
    Thanks to cag for his speed improvements and finding of bugs.
    
    Version History
    ===============
    1.7
    - Moved all text into descriptions.plist for easier localisation.
    
    1.6
    - Sorted targets by type.
    
    1.5
    - Bug fixes.
    - Code refactoring.
    
    1.4
    - Removed incompatible behaviour when "Target Autolock Plus" OXP is active.
    
    1.3
    - Removed hard-coded scanner range value.
    - Improved integration with EscortDesk, thanks to Norby and cag.
    
    1.2
    - Code optimisations with thanks to cag.
    
    1.1
    - Small tweak to the method of identifying hostile ships.
    
    1.0
    - Initial release.

    Equipment

    Name Visible Cost [deci-credits] Tech-Level
    Fast Target Selector yes 100 5+
    FTS Mode Changer no 100 5+
    Remove Fast Target Selector no 10 3+

    Ships

    This expansion declares no ships.

    Models

    This expansion declares no models.

    Scripts

    Path
    Scripts/fasttargetselector.js
    "use strict";
    this.name	     = "FastTargetSelector";
    this.author	     = "phkb";
    this.copyright   = "2016 phkb";
    this.description = "Allows quick targeting of ships or other entities visible on the scanner. Also highlights current target on the scanner.";
    this.license     = "CC BY-NC-SA 4.0";
    
    /* Borrows scanner highlight code from Thargoids Target Autolock OXP */
    /* Also borrows "sort by distance calc" from Timer's Targeter OXP */
    /* Bg thanks to cag for code optimisations */
    
    this._normalColorsDefault = {color1:"blueColor", color2:"yellowColor"};
    this._normalColorsStation = {color1:"blueColor", color2:"greenColor"};
    this._normalColorsPolice = {color1:"greenColor", color2:"0.5 0.0 1.0"};
    this._normalColorsRock = {color1:"blueColor", color2:"whiteColor"};
    this._normalColorsThargoid = {color1:"blueColor", color2:"redColor"};
    this._normalColorsMissile = {color1:"cyanColor", color2:"whiteColor"};
    this._normalColorsBuoy = {color1:"greenColor", color2:"whiteColor"};
    
    this._battleColorsDefault = {color1:"redColor", color2:"yellowColor"};
    this._battleColorsMissile = {color1:"redColor", color2:"cyanColor"};
    this._battleColorsPolice = {color1:"redColor", color2:"1.0 0.0 0.5"};
    
    this._npc = [];								// current scan data array
    this._index = -1;							// position of players selected target in array
    this._dataAge = 0;							// age of current scan data
    this._targetTimer = null;					// timer used to update the colors of the currently selected target, so it will stand out on the scanner
    this._trueCondition = 0;					// current alert condition of the player. Adjusted slightly so that red alert (3) is limited to when there are hostile ships
    this._oldCondition = 0;						// last state of the players alert condition
    this._oldTarget = null;						// reference to the players last selected target
    this._ignoreRoles = ["btd_dummy_entity"];	// ship roles that will be ignored (ie they won't be selected)
    //cag: increasing the length of _ignoreRoles requires chg's to $findNPCShips()
    this._active = false;						// flag to indicate the FTS is in operation
    this._scanMode = 0;							// mode 0 = normal scan operations, mode 1 = include all objects, even under condition red. Default to 0.
    this._removePrimableEquipment = false;		// flag to indicate whether the mode change primable equipment will be removed from the list. Default to false.
    this._trueValues = ["yes", "1", 1, "true", true];
    this._tapInstalled = false;
    this._ED = null;
    
    // configuration settings for use in Lib_Config
    this._ftsConfig = {
    	Name:this.name, 
    	Alias:expandDescription("[fts_config_alias]"), 
    	Display:expandDescription("[fts_config_display]"), 
    	Alive:"_ftsConfig", 
    	Bool:{
    		B0:{Name:"_removePrimableEquipment", Def:false, Desc:expandDescription("[fts_remove_changer]")},
    		Info:expandDescription("[fts_config_bool_info]")
    	},
    	EInt:{
    		E0:{Name:"_scanMode", Def:0, Desc:expandDescription("[fts_default_mode]").split("|"), Min:0, Max:1},
    		Info:expandDescription("[fts_config_eint_info]")
    	}
    };
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function() {
    	// register our settings, if Lib_Config is present
    	if (worldScripts.Lib_Config) worldScripts.Lib_Config._registerSet(this._ftsConfig);
    
    	if (missionVariables.FTS_RemoveModeChanger) this._removePrimableEquipment = (this._trueValues.indexOf(missionVariables.FTS_RemoveModeChanger) >= 0 ? true : false);
    	if (missionVariables.FTS_ScanMode) this._scanMode = parseInt(missionVariables.FTS_ScanMode);
    
    	if (worldScripts.targetAutolock) {
    		if (missionVariables.targetAutolock == "TRUE") this._tapInstalled = true;
    	}
    	if (worldScripts.escortdeck) {
    		this._ED = worldScripts.escortdeck;
    		this.$findNPCShips = this.$findNPCShips_withEscortDeck;
    	} else {
    		this.$findNPCShips = this.$findNPCShips_normal;
    	}
    	// make sure the mode change equipment isn't on board - we only push it into the equip list on launch
    	player.ship.removeEquipment("EQ_TARGETSELECTOR_MODECHANGER");
    	if (player.ship.equipmentStatus("EQ_TARGETSELECTOR") === "EQUIPMENT_OK") this._active = true;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillSaveGame = function() {
    	missionVariables.FTS_RemoveModeChanger = this._removePrimableEquipment;
    	missionVariables.FTS_ScanMode = this._scanMode;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipTargetAcquired = function(newTarget) {
    	if (this._active) this.$startTimer();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.alertConditionChanged = function(newCondition, oldCondition) {
    	if (this._active === false) return;
    	if (this._trueCondition !== newCondition && ((newCondition === 3 && player.alertHostiles === true) || newCondition < 3)) {
    		this._trueCondition = newCondition;
    		this.$startTimer();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipTargetLost = this.shipTargetDestroyed = function(target) {
    	if (this._active === false) return;
    	this.$resetOldTarget();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDockedWithStation = function(station) {
    	this.$resetOldTarget();
    	// remove the mode changer
    	player.ship.removeEquipment("EQ_TARGETSELECTOR_MODECHANGER");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipLaunchedFromStation = function(station) {
    	// make sure the mode changer is installed, but only if (a) its not disabled, and the fts is not broken
    	if (this._removePrimableEquipment === false && player.ship.equipmentStatus("EQ_TARGETSELECTOR") === "EQUIPMENT_OK") {
    		player.ship.awardEquipment("EQ_TARGETSELECTOR_MODECHANGER");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = this.shipWillEnterWitchspace = function() {
    	if (this._active === false) return;
    	this.$resetOldTarget();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerBoughtEquipment = function(equipmentKey) {
    	if (equipmentKey === "EQ_TARGETSELECTOR_REMOVAL") {
    		player.ship.removeEquipment(equipmentKey);
    		player.ship.removeEquipment("EQ_TARGETSELECTOR_MODECHANGER");
    		player.ship.removeEquipment("EQ_TARGETSELECTOR");
    		this._active = false;
    	}
    	if (equipmentKey === "EQ_TARGETSELECTOR") this._active = true;
    	// monitor for when TAP is installed so we don't conflict with it
    	if (equipmentKey === "EQ_TARGET_AUTOLOCK") this._tapInstalled = true;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.equipmentDamaged = function(equipmentKey) {
    	if (equipmentKey === "EQ_TARGETSELECTOR") this._active = false;
    	if (equipmentKey === "EQ_SCANNER_SHOW_MISSILE_TARGET") this._tapInstalled = false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.equipmentRepaired = function(equipmentKey) {
    	if (equipmentKey === "EQ_TARGETSELECTOR") this._active = true;
    	// if the fts gets repaired in flight, add back the mode changer (if we haven't disabled it)
    	if (player.ship.isInSpace && equipmentKey === "EQ_TARGETSELECTOR" && this._removePrimableEquipment === false) player.ship.awardEquipment("EQ_TARGETSELECTOR_MODECHANGER");
    	if (equipmentKey === "EQ_TARGET_AUTOLOCK") this._tapInstalled = true;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.alertConditionChanged = function(newCondition, oldCondition) {
    	// update our "trueCondition" variable
    	this._trueCondition = newCondition;
    	// if this new condition is "red alert", see if we're actually under attack
    	if (newCondition === 3) {
    		if (player.alertHostiles === false) {
    			// if not, it's really only yellow alert from this OXP's point of view
    			this._trueCondition = 2;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // "n" key
    this.activated = function () {
    	// next target
    	var w = worldScripts.FastTargetSelector;
    	w.$lookForTarget(1);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // "b" key
    this.mode = function() {
    	// previous target
    	var w = worldScripts.FastTargetSelector;
    	w.$lookForTarget(-1);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // starts the lockon timer, if it isn't already
    this.$startTimer = function() {
    	if (this._targetTimer && this._targetTimer.isRunning) return;
    	this._targetTimer = new Timer(this, this.$lockOn, 0.25, 0);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // reset the scanner colors of the previous target
    this.$resetOldTarget = function() {
    	if (this._targetTimer && this._targetTimer.isRunning) this._targetTimer.stop();
    	if (this._oldTarget) {
    		if (this._tapInstalled === false) {
    			this._oldTarget.scannerDisplayColor1 = null;
    			this._oldTarget.scannerDisplayColor2 = null;
    		}
    		this._oldTarget = null;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // updates the scanner colors of the current target to make it more visible
    this.$lockOn = function $lockOn() {
    	//this.$resetOldTarget();
    	var p = player.ship;
    
    	// let's not lock onto ourselves shall we?
    	if(p.target === p) p.target = null;
    
    	// do we have a new target
    	if (p.target) {
    		// no changes, so don't do anything
    		if (p.target === this._oldTarget && this._trueCondition === this._oldCondition) return;
    
    		this.$resetOldTarget();
    		
    		this._oldTarget = p.target;
    		this._oldCondition = this._trueCondition;
    		var battle = false;
    		if (player.alertCondition === 3 && player.alertHostiles === true) {
    			// only switch to battle colors on ships that are actually trying to kill us
    			if (this._oldTarget.isShip && this._oldTarget.target === p) battle = true;
    		}
    		var currentColors = (battle === false ? this._normalColorsDefault : this._battleColorsDefault);
    		if (this._oldTarget.isValid) {
    			if (this._oldTarget.isMissile || this._oldTarget.isMine || this._oldTarget.isWeapon) {
    				currentColors = (battle === false ? this._normalColorsMissile : this._battleColorsMissile);
    			} else if (this._oldTarget.isPolice && (this._oldTarget.isShip && this._oldTarget.hasRole("RSsatellite") === false)) {
    				currentColors = (battle === false ? this._normalColorsPolice : this._battleColorsPolice);
    			} else if (this._oldTarget.isThargoid) {
    				currentColors = this._normalColorsThargoid;
    			} else if (this._oldTarget.isStation) {
    				currentColors = this._normalColorsStation;
    			} else if (this._oldTarget.isRock || this._oldTarget.isDerelict || this._oldTarget.isCargo || (this._oldTarget.isShip && this._oldTarget.hasRole("RSsatellite") === true)) {
    				currentColors = this._normalColorsRock;
    			} else if (this._oldTarget.scanClass === "CLASS_BUOY") {
    				currentColors = this._normalColorsBuoy;
    			}
    		}
    		if (this._tapInstalled === false) {
    			p.target.scannerDisplayColor1 = currentColors.color1;
    			p.target.scannerDisplayColor2 = currentColors.color2;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$lookForTarget = function(direction) {
    	
    	var currNPC = null;
    	var ps = player.ship;
    	var valid = false;
    	
    	this.$refreshArray();
    
    	var npc = this._npc;
    	if (npc && npc.length > 0) {
    		var lc = 0;
    		do {
    			this._index += direction;					
    			// select the next/previous index value, based on the direction
    			if (this._index < 0) 						
    				// if we get to the beginning of the array, go back to the end
    				this._index = npc.length - 1;
    			else if (this._index > npc.length - 1) 
    				// if we get to the end of the array, go back to the beginning
    				this._index = 0;
    			currNPC = npc[this._index];
    			valid = currNPC && currNPC.isValid === true && currNPC.isJamming === false 
    				&& currNPC.isCloaked === false;
    			if (valid && ps.position.distanceTo(currNPC) < ps.scannerRange) {
    				// if the item is valid, make it the player's target
    				ps.target = currNPC;
    				break;
    			}
    			lc += 1;
    		} while ( valid && lc < npc.length ); 	
    		// if we didn't get a target, keep looping until we do
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$refreshArray = function() {
    	// get the array of ships
    	if ((global.clock.adjustedSeconds - this._dataAge) > 1) { // limit to once/sec
    		this._npc = this.$findNPCShips();
    		this._dataAge = global.clock.adjustedSeconds;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // gives every ship a sort order as it spawns
    this.shipSpawned = function(ship) {
    	ship.fts_sort = 5;
    	if (ship.scanClass == "CLASS_NEUTRAL" || ship.isStation) ship.fts_sort = 3;
    	if (ship.isThargoid) ship.fts_sort = 0;
    	if (ship.bounty > 0 && ship.fts_sort == 5) ship.fts_sort = 2;
    	if (ship.bounty >= 50 && ship.fts_sort == 5) ship.fts_sort = 1;
    	if (ship.isPolice) ship.fts_sort = 4;
    	if (ship.isCargo) ship.fts_sort = 6;
    	if (ship.scanClass == "CLASS_BUOY") ship.fts_sort = 7;
    	if (ship.isRock && !ship.isStation) ship.fts_sort = 8;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // find all npc ships in range
    this.$findNPCShips_normal = function() {
    	function _isNPC(entity)	{
    		//var wF = worldScripts.FastTargetSelector; // shouldn't actually need this, as context should always be "this"
    		if (!entity.isShip) return false;
    		if (entity.isPlayer) return false;
    		if (entity.displayName === "Wreckage") return false;
    		if (this._trueCondition === 3 && this._scanMode === 0) {
    			// during red alert, only target thargoids, missiles and enemy ships
    			if (entity.isThargoid === true && entity.isCargo === false) return true;
    			if (entity.target === player.ship && entity.hasHostileTarget) return true;
    			if (entity.isMine) return true;
    			if (entity.isWeapon) return true;
    			return false;
    		} 
    		if (entity.isVisualEffect) return false ;
    		if (this._ignoreRoles.length === 1) {
    			if (entity.primaryRole === this._ignoreRoles[0]) return false;
    		} else {
    			// only do indexof if there is more than one entry in the ignoreroles list
    			if (this._ignoreRoles.indexOf(entity.primaryRole) === -1) return false;
    		}
    		return true;
    	}
    	return this.$sortList(system.filteredEntities(this, _isNPC, player.ship, player.ship.scannerRange));
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // find all npc ships in range
    this.$findNPCShips_withEscortDeck = function() {
    	function _isNPC(entity)	{
    		//var wF = worldScripts.FastTargetSelector; // shouldn't actually need this, as context should always be "this"
    		if (!entity.isShip) return false;
    		if (entity.isPlayer) return false;
    		if (entity.displayName === "Wreckage") return false;
    		if (this._trueCondition === 3 && this._scanMode === 0) {
    			// during red alert, only target thargoids, missiles and enemy ships
    			if (entity.isThargoid === true && entity.isCargo === false) return true;
    			if (entity.target === player.ship && entity.hasHostileTarget) return true;
    			if (entity.isMine) return true;
    			if (entity.isWeapon) return true;
    			return false;
    		} 
    		if (entity.isVisualEffect) return false ;
    		// thanks to Norby for this escortdeck exclusion code
    		var i = this._ED.$EscortDeckShip.indexOf(entity);
    		if (i >= 0 && this._ED.$EscortDeckShipPos[i]) //entity is on deck
    			return false; //so exclude this entity from target list
    		if (this._ignoreRoles.length === 1) {
    			if (entity.primaryRole === this._ignoreRoles[0]) return false;
    		} else {
    			// only do indexof if there is more than one entry in the ignoreroles list
    			if (this._ignoreRoles.indexOf(entity.primaryRole) === -1) return false;
    		}
    		return true;
    	}
    	return this.$sortList(system.filteredEntities(this, _isNPC, player.ship, player.ship.scannerRange));
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$sortList = function(targets) {
    	return targets.sort( function sortByID(a,b) {
    		return (a.fts_sort < b.fts_sort) ? -1 : (a.fts_sort > b.fts_sort) ? 1 : 0;
    	});
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // removes any wreckage items from the array, then calls the sorting algorithm
    /*this.$sortArray = function() {
    	// remove any wreckage items
    	for (var i = this._npc.length - 1; i >= 0; i--) {
    		if (this._npc[i].displayName === "Wreckage") this._npc.splice(i, 1);
    	}
    	// then sort
    	this.$sortByDistance(this._npc);
    }*/
    
    //-------------------------------------------------------------------------------------------------------------
    // sort by distance function courtesy of Thargoid
    /*this.$sortByDistance = function(targets) {
    	var cache = {};
    	// sort
    	targets.sort( function sortByDist(a,b) {
    		// cache distance
    		var ai = a.entityPersonality, bi = b.entityPersonality; // use some "uniq" ident
    		cache[ai] = cache[ai] || (a.isValid && a.position.distanceTo(player.ship.position));
    		cache[bi] = cache[bi] || (b.isValid && b.position.distanceTo(player.ship.position));
    		return ((cache[ai] < cache[bi]) ? -1 : ((cache[ai] > cache[bi]) ? 1 : 0));
    	});
    
    	// turn off the flag that indicates the data isn't sorted
    	this._sortRequired = false;
    }
    */
    Scripts/fasttargetselector_conditions.js
    "use strict";
    this.name	     = "FastTargetSelector_Conditions";
    this.author	     = "phkb";
    this.copyright   = "2016 phkb";
    this.description = "Condition script for equipment";
    this.licence     = "CC BY-NC-SA 4.0";
    
    //-------------------------------------------------------------------------------------------------------------
    this.allowAwardEquipment = function(equipment, ship, context) {
    	if (equipment === "EQ_TARGETSELECTOR_MODECHANGER" && context != "scripted") return false;
    	return true;
    }
    Scripts/fasttargetselector_modechanger.js
    "use strict";
    this.name	     = "FastTargetSelector_ModeChanger";
    this.author	     = "phkb";
    this.copyright   = "2016 phkb";
    this.description = "Changes the way the FTS works in Condition Red.";
    this.licence     = "CC BY-NC-SA 4.0";
    
    //-------------------------------------------------------------------------------------------------------------
    this.activated = function() {
    	var w = worldScripts.FastTargetSelector;
    	if (w) {
    		if (player.ship.equipmentStatus("EQ_TARGETSELECTOR") != "EQUIPMENT_OK") {
    			player.consoleMessage(expandDescription("[fts_damaged]"));
    			return;
    		}
    		w._scanMode += 1;
    		if (w._scanMode >= 2) w._scanMode = 0;
    		player.consoleMessage(expandDescription("[fts_scan_mode_" + w._scanMode + "]"));
    	}
    }