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

Expansion Tractor-Tow

Content

Warnings

  1. http://wiki.alioth.net/index.php/Tractor-Tow -> 404 Not Found
  2. Low hanging fuit: Information URL exists...

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Galcop's autonomous AI tractor beam package will lock onto and drag with it one or more neutralized hulks to be shepherded toward Station for tug operations, then refitted or broken up. A generous percentage is granted to Licensed Contractors on salvage. Galcop's autonomous AI tractor beam package will lock onto and drag with it one or more neutralized hulks to be shepherded toward Station for tug operations, then refitted or broken up. A generous percentage is granted to Licensed Contractors on salvage.
Identifier oolite.oxp.Reval.Tractor-Tow oolite.oxp.Reval.Tractor-Tow
Title Tractor-Tow Tractor-Tow
Category Equipment Equipment
Author Reval Reval
Version 1.1 1.1
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
  • oolite.oxp.Reval.Neutralizer:0
  • oolite.oxp.Reval.Neutralizer:0
  • Optional Expansions
    Conflict Expansions
  • oolite.oxp.Norby.Towbar:0
  • oolite.oxp.Norby.Towbar:0
  • Information URL https://wiki.alioth.net/index.php/Tractor-Tow_OXZ n/a
    Download URL https://wiki.alioth.net/img_auth.php/5/58/Tractor-Tow.oxz https://wiki.alioth.net/img_auth.php/5/58/Tractor-Tow.oxz
    License CC-BY-NC-SA 4.0 CC-BY-NC-SA 4.0
    File Size n/a
    Upload date 1781166177

    Relationships Diagram

    Documentation

    Equipment

    Name Visible Cost [deci-credits] Tech-Level
    Tractor Tow AI Package yes 8000 5+
    Remove Tractor-Tow Package yes 0 1+

    Ships

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

    Models

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

    Scripts

    Path
    Scripts/tt-script.js
    "use strict";
    this.name = "Tractor-Tow";
    this.author = "Reval";
    this.license = "CC-BY-NC-SA 4.0";
    this.version = "1.1";
    this.description = "Galcop's autonomous AI tractor beam package will lock onto and drag with it one or more neutralized hulks to be shepherded toward Station for tug operations, then refitted or broken up. A generous percentage is granted to Licensed Contractors on salvage.";
    
    /*
    	Version 1.1
    		Individual hulks can lose Tractor Lock by drifting too far.
    		On losing Tractor Lock, hulks drift again until re-acquired.
    		Cargo bonus (market value of any cargo the hulk still carries).
    		Tractor beam loses Lock if hulk's distance exceeds 5 km.
    		Tractor beam loses Lock if Torus or Injectors are engaged.
    		Hulk-spawn routines added for easier testing.
    		Multiple var optimizations, cleanups, reporting & logging additions.
    	
    	Version 1.0.2
    		Removed extraneous line from equipment.plist
    		Removed extraneous condition from tt_equipment_conditions.js
    
    	Version 1.0.1
    		NPB Neutralizer marked as a dependency in manifest.plist
    */
    
    /*
    	To Do:
    */
    
    
    // the aft-mounted Tractor Tow beam is _technically_ a weapon:
    //  it needs to be fired at the hulk to obtain a graviton-LOCK.
    this.shipAttackedOther = function(other) {
    	// don't proceed if TT is not installed
    	if (!this.$ttHasEQ) return;
        // proceed with lock-on if 'weapon' is Tractor
    	if (other.isValid && other.isShip) {
            if (player.ship.currentWeapon.equipmentKey == "EQ_WEAPON_TRACTOR_TOW") {
    			// only works on NPB-Neutralized ships
    			if (other.$neutralized && !other.$isTowed) {
                    this._ttInitiateTow(other);
                }
            }
        }
    };
    
    
    // Graviton beam is locked on. Prepare hulk for tractoring...
    this._ttInitiateTow = function(target) {
        // CAPTURE SCOPE: for accessing 'this' inside Timer callbacks
        var script = this;
        var ps = player.ship, pc = player.consoleMessage;
        var t = target, tn = target.name;
        
        if (this.$log) this._log("Tractor Beam: Locking onto " + tn);
        
        // set hulk's scanner blip to reflect its 'towed' status
        t.scannerDisplayColor1 = "grayColor";
        t.scannerDisplayColor2 = "blueColor";
        
        // set custom name property to indicate towed status
        t.displayName = 'Towed Neutralized ' + tn;
        
        // Lazy init array for towed ships
        if (!ps.$ttTowedShips) {
            ps.$ttTowedShips = [];
        }
        
        // Add hulk to array & flag it as 'towed'
        ps.$ttTowedShips.push(target);
        t.$isTowed = true;
        
        // render exhaust plumes invisible
        t.exhaustEmissiveColor = { r: 0, g: 0, b: 0, a: 0 };      
    
        // lights off
        t.lightsActive = false;   
        
        // make sure it is harmless
        t.bounty = 0;
        
        // restore partial manoevrability so hulk can be 'towed'
        this._ttReEnable(t);
        
        // Swap AI
        t.setAI("tow-target-AI.js"); 
       
        pc(tn+" locked. Tractoring " + ps.$ttTowedShips.length + " ship(s).", 5);
    
        // Calculate and report likely Fee and Bonus
        if (t && t.isValid) {
            // Main Station, for reporting cargo bonus
            var main = system.mainStation;
            var totalSalvage, totalCargo;
            // Calculate Salvage Fee
            totalSalvage = this._ttSalvageFee(t);
            // Calculate Cargo Bonus
            totalCargo = this._ttCargoBonus(t, main);
            // REPORT & LOG likely payments for this hulk
            pc(tn+' Salvage: '+totalSalvage.toFixed(2)+' cr.', 9);
            pc(tn+' Cargo: '+totalCargo.toFixed(2)+' cr.', 9);
            if (this.$log) this._log(tn+' Salvage: '+totalSalvage.toFixed(2)+' cr.');
            if (this.$log) this._log(tn+' Cargo: '+totalCargo.toFixed(2)+' cr.');
        }
        
        /* S P E E D  &  D I S T A N C E   M O N I T O R  -- T I M E R -- */
        
        // Stop existing timer, if running, before creating a new one
        if (this.$distanceTimer && this.$distanceTimer.isRunning) {
            this.$distanceTimer.stop();
        }
    
        this.$distanceTimer = new Timer(this, function() {
            var towedArray = ps.$ttTowedShips;
            if (towedArray && towedArray.length > 0) {
                // Check ALL towed ships for distance, not just the first
                // Iterate BACKWARDS to safely remove items with splice without skipping indices
                for (var i = towedArray.length - 1; i >= 0; i--) {
                    var currentTowed = towedArray[i];
                    if (currentTowed && currentTowed.isValid) {
                        // Calculate distance in meters
                        var distance = ps.position.distanceTo(currentTowed.position);
    
                        // 3.5 km = 3500 meters, warning; 7 km is Tractor cutoff.
                        if ((distance > 3500) && (distance < 6000)) {
                            pc("Tractor Lock Warning: REDUCE SPEED.", 5);
                        }
                        if (distance >= 6000) {
                            // Remove only this one ship from the array
                            towedArray.splice(i, 1);
                            // Set only this one ship adrift
                            script._ttUnTow(currentTowed);
                            pc("TOWED HULK LOST ("+currentTowed.name+"): Too far.", 9);
                            
                            // Check if array is now empty
                            if (towedArray.length === 0) {
                                pc("TRACTOR LOCKS LOST. Distance exceeded.", 9);
                                // Stop the timers as the tow is broken
                                script.$torusInjectorTimer.stop();
                                script.$distanceTimer.stop();
                                return; 
                            }
                            // Continue loop to check other ships (don't return yet)
                        }
                    }   
                }
            }   
        }, 5, 5); // Check every 5 seconds
    
        /* T O R U S  &  I N J E C T O R  M O N I T O R  -- T I M E R -- */
        
        // Stop existing timer, if running, before creating a new one
        if (this.$torusInjectorTimer && this.$torusInjectorTimer.isRunning) {
            this.$torusInjectorTimer.stop();
        }
    
        this.$torusInjectorTimer = new Timer(this, function() {
            if (ps.torusEngaged || ps.injectorsEngaged) {
                var towedArray = ps.$ttTowedShips;
                if (towedArray && towedArray.length > 0) {
                    // Break lock for ALL ships on overspeed
                    for (var i = towedArray.length - 1; i >= 0; i--) {
                        if (towedArray[i].isValid) {
                            script._ttUnTow(towedArray[i]);
                        }
                    }
                    ps.$ttTowedShips = [];
                    pc("TRACTOR LOCK LOST. Overspeed: Beam broken.", 9);
                }
                // Stop the timers as the tow is broken
                script.$torusInjectorTimer.stop();
                script.$distanceTimer.stop();
            }
        }, 1, 1); // Check every second
    };   
    
       
    // Set hulk adrift in a neutralized state
    this._ttUnTow = function(hulk) {
    	// unset 'towed' status
    	hulk.$isTowed = false;
    	// set them adrift (they can be re-acquired)
    	hulk.setAI('dumbAI.plist');
    	// they resume their 'neutralized' status
    	hulk.scannerDisplayColor1 = "grayColor";
    	hulk.scannerDisplayColor2 = "whiteColor";
    	// lights off
    	hulk.lightsActive = false;   
    	// return display name to 'Neutralized' + ship name
    	hulk.displayName = 'Neutralized '+hulk.name;
    	// reconfirm flag for hulk's neutralized state
    	hulk.$neutralized = true;
    }
    
    	
    // Restore neutralized hulk so it can be 'towed' (ie. follow player)
    this._ttReEnable = function(other) {
    	var ps = player.ship;
    	// partially re-instate their speed, turn rate, acceleration
    	other.maxSpeed = ps.maxSpeed*0.25;
    	other.maxThrust = ps.maxThrust*0.25;
    	other.maxPitch = ps.maxPitch*0.25;
    	other.maxRoll = ps.maxRoll*0.25;
    	other.maxYaw = ps.maxYaw*0.25;
    	// they are 'restored'
    	return true;
    }
    
    
    // Unlock towed hulks for tug operations in vicinity of Station
    this.shipEnteredStationAegis = function(station) {
        // no actions if TT is not installed
    	if (!this.$ttHasEQ) return;
    	// stop distance-timer, if running
    	if (this.$distanceTimer && this.$distanceTimer.isRunning)
            this.$distanceTimer.stop();
    	// stop torus/injector timer, if running	
    	if (this.$torusInjectorTimer && this.$torusInjectorTimer.isRunning) this.$torusInjectorTimer.stop();
    	// check that something is being towed
        var towedArray = player.ship.$ttTowedShips;
        if (towedArray && towedArray.length > 0) {
            for (var i = 0; i < towedArray.length; i++) {
                // unlock each hulk's tractor link
    			if (towedArray[i] && towedArray[i].isValid) {
                    towedArray[i].setAI("dumbAI.plist"); // Release them
                }
            }
            player.consoleMessage("Tractored hulks released for tug operations.", 9);
        }
    };
    
    
    // Handle Docking, Payment, and Cleanup
    this.shipDockedWithStation = function(station) {
        // no actions if TT is not installed
    	if (!this.$ttHasEQ) return;
    	
    	// stop distance-timer, if running
    	if (this.$distanceTimer && this.$distanceTimer.isRunning) {
            this.$distanceTimer.stop();
        }
    	// stop torus/injector timer, if running	
    	if (this.$torusInjectorTimer && this.$torusInjectorTimer.isRunning) this.$torusInjectorTimer.stop();
    	
    	var ps = player.ship, pc = player.consoleMessage;
    	
    	var towedArray = ps.$ttTowedShips;
        
    	// Tally total salvage fee + cargo bonus for every hulk towed
        if (towedArray && towedArray.length > 0) {
            var totalSalvage = 0, totalCargo = 0;
            var count = 0;
            
            for (var i = 0; i < towedArray.length; i++) {
                var ship = towedArray[i];
                // Verify ship still exists
                if (ship && ship.isValid) {
                    // Calculate Salvage Fee
                    totalSalvage += this._ttSalvageFee(ship);
    				// Calculate Cargo Bonus
    				totalCargo += this._ttCargoBonus(ship, station);
                    // relinquish hulk to its ultimate fate
    				ship.remove();
                    count++;
                }
            }
            // Pay and announce payment
            if (count > 0) {
                player.credits += (totalSalvage + totalCargo);
    			var hulk = 'hulk';
    			if (count>1) hulk = 'hulks';
                // Report and log salvage fee
    			pc("Salvage complete: " + count + " " + hulk + ". Cr " + totalSalvage.toFixed(2) + " paid.", 9);
    			if (this.$log) this._log("Salvage complete: " + count + " " + hulk + ". Cr " + totalSalvage.toFixed(2) + " paid.");
    			// Report and log cargo bonus
    			pc("Cargo Bonus for " + count + " " + hulk + ". Cr " + totalCargo.toFixed(2) + " paid.", 9);
    			if (this.$log) this._log("Cargo Bonus for " + count + " " + hulk + ". Cr " + totalCargo.toFixed(2) + " paid.");
            }
            
            // Clear Array
            ps.$ttTowedShips = [];
        }
    };
    
    
    // Compute salvage-percentage Fee granted to Licensee per vessel towed
    this._ttSalvageFee = function(towed) {
        var size = towed.boundingBox;
        var cargo = towed.cargoSpaceCapacity || 0;
        // maxSpeed has been tampered with, so need original speed
    	var shipData = Ship.shipDataForKey(towed.dataKey);
    	var speed = Number(shipData.max_flight_speed) || 0;
    	// get rationalized full market price
        var price = this._ttRationalizedPrice(size, cargo, speed);
    	// halve it
    	price = price / 2;
    	// grant Contractor 10% of this
    	return price * 0.1;
    }
    
    
    // Calculate a rationalized price for any ship, using cobra3 as a baseline
    this._ttRationalizedPrice = function(shipSize, cargoCapacity, shipMaxSpeed) {
    	const COBRA_SIZE = { x: 130, y: 30, z: 65 };   
        const COBRA_CARGO = 20;
        const COBRA_MAX_SPEED = 350;
        const COBRA_PRICE = 150000;
    
        const shipVolume = shipSize.x * shipSize.y * shipSize.z;
        const cobraVolume = COBRA_SIZE.x * COBRA_SIZE.y * COBRA_SIZE.z;
    
        const volumeMultiplier = shipVolume / cobraVolume;
        const cargoMultiplier = cargoCapacity / COBRA_CARGO;
        const speedMultiplier = shipMaxSpeed / COBRA_MAX_SPEED;
    
        // Current weighting: 25% volume, 30% cargo, 45% speed
        const combinedMultiplier = (volumeMultiplier * 0.25) + (cargoMultiplier * 0.3) + (speedMultiplier * 0.45);
    
        return Math.round(COBRA_PRICE * combinedMultiplier);
    }
    
    
    // Total Cargo Bonus for hulk at this station's market prices
    this._ttCargoBonus = function(ship, sta) {
    	var main = sta.name;
    	var cbonus = 0.0;
    	var holds = "";
    	for(var y=0; y<ship.cargoList.length; y++) {
    		var sc = ship.cargoList[y];
    		cbonus += this._ttMarketPriceByName(main, sc.commodity, sc.quantity);
    		holds += sc.displayName+"("+sc.quantity+") ";
    	}
    	if (this.$log) this._log(ship.name+" cargo: "+ holds);
    	
    	return cbonus;
    }
    
    
    // price of a good at a given station, in cr
    this._ttMarketPrice = function(sta, good) {
    	var price=0.0, mar=sta.market;
    	price = mar[good].price/10;
    	return price.toFixed(1);
    }
    
    
    // price of <quantity> <named> good, ie. "furs", at a <named> station, eg. "Coriolis Station"
    this._ttMarketPriceByName = function(namesta, namegood, quant) {
    	var price=0, s=system.stations;
    		
    	for (var i=0; i<s.length; i++)  {
    		if (s[i].name === namesta) { 
    			price = this._ttMarketPrice(s[i], namegood);
    			break;
    		}
    	}
    	return price*quant;
    }	
    
    
    // One-time verification of equipment presence
    this.shipWillLaunchFromStation = function() {
        this.$ttHasEQ = (player.ship.equipmentStatus("EQ_WEAPON_TRACTOR_TOW") === "EQUIPMENT_OK");
    };
    
    
    this.playerBoughtEquipment = function(equipment, paid) {
        var pc = player.consoleMessage;
        if (equipment == "EQ_WEAPON_TRACTOR_TOW") {
            this.$ttHasEQ = true;
            pc("Tractor-Tow package mounted.", 9);
        } else if (equipment == "EQ_WEAPON_TRACTOR_TOW_REM") {
            player.ship.removeEquipment("EQ_WEAPON_TRACTOR_TOW");
            player.ship.removeEquipment("EQ_WEAPON_TRACTOR_TOW_REM");
            this.$ttHasEQ = false;
            pc("Tractor-Tow package removed.", 9);
        }
    };
    
    
    this.startUp = function() {
        this.$ttHasEQ = false;
        this.$log = true;
    	this.$ttTest = true;
    	// Define [n] key: spawn neutralized test hulks
    	if (this.$ttTest)
    		// Support 1.91 and newer (recommended)
    		if (oolite.compareVersion("1.91") <= 0) {
    			setExtraGuiScreenKeys(this.name, {
    				guiScreen: "GUI_SCREEN_STATUS",
    				registerKeys: {
    					"spawn-hulks": [{key: "n"}]
    				},
    				callback: this._ttKeyHandler.bind(this)
    			});
    		}
    };
    
    
    this._ttKeyHandler = function(keyId) {
    	if (keyId === "spawn-hulks") {
            // Call hulk-spawner test function
    		this._ttSpawnHulks();
            return true; // Consume the keypress
        }
        return false;
    }
    
     
    // Spawn some dollies to test Tractor-Tow operations
    this._ttSpawnHulks = function() {
        if (this.$ttTest) {
            var ps = player.ship, pc = player.consoleMessage;
            // spawn assorted trader ships
            var dummy = system.addShips("trader", 4, ps.position);
    		// check if spawn succeeded
    		if (dummy && dummy.length > 0) {
    			for (var i = 0; i < dummy.length; i++) {
    				// Force Dumb AI immediately
                    dummy[i].setAI('dumbAI.plist');
    				// Give them some cargo
    				dummy[i].likelyCargo = dummy[i].cargoSpaceCapacity - 1;
    				dummy[i].setCargoType("SCARCE_GOODS");
    				// Neutralize them
    				this._ttUnTow(dummy[i]);
    			}
    			pc("Spawned " + dummy.length + " test hulks.", 5);
    			// remove any escorts spawned with the traders
    			var nh = this._ttNonHulksInScanner();
    			for (var n = 0; n < nh.length; n++) nh[n].remove();
    		} else {
    			pc("SPAWN FAILED: No ships created.", 5);
    			if (this.$log) this._log("Spawn failed. Return value: " + dummy);
    		}
    	}
    };
    
    
    // get a filtered array of non-hulks in scanner range
    this._ttNonHulksInScanner = function() {
        // Use checkScanner on the player's ship
        var scanResults = player.ship.checkScanner(true); 
        var nonHulks = [];
        
        for (var i = 0; i < scanResults.length; i++) {
            var entity = scanResults[i];
            // Filter for ships that are not neutralized hulks
            if (entity.isShip && !entity.isCargo && !entity.$neutralized) {
                nonHulks.push(entity);
            }
        }
        return nonHulks;
    };
    
    
    this._log = function(msg) {
        log(this.name + ".debug", msg);
    }; 
    
    Scripts/tt_equipment_conditions.js
    "use strict";
    
    this.condition = function(context, equipment) {
        // Only check on purchase or installation
        if (context == "purchase") {
            // Check player legal status
            if (player.ship.legalStatus != "clean") {
                // Optional: Display a message
                if (context == "purchase") {
                    player.consoleMessage("Galcop Restriction: Tractor Tow requires a clean legal record.", 5);
                }
                return false; // Deny purchase/install
            }
        }
        return true; // Allow
    };