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