Back to Index Page generated: Dec 20, 2024, 7:22:09 AM

Expansion Fighters

Content

Warnings

  1. No version in dependency reference to oolite.oxp.Thargoid.Armoury:null
  2. Optional Expansions mismatch between OXP Manifest and Expansion Manager at character position 0061 (DIGIT ZERO vs LATIN SMALL LETTER N)

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Tiny ships can use your cargo space as hangar: launch and attack your hostiles automatically. Finish core missions or travel to far galaxies for better fighters. You can control 8 fighters at once by default, buy a Fighter Bay to handle 16 and allow heavy fighters on board. The best hybrid types require Alien Items from Tharglets. Tiny ships can use your cargo space as hangar: launch and attack your hostiles automatically. Finish core missions or travel to far galaxies for better fighters. You can control 8 fighters at once by default, buy a Fighter Bay to handle 16 and allow heavy fighters on board. The best hybrid types require Alien Items from Tharglets.
Identifier oolite.oxp.Norby.Fighters oolite.oxp.Norby.Fighters
Title Fighters Fighters
Category Weapons Weapons
Author Norby, Shipbuilder, Thargoid, Killer Wolf, Knotty Norby, Shipbuilder, Thargoid, Killer Wolf, Knotty
Version 1.5 1.5
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
  • oolite.oxp.Thargoid.Armoury:0
  • oolite.oxp.Thargoid.Armoury:
  • Conflict Expansions
    Information URL http://wiki.alioth.net/index.php/Fighters n/a
    Download URL https://wiki.alioth.net/img_auth.php/8/8f/Fighters_1.5.oxz n/a
    License CC BY-NC-SA 4 CC BY-NC-SA 4
    File Size n/a
    Upload date 1610873369

    Documentation

    Also read http://wiki.alioth.net/index.php/Fighters

    readme.txt

    Fighters OXP
    
    Most ships are large in Oolite, even a cargo canister is as big (6x6x10m) as fighter planes.
    This allow to use Fuel Scoop as launcher of light ships stored in cargo space.
    No missiles and no cargo space on fighters but ejection seat is included so you should scoop your defeated pilots.
    
    Fighters are available in equipment shop and not in shipyard.
    Damaged fighters can not launch until not repaired in a station which has shipyard.
    Small scratches are repaired right when your fighters land back into your ship.
    
    You can force them to land either by run to green alert or initiate a hyperjump countdown, then cancel it before can happen and repeat until no more fighters outside.
    You can follow the activity of your fighters in MFD.
    
    In combat very hard to hit a fighter due to the extremely small size. The best if use your own fighters against them.
    
    Worth to get more firepower or even decoy against pirates for these cheap costs.
    
    
    Fighters
    
    Fighter     Class  Size(m)  Speed    Cost  Occupy En.+Arm Recharg Pitch Laser      Other
    Light       Knight 6x4x10   600      1000  5t     0+1     0       2.5   Pulse      No shields, just armor
    Robot       AI     7x2x8    400      5000  1t     1       2       3     Pulse      No pilot so need only 1t cargo space
    Swarm       Pawn   6x2x10   500      2000  5t     0+0.5   1       2     3*Pulse    3 ships in a group, Organic armor, need 3 Alien Items
    Raider      Bishop 13x3x16  400      3000  10t    2       1       2     Dual Pulse ECM, passive cloak in cloaked era
    GalTech     Rook   19x5x23  300/2100 5000  15t    2       2       1.8   Beam       ECM, Injectors, need Galaxy 6 or Constrictor mission
    Steel Shark Queen  20x8x22  200      10000 20t    2       2       1.6   Sniper     ECM, need Galaxy 7 or Nova Mission
    Shark       Queen  20x8x22  200/1400 10000 20t    2+2     2       1.4   Sniper     ECM, Organic, Injectors, need 5 Alien Items, Galaxy 7 or Nova
    Iron King   King   28x6x18  100      20000 25t    2       2       1.2   Thargoid   ECM, need Galaxy 8 or Thargoid Plans
    King        King   28x6x18  100/700  50000 25t    2+6     2       1     Thargoid   ECM, Organic, Injectors, need 10 Alien Items, Galaxy 8 or Thargoid Plans
    
    
    Light Fighter
    
    A standard Knight-class Light Fighter occupy 5t cargo space, use Pulse Laser but no shields, so damages repairable only when landed.
    Lightning fast: 1.5 times faster than Asp but no Quirium fuel within so no Injectors nor hyperdrive.
    Cost 1000 credits.
    
    
    Robot Fighter
    
    Very small AI controlled fighter, fit into a cargo canister and launch through Fuel Scoop. As fast as an Asp, use Pulse Laser, but has a single energy bank only and no fuel inside so no injectors nor hyperdrive. Use only 1t cargo space as hangar due to no pilot within, but 5 times more costly than a Light Fighter.
    
    
    Swarm Fighter Group
    
    Contain 3 Pawn-class fighters in a group, based on robot technology of Thargons which fly without pilots and save cargo space on mothership: need 1t for each fighter and additional 2t for supply so 5t in total.
    Slower and less durable than Knight-class Light Fighters, no shields but its organic armor regenerates slowly.
    A group together offer 3 Pulse Lasers but nothing else.
    
    Use a single control link only due to the group always stay in close formation.
    Named to Swarm because a Fighter Bay can handle 16 groups so 48 fighters! This huge swarm fit into a Python also using 100t cargo space: 16 x 5t = 80t, plus 20t for the bay.
    
    Need 3 Alien Items to build a group and cost 2000 credits.
    Repair first regroup 2 damaged groups into a single group without any cost. If only one group is damaged then need an Alien Item and 1000 credits or you can decide to wait until another group got damage then do merge.
    
    
    Raider
    
    Ships in Bishop-class need 10t space and Fighter Bay on your ship. Offer Dual Pulse Lasers, shields, ECM, more energy and faster recharge but slower than light fighters (still as fast as an Asp) and larger so less hard to hit.
    
    Raiders will get passive stealth (decloak when fire) after you acquired a Cloaking Device.
    No fuel within so missing injector speeds and hyperdrive.
    Cost 3000 credits.
    
    
    GalTech Interceptor
    
    The gap between Bishop and Queen fighter classes should be filled with a 15t Rook class ship which can use Injectors.
    This is why designers in Galaxy 6 made a smaller variant of GalTech Escort Fighter which is named to GalTech Interceptor.
    
    Prototypes where sizes are reduced to half in all 3 dimensions show good results using a Beam Laser.
    Stronger than a Raider due to the better energy recharge, longer fire range and unique intercept/runaway speed, at least until there are some fuel in the tank.
    
    If you defeat Constrictor then you can get this ship anywhere and not in Galaxy 6 only.
    
    
    Shark
    
    Developers made efforts to produce a Queen-class fast heavy fighter with some organic armor, Injectors and a new Sniper Laser, which is a Pulse Laser with a revolutional, very accurate targeting system named to Sniper Lock, where most shots will hit! Measured in real combat situations that pilots are able to deliver about 10 times more shots with this extremly useful system.
    
    The result need 5 Alien Items, 10000 credits, 20t cargo space, main station, moreover available in Galaxy 7 only until Nova mission is not completed.
    
    A weakness is if armor destroyed then Injectors are unusable and Shark need repair in a station to be usable again.
    
    You can buy "Steel Shark" variant (named by Commander Duggan) if you have no Alien Items, which missing organic armor and injectors.
    
    
    King
    
    The strongest King-class fighter occupy 25t in cargo bay, offer frangible but accurate turreted Thargoid Laser with Sniper Lock, ECM and shields, moreover very strong organic armour plates around the ship, which absorb all damages for a while and regenerates!
    
    Has strong scanner with double range (50km) which keep up target lock far from the mother and at hyperjump can follow you into the wormhole from this far.
    The slowest fighter but a King rarely retreat and Injectors can help a bit.
    
    Fire less powerful shots than a Shark but longer range (17.5km instead of 12.5km) and hold 3 times more armor. King also need armor to use Injectors and very slow without so better if you go to pick up those who send a message about lost armor.
    
    Very costly: need 50.000 credits and 10 Alien Items. Only main stations can build these, moreover the necessary parts are available in Galaxy 8 only. If you complete Thargoid Plans mission then you can buy it in other Galaxies also as a thank you from GalCop.
    
    The cheaper "Iron King" variant (named by Commander Cody) is available for 20.000 credits without Alien Items but no organic armor nor injectors within.
    
    
    
    Fighter Bay
    
    Fighters from 10t occupied cargo space like Raider, GalTech Interceptor, Shark and King are heavy fighters which need Fighter Bay to get enough large doors for launch.
    Allocate 20t cargo space for a control room to raise the number of fighters supported by your ship from 8 to 16.
    Help also to do not lose your fighters if you jump to another system without landing them first, except those which are out of your scanner so can not reach your ship in time.
    
    
    Fighter Support Center
    
    Add communication and supply rooms like in a carrier which allow to handle 40 fighters, but occupy 100t space and need a ship over 400t mass like Anaconda.
    
    
    Fighter Hangar
    
    Convert your ship to a well equipped carrier by allocating space for very large supply rooms, which can handle 80 fighters at once.
    Contain self-repair systems which make the hangar undamageable but not the stored fighters.
    Need a big ship over 8000t mass which can not fit into regular docks, like Andromeda or Condor.
    Occupy 200t space and cost 100.000 credits.
    
    
    Credits
    
    Model of Ejected Pilot comes from Knotty's Astronaut OXP.
    Model of Light Fighter comes from Shipbuilder's Colonial_Viper_Mark_1_V1.03.oxp.
    Model of Swarm comes from Thargoid's Swarm OXP.
    Model of Raider comes from Shipbuilder's Cylon_Raider_Mk1_lower_detail_model_V1.01.oxp.
    Model of GalTech Interceptor comes from Shipbuilder's GalTech Escort Fighter OXP.
    Model of Shark comes from Thargoid's Aquatics OXP.
    Model of King comes from Killer Wolf's kw-kc-capsule.dat in King Cobra OXP.
    
    License
    
    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/
    If you are re-using any piece of this OXP, please let me know by sending an e-mail to norbylite@gmail.com.
    
    
    Changelog
    2020.09.17. v1.5  Robot Fighter added.
    2017.05.16. v1.4  Fixed a rare error message around addDefenseTarget, thanks to cag.
    2017.02.02. v1.3  Fixed a minor bug of Light Fighter in fighters_ship.js, thanks to Rustem.
    2016.07.14. v1.2  Steel Shark and Iron King not need Alien Items but no organic armor nor injectors on these.
                      Repair of a Swarm group need a single Alien Item only instead of 2.
                      Fighter Hangar can handle 80 fighters on very big ships like Andromeda and Condor.
                      Fighter Support Center available on Anaconda again, as in v1.0.
    2016.07.09. v1.1  Improved target selection of Fighters, aim owner's target only when near.
                      Damaged Light Fighters do landing regardless of alert level.
                      Fighter Support Center require very big undockable ship over 8000t mass.
                      Fixed Galtech fighter's stopover by lower accuracy (8, which disable sniper mode).
                      Shark laser power halved, still very effective due to Sniper Lock.
                      Sniper Lock range reduced to 6km.
                      King cost raised to 50000Cr.
    2016.06.13. v1.0  First release.
    
    
    Norby
    

    Equipment

    Name Visible Cost [deci-credits] Tech-Level
    Fighter Bay yes 50000 1+
    Fighter Hangar yes 1000000 1+
    Remove all fighters and related equipments yes 70 1+
    Fighter Support Center yes 100000 1+
    GalTech Interceptor yes 50000 1+
    Iron King yes 200000 1+
    King yes 500000 1+
    Light Fighter yes 10000 1+
    Raider yes 30000 1+
    Robot Fighter yes 50000 1+
    Shark yes 100000 1+
    Steel Shark yes 100000 1+
    Swarm Fighter Group yes 20000 1+
    Refund a GalTech Interceptor yes 40 1+
    Refund an Iron King yes 60 1+
    Refund a King yes 60 1+
    Refund a Light Fighter yes 10 1+
    Refund a Raider yes 30 1+
    Refund a Robot Fighter yes 10 1+
    Refund a Shark yes 50 1+
    Refund a Steel Shark yes 50 1+
    Refund a Swarm Fighter Group yes 20 1+

    Ships

    Name
    Alien Item
    GalTech Interceptor
    fighters-galtech-player
    Iron King
    fighters-ironking-player
    King
    fighters-king-player
    Head of King Fighter
    fighters-kingsub
    Light Fighter
    fighters-light-player
    Ejected Pilot
    Raider
    fighters-raider-player
    fighters-raidersub
    Robot Fighter
    fighters-robot-player
    Shark
    fighters-shark-player
    fighters-sharkheadsub
    fighters-sharksub
    Steel Shark
    fighters-steelshark-player
    Swarm Group
    fighters-swarm-player
    Swarm Fighter

    Models

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

    Scripts

    Path
    Scripts/fighters-eqcond.js
    "use strict";
    this.name        = "fighters-eqcond";
    this.author      = "Norby";
    this.copyright   = "2016 Norbert Nagy";
    this.licence     = "CC BY-NC-SA 3.0";
    
    this.allowAwardEquipment = function(eqKey, ship, context) {
        if (eqKey == "EQ_FIGHTERBAY"
            || ( eqKey == "EQ_FIGHTERSUPPORT" && ship.mass >= 400000 && ship.mass < 8000000 )
            || ( eqKey == "EQ_FIGHTERHANGAR" && ship.mass >= 8000000 )
            || eqKey == "EQ_FIGHTERSREMOVE" ) {
            if( ship == player.ship && player.ship.docked
                && player.ship.dockedStation && player.ship.dockedStation.hasShipyard ) {
                    return true; //buy or remove these at stations with shipyard only
            }
        } else if (eqKey && (eqKey.indexOf("EQ_FIGHTER_") == 0 //new fighter
            || eqKey.indexOf("REFUND_EQ_FIGHTER_") == 0 ) ) {  //or refund
            if( context == "purchase" ) {
                if( ship == player.ship && player.ship.docked //need station with shipyard
                    && player.ship.dockedStation && player.ship.dockedStation.hasShipyard ) {
    
                    if( eqKey.indexOf("EQ_FIGHTER_") == 0 ) { //new fighter or repair
    
                        var w = worldScripts.fighters;
                        if( w && w._eqStats(ship, eqKey).dmg > 0 ) return true; //repair
    
                        var ai = player.ship.manifest.alien_items;
    
                        //Swarm fighter need 3 Alien Items
                        if( eqKey.indexOf("EQ_FIGHTER_SWARM") == 0 && !(ai>=3)) return false;
    
                        //GalTech fighter need Galaxy 6 or completed Constrictor Hunt core mission
                        if( eqKey.indexOf("EQ_FIGHTER_GALTECH") == 0 && galaxyNumber != 5
                                && missionVariables.conhunt != "MISSION_COMPLETE" )
                            return false;
    
                        //Shark fighter need 5 Alien Items, mainStation and Galaxy 7
                        //or completed Nova core mission
                        if( eqKey.indexOf("EQ_FIGHTER_SHARK") == 0 && ( !(ai>=5)
                            || player.ship.dockedStation != system.mainStation
                            || galaxyNumber != 6
                                && missionVariables.nova != "NOVA_HERO" ) )
                            return false;
                        if( eqKey.indexOf("EQ_FIGHTER_STEELSHARK") == 0 && (
                            player.ship.dockedStation != system.mainStation
                            || galaxyNumber != 6
                                && missionVariables.nova != "NOVA_HERO" ) )
                            return false;
    
                        //King fighter need 10 Alien Items, mainStation and Galaxy 8
                        //or completed Thargoid Plans core mission
                        if( eqKey.indexOf("EQ_FIGHTER_KING") == 0 && ( !(ai>=10)
                            || player.ship.dockedStation != system.mainStation
                            || galaxyNumber != 7
                                && missionVariables.thargplans != "MISSION_COMPLETE" ) )
                            return false;
                        if( eqKey.indexOf("EQ_FIGHTER_IRONKING") == 0 && (
                            player.ship.dockedStation != system.mainStation
                            || galaxyNumber != 7
                                && missionVariables.thargplans != "MISSION_COMPLETE" ) )
                            return false;
    
                        if( w && w.$FightersOfPlayer < w.$MaxFighters )
                            return true; //there is a room for a new fighter
    
                    } else { //refund fighter
                        var k = eqKey.substring(7); //chop REFUND_ to get the eqkey of fighter
                        if(ship.equipmentStatus(k) == "EQUIPMENT_OK")
                            return true; //there is an undamaged fighter from this type in this ship
                    }
                }
            } else return true; //award a fighter eq at landing in space
        }
        return false;
    }
    
    Scripts/fighters-ship.js
    "use strict";
    this.name        = "fighters-ship";
    this.author      = "Norby";
    this.copyright   = "2016 Norbert Nagy";
    this.licence     = "CC BY-NC-SA 4.0";
    this.description = "ship script of fighters";
    
    this.$FighterKey = ""; //store equipmentKey of this fighter to award back when landed
    this.$FighterHull = this.ship.maxEnergy; //must repair in hangar, avoid recharge when no shields
    this.$FighterNumber = 0; //order of this fighter in escort formation
    this.$FighterOwner = null; //store the mothership
    this.$FighterShields = true; //cache of this.ship.scriptInfo.npc_shields
    this.$ForcedLanding = false; //forced due to damage, do not stop landing with others
    this.$OwnerScoopPos = null; //store the mothership's scoop position for landing
    this.$SniperLock = false; //cache of shipdata.plist script_info fighters-sniperlock field
    this.$TimerLandNow = null; //trigger _landNow() after some time for sure landing
    this.$ws = worldScripts.fighters;
    
    //ship script events
    this.shipAttackedOther = function(other) {
            if( other && other.isDerelict && other == this.ship.target )
                    this.ship.target = null; //do not destroy derelicts, choose another target instead
    }
    
    this.shipBountyChanged = function(delta,reason) {
            //prevent orange scanner flag on player's fighters with ShipVersion OXP
            if(this.$FighterOwner == player.ship && this.ship.bounty > 0 ) {
                    this.ship.bounty = 0; //Warning: this change trigger this event again!
            }                             //So must check before that bounty is greater than 0
    }                                     //else this is an infinite loop which make Crash To Desktop!
    
    this.shipCollided = this.shipWasScooped = function(otherShip) {
            if(otherShip == this.$FighterOwner) {
                this.ship.energy = this.ship.maxEnergy; //sometimes prevent destruction
    
                if(this.ship.AIState == "LANDING") //put into the hangar
                    this._landNow(this.ship, this.$FighterOwner);
            }
    }
    
    this.shipLaunchedEscapePod = function(escapepod, passengers) {
            this.ship.target = null;
            this.ship.performHold();
            this.ship.switchAI("nullAI.plist");
    }
    
    this.shipTakingDamage = function(amount, whom, type) {
            this.$FighterHull -= amount; //workaround recharge when no shields
            if( !this.$FighterShields ) { //light fighter without shields
                    if(this.$FighterHull < 0) { //out of hull
                            delete this.shipTakingDamage;//prevent infinite loop
                            if(this.ship && this.ship.isValid) {
                                    this.ship.abandonShip(); //eject pilot
                                    this.ship.explode(); //destroy the defeated fighter
                            }
                    } else if(this.$FighterHull < this.ship.maxEnergy * 0.4) { //damaged
    			var fi = this.ship;
                            var fai = "fightersAI.plist";
                            if(fi.AI != fai ) fi.switchAI(fai);
                            fi.target = null; //clear target to stop attack
                            fi.AIState = "LANDING";
                            this.$ForcedLanding = true;
                    }
            } else { //make sure damages on heavy fighters are persistent by set ECM to damaged
                    if(this.ship && this.ship.energy < 0.4 * this.ship.maxEnergy ) {
                            if( this.ship.equipmentStatus("EQ_ECM") == "EQUIPMENT_OK" ) {
                                    this.ship.setEquipmentStatus("EQ_ECM", "EQUIPMENT_DAMAGED");
                                    if(this.ship.dataKey === "fighters-raider") { //lose a laser
                                            var su = this.ship.subEntities;
                                            if(su && su[1]) su[1].remove();//no true within to display message
                                    }
                            }
                    }
            }
    }
    
    this.shipSpawned = function() {
            if( !this.ship ) return;
            var si = this.ship.scriptInfo;
            if( si && si.npc_shields == "no" //Swarm has regenerating armor without CustiomShields
                && this.ship.primaryRole != "EQ_FIGHTER_SWARM" ) {
                    this.$FighterShields = false;
            } else this.$FighterShields = true;
            
            if( si && si.fighters_sniperlock ) this.$SniperLock = true;
            
            if( this.ship.primaryRole === "EQ_FIGHTER_RAIDER" //stealth fighters in cloaked era
                && ( player.ship.equipmentStatus("EQ_CLOAKING_DEVICE") != "EQUIPMENT_UNAVAILABLE"
                    || missionVariables.cloak === "COMPLETE"  ) ) {
                this.ship.awardEquipment("EQ_CLOAKING_DEVICE");
            }
    }
    
    this.spawnedAsEscort = function(mother) {  //for NPC mothers
            if(mother && mother.script) {
                if(!mother.script.$LaunchedFighters) mother.script.$LaunchedFighters = 1;
                else mother.script.$LaunchedFighters++;
                this.$FighterNumber = mother.script.$LaunchedFighters;
                if(!mother.script.$Fighters) mother.script.$Fighters = [];
                mother.script.$Fighters.push(this.ship);
                this.ship.displayName = this.ship.name+" #"+this.$FighterNumber;
            }
            this.$FighterOwner = mother;
    }
    
    this.shipTargetDestroyed = function(target) {
            if(this.ship == player.ship || this.$FighterOwner != player.ship )
                return; //exit if worldScript, do in ship script only
            if(target.primaryRole == "constrictor" && missionVariables.conhunt && missionVariables.conhunt == "STAGE_1") {
                    // just in case an escort kills the constrictor, let's not break the mission for the player...
                    missionVariables.conhunt = "CONSTRICTOR_DESTROYED";
            }
            
            if(target.isRock || target.isBoulder || target.isCargo || target.isWeapon)
                    return;
            
    //      player.score += 1; - score should remain personal
            var b = "";
            if(target.bounty > 0) {
                b = " : " + target.bounty + "₢ awarded.";
                player.credits += target.bounty;
            }
            player.consoleMessage(this.ship.displayName+" killed "+target.name+b, 5);
            log(this.name, this.ship.displayName+" killed " + target.name + " : " + target.bounty +"₢");
    }
    
    this.shipDied = function(whom,why) {
            if( !this.ship || this.ship == player.ship ) return; //exit if worldScript, do in ship script only
            if( this.ship.dataKey === "fighters-swarm" ) {
                var ai = system.addShips("fighters-alienitem", 1, this.ship.position, 20);
                if( ai && ai[0] && this.ship.velocity) ai[0].velocity = this.ship.velocity;
                var e = this.ship.subEntities;
                var l = 0;
                if( e ) l = e.length;
                if( l > 0 ) { //there are subents so not all swarm fighters are lost
                    var sw = system.addShips("fighters-swarm", 1, this.ship.position, 20)[0];
                    if(sw) {
                        sw.target = this.ship.target;
                        if(this.ship.target) sw.addDefenseTarget(this.ship.target);
                        if(sw.script) {
                            sw.script.$FighterKey = this.$FighterKey;
                            sw.script.$FighterNumber = this.$FighterNumber;
                            sw.script.$FighterOwner = this.$FighterOwner;
                            sw.script.$TrailsDisabled = 1; //reduce load with Trails OXP
                        }
                        this.$FighterOwner.escortGroup.addShip(sw);
                        var f;
                        if(this.$FighterOwner === player.ship) f = this.$ws.$Fighters;
                        else f = this.$FighterOwner.script.$Fighters;
                        var fi = f.indexOf(this.ship);
                        if( fi > -1 ) {
                            if(this.$FighterOwner === player.ship) this.$ws.$Fighters[fi] = sw;
                            else this.$FighterOwner.script.$Fighters = sw;
                        }
                        sw.velocity = this.ship.velocity;
                        sw.orientation = this.ship.orientation;
                        sw.displayName = this.ship.displayName;
                        if( this.$FighterOwner === player.ship ) {
                            sw.bounty = 0; //remove bounty from fighter given in shipdata.plist
                            sw.scannerDisplayColor1 = [0, 0.4, 0]; //green, mean frendly
                            //si.scannerDisplayColor2 = [0, 0.4, 0]; //green, mean frendly
                        } else {
                            if( ship.bounty == 0 ) sw.bounty = 0; //remove bounty from fighter given in shipdata.plist
                        }
                        var su = sw.subEntities;
                        if(su && su[1]) {
                            su[1].remove(true); //remove a swarm fighter
                            if( l == 1 ) su[0].remove(true); //remove another also
                        }
                    }
                }
            }
            if( this.ship.dataKey === "fighters-shark" ) {
                    var a = Math.ceil(this.$ws._alienItems("EQ_FIGHTER_SHARK")/2);
                    var ai = system.addShips("fighters-alienitem", a, this.ship.position, 100);
                    if( ai && this.ship.velocity )
                        for( var i = 0; i < ai.length; i++ )
                            if(ai[i]) ai[i].velocity = this.ship.velocity;
            }
            if( this.ship.dataKey === "fighters-king" ) {
                    var a = Math.ceil(this.$ws._alienItems("EQ_FIGHTER_KING")/2);
                    var ai = system.addShips("fighters-alienitem", a, this.ship.position, 100);
                    if( ai && this.ship.velocity )
                        for( var i = 0; i < ai.length; i++ )
                            if(ai[i]) ai[i].velocity = this.ship.velocity;
            }
            if( this.$FighterOwner == player.ship ) {
                    var d = " lost";
                    if( whom && whom.isValid && whom != player.ship ) {
                            d = " defeated by "+whom.displayName;
                    }
                    player.commsMessage(this.ship.displayName + d, 10);
                    this.$ws._MFD(); //update numbers in MFD
            }
            log(this.name, this.ship.displayName+" terminated by "+whom+" "+why); //log NPC fighters also
    }
    
    
    //fighter functions
    
    this._escortPosition = function() {this._escortPosition2(this.ship);} //for fighterAI.plist
    
    this._escortPosition2 = function(ship) {
            if( ship && ship.isDerelict ) {
                ship.target = null;
                ship.performHold();
                ship.switchAI("nullAI.plist");
                return;
            }
            var sc = ship.script;
            if( !sc || !sc.$FighterNumber || !sc.$FighterOwner || !sc.$FighterOwner.isValid
                || ship == player.ship ) return; //for sure
    
            if( sc.$FighterOwner == player.ship && //green alert or jump countdown
                ( player.alertCondition == 1 || this.$ws.$ForcedLanding ) ) {
                ship.AIState = "LANDING";
                this.$ws._MFD(); //update if owner is the player
                return;
            }
    
            if( ship.target && ship.target.hasHostileTarget ) {
                ship.setAI("interceptAI.plist");
                return;
            }
    
            var epos = sc.$ws.coordinatesForEscortPosition(sc.$FighterNumber);
            var pos = sc.$FighterOwner.position.add(epos);
            ship.savedCoordinates = pos;
            if( ship.injectorSpeedFactor > 0 )
                ship.desiredSpeed = ship.injectorSpeedFactor * ship.maxSpeed;
            else ship.desiredSpeed = 7 * ship.maxSpeed; //for Oolite 1.80
            ship.desiredRange = 0; //go to the escort position in the formation
            ship.performFlyToRangeFromDestination();
            ship.reactToAIMessage("OWNER_FAR");//go back to owner
            //var dist = ship.position.distanceTo(sc.$FighterOwner.position);
            //if(dist > 10000) ship.reactToAIMessage("OWNER_FAR");//go back to owner
            //else if(dist > 5000) ship.reactToAIMessage("OWNER_NEAR");//go back to owner
            //else ship.reactToAIMessage("OWNER_NEXT");//keep formation
    }
    
    this._landing = function() {this._landing2(this.ship);}
    
    this._landing2 = function(ship) {
            if( ship && ship.isDerelict ) {
                ship.target = null;
                ship.performHold();
                ship.switchAI("nullAI.plist");
                return;
            }
            var sc = ship.script;
            if( !sc || ship == player.ship ) return; //for sure
    
            var owner = sc.$FighterOwner;
            if( !owner || !owner.isValid ) return;
    
            ship.target = null; //clear target to stop attack
    
            var landingdist = 50; //m from the scoop position of owner where put into hangar
            //make a waypoint below the owner's scoop position
            var scooppos = owner.position;
            var sp = this.$OwnerScoopPos;
            if( !sp ) { //first time read scoop pos from shipdata.plist
                var shipdata = Ship.shipDataForKey(owner.dataKey);
                if( shipdata ) {
                    sp = shipdata["scoop_position"];
                    if(sp && sp != "undefined") {
                        sp = sp.split(" ",3);
                        if( sp && sp.length == 3 ) {
                            this.$OwnerScoopPos = sp;
                            //log(this.name, owner.name+" scoop_position:"+sp); //debug
                        }
                    }
                }
                if( !this.$OwnerScoopPos ) { //no scoop pos in shipdata so make one
                    this.$OwnerScoopPos = [];
                    this.$OwnerScoopPos[0] = 0;
                    this.$OwnerScoopPos[1] = -20;
                    this.$OwnerScoopPos[2] = 0;
                    sp = this.$OwnerScoopPos;
                }
            }
            scooppos = scooppos.add(owner.vectorRight.multiply(sp[0]))
                                .add(owner.vectorUp.multiply(sp[1]))
                                .add(owner.heading.multiply(sp[2]));
            var down = 1.5*owner.collisionRadius; //AI never enter into collisionRadius
            var downpos = scooppos.add(owner.vectorUp.multiply(-down));
            ship.savedCoordinates = downpos.add(owner.velocity.multiply(6)); //go below the owner
            // also must use setDestinationFromCoordinates in AI.plist
    
            var d = ship.position.distanceTo(scooppos);
            var d2 = ship.position.distanceTo(downpos);
            //log(this.name, d);
            if( this.$ws.$TractorBeam ) {
                    if( d < landingdist ) { //enough near?: add to the hangar
                            this._landNow(ship, owner);
                            return;
                    }
                    if( d2 < down ) { //arrived below the owner?
                            //if( owner.addCollisionException ) { //need Oolite v1.81
                            //    owner.addCollisionException(ship); //must go near
                                //log(this.name, "coll.ex.len:"+owner.collisionExceptions.length);//debug
                            //}
    
                            //go up to the owner's center point from below into the dock
                            ship.savedCoordinates = scooppos.add(owner.velocity.multiply(2));
                            // also must use setDestinationFromCoordinates in AI.plist
    
                            //apply "tractor beam" to pull this fighter to the scoop
                            var v = scooppos.subtract(ship.position);
                            ship.velocity = v.direction().multiply(80).add(v.multiply(0.4))
                                    .add(owner.velocity);
    
                            //make sure the fighter will land after 10 seconds
                            if( !this.$TimerLandNow )
                                    this.$TimerLandNow = new Timer(this, this._TimedLandNow.bind(this), 10);
    
                            ship.desiredSpeed = 0; //must set before "OWNER_NEXT"
                            ship.reactToAIMessage("OWNER_NEXT"); //go straight
                            return;
                    }
            } else { //land without tractor beam
                    if( d < landingdist ) {
                            //must be triggered outside collisionRadius of owner
                            this._landNow(ship, owner); //add to the hangar
                            return;
                    } else if( d2 < down ) { //surely land after 10 seconds
                            if( !this.$TimerLandNow )
                                    this.$TimerLandNow = new Timer(this, this._TimedLandNow.bind(this), 10);
                            //go up to the owner's center point from below into the dock
                            ship.savedCoordinates = scooppos.add(owner.velocity);
                            ship.desiredSpeed = ship.maxSpeed; //must set before "OWNER_NEXT"
                            ship.reactToAIMessage("OWNER_NEXT"); //go straight
                            return;
                    }
            }
    
            if( d < 2000 ) ship.reactToAIMessage("OWNER_NEAR"); //approach owner at normal speed
            else ship.reactToAIMessage("OWNER_FAR"); //use injectors in approach if available
    }
    
    this._landNow = function(ship, owner) { //put back into the internal hangar
            if(!ship || !owner || !owner.isValid) return; //for sure
            var f = [], f2 = []; //remove from $Fighters array
            this.$ws._awardFighter(ship, owner); //put back into the owner's equipment list
            if( owner == player.ship ) {
                if( this.$ws.$LaunchedFighters > 0 ) this.$ws.$LaunchedFighters--;
                f = this.$ws.$Fighters;
            } else if( owner.script ) {
                if( owner.script.$LaunchedFighters > 0 ) owner.script.$LaunchedFighters--;
                f = owner.script.$Fighters;
            }
            var out = 0;
            for(var i = 0; i < f.length; i++) {
                var fi = f[i];
                if( fi != ship ) {
                    f2.push(fi);
                    if( fi && fi.isValid ) out++;
                }
            }
            if( owner == player.ship ) {
                this.$ws.$Fighters = f2;
                this.$ws._MFD(); //update numbers in MFD
                if( out == 0 ) {
                    player.commsMessage("All fighters landed", 10);
                    player.consoleMessage(" ", 10); //make room to the countdown
                }
            } else if( owner.script ) owner.script.$Fighters = f2;
    
            //prevent double landing if both shipCollided and shipWasScooped is fired
            var sc = ship.script;
            if( sc ) sc.$FighterOwner = null;
    
            //if( owner.removeCollisionException ) { //need Oolite v1.81
            //    owner.removeCollisionException(ship); //must go near
                //log(this.name, "coll.ex.len:"+owner.collisionExceptions.length);//debug
            //}
            ship.remove(true); //landed
    }
    
    this._newTarget = function() {this._newTarget2(this.ship);} //AI in plist located new target
    
    this._newTarget2 = function() { //AI in plist located new target
            var sc = ship.script;
            if( !sc || ship == player.ship ) return; //for sure
            var owner = sc.$FighterOwner;
            if( !owner || !owner.isValid || owner != player.ship ) return; //player's fighters only
            var t = ship.target;
            if( !this.$ws._isValidFighterTarget(t, owner) ) return;
    
            player.consoleMessage(ship.name+" #"+this.$FighterNumber+" aim "+t.displayName, 4.5);
            this.$ws._MFD(); //update numbers in MFD
    }
    
    this._TimedLandNow = function() { 
            if( this.$TimerLandNow ) {
                    this.$TimerLandNow.stop();
                    delete this.$TimerLandNow;
            }
            this._landNow(this.ship, this.$FighterOwner);
    }
    
    Scripts/fighters-subent.js
    "use strict";
    this.name           = "fighters-subent";
    this.author         = "Norby";
    this.copyright      = "© 2016 Norby, Creative Commons: attribution, non-commercial, sharealike.";
    this.description    = "ship script for subentities of fighters";
    
    this.shipDied = function (whom,why) {
        if( !this.ship || !this.ship.owner || !this.ship.owner.isValid ) return; //for sure
    
        switch(this.ship.dataKey) {
            case "fighters-raidersub": {
                var m = "I lost a laser, but there is another!";
                var e = this.ship.owner.subEntities;
                if( !e || e.length < 1 ) m = "No more laser!";
                this.ship.owner.commsMessage(m);
                break;
            };
            case "fighters-swarmsub": {
                var ai = system.addShips("fighters-alienitem", 1, this.ship.owner.position, 20);
                if( ai && ai[0] && this.ship.velocity) ai[0].velocity = this.ship.velocity;
    
                var s = this.ship.owner.script;
                if( s && s.$FighterOwner == player.ship ) {//for fighters of player only
                    var by = "";
                    if(whom) by = "by "+whom.displayName;
                    player.consoleMessage("A Swarm fighter destroyed"+by);
                }
                log(this.name, this.ship.displayName+" destroyed by "+whom+" "+why); //log NPC fighters also
                break;
            };
            case "fighters-sharkheadsub": {
                this.ship.owner.commsMessage("Front armor lost.");
                break;
            };
            case "fighters-sharksub":
            case "fighters-kingsub": {
                this.ship.owner.commsMessage("No more armor, Injectors unusable.");
                this.ship.owner.fuel = 0; //prevent usage of injectors
                this.ship.owner.setEquipmentStatus("EQ_HEAT_SHIELD", "EQUIPMENT_DAMAGED");
                break; //landing script will set fighter eq to damaged so forced repair in station
            };
            case "fighters-kingheadsub": {
                this.ship.owner.commsMessage("I lost my turret.");
                break;
            };
        }
    };
    Scripts/fighters.js
    "use strict";
    this.name           = "fighters";
    this.author         = "Norby";
    this.copyright      = "© 2016 Norby, Creative Commons: attribution, non-commercial, sharealike.";
    this.description    = "worldscript for fighters";
    
    //settings
    this.$AutoRepair = true; //pay for fighter repairs right at arrival screen
    this.$FireDist = 12000; //where fighters accept new target, best within Pulse Laser range (12.5km)
    this.$LaunchDelay = 1; //delay between launch of fighters in seconds
    this.$MaxFightersIfNoBay = 8; //how many fighters you can control without Fighter Bay
    this.$MFDName = "Fighters"; //name appear in HUD and MFD Selector Interface
    this.$TractorBeam = true; //pull fighters to the scoop position of owner at landing
    
    //internal variables, should not touch
    this.$EscortPositions = []; //store positions for escort formation
    this.$FCB = null; //FrameCallBack for sniperlock of Kings and Sharks
    this.$FighterCost = []; //cost of fighters, read from equipment.plist in startUp
    this.$FighterKeys = []; //["EQ_FIGHTER_LIGHT","EQ_FIGHTER_RAIDER"]; //in equipment.plist
    this.$Fighters = []; //Active, launched fighters
    this.$FightersOfPlayer = 0; //actually how many figters owned by the player
    this.$ForcedLanding = false; //must land when countdown active
    this.$LastForced = false; //store forced status for next fighter launch
    this.$LastTarget = null; //store target for next fighter launch
    this.$LaunchedFighters = 0;  //how many fighters launched from the internal hangar
    this.$LaunchInProgress = false; //do not launch fighters too often
    this.$LeftBehindFighters = 0; //set in shipWillEnterWitchspace, read in shipExitedWormhole
    this.$LeftBehindSystem = "";  //set in shipWillEnterWitchspace, read in shipExitedWormhole
    this.$MaxFighters = this.$MaxFightersIfNoBay; //how many fighters you can control currently
    this.$simulator = false; //detect if combat simulator is running
    this.$Targets = []; //Ships once targeted by fighters, indexed by entityPersonality
    this.$TimerDelay = null; //for fighter launch delay
    
    
    //event handlers
    
    this.startUp = function() {
        this.$MaxFighters = this._maxFighters(player.ship);
        this.$FightersOfPlayer = missionVariables.$FightersOfPlayerPlusOne;
        if( !this.$FightersOfPlayer ) this.$FightersOfPlayer = 0;
        else this.$FightersOfPlayer--; //missionVariables store one more to avoid 0
    
        var j = 0;
        var alleq = EquipmentInfo.allEquipment;
        for(var i = 0; i < alleq.length; i++) { //read fighter costs into local array
            var eq = alleq[i];
            if( eq ) {
                var k = eq.equipmentKey;
                if( k && k.indexOf("EQ_FIGHTER_") == 0 ) {
                    this.$FighterKeys[j] = k;
                    //bubble sort keep smaller fighters first to ensure launch of heaviest first
                    var sp = EquipmentInfo.infoForKey( k ).requiredCargoSpace;
                    for( var x = j; x > 0 &&
                        sp < EquipmentInfo.infoForKey(this.$FighterKeys[x-1]).requiredCargoSpace;
                        x-- ) { //exchange the two fighters until smaller than the other
                        var tmpkey = this.$FighterKeys[x];
                        this.$FighterKeys[x] = this.$FighterKeys[x-1];
                        this.$FighterKeys[x-1] = tmpkey;
                    }
                    j++;
                }
            }
        }
        for(var i = 0; i < this.$FighterKeys.length; i++) { //read fighter costs into local array
            var k = this.$FighterKeys[i];
            this.$FighterCost[i] = EquipmentInfo.infoForKey( k ).price/10;
        }
    
        var feq = this._fighterEqsOf(player.ship);
        if( feq != this.$FightersOfPlayer ) {
            log(this.name, "Using "+feq+" fighter equipments instead of "
                +this.$FightersOfPlayer+" from missionVariables");
            this.$FightersOfPlayer = feq; //override by the number of counted fighter equipments
        }
    
        var h = worldScripts.hudselector;
        if( h && h.$HUDSelectorAddMFD ) h.$HUDSelectorAddMFD(this.name, this.$MFDName);
    
        if( !isValidFrameCallback(this.$FCB) ) {
            this.$FCB = addFrameCallback( this._FCB.bind(this) ); //sniperlock of Kings and Sharks
        }
    
    }
    
    this.shipSpawned = function(ship) { //NPC with fighters
        if(this === worldScripts.fighters) return; //do nothing in worldscript, just in NPC ship script
    
        this.$MaxFighters = this._maxFighters(ship);
    
        if( !isValidFrameCallback(this.$FCB) ) {
            this.$FCB = addFrameCallback( this._FCB.bind(this) ); //sniperlock of Kings and Sharks
        }
    
    }
    
    
    this.alertConditionChanged = function(newCondition, oldCondition) {
        var p = player.ship;
        var ship = this.ship;
        if( !ship ) ship = p; //called in worldScript
        this._MFD(); //update if player, even if not use fighters
        if( !this._playerHasFighters(ship) || player.docked ) return;
    
        if( newCondition == 1 ) { //green alert in space so fighters should go back to hangar
            var out = this._fightersLanding();
            if( out > 0 ) {
                var s = out+" fighters are";
                if( out == 1 ) s = "A fighter is";
                player.consoleMessage(s+" landing", 4.5);
            }
        } else if( newCondition == 3 && player.alertHostiles ) {
            this._fightersStopLanding(); //cancel landing in red alert
            this._launchFighters(ship.target, true); //forced launch even if no valid target
        }
    }
    
    this.coordinatesForEscortPosition = function(index) {
        var pos = this.$EscortPositions[index]; //cache of positions
        if( !pos ) pos = this.$EscortPositions[index] = this.coordinatesForEscortPosition2(index);
        var p = player.ship;
        var ship = this.ship;
        if( !ship ) ship = p; //called in worldScript
        if( ship && ship.isValid ) pos = pos.add(ship.velocity.multiply(4)); //adjusted for movement
        return(pos);
    }
    
    this.coordinatesForEscortPosition2 = function(index) {
            //4 groups in square form, 4*4=16 fighters
            //always show the xz plane of system due to zero second coordinates
            var positions = [new Vector3D(1,0,1), new Vector3D(-1,0,1),
                             new Vector3D(1,0,-1), new Vector3D(-1,0,-1)];
            var space = 100; //m space between escorts in the same group
            var zoom = 1500; //m, scale group positions around the mothership
    
            //small adjustment in x position to form horizontal lines within groups
            var layer = 1+Math.floor(index/positions.length);
            var xdir = 1; //spread one left, one right, starting from center to outside
            var i = layer/2;
            var fi = Math.floor(i);
            if( fi == i ) xdir = -1;
            var x = xdir * space * ( 0.5 + (fi % 4) ); //x positions within a group: -50 50 -150 150
    
            //small adjustment in y position for more than 16 fighters
            var y = space * ( -0.5 + Math.floor(index/(4*positions.length)));
    
            var group = index % positions.length;
            return positions[group].multiply(zoom).add([x,y,0]);
    }
    
    this.distressMessageReceived = function(aggressor, sender) {
        this._launchFighters(aggressor);
    }
    
    this.equipmentDamaged = this.equipmentRepaired = this.equipmentRemoved = this.equipmentAdded = function(equipment) {
        var p = player.ship;
        var ship = this.ship;
        if( !ship ) ship = p; //called in worldScript
        this.$MaxFighters = this._maxFighters(ship);
    }
    
    
    this.helpRequestReceived = function(ally, enemy) {
        this._launchFighters(enemy);
    }
    
    this.playerBoughtEquipment = function(equipmentKey) {
        if(equipmentKey === "EQ_FIGHTERSREMOVE") {
            player.ship.removeEquipment(equipmentKey);
            var ai = 0, refund = 0, k, eqcost;
            for(var i = 0; i < this.$FighterKeys.length; i++) { //remove all fighter types
                k = this.$FighterKeys[i];
                var e = this._eqStats(player.ship, k);
                eqcost = this.$FighterCost[i];
                refund += eqcost * e.ok + eqcost/2 * e.dmg; //half price for damaged fighters
                ai += (e.ok+e.dmg) * this._alienItems(k); //regain all built-in alien items
                //one less alien items in each damaged swarm fighter group
                if( k.indexOf("EQ_FIGHTER_SWARM") == 0 ) ai -= e.dmg;
                for(var j = 0; j < e.ok + e.dmg; j++) { //remove all fighters
                    player.ship.removeEquipment(k);//remove fighter
                }
            }
            var f = "", fb = "", fcc = "";
            if(this.$FightersOfPlayer > 1) f = this.$FightersOfPlayer+" fighters";
            else if(this.$FightersOfPlayer == 1 ) f = "a fighter";
    
            k = "EQ_FIGHTERBAY";
            if( player.ship.equipmentStatus(k) != "EQUIPMENT_UNAVAILABLE" ) {
                eqcost = EquipmentInfo.infoForKey( k ).price/10;
                if(player.ship.equipmentStatus(k) == "EQUIPMENT_OK") refund += eqcost;
                else refund += eqcost/2; //half money if damaged
                player.ship.removeEquipment(k);
                fb = "Fighter Bay";
                if(f.length > 0) f = " and "+f;
            }
            k = "EQ_FIGHTERSUPPORT";
            if( player.ship.equipmentStatus(k) != "EQUIPMENT_UNAVAILABLE" ) {
                eqcost = EquipmentInfo.infoForKey( k ).price/10;
                if(player.ship.equipmentStatus(k) == "EQUIPMENT_OK") refund += eqcost;
                else refund += eqcost/2; //half money if damaged
                player.ship.removeEquipment(k);
                if(f.length > 0 && fb.length > 0) fcc = ", ";
                else if(fb.length > 0) fcc = " and ";
                else if(f.length > 0) f = " and "+f;
                fcc += "Fighter Support Center";
            }
            k = "EQ_FIGHTERHANGAR";
            if( player.ship.equipmentStatus(k) != "EQUIPMENT_UNAVAILABLE" ) {
                eqcost = EquipmentInfo.infoForKey( k ).price/10;
                if(player.ship.equipmentStatus(k) == "EQUIPMENT_OK") refund += eqcost;
                else refund += eqcost/2; //half money if damaged
                player.ship.removeEquipment(k);
                if(f.length > 0 && fb.length > 0) fcc = ", ";
                else if(fb.length > 0) fcc = " and ";
                else if(f.length > 0) f = " and "+f;
                fcc += "Fighter Hangar";
            }
            var ais = "";
            if( ai > 0 ) {
                    player.ship.manifest.alien_items += ai;
                    ais=" and "+ai+" Alien Items";
            }
            player.credits += refund;
            player.consoleMessage(refund+" credits"+ais+" refunded for "+fb+fcc+f, 10);
            this.$FightersOfPlayer = this._fighterEqsOf(player.ship); //should be 0
            this.$MaxFighters = this._maxFighters(player.ship);
        }
    
        if(equipmentKey === "EQ_FIGHTERBAY" 
            || equipmentKey === "EQ_FIGHTERSUPPORT"
            || equipmentKey === "EQ_FIGHTERHANGAR" ) {
            this.$MaxFighters = this._maxFighters(player.ship);
            player.consoleMessage("You can control "+this.$MaxFighters+" fighters from now", 10);
        }
        if(equipmentKey.indexOf("REFUND_EQ_FIGHTER_") == 0 ) {
            player.ship.removeEquipment(equipmentKey);
            var refund = 0;
            var k = equipmentKey.substring(7); //chop REFUND_ to get the eqkey of fighter
            if(player.ship.equipmentStatus(k) == "EQUIPMENT_OK") {
                var eqcost = EquipmentInfo.infoForKey( k ).price/10;
                refund += eqcost;
                player.ship.removeEquipment(k);//remove fighter
                this.$FightersOfPlayer = this._fighterEqsOf(player.ship);
                //if( this.$FightersOfPlayer > 0 ) this.$FightersOfPlayer--;
                player.credits += refund;
                var ai = "", a = this._alienItems(k);
                if( a > 0 ) {
                    player.ship.manifest.alien_items += a;
                    ai = " and "+a+" Alien Items";
                }
                var n = "";
                if(equipmentKey.indexOf("REFUND_EQ_FIGHTER_I") == 0) n = "n"; //an Iron King
                player.consoleMessage(refund+" credits"+ai+" refunded for a"+n+" "
                    +EquipmentInfo.infoForKey( k ).name, 10);
            }
        } else if(equipmentKey.indexOf("EQ_FIGHTER_") == 0 ) {
            //if there is a damaged then this event is called with the same eqkey at repair, must exclude!
            if( this._eqStats(player.ship, equipmentKey).dmg == 0 ) { //all ok
                var a = this._alienItems(equipmentKey);
                if( a > 0 ) { //need some Alien Items to buy this fighter
                    if(!(player.ship.manifest.alien_items >= a)) {
                        player.ship.removeEquipment(equipmentKey);//remove fighter if not enough alien items
                        player.consoleMessage("You need "+a+" Alien Items to buy this fighter", 5);
                        return false;
                    }
                    player.ship.manifest.alien_items -= a;
                }
                this.$FightersOfPlayer = this._fighterEqsOf(player.ship);
                //if( this.$FightersOfPlayer < this.$MaxFighters ) this.$FightersOfPlayer++;
                var f = "";
                if( this.$FightersOfPlayer < this.$MaxFighters )
                    f = " of "+this.$MaxFighters+" fighters, buy again for more.";
                else f = " fighters, this is the maximum.";
                player.consoleMessage("You have "+this.$FightersOfPlayer+f, 5);
            } else { //handle repair
                if( equipmentKey.indexOf("EQ_FIGHTER_SWARM") == 0 ) {
                    var ki = this.$FighterKeys.indexOf("EQ_FIGHTER_SWARM");
                    if( this._eqStats(player.ship, equipmentKey).dmg == 1 ) {//exactly 1 damamged
                        if( player.ship.manifest.alien_items >= 1 ) {
                            player.ship.manifest.alien_items -= 1;
                            player.consoleMessage("Repair of Swarm Fighter Group consumed an Alien Item", 10);
                        } else {
                            player.consoleMessage("Fixing need an Alien Item or 2 damaged groups", 10);
                            player.ship.removeEquipment(equipmentKey);//remove the gift awarded by the core
                            player.credits += this.$FighterCost[ki] / 2; //refund the cost, nothing happened
                            this.$FightersOfPlayer = this._fighterEqsOf(player.ship);
                            return;
                        }
                    } else { //make an ungamaged group from 2 damaged groups
                        player.consoleMessage("Two gappy Swarm Fighter Groups merged to a full group", 10);
                        player.credits += this.$FighterCost[ki] / 2; //give back the repair cost
                        if(player.ship.equipmentStatus(equipmentKey) == "EQUIPMENT_OK") {
                            //there is an undamaged swarm fighter group
                            //so set a damaged group to ok due to the removeEq below will remove a good one
                            player.ship.setEquipmentStatus(equipmentKey, "EQUIPMENT_OK");
                        }
                        player.ship.removeEquipment(equipmentKey);
                    }
                }
                player.ship.removeEquipment(equipmentKey);//remove the gift awarded by the core
                player.ship.setEquipmentStatus(equipmentKey, "EQUIPMENT_OK");//fix one fighter only
                this.$FightersOfPlayer = this._fighterEqsOf(player.ship);
            }
        }
    }
    
    this.playerCancelledJumpCountdown = function() {  //stop landing
        if(!this._playerHasFighters(player.ship)) return;
        this.$ForcedLanding = false;
        this._fightersFollow();
    }
    
    this.playerJumpFailed = function(reason) {
        if(!this._playerHasFighters(player.ship)) return;
        this.$ForcedLanding = false;
        this._fightersFollow();
    }
    
    this.playerStartedJumpCountdown = function(type, seconds) { //landing fighters
        if(!this._playerHasFighters(player.ship)) return;
        this.$ForcedLanding = true;
        var out = this._fightersLanding();
        if( out > 0 ) {
            var s = out+" fighters";
            if( out == 1 ) s = " a fighter";
            player.consoleMessage("Warning: "+s+" need landing!", 10);
            player.consoleMessage(" ", 10); //make room to the countdown
        }
    }
    
    this.playerWillSaveGame = function(message) {
        this.$FightersOfPlayer = this._fighterEqsOf(player.ship);
        missionVariables.$FightersOfPlayerPlusOne = this.$FightersOfPlayer + 1;
    }
    
    this.shipAttackedOther = function(whom) {
        this._launchFighters(whom);
    }
    
    this.shipAttackedWithMissile = function(missile, whom) {
        this._launchFighters(whom);
    }
    
    this.shipBeingAttacked = function(whom) {
        this._launchFighters(whom);
    }
    
    this.shipWillExitWitchspace = function() {
        var lbf = this.$LeftBehindFighters;
        if( lbf > 0 ) {
            var s = lbf+" fighters";
            if( lbf == 1 ) s = "a fighter";
            var msg = "You left behind "+s+" in "+this.$LeftBehindSystem;
            player.commsMessage(msg, 10);
            log(this.name, msg);
            this.$LeftBehindFighters = 0;
        }
    }
    
    this.shipFiredMissile = function(missile, target) {
        this._launchFighters(target);
    }
    
    // check when the player launches if this is a simulator run
    this.shipLaunchedFromStation = function(station) {
        var p = player.ship;
        var ship = this.ship;
        if( !ship ) ship = p; //called in worldScript
    
        if( !this._playerHasFighters(ship) ) return;
    
        var w = worldScripts["Combat Simulator"];
        if (w && w.$checkFight && w.$checkFight.isRunning) this.$simulator = true;
        else this.$simulator = false;
    }
    
    this.shipTakingDamage = function(amount, whom, type) {
        this._launchFighters(whom);
    }
    
    this.shipTargetAcquired = function(t) {
        var p = player.ship;
        var ship = this.ship;
        if( !ship ) ship = p; //called in worldScript
        if( ship == p ) {
            this._MFD(); //update if player, even if not use fighters
            if( !this._playerHasFighters(ship) ) return;
            this._reconsiderTargets(ship, t); //update targets of launched fighters
        }
    }
    
    this.shipTargetLost = function(t) {
        var p = player.ship;
        var ship = this.ship;
        if( !ship ) ship = p; //called in worldScript
        if( ship == p ) {
            this._MFD(); //update if player, even if not use fighters
        }
    }
    
    this.shipWillDockWithStation = function(station) {
        var p = player.ship;
        var ship = this.ship;
        if( !ship ) ship = p; //called in worldScript
    
        // if this was a simulator run, reset the variable and return - no arrival report in this case
        if (this.$simulator == true) {
            this.$simulator = false;
        } else {
            if( ship.equipmentStatus("EQ_FIGHTERBAY") == "EQUIPMENT_DAMAGED"
                || ship.equipmentStatus("EQ_FIGHTERSUPPORT") == "EQUIPMENT_DAMAGED"
                || ship.equipmentStatus("EQ_FIGHTERHANGAR") == "EQUIPMENT_DAMAGED" ) {
                var b = "", l = "";
                if( ship.equipmentStatus("EQ_FIGHTERBAY") == "EQUIPMENT_DAMAGED" ) {
                    b = EquipmentInfo.infoForKey( "EQ_FIGHTERBAY" ).name;
                    l = " Without working "+b+" only light fighters can launch from your ship.";
                }
                if( ship.equipmentStatus("EQ_FIGHTERSUPPORT") == "EQUIPMENT_DAMAGED" ) {
                    if(b.length > 0) b += " and ";
                    b += EquipmentInfo.infoForKey( "EQ_FIGHTERSUPPORT" ).name;
                }
                if( ship.equipmentStatus("EQ_FIGHTERHANGAR") == "EQUIPMENT_DAMAGED" ) {
                    if(b.length > 0) b += " and ";
                    b += EquipmentInfo.infoForKey( "EQ_FIGHTERHANGAR" ).name;
                }
                var s = ""; if( this.$FightersOfPlayer > 1 ) s = "s"; //current fighters in plural
                player.addMessageToArrivalReport(expandDescription("[fighters-baydamaged]",
                    {bay:b, fightersofplayer:this.$FightersOfPlayer,
                        plural:s, maxfighters:this.$MaxFighters, light:l}));
            }
    
            if( this._playerHasFighters(ship)
                || ship.equipmentStatus("EQ_FIGHTERBAY") != "EQUIPMENT_UNAVAILABLE"
                || ship.equipmentStatus("EQ_FIGHTERSUPPORT") != "EQUIPMENT_UNAVAILABLE"
                || ship.equipmentStatus("EQ_FIGHTERHANGAR") != "EQUIPMENT_UNAVAILABLE" ) {
                var f = this.$Fighters;
                for(var i = 0; i < f.length; i++) {
                    var fi = f[i];
                    if( !fi || !fi.isValid ) this.$FightersOfPlayer--; //reduce total with lost ones
                    else {
                        this._awardFighter(fi, ship);
                        fi.remove(true); //do not leave fighters outside of the station
                    }
                }
                this.$FightersOfPlayer = this._fighterEqsOf(player.ship);
    
                var d = this._damagedFighter(player.ship); //how many damaged fighters are on board
                var s2 = ""; if( this.$FightersOfPlayer > 1 ) s2 = "s"; //current fighters in plural
                if( !d || !d[0] || d[0][0] === 0 ) { //no damaged fighters
                    if( this.$FightersOfPlayer === this.$MaxFighters ) {
                        var s = " is"; if( this.$MaxFighters > 1 ) s = "s are"; //fighters in plural
                        player.addMessageToArrivalReport(expandDescription("[fighters-allfighters]",
                            {maxfighters:this.$MaxFighters, plural:s}));
                    } else if( !station || !station.isValid || !station.hasShipyard ) {
                        //no shipyard, no replace
                        player.addMessageToArrivalReport(expandDescription("[fighters-noshipyard]",
                            {fightersofplayer:this.$FightersOfPlayer, plural:s2,
                                maxfighters:this.$MaxFighters}));
                    } else if( this.$FightersOfPlayer === 0 ) { //no fighters on board
                        player.addMessageToArrivalReport(expandDescription("[fighters-emptybay]"));
                    } else { //less than maxFighters are on board
                        var s = " is"; if( this.$MaxFighters > 1 ) s = "s are"; //fighters in plural
                        player.addMessageToArrivalReport(expandDescription("[fighters-lessfighters]",
                            {fightersofplayer:this.$FightersOfPlayer, plural:s2,
                                maxfighters:this.$MaxFighters}));
                    }
                } else { //there are damaged fighters
                    var dm = d[0][0];//number of damaged fighters
                    var dc = d[0][1];//credits needed for repair all damaged fighters
                    var s = ""; if( dm > 1 ) s = "s"; //damaged fighters in plural
    
                    if( !station || !station.isValid || !station.hasShipyard ) {
                        //no shipyard, no repair
                        player.addMessageToArrivalReport(expandDescription("[fighters-noshipyard]",
                            {fightersofplayer:this.$FightersOfPlayer, plural:s2,
                                maxfighters:this.$MaxFighters}));
                    } else if( player.credits < dc ) { //not enough money for all repairs
                        player.addMessageToArrivalReport(expandDescription("[fighters-notenoughmoney]",
                            {damagedfighters:dm, plural:s}));
                    } else if( !this.$AutoRepair ) { //repair disabled
                        player.addMessageToArrivalReport(expandDescription("[fighters-damaged]",
                            {damagedfighters:dm, plural:s}));
                    } else { //repair damaged fighters
                        var dc2 = 0; //must count again to exclude cost of damaged swarm groups
                        var dm2 = 0; //must count again to exclude number of damaged swarm groups
                        for(var i = 1; d[i]; i++) {
                            var k = d[i][0];
                            if( k && k.indexOf("EQ_FIGHTER_SWARM") == 0 ) {
                                continue; //do not repair swarm groups
                            }
                            for(var j = 0; j < d[i][1]; j++) {
                                player.ship.setEquipmentStatus(k, "EQUIPMENT_OK");
                                player.credits -= d[i][2];
                                dc2 += d[i][2];
                                dm2++;
                                log(this.name, "Repair of "+k+" cost "+d[i][2]+" credits");
                            }
                        }
                        if( dm2 === 0 ) { //only swarm damages without repair
                            player.addMessageToArrivalReport(expandDescription("[fighters-damaged]",
                                    {damagedfighters:dm, plural:s}));
                        } else { //repaired fighters
                            s = ""; if( dm2 > 1 ) s = "s"; //damaged fighters in plural
                            player.addMessageToArrivalReport(expandDescription("[fighters-repaired]",
                                {cost:dc2, repaired:dm2, plural:s,
                                    fightersofplayer:this.$FightersOfPlayer, plural2:s2,
                                    maxfighters:this.$MaxFighters}));
                        }
                    }
                }
            }
        }
    
        //free up arrays both in NPC ship scripts and player's array in worldScript
        delete this.$Fighters;
        delete this.$Targets;
        this.$Fighters = [];
        this.$Targets = [];
        this.$LaunchedFighters = 0;  //how many fighters launched from the internal hangar
    
    }
    
    this.shipWillEnterWitchspace = function() {
        var p = player.ship;
        var ship = this.ship;
        if( !ship ) ship = p; //called in worldScript
        this.$ForcedLanding = false;
    
        if( this._playerHasFighters(ship) ) {
            var bay = false;
            //Fighter Bays help to do not lose fighters which see the wormhole in its scanner
            if( ship.equipmentStatus("EQ_FIGHTERBAY") != "EQUIPMENT_UNAVAILABLE"
                || ship.equipmentStatus("EQ_FIGHTERSUPPORT") != "EQUIPMENT_UNAVAILABLE"
                || ship.equipmentStatus("EQ_FIGHTERHANGAR") != "EQUIPMENT_UNAVAILABLE" )
                bay = true;
            this.$LeftBehindFighters = 0; //will be displayed in shipExitedWormhole event
            this.$LeftBehindSystem = system.name;
            var f = this.$Fighters;
            for(var i = 0; i < f.length; i++) {
                var fi = f[i];
                if( !fi || !fi.isValid ) this.$FightersOfPlayer--; //reduce total with lost ones
                else if( fi.script && fi.script.$TimerLandNow ) {
                    this.$TimerLandNow.stop();
                    delete this.$TimerLandNow;
                    fi.script._landNow(fi, ship); //use the last moment to land before left behind
                } else if( bay && fi.position.distanceTo(ship.position) < fi.scannerRange ) {
                    //using the fighter's (and not mother's) scannerRange for King's 50km scanner
                    fi.script._landNow(fi, ship); //Fighter bay help land fighters
                } else {
                    this.$FightersOfPlayer--; //reduce total with left behind ones
                    this.$LeftBehindFighters++; //increase left behind fighters
                }
            }
            //log(this.name,"Left behind "+this.$LeftBehindFighters+" fighters in "+this.$LeftBehindSystem);
    
            this.$FightersOfPlayer = this._fighterEqsOf(player.ship);
            delete this.$Fighters;
            delete this.$Targets;
            this.$Fighters = [];
            this.$Targets = [];
            this.$LaunchedFighters = 0;  //how many fighters launched from the internal hangar
        }
    }
    
    this.shipWillLaunchFromStation = function(station) {
        this.$ForcedLanding = false;
        this._MFD(); //show mfd as early as other MFDs
    }
    
    
    //local functions
    
    this._alienItems = function(k) { //how many alien items needed to buy this fighter equipment
        if( !k || !(k.length > 0)) return(0); //for sure
        var a = 0;
        if( k.indexOf("EQ_FIGHTER_SWARM") == 0 ) a = 3;
        if( k.indexOf("EQ_FIGHTER_SHARK") == 0 ) a = 5;
        if( k.indexOf("EQ_FIGHTER_KING") == 0 ) a = 10;
        return(a);
    }
    
    this._awardFighter = function(ship, owner) { //put back into the owner's equipment list when landed
        if(ship && owner && owner.isValid) { //must not require ship.isValid!
            var sc = ship.script;
            if( sc && sc.$FighterKey ) {
                var damaged = false;
                if( sc.$FighterKey == "EQ_FIGHTER_SWARM" ) {
                    //for Swarm only - do not set King fighter to damaged when a subent is lost
                    if(!ship.subEntities || ship.subEntities.length < ship.subEntityCapacity ) {
                        if( this._eqStats(owner, sc.$FighterKey).dmg > 0 ) {
                            //there is a damamged group on board so fix that instead of award eq
                            owner.setEquipmentStatus(sc.$FighterKey, "EQUIPMENT_OK");
                            if( owner === player.ship )
                                player.consoleMessage("Damaged Swarm Fighter Groups are merged", 4.5);
                            return; //used up to fix another damaged group, so prevent award it
                        } else damaged = true; //at least one ship is lost from the group
                    }
                } else if( sc.$FighterShields ) { //heavy fighters, including King
                    var eq = ship.equipment; 
                    var i=-1;
                    if(eq) while(eq[++i]) {
                        if(ship.equipmentStatus(eq[i].equipmentKey) == "EQUIPMENT_DAMAGED") {
                            damaged = true; //if an equipment like ECM is damaged in shielded fighters
                            break;          //then set the fighter eq in owner to damaged
                        }
                    }
                } else { //if no shields nor swarm fighter group then check hull damage
                    if( sc.$FighterHull < ship.maxEnergy * 0.4 ) //damaged
                        damaged = true;
                }
                owner.awardEquipment(sc.$FighterKey);
                if(damaged) owner.setEquipmentStatus(sc.$FighterKey, "EQUIPMENT_DAMAGED");
            }
        }
    }
    
    this._damagedFighter = function(owner) { //how many damaged fighters are on board
        var damaged = [];
        damaged[0] = [0,0];//damaged fighters and needed credits for repair
        if(owner && owner.isValid) {
            for(var i = 0; i < this.$FighterKeys.length; i++) {
                var k = this.$FighterKeys[i];
                var d = this._eqStats(owner, k).dmg;
                if(d > 0) {
                    damaged[0][0] += d;
                    var c = this.$FighterCost[i]/2; //fixing cost of a fighter
                    damaged[0][1] += c;
                    damaged.push([k, d, c]); //key, damaged fighters and needed credits
                }
            }
        }
        if(damaged[0][0] > 0) return(damaged);
        else return(false);
    }
    
    this._eqStats = function(ship, eqKey) { //count damaged and ok eqs
        var ret = {ok:0, dmg:0};
        if(!ship || !ship.isValid
            || ship.equipmentStatus(eqKey) != "EQUIPMENT_DAMAGED"
                && ship.equipmentStatus(eqKey) != "EQUIPMENT_OK") 
            return(ret); //for sure
        if (0 < oolite.compareVersion("1.81")) { //for Oolite v1.80
            var e = ship.equipment;
            var len = e.length;
            if(e) for(var i=0; i < len; i++) {
                if(e[i] && e[i].equipmentKey === eqKey) {
                    var s = ship.equipmentStatus(eqKey);//report the best status
                    if( s === "EQUIPMENT_OK" ) {
                        ret.ok++;
                        ship.removeEquipment(eqKey); //must remove to uncover damaged ones
                        e = ship.equipment; //reread the shorter equipment array
                        i--; //check again this item due to the remove it was the next
                        len = e.length; //the equipment array is shorter due to the remove
                    } else if( s === "EQUIPMENT_DAMAGED" ) {
                        ret.dmg++;
                    }
                }
            }
            for(var i=0; i < ret.ok; i++) { //award back the removed ok ones
                ship.awardEquipment(eqKey);
            }
        } else { //from Oolite v1.81
            ret.ok = ship.equipmentStatus(eqKey, true).EQUIPMENT_OK;
            ret.dmg = ship.equipmentStatus(eqKey, true).EQUIPMENT_DAMAGED;
        }
        return(ret);
    }
    
    this._fightersAIState = function(state, stoplanding) { //set AI state of fighters to one defined in plist
        var p = player.ship; //AI is not applicable on player, must check to prevent error in log
        var f = this.$Fighters;
        var out = 0;
        for(var i = 0; i < f.length ; i++) {
            var fi = f[i];
            if( fi && fi.isValid && fi != p ) {
                var fai = "fightersAI.plist";
                if(fi.AI != fai ) fi.switchAI(fai);
                fi.target = null; //clear target to stop attack
    
                if( fi.script && fi.script.$ForcedLanding ) {
                    fi.AIState = "LANDING";
                } else if( !stoplanding || fi.AIState == "LANDING" ) {
                    fi.AIState = state; //FOLLOW or LANDING
                }
                out++;
            }
        }
        if( out > 0 ) this._MFD(); //refresh
        return(out); //report how many fighters affected
    }
    
    this._fightersFollow = function() { //set AI of all valid fighters to follow
        return(this._fightersAIState("FOLLOW"));
    }
    
    this._fightersLanding = function() { //start landing
        return(this._fightersAIState("LANDING"));
    }
    
    this._fightersStopLanding = function() { //stop landing only, others untouched
        return(this._fightersAIState("FOLLOW", true));
    }
    
    this._fighterEqsOf = function(ship) { //count all equipments represents fighters on the given ship
        var e, f = 0, k;
        for(var i = 0; i < this.$FighterKeys.length; i++) {
            k = this.$FighterKeys[i];
            e = this._eqStats(ship, k);
            f += e.dmg;
            f += e.ok;
        }
        return(f);
    }
    
    this._isNewTargetOrNeedMoreFighters_unused = function(ship, target) { 
        //deprecated, unused function
        if( !target || !target.isValid ) return false;
        //count nearby escorting fighters
        var ships = ship.checkScanner(true);
        //max. 32 ships are returned so this way allow to launch more than the limit if really needed
        //due to some escorts will not be in the returned 32 so leave space to launch another
        var escorts = 0;
        for( var i = 0; i < ships.length; i++ ) {
            var nearbyship = ships[i];
            if( nearbyship && nearbyship.isValid ) {
                if( this.$Fighters.indexOf(nearbyship) > -1 ) {
                    escorts++;
                    if( escorts > 24 ) return false; //there are enough fighters nearby
                } else {
                    if( ship.escortGroup.containsShip(nearbyship) )
                        ship.escortGroup.removeShip(nearbyship); //make room for sure
                }
            }
        }
        return true; //too few fighters escorting so launch more
    }
    
    this._isNewTargetOrNeedMoreFighters_old = function(ship, target) {
        //deprecated function, unused due to can not provide proper limit of launched fighters
        minTargeters = 3; //how many active fighters must keep target lock to avoid additional launch
        if( !target || !target.isValid || target.script && target.script.$FighterOwner == ship ) return false;
        if( this.$Targets[target.entityPersonality] != target ) {
            this.$Targets[target.entityPersonality] = target;
            return true; //new target, launch a fighter group
        }
        var targeters = 0; //how many active fighters has target lock on this attacker
        var f = this.$Fighters;
        for(var i = 0; i < f.length; i++) {
            var fi = f[i];
            if( fi && fi.isValid && fi.target == target ) {
                targeters++;
                if( targeters >= minTargeters ) return false; //there are enough fighters on this target
            }
        }
        return true; //too few fighters follow this target so launch more
    }
    
    this._isStation = function(entity) {
        return this._isStation2(entity, player.ship);
    }
    
    this._isStation2 = function(entity, ship) {
        return (entity && entity.isValid && entity.isStation
            && entity.target != ship
            && entity.dataKey.indexOf("carrier") == -1 );
    }
    
    this._isValidFighterTarget = function(target, owner) {
        if( target && target.isValid && !target.isDerelict
            //check hasHostileTarget but this is never set for player
            && ( target.hasHostileTarget || target == player.ship )
            && ( !owner || !owner.isValid || //next conditions need valid owner
                target != owner  //do not target its own mothership
                //do not target friendly figters
                && ( !target.script || target.script.$FighterOwner != owner )
                //distance from owner is not too much
                && target.position.distanceTo(owner.position) < owner.scannerRange
                ) ) {
            return(target); //valid target for fighters
        } else return(false);
    }
    
    this._launchFighters = function(target, forced) {
        var ship = this.ship;
        if( !ship ) ship = player.ship; //called in worldScript
        if( !target || !target.isValid || target.isDerelict || target === ship //for sure
            || ship === player.ship && !this._playerHasFighters(ship)) return;
        //continue if NPC or player use fighters
    
        if( !forced && !this._isValidFighterTarget(target, ship) ) //forced mean launch without valid target
            return;//skip invalids like own fighters
        this.$LastForced = forced;//for timed launches in red alert
    
        if(target) {
            if(target.maxSpeed > 0) //exclude stations but include mobile dockables
                ship.addDefenseTarget(target); //turrets and thargoid lasers will fire
            this._reconsiderTargets(ship, target); //update targets of launched fighters
        }
    
        if( !this.$LaunchInProgress && this.$LaunchedFighters < this.$MaxFighters
            && ( ship != player.ship || this.$LaunchedFighters < this.$FightersOfPlayer
                 && ( forced || !ship.withinStationAegis || player.alertCondition === 3 ) ) ) {
            //launch more fighters, must place far below the owner to avoid kill by collision at injector speed
    
            var scoop = true, bay = true;
            if( ship.equipmentStatus("EQ_FIGHTERBAY") != "EQUIPMENT_OK"
                && ship.equipmentStatus("EQ_FIGHTERSUPPORT") != "EQUIPMENT_OK"
                && ship.equipmentStatus("EQ_FIGHTERHANGAR") != "EQUIPMENT_OK" )
                bay = false;
            if( !bay && ship.equipmentStatus("EQ_FUEL_SCOOPS") != "EQUIPMENT_OK"
                && ship.equipmentStatus("EQ_CARGO_SCOOPS") != "EQUIPMENT_OK" )
                scoop = false;
    
            var heavy, key, ships;
            var pos = ship.position.add(ship.vectorUp.multiply( -ship.collisionRadius ));
            for(var i = this.$FighterKeys.length-1; i >= 0; i--) { //launch heavy fighters first
                key = this.$FighterKeys[i];
                var eqst = ship.equipmentStatus(key);
                if( eqst === "EQUIPMENT_OK" ) { //prevent launch of damaged fighters
                    if( EquipmentInfo.infoForKey( key ).requiredCargoSpace >= 10 ) { //heavy
                        if( !bay ) continue; //will not launch heavy fighters without undamaged fighter bay
                        else heavy = true; //from 10t it is a heavy fighter, will get ECM
                    } else {
                        if( !scoop ) continue; //will not launch light fighters without scoop or fighter bay
                        else heavy = false; //below 10t it is a light fighter
                    }
                    ships = system.addShips(key, 1, pos, 0); //eq key must be in roles in shipdata.plist
                    if(ships && ships[0]) break;
                }
            }
    
            var name = "Fighter";
            var oldlf = this.$LaunchedFighters;
            if(ships && ships[0]) for(var i = 0; i < ships.length; i++) {
                var si = ships[i];
                if(si) {
                    ship.removeEquipment(key); //reduce fighter number in F5 screen
                    this.$Fighters.push(si);
                    this.$LaunchedFighters++;
                    this.$LaunchInProgress = true;
                    if(target) {
                        this.$LastTarget = target;
                        si.target = target;
                        si.addDefenseTarget(target);
                    }
                    si.displayName = si.name+" #"+this.$LaunchedFighters;
                    name = si.name;
                    if(!si.script) si.script = "fighters-ship.js"; //for sure
                    if(si.script) {
                        si.script.$FighterKey = key;
                        si.script.$FighterNumber = this.$LaunchedFighters;
                        si.script.$FighterOwner = ship; //prevent fighters targeting each other
                        si.script.$TrailsDisabled = 1; //reduce load with Trails OXP
                    }
                    ship.escortGroup.addShip(si);
                    //if(si.AI == "escortAI.plist") si.AIState = "NEXT_TARGET";
                    si.velocity = ship.velocity.add(ship.vectorForward.multiply(500)); //initial push
                    //horizontal line formation with 50m space between ships at -75, -25, 25 and 75m
                    //si.position = pos.add(ship.vectorRight.multiply(50*(i-(ships.length-1)/2)));
                    si.orientation = ship.orientation;
                    if(key != "EQ_FIGHTER_SWARM") si.awardEquipment("EQ_ESCAPE_POD"); //swarm is unpiloted
                    if(heavy) {
                        si.awardEquipment("EQ_ECM");
                        si.awardEquipment("EQ_HEAT_SHIELD");
                    }
                    si.performAttack();
                    if( ship == player.ship ) {
                        si.bounty = 0; //remove bounty from fighter given in shipdata.plist
                        si.scannerDisplayColor1 = [0, 0.4, 0]; //green, mean frendly
                        //si.scannerDisplayColor2 = [0, 0.4, 0]; //green, mean frendly
                    } else {
                        if( ship.bounty == 0 ) si.bounty = 0; //remove bounty from fighter given in shipdata.plist
                        si.displayName += " of "+ship.name; //not owned by the player
                    }
                }
            }
            if( this.$LaunchInProgress && !this.$TimerDelay ) {//disable future launches until delay
                this.$TimerDelay = new Timer(this, this._TimedDelay.bind(this), this.$LaunchDelay, this.$LaunchDelay);
            }
            if( ship === player.ship && this.$LaunchedFighters > oldlf ) {
                var newf = this.$LaunchedFighters - oldlf;
                var remain = this.$FightersOfPlayer - this.$LaunchedFighters;
                var t = "";
                if(target) t = " to "+target.name;
                player.consoleMessage(name+" launched"+t, 4.5);
            }
        }
    }
    
    this._mass = function(mass) { //format ship mass to display it in MFD
        if(mass > 0) {
            if(mass > 1000000000) return(Math.floor(mass/1000000000)+"Mt");
            if(mass > 1000000) return(Math.floor(mass/1000000)+"kt");
            return(Math.ceil(mass/1000)+"t"); //ceil needed by EscortDeck and ships under 1t
        } else return("");
    }
    
    this._maxFighters = function(ship) {
        var m = this.$MaxFightersIfNoBay; //base value
        if( ship.equipmentStatus("EQ_FIGHTERHANGAR") == "EQUIPMENT_OK" ) m = 80; //in equipment.plist also
        else if( ship.equipmentStatus("EQ_FIGHTERSUPPORT") == "EQUIPMENT_OK" ) m = 40; //in equipment.plist also
        else if( ship.equipmentStatus("EQ_FIGHTERBAY") == "EQUIPMENT_OK" ) m = 16; //in equipment.plist also
        return(m);
    }
    
    this._nameWithMass = function(t) {  //format ship name and mass to display it in MFD
        if( !t || !t.isValid ) return("");
        return( t.displayName + " (" + this._mass(t.mass)+")" );
    }
    
    this._playerHasFighters = function(ship) { //check if this ship is a player ship and hold at least one fighter
        if( ship && ship.isValid && ship == player.ship ) {
            if( this.$FightersOfPlayer > 0 ) return true;
        }
        return false;
    }
    
    this._reconsiderTargets = function(owner, target) { //all fighters pick targets
        target = this._isValidFighterTarget(target, owner);
        var f = this.$Fighters;
        var invalidfighters = 0;
        var oldtargetistargetofowner = 0; //how many fighters attacked the owner's target before reconsider
        var ownertarget;
        if( owner ) {
            if( owner == player.ship && player.alertCondition == 1 ) { //green alert
                this.alertConditionChanged(1, 1); //keep up landing, do not target anything
                this._MFD(); //update if owner is the player
                return;
            }
            ownertarget = this._isValidFighterTarget(owner.target, owner);
        }
    
        var firedist = this.$FireDist; //fighters accept new target within this distance
        for(var i = 0; i < f.length; i++) {
            var fi = f[i];
            if( fi && fi.isValid ) {
                var oldtarget = fi.target; //call performAttack at the end if change
    
                if( !owner && fi.script ) {
                    owner = fi.script.$FighterOwner; //for sure
                    if( owner ) ownertarget = this._isValidFighterTarget(owner.target, owner);
                }
                if( owner && owner.isValid ) {
                    if( fi.target && fi.target.isValid && fi.target == ownertarget ) {
                        oldtargetistargetofowner++;
                    }
                    //forget current target?
                    if( fi.position.distanceTo(owner.position) > 20000 //go back to owner
                        || !this._isValidFighterTarget(fi.target, owner) ) {
                        fi.target = null;
                    }
                }
    
                //if no target then aim the nearest defensive target?
                var mindist = fi.scannerRange;
                if( owner && owner.isValid && ( !fi.target || !fi.target.isValid ) ) {
                    var d = owner.defenseTargets;
                    if( d && d.length > 0 ) {
                        var dmin = -1; //index of nearest defensive target
                        for(var j = 0; j < d.length; j++) {
                            var dj = d[j]; //need valid non-stationary target
                            if( this._isValidFighterTarget(dj, owner) && dj.maxSpeed > 0 ) {
                                var df = dj.position.distanceTo(fi.position);
                                if( df < mindist ) {
                                    mindist = df; //nearest to this fighter
                                    dmin = j;
                                }
                            }
                        }
                        if( dmin > -1 ) {
                            fi.target = d[dmin];
                            fi.addDefenseTarget(d[dmin]);
                        }
                    }
                }
    
                //if still no target or owner's tagret is nearer and within firedist then aim it
                if( ownertarget && ownertarget != fi.target && ownertarget != fi ) {
                    //only the half of fighters attack the mother's target - removed
                    //&& ( i - invalidfighters ) * 2 < f.length - invalidfighters
                    var aim = false;
                    if( !fi.target || !fi.target.isValid ) aim = true;
                    else {
                        var otd = ownertarget.position.distanceTo(fi.position);
                        if( otd < firedist && otd < mindist ) {
                            fi.target = ownertarget; //attack the mother's target (except friends)
                            fi.addDefenseTarget(ownertarget);
                        }
                    }
                }
    
                //only the half of fighters attack the mother's target - removed
                //if( fi.target && fi.target.isValid && fi.target == ownertarget ) {
                //    if( oldtargetistargetofowner*2 > f.length - invalidfighters
                //        && target && target.isValid ) {
                //        fi.target = null;
                //    }
                //}
    
    
                //aim the new target?
                if( target && target != fi && target != fi.target ) {
                    var aim = false;
                    if( !fi.target || !fi.target.isValid ) aim = true;
                    else {
                        var ftd = fi.target.position.distanceTo(fi.position);
                        var td = target.position.distanceTo(fi.position);
                        //replace any target if the given new target is more than 5km nearer
                        if( ftd - 5000 > td ) aim = true;
                        //also if new target is within Pulse Laser range and old is almost outside
                        else if( ftd < firedist && td > firedist ) aim = true;
                    }
                    if( aim ) {
                        fi.target = target; //lock on the newest aggressor
                        fi.addDefenseTarget(target);
                    }
                }
    
                if(fi.target) { //attack the target
                    if(fi.AI == "fightersAI.plist" ) fi.setAI("interceptAI.plist");
                    if(fi.target != oldtarget) fi.performAttack();
                } else { //follow the owner at last resort
                    if( fi.AI == "interceptAI.plist" ) fi.exitAI(); //step back to fighterAI
                    if( fi.AI == "fightersAI.plist" && fi.AIState != "LANDING" ) fi.AIState = "FOLLOW";
                }
            } else invalidfighters++; //count lost fighters to exclude from total when calculate half of group
        }
        if( owner && owner == player.ship ) { //tell the result of reconsider to the player
            if( owner.target && owner.target.isValid ) {
                //count how many fighters target the same ship than owner
                var currenttargetistargetofowner = 0;
                for(var i = 0; i < f.length; i++) {
                    var fi = f[i];
                    if( fi && fi.isValid && fi.target == owner.target) {
                        currenttargetistargetofowner++;
                    }
                }
                if( currenttargetistargetofowner > 0
                    && currenttargetistargetofowner != oldtargetistargetofowner ) {
                    var s = "";
                    if( currenttargetistargetofowner > 1 ) s = "s";
                    player.consoleMessage( currenttargetistargetofowner
                        +" fighter"+s+" aim "+owner.target.name, 3 );
                }
            }
            this._MFD(); //update if owner is the player
        }
    }
    
    
    this._FCB = function( delta ) { //FrameCallBack for sniperlock of Kings and Sharks
        var f = this.$Fighters;
        for(var i = 0; i < f.length; i++) {
            var fi = f[i];
            if( fi && fi.isValid && fi.target && fi.target.isValid
                && fi.script && fi.script.$SniperLock && fi.AIState != "LANDING"
                && fi.AIState != "FLEE" && fi.AIState != "FLEE_FOR_CLOAKED"
                && fi.AI != "fleeQMineAI.plist" && fi.energy > fi.maxEnergy/2
                && fi.target.position.distanceTo(fi.position) < 6000 ) { //exclude far targets
                var v = fi.target.position.subtract( fi.position ).direction();
                var angle = fi.heading.angleTo(v);
                var r = Math.min(fi.maxPitch*delta, angle);//rotation speed is limited by maxPitch
                var c = fi.heading.cross(v).direction(); //set the plane where we should rotate in
                fi.orientation = fi.orientation.rotate( c, -r );
                //log(this.name, fi.displayName+" rotated by "+r);
            }
        }
    }
    
    this._MFD = function() { //update Fighters MFD
        var p = player.ship; if( !p || !p.isValid ) return;
    
        var f = this.$Fighters;
        var t = []; //targets
        var n = []; //number of fighters on each targets
        var free = 0; //fighters without target
        var lost = 0; //lost fighters since last dock
        for(var i = 0; i < f.length; i++) {
            var fi = f[i];
            if( fi && fi.isValid ) {
                if( fi.target && fi.target.isValid
                    //&& fi.target.position.distanceTo(p.position) < 25600 //exclude far targets
                    ) {
                    var j = t.indexOf(fi.target);
                    if( j == -1 ) {
                        j = t.length;
                        t.push(fi.target);
                    }
                    if( n[j] > 0 ) n[j]++; //one more fighter lock on this target
                    else n[j] = 1; //this is the first fighter who lock on this target
                } else free++; //no target
            } else lost++; //launched but destroyed
        }
    
        var mfd = "Fighters: ";
        if( free > 0 ) mfd += free+" free, ";
        var stay = this.$FightersOfPlayer - this.$LaunchedFighters;
        var d = this._damagedFighter(p);
        if( d ) {
            mfd += (stay-d[0][0])+"+"+d[0][0]; //damaged ones after +
        } else if( this.$FightersOfPlayer > 0 ) {
            mfd += stay;
            //if(this.$LaunchedFighters == 0) mfd += " stay";
        } else mfd += "no one";  // none on Cobra Mark III
        mfd += " in hangar";//+p.name;
        if( lost > 0 ) mfd += ", "+lost+" lost";
        mfd += "\n";
    
        var pti = -1; //player target's index in t array
        var pt = p.target;
        if( pt && pt.isValid && (!pt.script || pt.script.$FighterOwner != p) ) {
            pti = t.indexOf(pt);
            var ptn = "None";
            if( pti > -1 ) ptn = n[pti];
            var ne = "";
            if( pt.isDerelict ) ne = "derelict ";
            else if( !pt.hasHostileTarget ) ne = "neutral ";
            mfd += ptn+" aim your "+ne+"target "+this._nameWithMass(pt)+"\n";
        }
    
        var max = 9;  //max. 9 lines left for targets in MFD
        for(var i = 0; i < t.length && i < max; i++) {
            if( i != pti ) { //exclude player's target due to already displayed as first target
                mfd += n[i]+" aim "+this._nameWithMass(t[i])+"\n";
            }
        }
        //while( i++ < max) mfd += "\n"; //scroll down to the last line
    
        if( p.setMultiFunctionText ) p.setMultiFunctionText(this.$MFDName, mfd, true);
    }
    
    this._MFDAlign = function(left, right) { //insert as many spaces between as fill the line
            var width = 14.2;
            var space = " ";
            left += space;
            var t = left + right;
            while( defaultFont.measureString(t) < width ) {
                    left += space;
                    t = left + right;
            }
            //fine tune with hair spaces - from spara's marketObserver
            var hairSpace = String.fromCharCode(31);
            while ( defaultFont.measureString(t + hairSpace) < width ) {
                    left += hairSpace;
                    t = left + right;
            }
            return(t);
    }
    
    this._MFDFrom = function(whomposition) { //direction mark
            var ship = player.ship;
            if( !ship || !ship.isValid || !whomposition || !whomposition.dot ) return ("");
            var v = ship.position.subtract(whomposition);
            var fw = ship.vectorForward.angleTo(v);
            var ri = ship.vectorRight.angleTo(v);
            var up = ship.vectorUp.angleTo(v);
            var s = ""; // refine if out of front 1 degree cone
            if( ri < 1.56 ) s += "<";
            if( up > 1.58 ) s += "^";
            else if( up < 1.56 ) s += "v";
            if( ri > 1.58 ) s += ">";
            return( s );
    }
    
    this._TimedDelay = function() {
            this.$LaunchInProgress = false; //enable fighter launch again
            var p = player.ship;
            var ship = this.ship;
            if( !ship ) ship = p; //called in worldScript
            if( ship == p && //player in red alert of forced launch
                ( this.$LastForced || player.alertCondition == 3 && player.alertHostiles )
                || ship.target && ship.target.hasHostileTarget ) {//NPC in attack mode
                this._launchFighters(this.$LastTarget, this.$LastForced); //launch next fighter
            } else {
                this.$LastTarget = null;
                if( this.$TimerDelay ) {
                    this.$TimerDelay.stop();
                    delete this.$TimerDelay;
                }
            }
    }