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

Expansion Star Destroyer

Content

Warnings

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

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Imperial-class Star Destroyer is probably the largest and most powerful warship which is available in a few Shipyards, for astronomical cost. Longer than a station but can dock using a Shuttle or ILS. Hold 30 turrets and 72 TIE Fighters which counterattack automatically and cheap to replace. Imperial-class Star Destroyer is probably the largest and most powerful warship which is available in a few Shipyards, for astronomical cost. Longer than a station but can dock using a Shuttle or ILS. Hold 30 turrets and 72 TIE Fighters which counterattack automatically and cheap to replace.
Identifier oolite.oxp.staer9.StarDestroyer oolite.oxp.staer9.StarDestroyer
Title Star Destroyer Star Destroyer
Category Ships Ships
Author Staer9, GGShinobi, Norby Staer9, GGShinobi, Norby
Version 1.6 1.6
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
  • oolite.oxp.Thargoid.Bigships:0
  • oolite.oxp.Thargoid.Bigships:
  • Conflict Expansions
    Information URL http://wiki.alioth.net/index.php/Star_Destroyer n/a
    Download URL https://wiki.alioth.net/img_auth.php/2/20/StarDestroyer_1.6.oxz n/a
    License CC BY-NC-SA 4 CC BY-NC-SA 4
    File Size n/a
    Upload date 1610873245

    Documentation

    Also read http://wiki.alioth.net/index.php/Star%20Destroyer

    readme.txt

    This is a modified version of Staer9's awesome Star Destroyer.
    
    Changes in v1.6 by Norby:
    -Fixed for Trophy Collector OXP, thanks to montana05.
    
    Changes in v1.5 by Norby:
    -Fighters got hull value instead of zero recharge to fix exhausted fighters.
    -Fighters not owned by the player show owner in the name.
    -Interstellar Tweaks OXP supported by naval fighters with blue scanner color.
    -Ball turrets range increased to 7000m.
    -Fixed Shuttle model in Oolite 1.80.
    
    Changes in v1.4 by Norby:
    -Player version added, available from TL12 and cost 100 million credits.
    -30 turrets oriented around the hull in each 45 degree so no holes in the defensive sphere.
    -TIE Fighters and TIE Interceptors added from StarWars OXP as escorts of Star Destroyers.
    -There are 72 fighters in internal hangar which intercept attackers.
    -Player get replacement fighters at arrival screen in the next station for cheap cost.
    -Hexagon escort formation around Star Destroyers, following the Imperial logo.
    -Fighter groups and actual targets are listed in MFD.
    -Fighters land back in green alert, tractor beam will help to arrive into the hangar.
    -Fighters must land before hyperjump, you should cancel countdown if time is not enough.
    -TIE Interceptors require advanced Imperial-class Star Destroyer which cost 200MCr.
    -Can dock to stations by Shuttle or ILS (cautiously) if put target lock to the station.
    -Most ship stats are set between Staer9's original and GGShinobi's nightmare version.
    -New gray textures made in MaPZone, new normal map and shaders added.
    -Contain Heavy Sniper Gun mounts in forward and aft views for Sniper Gun OXP.
    -Spawn script removed, new pirate roles added and bigTrader role for BigShips OXP.
    -Packaged into OXZ format for in-game expansion manager.
    
    
    Previous version by GGShinobi:
    stardestroyerV1.3_inofficial_nightmare_version_0.0.3_2013_03_03.oxp.zip
    
    ========
    Changes:
    ========
    - reduced manoeuverability, but higher top speed.
    - massively increased energy recharge rate and max energy
    - high probabilty that it has advanced equipment like military scanner filters or shield boosters
    - massively increased offensive potential: in addition to the already present turrets, it now has
      military lasers (fore/aft) and thargoid lazers (sides), giving it the ability to attack targets
      from far away and also shoot targets at medium range regardless of angle (thargoid lazers).
    - many missiles, and it uses them!
    - many escape capsules which you might scoop
    - there is a chance that the star destroyer doesn't merely explode, but that the reactor core is
      hit, which will result in a massive explosion similar to the detonation of a Q-Bomb! The crew of
      the Star Destroyer will broadcast a desperate message if that happens, which will give you enough
      time to clear the blast radius (if you are lucky)
    
    ========
    Credits:
    ========
    stardestroyer: Staer9
    modifications by GGShinobi:
      - changes to shipdata.plist
      - additional files:
        stardestroyer.js
        doomedStarDestroyer.js
        stardestroyer_reactorBreachAI.plist
        descriptions.plist
    
    ========
    License:
    ========
    The work of GGShinobi is licensed under the Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/3.0/
    
    

    Equipment

    Name Visible Cost [deci-credits] Tech-Level
    TIE Fighter yes 10000 1+
    TIE Fighter Factory yes 100000 1+
    TIE Interceptor yes 20000 1+
    TIE Interceptor Factory yes 200000 1+

    Ships

    Name
    Star Destroyer Heavy Sniper Gun
    Star Destroyer
    Imperial Star Destroyer
    Black Star Destroyer
    stardestroyer-player
    stardestroyer-player2
    Shuttle
    Star Destroyer Turret
    TIE Fighter
    TIE Interceptor
    Star Destroyer [reactor breach detected]

    Models

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

    Scripts

    Path
    Scripts/doomedStarDestroyer.js
    "use strict";
    this.name           = "doomed_stardestroyer";
    this.author         = "GGShinobi";
    this.copyright      = "© 2013 GGShinobi, Creative Commons: attribution, non-commercial, sharealike.";
    this.description    = "some special actions for the stardestroyer.";
    this.version        = "0.0.0";
    
    // detonate subentites.
    // uses this.$number2BlowUp
    this.$detonateSubents = function() {
      for (subent in this.subEntities) {
        if (this.$number2BlowUp === 0 || ! this.subEntities[subent]) return;
        this.subEntities[subent].explode();
        this.$number2BlowUp--;
      }
    }
    
    // make the death a little more spectacular by detonating the subentities:
    // detonate subentites every 0.5 seconds
    // params: doomedStarDestroyer: the ship of which the entities are to be blown up
    this.$startSubDetonations = function(doomedStarDestroyer) {
      if (doomedStarDestroyer.subEntities) {
        doomedStarDestroyer.$number2BlowUp = doomedStarDestroyer.subEntities.length/8;
        // 5 seconds countdown till detonation, but Timer starts only after 1 second => 4 (* 2 -> every 0.5 seconds)
        doomedStarDestroyer.$detonationTimer = new Timer(doomedStarDestroyer, doomedStarDestroyer.$detonateSubents, 1, 0.5);
        log("doomed stardestroyer", this + ": number2BlowUp: " + doomedStarDestroyer.$number2BlowUp + " (subentities: " + doomedStarDestroyer.subEntities.length + ")");
      } else {
        log("doomed stardestroyer", ship + ": has no subentities.");
      }
      doomedStarDestroyer.entityDestroyed = function() {
        log(this, "cleaning up detonation timer: ", this.$detonationTimer);
        if (this.$detonationTimer) {
          if (this.$detonationTimer.isRunning) this.$detonationTimer.stop();
          delete this.$detonationTimer;
        }
      }
    }
    
    // cleanup after ship has been destroyed
    this.entityDestroyed = function() {
      log(this, "cleaning up detonation timer: ", this.$detonationTimer);
      if (this.$detonationTimer) {
        if (this.$detonationTimer.isRunning) this.$detonationTimer.stop();
        delete this.$detonationTimer;
      }
    }
    
    Scripts/spawn.js-
    // Configuration -- customize here 
    this.role = "stardestroyer"; 
    this.count = 4; 
    
    // Standard attributes 
    this.name           = "Spawn-" + this.role; 
    this.author         = "Staer9"; 
    this.copyright      = "This script is hereby placed in the public domain."; 
    this.version        = "1"; 
    this.description    = "Script to make several ships of a given role appear at the witchpoint after every jump." 
    
    
    this.shipWillLaunchFromStation = function() 
    { 
        system.legacy_addSystemShips(this.role, this.count, 1.0); 
        log("testscript.spawn", "Generated " + this.count + " " + this.role + " for testing purposes."); 
    } 
    
    Scripts/stardestroyer-eqcond.js
    "use strict";
    this.name        = "stardestroyer_eqcond";
    this.author      = "Norby";
    this.copyright   = "2016 Norbert Nagy";
    this.licence     = "CC BY-NC-SA 3.0";
    this.version     = "1.0";
    
    this.allowAwardEquipment = function(eqKey, ship, context) {
        if (eqKey == "EQ_TIE_F" || eqKey == "EQ_TIE_I" ) {
            if( ship == player.ship && player.ship.docked
                && player.ship.dockedStation && player.ship.dockedStation.hasShipyard ) {
                var w = worldScripts.stardestroyer;
                if( w.$FightersOfPlayer < w.$MaxFighters )
                    return true;
            }
        } else if (eqKey == "EQ_TIE_FF" ) {
            if( ship.dataKey == "stardestroyer" || ship.dataKey ==  "stardestroyer-player" )
                return true;
        } else if (eqKey == "EQ_TIE_IF" ) {
            if( ship.dataKey == "stardestroyer-pirate" || ship.dataKey == "stardestroyer-pirate2"
                || ship.dataKey ==  "stardestroyer-player2" )
                return true;
        }
        return false;
    }
    
    Scripts/stardestroyer-fighter.js
    "use strict";
    this.name        = "stardestroyer-fighter";
    this.author      = "Norby";
    this.copyright   = "2016 Norbert Nagy";
    this.licence     = "CC BY-NC-SA 4.0";
    this.description = "support functions for fighters";
    
    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 = false; //cache of this.ship.scriptInfo.npc_shields
    this.$OwnerScoopPos = null; //store the mothership's scoop position for landing
    this.$TimerLandNow = null; //trigger _landNow() after some time for sure landing
    this.$ws = worldScripts.stardestroyer;
    
    //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.shipTakingDamage = function(amount, whom, type) {
            this.$FighterHull -= amount; //workaround recharge when no shields
            if( !this.$FighterShields ) {
                    if(this.$FighterHull < 0) {
                        delete this.shipTakingDamage;//prevent infinite loop
                        this.ship.explode();
                    }
            }
    }
    
    this.shipSpawned = function() {
            if( this.ship && this.ship.scriptInfo && this.ship.scriptInfo.npc_shields == "no" ) {
                    this.$FighterShields = false;
            } else this.$FighterShields = true;
    }
    
    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 == player.ship) return; //exit if worldScript, do in ship script only
            //var w = worldScripts.escortdeck;
    
            //increase steering and acceleration in proportion with the smaller mass on deck
            //w.$EscortDeck_SetMaxs( player.ship, w );
    
            //var pad = w.$EscortDeckShip.indexOf(this.ship);
            //if( pad > -1 ) w.$EscortDeckShipData[pad] = null; //prevent recreation in dock
            
            if( whom && whom.isValid && whom != player.ship && this.$FighterOwner == player.ship ) {
                    player.commsMessage("A "+this.ship.name+" is lost.", 5);
                    this.$ws._MFD(); //update numbers in MFD
            }
            log(this.name, this.ship.displayName+" terminated by "+whom+" "+why);
    }
    
    
    //fighter functions
    
    this._escortPosition = function() {this._escortPosition2(this.ship);} //for stardestroyer-fighterAI.plist
    
    this._escortPosition2 = function(ship) {
            var sc = ship.script;
            if( !sc || !sc.$FighterNumber || !sc.$FighterOwner || !sc.$FighterOwner.isValid
                || ship == player.ship ) return; //for sure
            ship.scanClass = "CLASS_BUOY";//for scoop deactivation at stopped landing and no masslock
    
            if( sc.$FighterOwner == player.ship && player.alertCondition == 1 ) { //green alert
                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) {
            var sc = ship.script;
            if( !sc || ship == player.ship ) return; //for sure
            ship.scanClass = "CLASS_CARGO";//for scoop activation
    
            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(200).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
            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/stardestroyer-shuttle.js
    "use strict";
    this.name	= "stardestroyershuttle";
    this.author	= "Norby";
    this.copyright	= "2016 Norby";
    this.description= "Ship script for stardestroyershuttle";
    this.licence	= "CC BY-NC-SA 4.0";
    
    this.$FighterOwner = null; //store the mothership
    
    //ship script events
    this.shipCollided = function(otherShip) {
    	//prevent destruction, at least sometimes
    	if(otherShip == this.$FighterOwner) this.ship.energy = this.ship.maxEnergy;
    }
    
    this.shipDied = function() {
            if( this.$FighterOwner == player.ship ) {
                    var w = worldScripts.stardestroyer;
                    if(this.ship == w.$stardestroyershuttle)
                            w.$stardestroyershuttle = null;
            } else if(this.$FighterOwner && this.$FighterOwner.script
                    && this.ship == this.$FighterOwner.script.$stardestroyershuttle) {
                    this.$FighterOwner.script.$stardestroyershuttle = null;
            }
    
    }
    
    this.shipWillDockWithStation = function(station) {
            if( this.$FighterOwner == player.ship ) {
                    var w = worldScripts.stardestroyer;
    	        if(this.ship == w.$stardestroyershuttle) {
    		        w.$stardestroyershuttle = null;
                    	player.ship.target = null;
                            station.commsMessage("Your Shuttle arrived to "+station.name, player.ship);
    	                station.dockPlayer();
                    }
            } else if(this.$FighterOwner && this.$FighterOwner.script
                    && this.ship == this.$FighterOwner.script.$stardestroyershuttle) {
                    this.$FighterOwner.script.$stardestroyershuttle = null;
                    this.$FighterOwner.target = station; //NPC SD launch another Shuttle for ambience
            }
            this.ship.remove(true);
    }
    Scripts/stardestroyer.js
    "use strict";
    this.name           = "stardestroyer";
    this.author         = "GGShinobi, Norby";
    this.copyright      = "© 2013 GGShinobi, Creative Commons: attribution, non-commercial, sharealike.";
    this.description    = "some special actions for the stardestroyer.";
    
    // CHANGELOG:
    // changes from 0.0.2 to 0.0.3: - added player ship handler, fighters and ILS support by Norby
    // changes from 0.0.1 to 0.0.2: - added "throw_sparks = yes;" to wrecked stardestroyer in shipdata.plist
    // changes from 0.0.0 to 0.0.1: - fixed bug that orientation of doomedStarDestroyer didn't match the orientation
    //   of the original stardestroyer. Thanx to cim!
    
    //this script works as NPC ship script and also worldScript for the player
    
    //settings
    this.$FighterCost = [1000, 2000]; //cost of TIE Fighter and Interceptor, equipment.plist also!
    this.$LaunchDelay = 1; //delay between launch of fighter groups in seconds
    this.$MaxFighters = 72; //capacity of the internal fighter hangar
    this.$MFDName = "Star Destroyer 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.$Fighters = []; //Active, launched fighters
    this.$FightersOfPlayer = 0; //actually how many figters are in player's star destroyer
    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.$simulator = false; //detect if combat simulator is running
    this.$stardestroyershuttle = null; //player docking support ship
    this.$Targets = []; //Ships once targeted by fighters, indexed by entityPersonality
    this.$TimerDelay = null; //for fighter launch delay
    
    
    //event handlers
    
    this.startUp = function() {
        this.$FightersOfPlayer = missionVariables.$StarDestroyerFightersOfPlayerPlusOne;
        if( !this.$FightersOfPlayer ) this.$FightersOfPlayer = this.$MaxFighters;
        else this.$FightersOfPlayer--; //missionVariables store one more to avoid 0
    
        var h = worldScripts.hudselector;
        if( h && h.$HUDSelectorAddMFD ) h.$HUDSelectorAddMFD(this.name, this.$MFDName);
        
        var e, p = player.ship;
        e="EQ_DTADAM"; if(p.equipmentStatus(e)==="EQUIPMENT_OK") p.removeEquipment(e);//fix for towbar
        e="EQ_DTADER"; if(p.equipmentStatus(e)==="EQUIPMENT_OK") p.removeEquipment(e);//fix for towbar
        e="EQ_DTAEMP"; if(p.equipmentStatus(e)==="EQUIPMENT_OK") p.removeEquipment(e);//fix for towbar
        e="EQ_DTAMIN"; if(p.equipmentStatus(e)==="EQUIPMENT_OK") p.removeEquipment(e);//fix for towbar
        e="EQ_DTAUSA"; if(p.equipmentStatus(e)==="EQUIPMENT_OK") p.removeEquipment(e);//fix for towbar
        e="EQ_DTAWEA"; if(p.equipmentStatus(e)==="EQUIPMENT_OK") p.removeEquipment(e);//fix for towbar
    }
    
    this.startUpComplete = function() {
        var t = worldScripts.trophy_col;
        if(t) { //remove bogus trophy entry
            var n = "Star Destroyer [reactor breach detected]";
            for (var j = t.$trophyArray.length - 1; j >= 0; j--) {
                if (t.$trophyArray[j][0] === n) t.$trophyArray.splice(j,1);
            }
            for (var j = t.$trophyLog.length - 1; j >= 0; j--) {
                if (t.$trophyLog[j][0] === n) t.$trophyLog.splice(j,1);
            }
        }
    }
    
    this.alertConditionChanged = function(newCondition, oldCondition) {
        var p = player.ship;
        var ship = this.ship;
        if( !ship ) ship = p; //called in worldScript
        if( !this._playerInStarDestroyer(ship) || player.docked ) return;
        this._MFD();
    
        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 == 2 || newCondition == 3 && player.alertHostiles ) {
            this._fightersStopLanding(); //cancel landing in yellow or red alert
            this._reconsiderTargets(p, p.target); //update targets of launched fighters
        }
    }
    
    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) {
            //6 groups in hexagon form like the Imperial Logo, for 6*4=24 fighters
            //the ring of groups always show the xz plane of system due to zero second coordinates
            var positions = [new Vector3D(0,0,1), new Vector3D(0,0,-1),
                             new Vector3D(-0.8,0,0.5), new Vector3D(0.8,0,0.5),
                             new Vector3D(-0.8,0,-0.5), new Vector3D(0.8,0,-0.5)];
            var space = 100; //m space between escorts in the same group
            var zoom = 3000; //m, radius of group positions around the main ship
    
            //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 24 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.helpRequestReceived = function(ally, enemy) {
        this._launchFighters(enemy);
    }
    
    this.playerBoughtEquipment = function(equipmentKey) {
        if(equipmentKey === "EQ_TIE_F" || equipmentKey === "EQ_TIE_I") {
            player.ship.removeEquipment(equipmentKey);
            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, hangar is full.";
            player.consoleMessage("You have "+this.$FightersOfPlayer+f, 5);
        }
    }
    
    this.playerCancelledJumpCountdown = function() {  //stop landing
        if(!this._playerInStarDestroyer(player.ship)) return;
        this._fightersFollow();
    }
    
    this.playerJumpFailed = function(reason) {
        if(!this._playerInStarDestroyer(player.ship)) return;
        this._fightersFollow();
    }
    
    this.playerStartedJumpCountdown = function(type, seconds) { //landing fighters
        if(!this._playerInStarDestroyer(player.ship)) return;
        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) {
            missionVariables.$StarDestroyerFightersOfPlayerPlusOne = 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.shipCollided = function(otherShip) {
        var p = player.ship;
        var ship = this.ship;
        if( !ship ) ship = p; //called in worldScript
        if( !this._playerInStarDestroyer(ship) ) return;
    
        if( this._isStation(otherShip) ) {
            //check if facing to a station - must exclude non-facig to skip collisions at launch
            var angle = ship.heading.angleTo( otherShip.position.subtract(ship.position) );
            otherShip.commsMessage("Docking sequence initiated", player.ship); //angle); //debug
            if( angle < 1.5 ) otherShip.dockPlayer(); //this help needed by ILS OXP to dock Star Destroyer
        }
    }
    
    
    // sometimes, the reactor core of the stardestroyer breaches, resulting in a gigantic explosion
    // of a destructive force equivalent to that of a q-bomb! Beware!!
    this.shipDied = function(whom, why) {
    //  if (Math.random() > 0.77) { // chance for reactor breach: 33 %
        // this.ship.spawn("stardestroyer_reactor_breach", 1);
        if(!this.ship || !this.ship.position) return;
    
        if(this.ship.primaryRole == "pirate-aegis-raider" //aegis raider should not blow up the station
            || this.ship.dataKey == "stardestroyer-pirate2") //no black reactor breach ship in shipdata
            return;
    
        var doomedStarDestroyer = system.addShips("stardestroyer_reactor_breach", 1, this.ship.position, 0)[0];
        doomedStarDestroyer.orientation = this.ship.orientation; // change orientation to that of the original ship
        doomedStarDestroyer.velocity = this.ship.velocity;
        doomedStarDestroyer.script.$startSubDetonations(doomedStarDestroyer);
    //  }
    }
    
    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._playerInStarDestroyer(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 ) {
            if( !this._playerInStarDestroyer(ship) ) return;
            this._MFD();
            this._reconsiderTargets(ship, t); //update targets of launched fighters
        }
    
        if( this._isStation2( t, ship ) //launch shuttle if station is targeted
            //player also must have no undamaged ILS equipment on board (NPC SD always send Shuttle)
            && ( ship != p || p.equipmentStatus("EQ_ILS") != "EQUIPMENT_OK" ) ) {
            var s = this.$stardestroyershuttle;
            if( s && s.isValid && s.target != t ) s.target = t;
            if( !s && ( ship != p || !p.missilesOnline ) ) {
                s = ship.spawnOne("[stardestroyershuttle]");
                if( s && s.isValid ) {
                    this.$stardestroyershuttle = s;
                    s.target = t;
                    var msg = "";
                    if( !s.target ) {
                        msg = "Your Shuttle is not launched due to no friendly station nearby";
                        if(ship == p) player.consoleMessage(msg, 4.5);
                        s.remove(true);
                    } else {
                        msg = "Your Shuttle is going to "+s.target.name;
                        if(ship == p) player.consoleMessage(msg, 10);
                        s.position = ship.position.add(ship.vectorUp.multiply(-150));
                        s.orientation = ship.orientation;
                        if( s.injectorSpeedFactor > 0 ) 
                            s.desiredSpeed = s.injectorSpeedFactor * s.maxSpeed;
                        else s.desiredSpeed = 7 * s.maxSpeed; //for Oolite 1.80
                        s.velocity = ship.velocity.add(ship.vectorForward.multiply(700)); //initial push
                        if(!s.script) s.script = "stardestroyer-shuttle.js"; //for sure
                        if(s.script) s.script.$FighterOwner = ship;//store owner for ship script
                        ship.target = s; //lock to the shuttle: player can see it, NPC avoid to dock by ILS
                    }
                }
            }
        }
    }
    
    this.shipTargetLost = function(t) {
        var p = player.ship;
        var ship = this.ship;
        if( !ship ) ship = p; //called in worldScript
        if( this._playerInStarDestroyer(ship) ) {
            this._MFD();
        }
    }
    
    this.shipWillDockWithStation = function(station) {
        var p = player.ship;
        var ship = this.ship;
        if( !ship ) ship = p; //called in worldScript
        
        if( this._playerInStarDestroyer(ship) ) {
            this.$stardestroyershuttle = null; //player docking support ship
    
            // if this was a simulator run, reset the variable and return - no docking fee in this case
            if (this.$simulator == true) {
                this.$simulator = false;
            } else if( ship.equipmentStatus("EQ_TIE_FF") != "EQUIPMENT_OK"
                        && ship.equipmentStatus("EQ_TIE_IF") != "EQUIPMENT_OK" ) {
                var s = ""; if( this.$FightersOfPlayer > 1 ) s = "s"; //current fighters in plural
                player.addMessageToArrivalReport(expandDescription("[stardestroyer-factorydamaged]",
                        {fightersofplayer: this.$FightersOfPlayer, plural:s, maxfighters:this.$MaxFighters}));
            } else {
                var itc = 0; //index of TIE Fighter's cost in this.$FighterCost array
                if( ship.equipmentStatus("EQ_TIE_IF") == "EQUIPMENT_OK" )
                    itc = 1; //index of TIE Interceptor's cost when ride Imperial variant of SD
                var fee = this.$FighterCost[itc];
                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 fi.remove(true); //do not leave fighters outside of the station
                }
                var bf = 0; //bought fighters
                var lf = 0; //lost fighters
                for(var i = this.$FightersOfPlayer; i < this.$MaxFighters; i++) {
                    lf++;
                    if (player.credits > fee) { // make sure the player has enough credits
                        player.credits -= fee; //buy a fighter
                        this.$FightersOfPlayer++;
                        bf++;
                    }
                }
                var cbf = fee * bf; //cost of bougth fighters
                var s = ""; if( lf > 1 ) s = "s"; //lost fighters in plural
                var s2 = ""; if( this.$FightersOfPlayer > 1 ) s2 = "s"; //current fighters in plural
                if( !(lf > 0) ) {
                    //no lost fighters, have a nice trip
                    var s = " is"; if( this.$MaxFighters > 1 ) s = "s are"; //fighters in plural
                    player.addMessageToArrivalReport(expandDescription("[stardestroyer-allfighters]",
                        {maxfighters:this.$MaxFighters, plural:s}));
                } else if( !station || !station.isValid || !station.hasShipyard ) {
                    //no shipyard, no replace
                    player.addMessageToArrivalReport(expandDescription("[stardestroyer-noshipyard]",
                        {sdlostfighters:lf, plural:s, maxfighters:this.$MaxFighters}));
                } else if( bf == lf ) {
                    //bought all missing fighters
                    player.addMessageToArrivalReport(expandDescription("[stardestroyer-boughtfighters]",
                        {sdlostfighters:lf, plural:s, costofbougthfighters:cbf,
                            fightersofplayer: this.$FightersOfPlayer, plural2:s2}));
                } else if( bf > 0 ) {
                    //bought some but not all missing fighters
                    player.addMessageToArrivalReport(expandDescription("[stardestroyer-notenoughmoney]",
                        {sdlostfighters:lf, plural:s, costofbougthfighters:cbf, numberofbougthfighters:bf,
                            fightersofplayer: this.$FightersOfPlayer, plural2:s2}));
                } else if( lf >= this.$MaxFighters && bf == 0 ) {
                    //all fighters are missing but none bought
                    player.addMessageToArrivalReport(expandDescription("[stardestroyer-emptyhangar]",
                        {sdlostfighters:lf, plural:s, fightercost: fee}));
                } else if( bf == 0 ) {
                    //at least one fighter is missing but none bought
                    player.addMessageToArrivalReport(expandDescription("[stardestroyer-nomoneyforone]",
                        {sdlostfighters:lf, plural:s, fightersofplayer: this.$FightersOfPlayer, plural2:s2}));
                } else {
                    //not a real chance to arrive here, just display the current fighter status for sure
                    player.addMessageToArrivalReport(expandDescription("[stardestroyer-noshipyard]",
                        {sdlostfighters:lf, plural:s, 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
    
        if( this._playerInStarDestroyer(ship) ) {
            this.$stardestroyershuttle = null; //player docking support ship
    
            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, p); //use the last chance to land before left behind
                } 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);
    
            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) {
        var p = player.ship;
        var ship = this.ship;
        if( !ship ) ship = p; //called in worldScript
    
        if( this._playerInStarDestroyer(ship) ) {
            p.position = p.position.add(p.vectorForward.multiply(700)); //prevent collisions at launch
            this._MFD(); //show mfd as early as other MFDs
        }
    }
    
    
    //local functions
    
    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 fighterai = "stardestroyer-fighterAI.plist";
                if(fi.AI != fighterai ) fi.switchAI(fighterai);
                fi.target = null; //clear target to stop attack
    
                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._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) {
        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._playerInStarDestroyer(ship)) return;
        //continue if NPC or player in Star Destroyer
    
        if( !this._isValidFighterTarget(target, ship) ) return;//skip invalids like own fighters
    
        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
                 && ( !ship.withinStationAegis || player.alertCondition == 3 ) ) ) {
            //&& this._isNewTargetOrNeedMoreFighters(ship, target) ) {
            //launch more fighters, must far below to avoid kill by collision at injector speed
            var pos = ship.position.add(ship.vectorUp.multiply( -200 ));
            var tiekey = "tiefighter";
            if( ship == player.ship && ship.equipmentStatus("EQ_TIE_IF") != "EQUIPMENT_UNAVAILABLE"
                || ship != player.ship && ship.dataKey.indexOf("stardestroyer-pirate") == 0 ) //NPC ISD
                tiekey = "tieinterceptor"; //only in Imperial-class Star Destroyers
            else if( ship.dataKey.indexOf("IST_destroyer") == 0 )
                tiekey = "IST_tiefighter"; //naval, asked by UK_Eliter for Interstellar Tweaks OXP
            var ties = Math.min(this.$MaxFighters-this.$LaunchedFighters,4); //launch 4 fighters at once
    
            var ships = system.addShips(tiekey, ties, pos, 50);
            var oldlf = this.$LaunchedFighters;
            if(ships) for(var i = 0; i < ships.length; i++) {
                var si = ships[i];
                if(si) {
                    this.$Fighters.push(si);
                    this.$LaunchedFighters++;
                    this.$LaunchInProgress = true;
                    si.displayName = si.name+" #"+this.$LaunchedFighters;
                    if(!si.script) si.script = "stardestroyer-fighter.js"; //for sure
                    if(si.script) {
                        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;
                    si.target = target;
                    si.performAttack();
                    if( ship == player.ship ) {
                        si.bounty = 0; //remove bounty from fighter given in shipdata.plist
                        //si.setBounty(0, "player's stardestroyer launched a fighter");
                        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.displayName; //not owned by the player
                        if(ship.dataKey.indexOf("stardestroyer-pirate2") == 0 )
                            si.scannerDisplayColor1 = [0, 0, 0.4]; //darkblue
                    }
                }
            }
            if( this.$LaunchInProgress && !this.$TimerDelay ) {//disable future launches until delay
                this.$TimerDelay = new Timer(this, this._TimedDelay.bind(this), this.$LaunchDelay);
            }
            if( ship == player.ship && this.$LaunchedFighters > oldlf ) {
                var newf = this.$LaunchedFighters - oldlf;
                var remain = this.$FightersOfPlayer - this.$LaunchedFighters;
                player.consoleMessage(newf+" fighters launched to "+target.name+", "+remain+" remain", 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._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._playerInStarDestroyer = function(ship) {
        if( ship && ship.isValid && ship == player.ship ) {
            if( ship.dataKey.indexOf("stardestroyer-player") == 0 )
                return true; //detect both stardestroyer-player and stardestroyer-player2 ships
            else this.$FightersOfPlayer = 0; //no TIE fighters on other ship types
        }
        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);
        }
    
        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 owner's tagret?
                if( !fi.target && ownertarget && ownertarget != fi.target
                    && ownertarget != fi //do not target itself
                    //only the half of fighters should attack the mother's target
                    //&& ( i - invalidfighters ) * 2 < f.length - invalidfighters
                    ) {
                    fi.target = ownertarget; //attack the mother's target (except friends)
                }
    
                //only the half of fighters attack the mother's target
                //if( fi.target && fi.target.isValid && fi.target == ownertarget ) {
                //    if( oldtargetistargetofowner*2 > f.length - invalidfighters
                //        && target && target.isValid ) {
                //        fi.target = null;
                //    }
                //}
    
                //if still no target then aim the nearest defensive target?
                if( owner && owner.isValid && ( !fi.target || !fi.target.isValid ) ) {
                    var d = owner.defenseTargets;
                    if( d && d.length > 0 ) {
                        var mindist = fi.scannerRange;
                        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];
                    }
                }
    
                //aim the new target?
                if( target && target != fi && ( !fi.target || !fi.target.isValid
                    //replace any target if the given new target is more than 10km nearer
                    || fi.target.position.distanceTo(fi.position) - 10000
                            > target.position.distanceTo(fi.position) ) ) {
                    fi.target = target; //lock on the newest aggressor
                }
    
                if(fi.target) { //attack the target
                    if(fi.AI == "stardestroyer-fighterAI.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 == "stardestroyer-fighterAI.plist" ) 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._MFD = function() { //update Star Destroyer 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;
        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
            if( this.$TimerDelay ) {
                    this.$TimerDelay.stop();
                    delete this.$TimerDelay;
            }
    }