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