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