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

Expansion Jaguar Company

Content

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Adds in an Elite group of pilots to patrol the space lanes in Anarchy, Feudal and Multi-Government systems. Jaguar Company can also be found sometimes in interstellar space. Adds in an Elite group of pilots to patrol the space lanes in Anarchy, Feudal and Multi-Government systems. Jaguar Company can also be found sometimes in interstellar space.
Identifier oolite.oxp.Tricky.Jaguar_Company oolite.oxp.Tricky.Jaguar_Company
Title Jaguar Company Jaguar Company
Category Mechanics Mechanics
Author Tricky Tricky
Version 2.5.29 2.5.29
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Information URL http://wiki.alioth.net/index.php/Jaguar_Company n/a
Download URL https://wiki.alioth.net/img_auth.php/1/1b/Jaguar_Company_2.5.29.oxz n/a
License CC BY-NC-SA 3.0 CC BY-NC-SA 3.0
File Size n/a
Upload date 1610873242

Documentation

Also read http://wiki.alioth.net/index.php/Jaguar%20Company

Equipment

Name Visible Cost [deci-credits] Tech-Level
Jaguar Company Black Box yes 100000 101+
Small ECM Hardened Missile yes 5000 101+

Ships

Name
jaguar_company_asteroid
Jaguar Company Base Docking Bay
jaguar_company_base_buoy_beacon
jaguar_company_base_buoy_no_beacon
jaguar_company_base_buoy_subent
Jaguar Company Base Buoy
jaguar_company_base_discount
jaguar_company_base_discount_and_docking
jaguar_company_base_no_discount
Jaguar Company Base
Jaguar Company Base Ball Turret
jaguar_company_base_turret2
jaguar_company_base_turret3
jaguar_company_base_turret4
jaguar_company_base_turret5
jaguar_company_base_turret6
jaguar_company_base_turret7
Jaguar Company Base Ball Turret Holders
jaguar_company_boulder
ECM Hardened Missile
Small ECM Hardened Missile
Jaguar Company Patrol Ship - Forward Weapon
Jaguar Company Patrol Ship - Forward Weapon (Scuffed)
Jaguar Company Mining Ship
jaguar_company_patrol
jaguar_company_patrol_scuffed
Jaguar Company Splinter Ship
Jaguar Company
Tracker: Jaguar Company Patrol
jaguar_company_transporter_template
Jaguar Company Tug

Models

This expansion declares no models.

Scripts

Path
Scripts/jaguar_company.js
/*jslint bitwise: true, es5: true, newcap: true, nomen: true, regexp: true, unparam: true, todo: true, white: true,
indent: 4, maxerr: 50, maxlen: 120 */
/*jshint boss: true, curly: true, eqeqeq: true, eqnull: true, es5: true, evil: true, forin: true, laxbreak: true,
loopfunc: true, noarg: true, noempty: true, strict: true, nonew: true, undef: true */
/*global Math, JSON, Object, System, Timer, Vector3D, expandDescription, expandMissionText, galaxyNumber, log, mission,
missionVariables, oolite, player, system, worldScripts */

/* Jaguar Company
 *
 * Copyright © 2012-2013 Richard Thomas Harrison (Tricky)
 *
 * This work is licensed under the Creative Commons
 * Attribution-Noncommercial-Share Alike 3.0 Unported License.
 *
 * To view a copy of this license, visit
 * http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter
 * to Creative Commons, 171 Second Street, Suite 300, San Francisco,
 * California, 94105, USA.
 *
 * World script to setup Jaguar Company.
 */

(function () {
    "use strict";

    /* Standard public variables for OXP scripts. */
    this.name = "Jaguar Company";
    this.author = "Tricky";
    this.copyright = "© 2012-2013 Richard Thomas Harrison (Tricky)";
    this.license = "CC BY-NC-SA 3.0";
    this.description = "Script to initialise the Jaguar Company.";
    this.version = "2.5";

    /* Private variables. */
    var p_main = {},
    p_const = {};

    /* This should really be defined as a const, but Notepad++ jsLint doesn't like that.
     * Set 'configurable' so that they can be deleted by $killSelf().
     */
    Object.defineProperties(p_const, {
        "shipNames" : {
            value : [
                /* OU's, GOU's, LOU's and (d)ROU's. Also some names I really like. */
                "Profit Margin", "Trade Surplus", "Limiting Factor", "Gunboat Diplomat", "Zealot", "Xenophobe",
                "God Told Me To Do It", "Just Another Victim Of The Ambient Morality", "Synchronize Your Dogmas",
                "Thank you And Goodnight", "Well I Was In The Neighbourhood", "You'll Thank Me Later",
                "Shoot Them Later", "Attitude Adjuster", "Killing Time", "I Blame Your Mother", "I Blame My Mother",
                "Heavy Messing", "Frank Exchange Of Views", "Nuisance Value",
                "All Through With This Niceness And Negotiation Stuff", "I Said, I've Got A Big Stick",
                "Hand Me The Gun And Ask Me Again", "But Who's Counting?", "Germane Riposte",
                "We Haven't Met But You're A Great Fan Of Mine", "All The Same, I Saw It First",
                "Ravished By The Sheer Implausibility Of That Last Statement", "Zero Credibility",
                "Charming But Irrational", "Demented But Determined", "You May Not Be The Coolest Person Here",
                "Lucid Nonsense", "Awkward Customer", "Conventional Wisdom", "Fine Till You Came Along",
                "I Blame The Parents", "Inappropriate Response", "A Momentary Lapse Of Sanity", "Lapsed Pacifist",
                "Reformed Nice Guy", "Pride Comes Before A Fall", "Injury Time", "Now Look What You've Made Me Do",
                "Kiss This Then", "Eight Rounds Rapid", "You'll Clean That Up Before You Leave", "Me, I'm Counting",
                "The Usual But Etymologically Unsatisfactory", "Falling Outside The Normal Moral Constraints",
                "Hylozoist", "No One Knows What The Dead Think", "Flick to Kick", "Your Egg's Broken But Mine Is Ok",
                "Shall I Be Mummy?", "Is This Galaxy Taken?", "Famous Last Words", "Road Rage", "Live A Little",
                "Not in My Back Yard", "Playing A Sweeper", "You're Going Home In A Fracking Ambulance", "Rear Entry",
                "Open Wide, Say Aaaarrgghhh", "Hope You Like Explosions", "I Haven't Seen One Of Those For Years",
                "Are You Religious?", "Not Now Dear", "Something Had To Be Done",
                "Hideously Indefensible Sense Of Humour", "Camouflage",
                "Come And Have A Go If You Think You're Hard Enough", "Throwing Toys Out The Crib",
                "Podex Perfectus Es", "Stercorem Pro Cerebro Habes", "Futue Te Ipsum Et Caballum Tuum",
                "Remember To Wash Your Hands", "One Out All Out", "Looking At Me, Pal?",
                "You Showed Me Yours, Now I'll Show You Mine", "Salt In Your Vaseline", "Cracking My Knuckles",
                "Break Glass In Case Of War", "My Turn", "No Pun Intended", "Look No Hands", "Very Sharp Stick",
                "Weapons Of Mass Deception", "...And Another Thing", "Clerical Error", "Silly Mid On",
                "You And Whose Army?", "This Sector Ain't Big Enough For The Both Of Us",
                "Diplomacy Was Never My Strong Suite", "Such A Pretty Big Red Button",
                "Synthetic Paragon Rubber Company", "Forget And Fire", "I Was Just Following Orders",
                "Weapon of Mass Distraction", "Forgive and Forget", "Innocence Is No Excuse",
                "Psychosis Is Only One State Of Mind", "Lets Dance", "AI Avenger", "Dead Man Walking",
                "A Little Less Conversation", "Here One Minute, Gone The Next", "Here, Let Me Escort You",
                "Killed With Superior Skill", "External Agitation", "Catch Me If You Can",
                "But What About The Children?", "Single Fingered Hand Gestures", "A World Of Hurt",
                "Looking Down The Gun Barrel", "Terminal Atomic Headache", "Know Thy Enemy",
                "Cold Steel For An Iron Age", "The Malevolent Creation", "Gamma Ray Goggles", "End Of Green",
                "Terrorwheel", "Sickening Sense Of Humour", "Mines Bigger", "Friendly Fire Isn't",
                "No Need For Stealth", "All Guns Blazin!", "Harmony Dies", "The Controlled Psychopath", "It Ends Now",
                "Forced To Be Nice", "Axis of Advance", "Acts of God", "The Feeling's Mutual",
                "The Beautiful Nightmare", "If You Can Read This...", "Are You Saved?", "Cunning Linguist",
                "Gay Abandon", "My Finger", "Got Legs", "Hose Job", "Protect And Sever", "Rebuttal", "Not In The Face",
                "I Have Right Of Way", "It Ran Into My Missile", "Have A Nice Rest Of Your Life", "Nose Job",
                "Get My Point?", "Grid Worker", "Eraserhead", "What Star?", "All This (And Brains)",
                "Random Acts Of Senseless Violence", "God Will Recognize His Own",
                "Would You Like A Quick Suppository With That?",
                "Pop Me A Couple More Of Those Happy Pills (Eccentric)", "Trouble Maker?", "Talk Is Cheap",
                "Tightly Strung", "Have You Kept The Receipt?", "It Was Broke When I Got Here",
                "Insanity Plea Rejected", "Thora Hird", "Barbara Cartland", "Freddy Starr Ate My Hamster",
                "And You Thought You Knew What Terror Means", "I'm A 'Shoot First, Ask Questions Later' Kinda Guy",
                "Duck You Suckers", "Trumpton Riots", "Dodgy Transformer"
            ],
            writable : false,
            configurable : true,
            enumerable : true
        },
        "snoopersErrorCodes" : {
            value : [
                /* Warnings. */
                "Snoopers buffer is full (max 10 news).",
                "No free storing slot available.",
                "CRC buffer is full.",
                "CRC is still active.",
                "Caller already sent a message (1 news per worldScript).",
                /* News inserted successfully. */
                "Success.",
                /* Error messages. */
                "Required properties not found (ID and Message).",
                "Unknown properties passed.",
                "To few or too much passed properties (at least 2).",
                "Request from invalid caller (no worldScript).",
                "Property 'Message' not a string (wrong type).",
                "Property 'Message' too short or too long (expected >10 and <700 chars).",
                "Property 'Message' starts with whitespace (\\f \\t \\r \\n or space).",
                "Property 'Message' - Sent message not expandable.",
                "Property 'Message' - Number of opening brackets doesn't match number of closing brackets.",
                "Property 'Message' - Expanded key (descriptions.plist) too long (limit 700 chars).",
                "Property 'Message' - Expanded key (missiontext.plist) too long (limit 700 chars).",
                "Property 'Message' - Expanded Message too long.",
                "Property 'Message' - Word with overlength detected (limit 79 chars).",
                "Property 'Message' - To many linebreaks (limit 10).",
                "Property 'Agency' - not valid (expected number in range 1 - 3).",
                "Property 'Priority' - not valid (expected number in range 1 - 3).",
                "Property 'Pic' - wrong type (expected string).",
                "Property 'Pic' - not a valid fileextension.",
                "Property 'Music' - wrong type (expected string).",
                "Property 'Music' not a valid fileextension.",
                "Property 'Model' - wrong type (expected string).",
                "Property 'Pos' - wrong type (expected array).",
                "Property 'Pos' - wrong number of arguments (expected 3 numbers).",
                "Property 'Pos' - contains NaN.",
                "Property 'Ori' - wrong type (expected number or array).",
                "Property 'Ori' - not valid (expected 1, 2, 4 or 8).",
                "Snoopers was shutdown. Requirements not fullfilled.",
                "Player not valid anymore.",
                "Player not docked while trying to display a direct mission screen.",
                "Attempt to override a missionscreen blocked."
            ],
            writable : false,
            configurable : true,
            enumerable : true
        },
        "defaultPlayerVar" : {
            /* Default player variables. */
            value : {
                attacker : false,
                delayedAward : null,
                locationsActivated : [false, false, false, false, false, false, false, false],
                newsForSnoopers : [],
                reputation : [0, 0, 0, 0, 0, 0, 0, 0],
                visitedBase : false
            },
            writable : false,
            configurable : true,
            enumerable : true
        }
    });

    /* Public constants. */
    Object.defineProperties(this, {
        /* value will be 'true' if using Oolite v1.77 and newer, false if older. */
        "$gte_v1_77" : {
            value : (0 >= oolite.compareVersion("1.77")),
            writable : false,
            configurable : true,
            enumerable : true
        },
        /* Maximum number of Jaguar Company patrol ships allowed. */
        "$maxPatrolShips" : {
            value : 4,
            writable : false,
            configurable : true,
            enumerable : true
        },
        /* Seed for the pseudo random number generator.
         * Affects generation of Jaguar Company and placement of the base.
         */
        "$salt" : {
            value : 19720231,
            writable : false,
            configurable : true,
            enumerable : true
        },
        /* Minimum reputation to be considered a helper. Equivalent to 5 observed hits. */
        "$reputationHelper" : {
            value : 5,
            writable : false,
            configurable : true,
            enumerable : true
        },
        /* Minimum reputation to use the black box. Equivalent to 3 kills. */
        "$reputationBlackbox" : {
            value : 30,
            writable : false,
            configurable : true,
            enumerable : true
        },
        /* Minimum reputation to see the locations for Jaguar Company. Equivalent to 5 kills. */
        "$reputationLocations" : {
            value : 50,
            writable : false,
            configurable : true,
            enumerable : true
        },
        /* Set value to 'true' to use visual effects. Ignored if using Oolite v1.76.1 and older. */
        "$visualEffects" : {
            value : true,
            writable : false,
            configurable : true,
            enumerable : true
        }
    });

    /* Public variables used by OXP Config. */
    /* Turn logging on or off */
    this.$logging = false;
    /* Report AI messages for Jaguar Company if true */
    this.$logAIMessages = false;
    /* Log extra debug info. Only useful during testing. */
    this.$logExtra = false;
    /* Spawn Jaguar Company always if true */
    this.$alwaysSpawn = false;

    /* OXPConfig settings. */
    this.oxpcSettings = {
        Info : {
            Name : this.name,
            Display : "Jaguar Company",
            Notify : true,
            InfoB : "Development frontend for the Jaguar Company OXP."
        },
        Bool0 : {
            Name : "$logging",
            Def : false,
            Desc : "Turn logging on or off."
        },
        Bool1 : {
            Name : "$logAIMessages",
            Def : false,
            Desc : "Log AI messages if true."
        },
        Bool2 : {
            Name : "$logExtra",
            Def : false,
            Desc : "Log extra debug info if true."
        },
        Bool3 : {
            Name : "$alwaysSpawn",
            Def : false,
            Desc : "Always spawn Jaguar Company if true."
        }
    };

    /* Other public variables. */

    /* Setup player variables. */
    this.$playerVar = p_const.defaultPlayerVar;

    /* World script event handlers. */

    /* NAME
     *   startUp
     *
     * FUNCTION
     *   We only need to do this once.
     *   This will get redefined after a new game or loading of a new Commander.
     */
    this.startUp = function () {
        var cabalScript = worldScripts.Cabal_Common_Functions,
        cclVersion,
        attacker,
        delayedAward,
        locationsActivated,
        reputation,
        visitedBase,
        name,
        counter,
        length;

        if (!cabalScript || cabalScript.Cabal_Common === 'undefined') {
            this.$killSelf(" -> Cabal Common Library is missing.");

            return;
        }

        this.$ccl = new cabalScript.Cabal_Common();
        cclVersion = this.$ccl.internalVersion;

        if (cclVersion < 14) {
            this.$killSelf(" -> Cabal Common Library is too old for any Oolite version.");

            return;
        }

        if (cclVersion === 14 && this.$gte_v1_77) {
            /* Oolite v1.77 and newer. */
            this.$killSelf(" -> Cabal Common Library is too old for Oolite v1.77 (and newer Oolite versions).");

            return;
        }

        if (cclVersion > 14 && !this.$gte_v1_77) {
            /* Oolite v1.76.1 and older. */
            this.$killSelf(" -> Cabal Common Library is too new for Oolite v1.76.1 (and older Oolite versions).");

            return;
        }

        /* Find the shortest name length. */
        this.$shortestNameLength = -1;
        length = p_const.shipNames.length;

        for (counter = 0; counter < length; counter += 1) {
            name = p_const.shipNames[counter];

            if (this.$shortestNameLength === -1 || name.length < this.$shortestNameLength) {
                this.$shortestNameLength = name.length;
            }
        }

        if (missionVariables.jaguar_company) {
            /* Retrieve the player variable and parse it. */
            this.$playerVar = JSON.parse(missionVariables.jaguar_company);
            /* Clean the player variable. */
            this.$cleanPlayerVariable();
        } else {
            /* Convert old mission variables. */
            attacker = missionVariables.jaguar_company_attacker;
            delayedAward = missionVariables.jaguar_company_reputation_post_launch;
            locationsActivated = missionVariables.jaguar_company_locations_activated;
            reputation = missionVariables.jaguar_company_reputation;
            visitedBase = missionVariables.jaguar_company_visited_base;

            if (attacker !== null) {
                this.$playerVar.attacker = attacker;

                delete missionVariables.jaguar_company_attacker;
            }

            if (delayedAward !== null) {
                if (delayedAward) {
                    this.$playerVar.delayedAward = delayedAward;
                }

                delete missionVariables.jaguar_company_reputation_post_launch;
            }

            if (locationsActivated !== null) {
                for (counter = galaxyNumber; counter >= 0; counter -= 1) {
                    this.$playerVar.locationsActivated[counter] = locationsActivated;
                }

                delete missionVariables.jaguar_company_locations_activated;
            }

            if (reputation !== null) {
                for (counter = galaxyNumber; counter >= 0; counter -= 1) {
                    this.$playerVar.reputation[counter] = reputation;
                }

                delete missionVariables.jaguar_company_reputation;
            }

            if (visitedBase !== null) {
                this.$playerVar.visitedBase = visitedBase;

                delete missionVariables.jaguar_company_visited_base;
            }

            /* Stringify the player variable and store it. */
            missionVariables.jaguar_company = JSON.stringify(this.$playerVar);
        }

        /* Setup the private main variable + some public variables. */
        this.$setUp();
        /* Remove join navy variable. */
        p_main.joinNavy = null;
        /* Remove the closest naval ship variable. */
        p_main.closestNavyShip = null;
        /* Check if we need to create Jaguar Company in this system. Delay it. */
        this.$setUpCompanyTimerReference = new Timer(this, this.$setUpCompany, 2);
        /* Add the interface system if Oolite v1.77 and newer is used. */
        this.$addInterface();

        log(this.name + " " + this.version + " loaded.");

        /* No longer needed after setting up. */
        delete this.startUp;
    };

    /* NAME
     *   playerWillSaveGame
     *
     * FUNCTION
     *   Player is about to save the game.
     */
    this.playerWillSaveGame = function () {
        /* Clean the player variable. */
        this.$cleanPlayerVariable();
        /* Stringify the player variable and store it. */
        missionVariables.jaguar_company = JSON.stringify(this.$playerVar);
    };

    /* NAME
     *   shipWillLaunchFromStation
     *
     * FUNCTION
     *   Player is about to launch from a station.
     */
    this.shipWillLaunchFromStation = function () {
        /* Remove the interface system if Oolite v1.77 and newer is used. */
        this.$removeInterface();
    };

    /* NAME
     *   shipLaunchedFromStation
     *
     * FUNCTION
     *   Player launched from a station.
     *
     * INPUT
     *   station - entity of the station
     */
    this.shipLaunchedFromStation = function (station) {
        var delayedAward;

        if (station.hasRole("jaguar_company_base")) {
            /* Reset welcomed flag on launch from base. */
            p_main.playerWelcomed = false;

            delayedAward = this.$playerVar.delayedAward;

            if (typeof delayedAward === "number") {
                /* Add on any reputation awarded on docking with an escape pod. */
                this.$playerVar.reputation[galaxyNumber] += delayedAward;
                this.$playerVar.delayedAward = null;
            }
        }
    };

    /* NAME
     *   shipDockedWithStation
     *
     * FUNCTION
     *   Player docked with a station.
     */
    this.shipDockedWithStation = function () {
        var rescuedNames,
        lastName;

        if (this.$snoopersRescued.length) {
            if (this.$snoopersRescued.length === 1) {
                /* Send rescued news for the pilot the player brought in to Snoopers. */
                this.$sendNewsToSnoopers(expandDescription("[jaguar_company_rescue_news]", {
                        jaguar_company_pilot_name : this.$snoopersRescued.shift()
                    }));
            } else {
                /* Send rescued news for the multiple pilots the player brought in to Snoopers. */
                lastName = this.$snoopersRescued.pop();
                rescuedNames = this.$snoopersRescued.join(", ") + " and " + lastName;
                this.$snoopersRescued = [];
                this.$sendNewsToSnoopers(expandDescription("[jaguar_company_rescue_multiple_news]", {
                        jaguar_company_pilot_names : rescuedNames
                    }));
            }
        }

        /* Add the interface system if Oolite v1.77 and newer is used. */
        this.$addInterface();
    };

    /* NAME
     *   shipWillExitWitchspace
     *
     * FUNCTION
     *   Player is about to exit from Witchspace.
     *   Reset everything just before exiting Witchspace.
     */
    this.shipWillExitWitchspace = function () {
        /* Stop and remove the timers. */
        this.$removeTimers();

        if (!system.shipsWithRole("jaguar_company_patrol").length) {
            /* Setup the private main variable + some public variables. */
            this.$setUp();
        } else {
            /* Followed Jaguar Company from interstellar space. */

            /* Remove the hyperspace follow co-ordinates. */
            this.$hyperspaceFollow = null;
        }

        /* Remove join navy variable. */
        p_main.joinNavy = null;
        /* Remove the closest naval ship variable. */
        p_main.closestNavyShip = null;
        /* Not visited the base. */
        this.$playerVar.visitedBase = false;
    };

    /* NAME
     *   shipExitedWitchspace
     *
     * FUNCTION
     *   Player exited Witchspace.
     */
    this.shipExitedWitchspace = function () {
        /* Check if we need to create Jaguar Company in this system. */
        this.$setUpCompany();
    };

    /* NAME
     *   playerEnteredNewGalaxy
     *
     * FUNCTION
     *   Remove some player variables if the player jumps galaxies.
     */
    this.playerEnteredNewGalaxy = function () {
        this.$playerVar.attacker = false;
    };

    /* NAME
     *   shipSpawned
     *
     * FUNCTION
     *   A ship has been born.
     *
     * INPUT
     *   whom - entity that was created
     */
    this.shipSpawned = function (whom) {
        var shipsScript = worldScripts["Jaguar Company Ships"],
        friendList;

        if (!shipsScript) {
            /* Ships world script not setup yet. */
            return;
        }

        /* Get friend roles from the ships world script. */
        friendList = shipsScript.$friendList;

        if (!friendList || friendList.indexOf(whom.entityPersonality) === -1) {
            /* Ignore non-Jaguar Company ships. */
            return;
        }

        if (this.$logAIMessages) {
            /* Turn AI reporting on for the ship. */
            whom.reportAIMessages = true;
        }
    };

    /* NAME
     *   shipDied
     *
     * FUNCTION
     *   Stop and remove the timers if the player dies.
     */
    this.shipDied = function () {
        /* Stop and remove the timers. */
        this.$removeTimers();
    };

    /* NAME
     *   missionScreenOpportunity
     *
     * FUNCTION
     *   Show a welcome message on docking as a mission screen.
     */
    this.missionScreenOpportunity = function () {
        var base = this.$jaguarCompanyBase;

        if (!base || !base.isValid) {
            /* Base not setup. */
            return;
        }

        if (player.ship.dockedStation.entityPersonality === base.entityPersonality && !p_main.playerWelcomed) {
            /* Player docked with Jaguar Company Base. */
            this.$welcomeMessage();
        }
    };

   /* NAME
     *   shipScoopedOther
     *
     * FUNCTION
     *   Player has scooped something.
     *
     * INPUT
     *   whom - entity of the scooped object
     */
    this.shipScoopedOther = function (whom) {
        if (!whom.$jaguarCompany) {
            /* Does not contain a member of Jaguar Company. */
            return;
        }

        if (this.$logging && this.$logExtra) {
            log(this.name, "shipScoopedOther::Scooped Jaguar Company member: " + whom.$pilotName);
        }

        /* Save the pilot's name that was rescued. */
        this.$pilotsRescued.push(whom.$pilotName);
        this.$snoopersRescued.push(whom.$pilotName);
    };

    /* NAME
     *   guiScreenChanged
     *
     * FUNCTION
     *   Show Jaguar Company Base locations on certain GUI screens.
     *
     * INPUTS
     *   to - GUI screen the player has gone to
     *   from - GUI screen the player has come from
     */
    this.guiScreenChanged = function (to, from) {
        var counter,
        length;

        if (player.ship.equipmentStatus("EQ_JAGUAR_COMPANY_BLACK_BOX") !== "EQUIPMENT_OK" ||
            !this.$playerVar.locationsActivated[galaxyNumber]) {
            /* No software patch uploaded to the black box. */
            return;
        }

        if (this.$gte_v1_77) {
            /* Oolite v1.77 and newer. */
            if (to === "GUI_SCREEN_LONG_RANGE_CHART") {
                /* Add the marked systems to the long range chart. */
                length = this.$jaguarCompanySystemIDs.length;

                for (counter = 0; counter < length; counter += 1) {
                    mission.markSystem({
                        system : this.$jaguarCompanySystemIDs[counter],
                        name : this.name,
                        markerColor : "orangeColor",
                        markerScale : 1.5,
                        markerShape : "MARKER_SQUARE"
                    });
                }

                player.consoleMessage("Orange coloured squares show Jaguar Company Base locations.", 5);

                if (player.ship.docked) {
                    player.consoleMessage("Press F4 for a list of Jaguar Company Base locations.", 5);
                } else {
                    player.consoleMessage("Press F7 then F5 for a list of Jaguar Company Base locations.", 5);
                }
            }

            if (from === "GUI_SCREEN_LONG_RANGE_CHART") {
                /* Remove the marked systems from the long range chart. */
                length = this.$jaguarCompanySystemIDs.length;

                for (counter = 0; counter < length; counter += 1) {
                    mission.unmarkSystem({
                        system : this.$jaguarCompanySystemIDs[counter],
                        name : this.name
                    });
                }
            }
        } else {
            /* Oolite v1.76.1 and older. */
            if (to === "GUI_SCREEN_LONG_RANGE_CHART") {
                player.consoleMessage("Press F7 then F5 for a list of Jaguar Company Base locations.", 5);
            }
        }

        if (from === "GUI_SCREEN_SYSTEM_DATA" && to === "GUI_SCREEN_STATUS") {
            if (this.$gte_v1_77 && player.ship.docked) {
                /* Oolite v1.77 and newer use the interface screen when docked. */
                return;
            }

            this.$showBaseLocations();
        }
    };

    /* NAME
     *   viewDirectionChanged
     *
     * FUNCTION
     *   Reset page count when the player view changes.
     */
    this.viewDirectionChanged = function () {
        this.$printIndex = 0;
    };

    /* Other global functions. */

    /* NAME
     *   oxpcNotifyOnChange
     *
     * FUNCTION
     *   This function is called by OXPConfig when settings are changed.
     *
     * INPUT
     *   n - number
     *     1 - boolean settings changed
     *     2 - short unsigned integers changed
     *     4 - unsigned 24Bit integer changed
     */
    this.oxpcNotifyOnChange = function (n) {
        if (n & 1 && this.$alwaysSpawn && !this.$jaguarCompanyBase) {
            /* $alwaysSpawn has been set in OXPConfig and the base doesn't exist.
             * Check if we need to create Jaguar Company in this system. Delay it.
             */
            this.$setUpCompanyTimerReference = new Timer(this, this.$setUpCompany, 2);
        }
    };

    /* NAME
     *   $setUp
     *
     * FUNCTION
     *   Setup the private main variable + some public variables.
     */
    this.$setUp = function () {
        var saveGalaxyNumber = null;

        if (typeof p_main.galaxyNumber === "number") {
            /* Save the internal galaxy number used by $cacheJaguarCompanySystems() */
            saveGalaxyNumber = p_main.galaxyNumber;
        }

        /* Initialise the p_main variable object.
         * Encapsulates all private global data.
         */
        p_main = {
            /* Initialise the available ship names. */
            availableShipNames : p_const.shipNames,
            /* Initial state of the black box ASC tracker. */
            blackboxASCActivated : false,
            /* Initial state of the black box holo-tracker. */
            blackboxHoloActivated : false,
            /* Internal galaxy number used by $cacheJaguarCompanySystems() */
            galaxyNumber : saveGalaxyNumber,
            /* Player welcomed. Used to control the mission screen display. */
            playerWelcomed : false,
            /* Current route index. */
            routeIndex : 0,
            /* Routes are initialised when Jaguar Company is spawned. */
            routes : [],
            /* Initialise main seed for galaxy 1. */
            seed : {
                w0 : 0x5a4a,
                w1 : 0x0248,
                w2 : 0xb753
            }
        };

        if (!this.$pilotsRescued || !this.$snoopersRescued) {
            /* Array of Jaguar Company pilot names that have been rescued.
             *
             *   $pilotsRescued - used when unloading pilots from their escape pods at a station
             *   $snoopersRescued - used when docked to send a report to Snoopers news services (if installed)
             */
            if (!this.$pilotsRescued) {
                this.$pilotsRescued = [];
            }

            if (!this.$snoopersRescued) {
                this.$snoopersRescued = [];
            }
        }

        /* Tracker object. */
        this.$tracker = null;
        /* Visual tracker object. */
        this.$visualTracker = null;
        /* Base has not been setup yet. */
        this.$jaguarCompanyBase = false;
        /* New base so clear this variable. */
        this.$swapBase = false;
        /* Remove the hyperspace follow co-ordinates. */
        this.$hyperspaceFollow = null;
        /* Create an array of Jaguar Company Base locations. */
        this.$cacheJaguarCompanySystems();
    };

    /* NAME
     *   $killSelf
     *
     * FUNCTION
     *   Removes all functions and variables.
     *
     * INPUT
     *   desc - description for the removal (optional)
     */
    this.$killSelf = function (desc) {
        var prop;

        if (desc && typeof desc === "string") {
            player.consoleMessage(this.name + " - Check your Latest.log", 10);
            log(this.name, this.name + " - Shutting down" + desc);
        }

        /* Delete public functions and variables. */
        for (prop in this) {
            if (this.hasOwnProperty(prop)) {
                if (prop !== 'name' && prop !== 'version') {
                    delete this[prop];
                }
            }
        }

        /* Set the deactivated flag for Cabal Common Library. */
        this.deactivated = true;

        return;
    };

    /* NAME
     *   $showProps
     *
     * FUNCTION
     *   For debugging only.
     */
    this.$showProps = function () {
        var result = "",
        prop,
        subProp,
        route,
        routeCounter,
        routeLength,
        news,
        counter,
        length;

        for (prop in this) {
            if (this.hasOwnProperty(prop)) {
                if (typeof this[prop] !== "function") {
                    if (prop !== "$playerVar") {
                        result += "this." + prop + ": " + this[prop] + "\n";
                    } else {
                        for (subProp in this.$playerVar) {
                            if (this.$playerVar.hasOwnProperty(subProp)) {
                                result += "this.$playerVar." + subProp + ": " + this.$playerVar[subProp] + "\n";
                            }
                        }
                    }
                } else {
                    result += "this." + prop + " = function ()\n";
                }
            }
        }

        for (prop in p_main) {
            if (p_main.hasOwnProperty(prop)) {
                result += "p_main." + prop + ": " + p_main[prop] + "\n";
            }
        }

        length = p_main.routes.length;

        if (length) {
            result += "Routes (" + length + ")\n";

            for (counter = 0; counter < length; counter += 1) {
                result += "#" + (counter + 1) + ") ";
                route = p_main.routes[counter];
                routeCounter = 1;
                routeLength = Object.keys(route).length;

                for (prop in route) {
                    if (route.hasOwnProperty(prop)) {
                        result += prop + ": " + route[prop] + (routeCounter === routeLength ? "\n" : ", ");
                        routeCounter += 1;
                    }
                }
            }
        }

        length = this.$playerVar.newsForSnoopers.length;

        if (length) {
            result += "News for Snoopers (" + length + ")\n";

            for (counter = 0; counter < length; counter += 1) {
                news = this.$playerVar.newsForSnoopers[counter];
                result += "#" + (counter + 1) + ") " +
                "ID: " + news.ID + ", " +
                "Message: " + news.Message + ", " +
                "Agency: " + news.Agency + "\n";
            }
        }

        log(this.name, "$showProps::\n" + result);
    };

    /* NAME
     *   $cleanPlayerVariable
     *
     * FUNCTION
     *   Clean up the player variable for loading or saving.
     */
    this.$cleanPlayerVariable = function () {
        var playerVarsProps,
        defaultPlayerVarProps,
        prop,
        counter,
        length;

        /* Get the properties of the player variables. */
        playerVarsProps = Object.keys(this.$playerVar);
        /* Get the properties of the default player variables. */
        defaultPlayerVarProps = Object.keys(p_const.defaultPlayerVar);

        /* Remove old properties. */
        for (prop in this.$playerVar) {
            if (this.$playerVar.hasOwnProperty(prop)) {
                if (defaultPlayerVarProps.indexOf(prop) === -1) {
                    /* Not a default property. */
                    delete this.$playerVar[prop];
                }
            }
        }

        /* Cache the length. */
        length = defaultPlayerVarProps.length;

        /* Add new properties. */
        for (counter = 0; counter < length; counter += 1) {
            prop = defaultPlayerVarProps[counter];

            if (playerVarsProps.indexOf(prop) === -1) {
                /* Missing a default property. */
                this.$playerVar[prop] = p_const.defaultPlayerVar[prop];
            }
        }
    };

    /* NAME
     *   $removeTimers
     *
     * FUNCTION
     *   Stop and remove the timers.
     */
    this.$removeTimers = function () {
        /* Stop and remove the script sanity timer. */
        if (this.$scriptSanityTimerReference) {
            if (this.$scriptSanityTimerReference.isRunning) {
                this.$scriptSanityTimerReference.stop();
            }

            this.$scriptSanityTimerReference = null;
        }

        /* Stop and remove the Black Box timer. */
        if (this.$blackBoxTimerReference) {
            if (this.$blackBoxTimerReference.isRunning) {
                this.$blackBoxTimerReference.stop();
            }

            this.$blackBoxTimerReference = null;
        }

        /* Stop and remove the base swap timer. */
        if (this.$baseSwapTimerReference) {
            if (this.$baseSwapTimerReference.isRunning) {
                this.$baseSwapTimerReference.stop();
            }

            this.$baseSwapTimerReference = null;
        }
    };

    /* NAME
     *   $addInterface
     *
     * FUNCTION
     *   Add the interface system if Oolite v1.77 and newer is used and
     *   docked and the software patch is uploaded to the black box (which has to be present).
     */
    this.$addInterface = function () {
        if (this.$gte_v1_77 && player.ship.docked &&
            player.ship.equipmentStatus("EQ_JAGUAR_COMPANY_BLACK_BOX") === "EQUIPMENT_OK" &&
            this.$playerVar.locationsActivated[galaxyNumber]) {
            player.ship.dockedStation.setInterface("jaguar_company_base_list", {
                title : "Jaguar Company Base locations",
                summary : "Displays a list of Jaguar Company Base locations within the current galaxy.",
                category : expandDescription("[interfaces-category-organisations]"),
                callback : this.$showBaseLocations.bind(this)
            });
        }
    };

    /* NAME
     *   $removeInterface
     *
     * FUNCTION
     *   Remove the interface system if Oolite v1.77 and newer is used.
     */
    this.$removeInterface = function () {
        if (this.$gte_v1_77 && player.ship.docked) {
            /* Oolite v1.77 and newer and docked. */
            player.ship.dockedStation.setInterface("jaguar_company_base_list", null);
        }
    };

    /* NAME
     *   $showBaseLocations
     *
     * FUNCTION
     *   Show the base locations as a 2 column list.
     */
    this.$showBaseLocations = function () {
        var choicesKey,
        locations;

        /* Initial index. */
        this.$printIndex = 0;
        /* Need to work out the first choices key before we create the list. 2 column layout. */
        choicesKey = this.$firstChoicesKey(this.$jaguarCompanySystemNames, 2);
        /* Create the list. */
        locations = this.$listNames(this.$jaguarCompanySystemNames);
        /* Display it as a mission screen. */
        mission.runScreen({
            title : "Jaguar Company Base locations",
            message : locations + "\n",
            choicesKey : choicesKey,
            /* exitScreen is ignored by Oolite v1.76.1 and older. */
            exitScreen : "GUI_SCREEN_INTERFACES"
        }, this.$locationChoices, this);
    };

    /* NAME
     *   $firstChoicesKey
     *
     * FUNCTION
     *   Figure out the first choices key for the pager.
     *   Modifies the maximum amount of lines that can be used for displaying the list.
     *
     * INPUTS
     *   list - array of strings to be displayed
     *   columns - number of columns displayed
     *
     * RESULT
     *   result - choices key
     */
    this.$firstChoicesKey = function (list, columns) {
        var choicesKey;

        if (list.length <= columns * 19) {
            /* Maximum lines available for the list on the mission screen with 1 choice and a blank line. */
            this.$lines = 19;
            /* Initial choices key. */
            choicesKey = "jaguar_company_choices_1_page";
        } else if (list.length <= 2 * columns * 18) {
            /* Maximum lines available for the list on the mission screen with 2 choices and a blank line. */
            this.$lines = 18;
            /* Initial choices key. */
            choicesKey = "jaguar_company_choices_1_of_2";
        } else {
            /* Maximum lines available for the list on the mission screen with 3 choices and a blank line. */
            this.$lines = 17;
            /* Initial choices key. */
            choicesKey = "jaguar_company_choices_start_of_many";
        }

        return choicesKey;
    };

    /* NAME
     *   $nextChoicesKey
     *
     * FUNCTION
     *   Figure out the next choices key for the pager.
     *
     * INPUTS
     *   choice - choice selected
     *   list - array of strings to be displayed
     *   columns - number of columns displayed
     *
     * RESULT
     *   result - choices key
     */
    this.$nextChoicesKey = function (choice, list, columns) {
        var choicesKey;

        if (choice === "M_1_FIRST_PAGE") {
            this.$printIndex = 0;
            choicesKey = "jaguar_company_choices_start_of_many";
        } else if (choice === "2_1_NEXT_PAGE") {
            this.$printIndex = columns * this.$lines;
            choicesKey = "jaguar_company_choices_2_of_2";
        } else if (choice === "M_1_NEXT_PAGE") {
            this.$printIndex = this.$printIndex + (columns * this.$lines);

            if (this.$printIndex + (columns * this.$lines) < list.length - 1) {
                choicesKey = "jaguar_company_choices_middle_of_many";
            } else {
                choicesKey = "jaguar_company_choices_end_of_many";
            }
        } else if (choice === "2_1_PREV_PAGE") {
            this.$printIndex = 0;
            choicesKey = "jaguar_company_choices_1_of_2";
        } else if (choice === "M_2_PREV_PAGE") {
            this.$printIndex = this.$printIndex - (columns * this.$lines);

            if (this.$printIndex) {
                choicesKey = "jaguar_company_choices_middle_of_many";
            } else {
                choicesKey = "jaguar_company_choices_start_of_many";
            }
        } else if (choice === "M_2_LAST_PAGE") {
            this.$printIndex = Math.floor(list.length / (columns * this.$lines)) * (columns * this.$lines);
            choicesKey = "jaguar_company_choices_end_of_many";
        } else if (choice === "1_1_EXIT" || choice === "2_2_EXIT" || choice === "M_3_EXIT") {
            choicesKey = "EXIT";
        } else {
            player.consoleMessage("Error logged. Inform the author of Jaguar Company OXP.");
            log(this.name, "$nextChoicesKey::choice: " + choice + "\n" +
                "* list: " + list.join(", ") + " (" + list.length + ")\n" +
                "* columns: " + columns + "\n" +
                "* $printIndex: " + this.$printIndex + "\n" +
                "* $lines: " + this.$lines + "\n");
            choicesKey = "ERROR";
        }

        return choicesKey;
    };

    /* NAME
     *   $locationChoices
     *
     * FUNCTION
     *   Callback for base location lister.
     *
     * INPUT
     *   choice - key of the choice selected
     */
    this.$locationChoices = function (choice) {
        var choicesKey = this.$nextChoicesKey(choice, this.$jaguarCompanySystemNames, 2),
        locations;

        if (choicesKey === "EXIT" || choicesKey === "ERROR") {
            /* Exit selected or there was an error. */
            return;
        }

        locations = this.$listNames(this.$jaguarCompanySystemNames);
        mission.runScreen({
            title : "Jaguar Company Base locations",
            message : locations + "\n",
            choicesKey : choicesKey,
            /* exitScreen is ignored by Oolite v1.76.1 and older. */
            exitScreen : "GUI_SCREEN_INTERFACES"
        }, this.$locationChoices, this);
    };

    /* NAME
     *   $listNames
     *
     * FUNCTION
     *   Build a 2 column list of Jaguar Company Base locations.
     *   Original idea from Spara's Trophy Collector OXP.
     *   Highly modified and simplified.
     *   Modified using Cabal Common Library for Oolite v1.77 and newer.
     *
     * INPUT
     *   list - array of strings to be displayed
     *
     * RESULT
     *   result - columnized list of names as a string
     */
    this.$listNames = function (list) {
        var columnized = "",
        row,
        start = this.$printIndex,
        /* Maximum number of rows. */
        lines = this.$lines,
        lname,
        rname,
        i;

        /* No Bases? */
        if (!list.length) {
            return "No bases in this sector.\n";
        }

        /* Less entries than rows? */
        if (list.length - start < lines) {
            lines = list.length - start;
        }

        for (i = 0; i < lines; i += 1) {
            if (start + i + lines < list.length) {
                /* Two column layout. */
                /* Left column. Truncated or padded with spaces. */
                lname = this.$ccl.strToWidth(list[start + i], 15, " ");
                /* Right column. Truncated. */
                rname = this.$ccl.strToWidth(list[start + i + lines], 15);

                /* Create the row. */
                if (this.$gte_v1_77) {
                    /* Oolite v1.77 and newer. */
                    row = this.$ccl.strAdd2Columns(lname, 1, rname, 17);
                } else {
                    /* Oolite v1.76.1 and older. */
                    row = " " + lname + " " + rname;
                }
            } else {
                /* One column layout. */
                /* Left column. Truncated. */
                lname = this.$ccl.strToWidth(list[start + i], 31);

                /* Create the row. */
                if (this.$gte_v1_77) {
                    /* Oolite v1.77 and newer. */
                    row = this.$ccl.strAddIndentedText(lname, 1);
                } else {
                    /* Oolite v1.76.1 and older. */
                    row = " " + lname;
                }
            }

            columnized += row + "\n";
        }

        return columnized;
    };

    /* NAME
     *   $cacheJaguarCompanySystems
     *
     * FUNCTION
     *   Keep a record of system IDs and names for the current galaxy.
     */
    this.$cacheJaguarCompanySystems = function () {
        var a,
        b,
        c,
        government,
        governmentNames = [
            "Anarchy",
            "Feudal",
            "Multi-Government"
        ],
        scrambledPRN,
        systemProbability,
        counter,
        logMsg = "$cacheJaguarCompanySystems::\n";

        /* Have the base locations for this galaxy been setup? */
        if (typeof p_main.galaxyNumber === "number" && p_main.galaxyNumber === galaxyNumber) {
            /* Already setup. */
            return;
        }

        /* Save the galaxy number. */
        p_main.galaxyNumber = galaxyNumber;
        /* Clear the base location arrays. */
        this.$jaguarCompanySystemIDs = [];
        this.$jaguarCompanySystemNames = [];
        this.$jaguarCompanyInterstellar = [];

        /* Alter the seed for the current galaxy. */
        for (counter = 0; counter < galaxyNumber; counter += 1) {
            this.$rng_nextgalaxy();
        }

        /* Reset the random seed. */
        p_main.rnd_seed = {};

        /* Check systems for Jaguar Company Base. */
        for (counter = 0; counter < 256; counter += 1) {
            /* Figure out pseudoRandomNumber, as a 24-bit integer, for the system being checked. */
            p_main.rnd_seed.a = p_main.seed.w1 & 0xff;
            p_main.rnd_seed.b = (p_main.seed.w1 >> 8) & 0xff;
            p_main.rnd_seed.c = p_main.seed.w2 & 0xff;
            p_main.rnd_seed.d = (p_main.seed.w2 >> 8) & 0xff;
            a = this.$gen_rnd_number();
            b = this.$gen_rnd_number();
            c = this.$gen_rnd_number();
            a = (a << 16) | (b << 8) | c;

            /* Re-implementation of system.scrambledPseudoRandomNumber
             * Add the salt to the pseudoRandomNumber to enable generation of different sequences.
             */
            a += this.$salt;
            /* Scramble with basic LCG psuedo-random number generator. */
            a = (214013 * a + 2531011) & 0xFFFFFFFF;
            a = (214013 * a + 2531011) & 0xFFFFFFFF;
            a = (214013 * a + 2531011) & 0xFFFFFFFF;
            /* Convert from (effectively) 32-bit signed integer to float in [0..1]. */
            scrambledPRN = a / 4294967296.0 + 0.5;

            /* Calculate the system government from the current seed. */
            government = (p_main.seed.w1 >> 3) & 7;

            /* Now we do the actual system check for Jaguar Company. */
            if (government <= 2) {
                /* We only use the first 3 government types.
                 * Therefore probabilities will be:
                 *   Anarchy:          37.5%
                 *   Feudal:           25.0%
                 *   Multi-Government: 12.5%
                 *
                 * Intestellar space will halve these probabilites.
                 */
                systemProbability = 0.125 * (3 - government);

                if (scrambledPRN <= systemProbability) {
                    if (this.$logging && this.$logExtra) {
                        logMsg += "* Name: " + System.systemNameForID(counter) +
                        ", Government type: " + governmentNames[government] + "\n";
                    }

                    /* Insert the ID into an array. */
                    this.$jaguarCompanySystemIDs.push(counter);
                    /* Insert the name with government type into an array. */
                    this.$jaguarCompanySystemNames.push(System.systemNameForID(counter) + " " +
                        "(" + governmentNames[government] + ")");
                }

                if (scrambledPRN <= systemProbability / 2) {
                    if (this.$logging && this.$logExtra) {
                        logMsg += "** Interstellar.\n";
                    }

                    this.$jaguarCompanyInterstellar.push(counter);
                }
            }

            /* Tweak the main seed for the next system. */
            this.$rng_tweakseed();
            this.$rng_tweakseed();
            this.$rng_tweakseed();
            this.$rng_tweakseed();
        }

        /* Sort the names. */
        this.$jaguarCompanySystemNames.sort();

        if (this.$logging && this.$logExtra) {
            log(this.name, logMsg);
        }
    };

    /* NAME
     *   $rng_rotatel
     *
     * FUNCTION
     *   Rotate 8-bit number leftwards.
     *
     * INPUT
     *   x - 8-bit number to rotate leftwards
     *
     * RESULT
     *   result - rotated 8-bit number
     */
    this.$rng_rotatel = function (x) {
        x = (x & 0xff) * 2;

        return (x & 0xff) | (x > 0xff);
    };

    /* NAME
     *   $rng_twist
     *
     * FUNCTION
     *   Twist 16-bit number.
     *
     * INPUT
     *   x - 16-bit number to twist
     *
     * RESULT
     *   result - twisted 16-bit number
     */
    this.$rng_twist = function (x) {
        return (this.$rng_rotatel(x >> 8) << 8) + this.$rng_rotatel(x & 0xff);
    };

    /* NAME
     *   $rng_nextgalaxy
     *
     * FUNCTION
     *   Next galaxy.
     *
     *   Apply to main seed; once for galaxy 2
     *   twice for galaxy 3, etc.
     *   Eighth application gives galaxy 1 again.
     */
    this.$rng_nextgalaxy = function () {
        p_main.seed.w0 = this.$rng_twist(p_main.seed.w0);
        p_main.seed.w1 = this.$rng_twist(p_main.seed.w1);
        p_main.seed.w2 = this.$rng_twist(p_main.seed.w2);
    };

    /* NAME
     *   $rng_tweakseed
     *
     * FUNCTION
     *   Main seed tweaker.
     */
    this.$rng_tweakseed = function () {
        var tmp;

        tmp = p_main.seed.w0 + p_main.seed.w1 + p_main.seed.w2;
        tmp &= 0xffff;

        p_main.seed.w0 = p_main.seed.w1;
        p_main.seed.w1 = p_main.seed.w2;
        p_main.seed.w2 = tmp;
    };

    /* NAME
     *   $gen_rnd_number
     *
     * FUNCTION
     *   Random number generator.
     *
     * RESULT
     *   result - random number
     */
    this.$gen_rnd_number = function () {
        var x = (p_main.rnd_seed.a * 2) & 0xFF,
        a = x + p_main.rnd_seed.c;

        if (p_main.rnd_seed.a > 127) {
            a += 1;
        }

        p_main.rnd_seed.a = a & 0xFF;
        p_main.rnd_seed.c = x;

        /* a = any carry left from above */
        a = a / 256;
        x = p_main.rnd_seed.b;
        a = (a + x + p_main.rnd_seed.d) & 0xFF;
        p_main.rnd_seed.b = a;
        p_main.rnd_seed.d = x;

        return a;
    };

    /* NAME
     *   $scriptSanityTimer
     *
     * FUNCTION
     *   Periodic function to check if Jaguar Company has spawned correctly.
     *
     *   Checks the base, asteroids, black box and tracker.
     *   Patrol ships, tug, buoy and miner are checked within the base ship script.
     *
     *   The order that this is done in is important.
     */
    this.$scriptSanityTimer = function () {
        var base = this.$jaguarCompanyBase,
        asteroids,
        asteroid,
        equipment,
        blackbox,
        counter,
        length;

        if (!base || !base.isValid) {
            /* Not setup yet. */
            return;
        }

        /* Check the base. */
        if (!this.$baseOK) {
            if (base.script.name !== "jaguar_company_base.js") {
                /* Reload the ship script. */
                base.setScript("jaguar_company_base.js");
                base.script.shipSpawned();

                if (this.$logging && this.$logExtra) {
                    log(this.name, "Script sanity check - fixed the base.");
                }
            } else {
                /* Don't re-check. */
                this.$baseOK = true;
            }
        }

        /* Check the asteroids. */
        if (!this.$asteroidsOK) {
            /* Search for asteroids around the base. */
            asteroids = system.shipsWithPrimaryRole("jaguar_company_asteroid");

            if (asteroids.length > 0) {
                /* Set the counter to all entities found. */
                p_main.asteroidsToCheck = asteroids.length;
                /* Cache the length. */
                length = asteroids.length;

                /* Iterate through each of the asteroids. */
                for (counter = 0; counter < length; counter += 1) {
                    asteroid = asteroids[counter];

                    if (asteroid.script.name !== "jaguar_company_asteroid.js") {
                        /* Reload the ship script. */
                        asteroid.setScript("jaguar_company_asteroid.js");
                        asteroid.script.shipSpawned();

                        if (this.$logging && this.$logExtra) {
                            log(this.name, "Script sanity check - fixed an asteroid.");
                        }
                    } else {
                        p_main.asteroidsToCheck -= 1;
                    }
                }

                if (!p_main.asteroidsToCheck) {
                    /* Don't re-check. */
                    this.$asteroidsOK = true;
                    p_main.asteroidsToCheck = null;
                }
            }
        }

        if (player.ship.equipmentStatus("EQ_JAGUAR_COMPANY_BLACK_BOX") !== "EQUIPMENT_OK") {
            /* Doesn't have the black box locator or is damaged. */
            return;
        }

        /* Check the black box. */
        if (!this.$blackboxOK) {
            equipment = player.ship.equipment;
            length = equipment.length;

            /* Find the black box in the player's equipment list. */
            for (counter = 0; counter < length; counter += 1) {
                if (equipment[counter].equipmentKey === "EQ_JAGUAR_COMPANY_BLACK_BOX") {
                    blackbox = equipment[counter];

                    break;
                }
            }

            if (blackbox.scriptName !== "jaguar_company_blackbox.js") {
                /* Reload the ship script. */
                blackbox.setScript("jaguar_company_blackbox.js");

                if (this.$logging && this.$logExtra) {
                    log(this.name, "Script sanity check - fixed the black box.");
                }
            } else {
                /* Don't re-check. */
                this.$blackboxOK = true;
            }
        }

        if (this.$blackboxOK && (!this.$trackerOK || !this.$visualTrackerOK)) {
            /* Black box script has been fixed. Check the trackers. */

            /* Check the ASC tracker. */
            if (!this.$trackerOK && this.$tracker && this.$tracker.isValid) {
                if (this.$tracker.script.name !== "jaguar_company_tracker.js") {
                    /* Reload the ship script. */
                    this.$tracker.setScript("jaguar_company_tracker.js");
                    this.$tracker.script.shipSpawned();

                    if (this.$logging && this.$logExtra) {
                        log(this.name, "Script sanity check - fixed the tracker.");
                    }
                } else {
                    /* Don't re-check. */
                    this.$trackerOK = true;
                }
            }

            /* Check the holo-tracker. */
            if (!this.$visualTrackerOK && this.$visualTracker && this.$visualTracker.isValid) {
                if (this.$visualTracker.script.name !== "jaguar_company_tracker.js") {
                    /* Reload the ship script. */
                    this.$visualTracker.setScript("jaguar_company_tracker.js");
                    this.$visualTracker.script.effectSpawned();

                    if (this.$logging && this.$logExtra) {
                        log(this.name, "Script sanity check - fixed the visual tracker.");
                    }
                } else {
                    /* Don't re-check. */
                    this.$visualTrackerOK = true;
                }
            }
        }
    };

    /* NAME
     *   $blackBoxTimer
     *
     * FUNCTION
     *   If the player has received the black box and then attacks Jaguar Company,
     *   this will remove it and the tracker and this timer.
     *
     *   Also checks if we are within 5km of the patrol ships, if so we remove the tracker.
     *
     *   Called every 5 seconds.
     */
    this.$blackBoxTimer = function () {
        var blackBoxStatus,
        patrolShips;

        if (this.$playerVar.attacker) {
            /* The player is an attacker of Jaguar Company. */
            blackBoxStatus = player.ship.equipmentStatus("EQ_JAGUAR_COMPANY_BLACK_BOX");

            if (blackBoxStatus === "EQUIPMENT_OK" || blackBoxStatus === "EQUIPMENT_DAMAGED") {
                /* Remove the software patch from the black box. */
                this.$playerVar.locationsActivated[galaxyNumber] = false;
                /* Remove the black box. */
                player.ship.removeEquipment("EQ_JAGUAR_COMPANY_BLACK_BOX");
                player.commsMessage("Black Box self-destructed!");
                /* Reset the black box. */
                this.$blackboxASCReset(false);
                this.$blackboxHoloReset(false);

                /* Stop and remove the Black Box timer. */
                if (this.$blackBoxTimerReference) {
                    if (this.$blackBoxTimerReference.isRunning) {
                        this.$blackBoxTimerReference.stop();
                    }

                    this.$blackBoxTimerReference = null;
                }
            }
        } else if ((this.$tracker && this.$tracker.isValid) || (this.$visualTracker && this.$visualTracker.isValid)) {
            if (player.ship.equipmentStatus("EQ_ADVANCED_COMPASS") !== "EQUIPMENT_OK") {
                player.consoleMessage("Tracker deactivating.");
                player.consoleMessage("Advanced Space Compass damaged.");
                /* Reset the black box. */
                this.$blackboxASCReset(false);
                this.$blackboxHoloReset(false);
            } else {
                patrolShips = system.shipsWithPrimaryRole("jaguar_company_patrol", player.ship);

                if (patrolShips.length > 0 && player.ship.position.distanceTo(patrolShips[0].position) < 5000) {
                    player.consoleMessage("Tracker deactivating.");
                    player.consoleMessage("Patrol ships close by.");
                    /* Reset the black box. */
                    this.$blackboxASCReset(false);
                    this.$blackboxHoloReset(false);
                }
            }
        }
    };

    /* NAME
     *   $baseSwapTimer
     *
     * FUNCTION
     *   Swap the base role dependent on the reputation mission variable.
     *
     *   Called every 5 seconds.
     */
    this.$baseSwapTimer = function () {
        var base = this.$jaguarCompanyBase,
        position,
        orientation,
        reputation,
        displayName,
        newBase,
        newBaseRole,
        entities,
        entity,
        distance,
        direction,
        entityCounter,
        entityLength;

        if (!base || !base.isValid) {
            /* Stop and remove the base swap timer. */
            this.$baseSwapTimerReference.stop();
            this.$baseSwapTimerReference = null;

            return;
        }

        reputation = this.$playerVar.reputation[galaxyNumber];

        /* Set up the role that the base should have. */
        if (reputation < this.$reputationHelper) {
            newBaseRole = "jaguar_company_base_no_discount";
        } else if (reputation < this.$reputationBlackbox) {
            newBaseRole = "jaguar_company_base_discount";
        } else {
            newBaseRole = "jaguar_company_base_discount_and_docking";
        }

        if (base.hasRole(newBaseRole)) {
            /* The base already has this new role. No need to swap. */
            return;
        }

        /* Shift any entities that are launching. Hopefully there should only be 1 ship in the launch tube (if any).
         * There really shouldn't be anything close by to the new position as we are only placing the
         * entity a small distance outside the docking port.
         */
        entities = system.filteredEntities(this, function (entity) {
                return (entity && entity.isValid);
            }, base, base.collisionRadius);

        if (entities.length) {
            /* Cache the length. */
            entityLength = entities.length;

            for (entityCounter = 0; entityCounter < entityLength; entityCounter += 1) {
                entity = entities[entityCounter];
                /* Current distance of the entity from the base. */
                distance = entity.position.distanceTo(base.position);
                /* New distance to move the entity by. */
                distance = (base.collisionRadius - distance) + entity.collisionRadius + 10;
                /* Update position along the original direction vector. */
                direction = entity.position.subtract(base.position).direction();
                entity.position = entity.position.add(direction.multiply(distance));
            }
        }

        /* Copy some properties. */
        position = base.position;
        orientation = base.orientation;
        displayName = base.displayName;
        /* This is checked in the base ship script. If set, it will not set up various properties in
         * the 'shipSpawned' base ship script event as we will be copying over the originals here.
         * $swapBase will be reset in the 'shipSpawned' base ship script once the base has fully spawned.
         */
        this.$swapBase = true;
        /* Create a new base. */
        //        newBase = base.spawnOne(newBaseRole);
        /* Remove the original base quietly: don't trigger 'shipDied' in the ship script. */
        base.remove(true);
        /* Setup the new base with the original properties. */
        newBase = system.addShips(newBaseRole, 1, position, 0)[0];
        newBase.position = position;
        newBase.orientation = orientation;
        newBase.displayName = displayName;
        /* Stop any kick in velocity we may get from any nearby entity.
         * Imagine a station that you need injectors to out run.
         */
        newBase.velocity = new Vector3D(0, 0, 0);
        /* Update the base reference. */
        this.$jaguarCompanyBase = newBase;
    };

    /* NAME
     *   $blackboxToggle
     *
     * FUNCTION
     *   Toggle the activation of the black box ASC equipment.
     */
    this.$blackboxToggle = function () {
        var playerShip = player.ship,
        patrolShips = system.shipsWithPrimaryRole("jaguar_company_patrol", playerShip),
        ascStatus = playerShip.equipmentStatus("EQ_ADVANCED_COMPASS"),
        blackboxStatus = playerShip.equipmentStatus("EQ_JAGUAR_COMPANY_BLACK_BOX");

        if (ascStatus !== "EQUIPMENT_OK") {
            player.consoleMessage("You need a working Advanced Space Compass for this equipment.");
        } else if (blackboxStatus === "EQUIPMENT_OK") {
            if (!patrolShips.length) {
                player.consoleMessage("Can not show tracker. No patrol ships found.");
            } else if (playerShip.position.distanceTo(patrolShips[0].position) < 5000) {
                player.consoleMessage("Tracker not activated. Patrol ships close by.");
            } else {
                if (p_main.blackboxASCActivated) {
                    this.$blackboxASCReset(true);
                } else {
                    this.$blackboxASCSet(true);
                }
            }

            p_main.blackboxASCActivated = !p_main.blackboxASCActivated;
        } else if (blackboxStatus === "EQUIPMENT_DAMAGED") {
            player.commsMessage("Black Box Damaged!");
            player.commsMessage("Return to the nearest Jaguar Company Base for repairs.");
        }
    };

    /* NAME
     *   $blackboxMode
     *
     * FUNCTION
     *   Toggle the activation of the black box holo equipment.
     */
    this.$blackboxMode = function () {
        var playerShip = player.ship,
        patrolShips = system.shipsWithPrimaryRole("jaguar_company_patrol", playerShip),
        ascStatus = playerShip.equipmentStatus("EQ_ADVANCED_COMPASS"),
        blackboxStatus = playerShip.equipmentStatus("EQ_JAGUAR_COMPANY_BLACK_BOX");

        if (ascStatus !== "EQUIPMENT_OK") {
            player.consoleMessage("You need a working Advanced Space Compass for this equipment.");
        } else if (blackboxStatus === "EQUIPMENT_OK") {
            if (!patrolShips.length) {
                player.consoleMessage("Can not show holo-tracker. No patrol ships found.");
            } else if (playerShip.position.distanceTo(patrolShips[0].position) < 5000) {
                player.consoleMessage("Holo-tracker not activated. Patrol ships close by.");
            } else {
                if (p_main.blackboxHoloActivated) {
                    this.$blackboxHoloReset(true);
                } else {
                    this.$blackboxHoloSet(true);
                }
            }

            p_main.blackboxHoloActivated = !p_main.blackboxHoloActivated;
        } else if (blackboxStatus === "EQUIPMENT_DAMAGED") {
            player.commsMessage("Black Box Damaged!");
            player.commsMessage("Return to the nearest Jaguar Company Base for repairs.");
        }
    };

    /* NAME
     *   $blackboxASCSet
     *
     * FUNCTION
     *   Setup the black box ASC equipment.
     *
     * INPUT
     *   showMsg - boolean
     *     true - show console message
     *     false - do not show console message
     */
    this.$blackboxASCSet = function (showMsg) {
        var patrolShips = system.shipsWithPrimaryRole("jaguar_company_patrol", player.ship);

        if (patrolShips.length && (!this.$tracker || !this.$tracker.isValid)) {
            /* Invisible object. */
            this.$tracker = system.addShips("jaguar_company_tracker", 1, patrolShips[0].position, 10000)[0];

            if (showMsg && this.$tracker && this.$tracker.isValid) {
                player.consoleMessage("Black Box ASC tracker activated.");
                player.consoleMessage("Follow beacon code 'T' on your ASC.");
            }
        }
    };

    /* NAME
     *   $blackboxHoloSet
     *
     * FUNCTION
     *   Setup the black box holo-tracker equipment.
     *
     * INPUT
     *   showMsg - boolean
     *     true - show console message
     *     false - do not show console message
     */
    this.$blackboxHoloSet = function (showMsg) {
        var patrolShips = system.shipsWithPrimaryRole("jaguar_company_patrol", player.ship);

        if (this.$visualEffects && this.$gte_v1_77 &&
            patrolShips.length && (!this.$visualTracker || !this.$visualTracker.isValid)) {
            /* Visual effect for Oolite v1.77 and newer. */
            this.$visualTracker = system.addVisualEffect("jaguar_company_tracker", player.ship.position);

            if (showMsg && this.$visualTracker && this.$visualTracker.isValid) {
                player.consoleMessage("Black Box holo-tracker activated.");
                player.consoleMessage("Green is fore, red is aft.");
            }
        }
    };

    /* NAME
     *   $blackboxASCReset
     *
     * FUNCTION
     *   Reset the black box ASC equipment.
     *
     * INPUT
     *   showMsg - boolean
     *     true - show console message
     *     false - do not show console message
     */
    this.$blackboxASCReset = function (showMsg) {
        if (this.$tracker && this.$tracker.isValid) {
            /* Remove the tracker quietly: don't trigger 'shipDied' in the ship script. */
            this.$tracker.remove(true);
            this.$trackerOK = false;

            if (showMsg) {
                player.consoleMessage("Black Box ASC tracker deactivated.");
            }
        }
    };

    /* NAME
     *   $blackboxHoloReset
     *
     * FUNCTION
     *   Reset the black box holo-tracker equipment.
     *
     * INPUT
     *   showMsg - boolean
     *     true - show console message
     *     false - do not show console message
     */
    this.$blackboxHoloReset = function (showMsg) {
        if (this.$visualEffects && this.$gte_v1_77 && this.$visualTracker && this.$visualTracker.isValid) {
            /* Remove the visual tracker. */
            this.$visualTracker.remove();
            this.$visualTrackerOK = false;

            if (showMsg) {
                player.consoleMessage("Black Box holo-tracker deactivated.");
            }
        }
    };

    /* NAME
     *   $welcomeMessage
     *
     * FUNCTION
     *   Show a welcome message.
     */
    this.$welcomeMessage = function () {
        var reputation = this.$playerVar.reputation[galaxyNumber],
        helperLevel = this.$reputationHelper,
        blackboxLevel = this.$reputationBlackbox,
        locationsLevel = this.$reputationLocations,
        welcome,
        logMsg;

        if (typeof this.$playerVar.delayedAward === "number") {
            /* Add on the delayed award to the reputation. */
            reputation += this.$playerVar.delayedAward;
        }

        if (this.$logging && this.$logExtra) {
            logMsg = "$welcomeMessage::reputation: " + reputation + "\n" +
                "$welcomeMessage::visitedBase: " + this.$playerVar.visitedBase + "\n";
        }

        p_main.playerWelcomed = true;

        welcome = expandDescription("[jaguar_company_base_greeting] ");

        if (!this.$playerVar.visitedBase) {
            welcome += expandDescription("[jaguar_company_base_docked]");
        } else {
            welcome += expandDescription("[jaguar_company_base_visited]");
        }

        if (reputation >= helperLevel) {
            welcome += " " + expandMissionText("jaguar_company_base_thankyou");
        }

        if (reputation >= blackboxLevel) {
            if (player.ship.equipmentStatus("EQ_JAGUAR_COMPANY_BLACK_BOX") !== "EQUIPMENT_OK") {
                /* Doesn't have the black box locator or is damaged. */
                if (player.ship.equipmentStatus("EQ_JAGUAR_COMPANY_BLACK_BOX") === "EQUIPMENT_DAMAGED") {
                    /* Black box damaged. */
                    welcome += expandMissionText("jaguar_company_base_fix_black_box");
                    player.ship.setEquipmentStatus("EQ_JAGUAR_COMPANY_BLACK_BOX", "EQUIPMENT_OK");
                } else {
                    /* No black box locator. */
                    if (this.$visualEffects && this.$gte_v1_77) {
                        /* Oolite v1.77 and newer. */
                        welcome += expandMissionText("jaguar_company_base_no_black_box2");
                    } else {
                        /* Visual effects off or Oolite v1.76.1 and older. */
                        welcome += expandMissionText("jaguar_company_base_no_black_box1");
                    }

                    player.ship.awardEquipment("EQ_JAGUAR_COMPANY_BLACK_BOX");

                    if (!this.$blackBoxTimerReference || !this.$blackBoxTimerReference.isRunning) {
                        if (!this.$blackBoxTimerReference) {
                            /* Create a new timer. Checks every 5 seconds. */
                            this.$blackBoxTimerReference = new Timer(this, this.$blackBoxTimer, 5, 5);
                        } else {
                            /* Start the timer if it exists and has stopped. */
                            this.$blackBoxTimerReference.start();
                        }
                    }

                    /* Reset the check flag. */
                    this.$blackboxOK = false;
                }
            }
        }

        if (reputation >= locationsLevel && !this.$playerVar.locationsActivated[galaxyNumber]) {
            /* Upload the software patch to the black box. */
            this.$playerVar.locationsActivated[galaxyNumber] = true;
            /* Add the interface system if Oolite v1.77 and newer is used. */
            this.$addInterface();

            if (this.$gte_v1_77) {
                /* Oolite v1.77 and newer. */
                welcome += expandMissionText("jaguar_company_base_no_locator2");
            } else {
                /* Oolite v1.76.1 and older. */
                welcome += expandMissionText("jaguar_company_base_no_locator1");
            }
        }

        if (reputation >= helperLevel && !system.isInterstellarSpace) {
            /* Add on a market message if reputation is high enough and not in interstellar space. */
            welcome += expandMissionText("jaguar_company_base_market");

            if (player.ship.manifest.food ||
                player.ship.manifest.textiles ||
                player.ship.manifest.liquorWines ||
                player.ship.manifest.luxuries ||
                player.ship.manifest.furs ||
                player.ship.manifest.alienItems) {
                /* Add a message for some wanted items in the player's hold. */
                welcome += expandMissionText("jaguar_company_base_market_want");

                if (player.ship.manifest.food) {
                    welcome += expandMissionText("jaguar_company_base_market_want_a", {
                        jaguar_company_commodity : "food"
                    });
                }

                if (player.ship.manifest.textiles || player.ship.manifest.furs) {
                    welcome += expandMissionText("jaguar_company_base_market_want_a", {
                        jaguar_company_commodity : "clothing"
                    });
                }

                if (player.ship.manifest.liquorWines) {
                    welcome += expandMissionText("jaguar_company_base_market_want_a", {
                        jaguar_company_commodity : "alcohol"
                    });
                }

                if (player.ship.manifest.luxuries) {
                    welcome += expandMissionText("jaguar_company_base_market_want_a", {
                        jaguar_company_commodity : "luxuries"
                    });
                }

                if (player.ship.manifest.alienItems) {
                    welcome += expandMissionText("jaguar_company_base_market_want_a", {
                        jaguar_company_commodity : "alien technology"
                    });
                }
            }
        }

        if (this.$logging && this.$logExtra) {
            logMsg += "$welcomeMessage::welcome: " + welcome;
            log(this.name, "\n" + logMsg);
        }

        this.$playerVar.visitedBase = true;
        mission.runScreen({
            title : this.$jaguarCompanyBase.displayName,
            message : welcome
        });
    };

    /* NAME
     *   $scanForWitchpointBuoy
     *
     * FUNCTION
     *   Scan for a witchpoint buoy.
     *
     * RESULT
     *   result - witchpoint buoy entity or a fake entity if it can't be found
     */
    this.$scanForWitchpointBuoy = function () {
        var buoys,
        buoy;

        /* Fake witchpoint buoy entity. Updated if one is found. */
        buoy = {
            isValid : true,
            position : new Vector3D(0, 0, 0),
            collisionRadius : 100
        };

        if (!system.isInterstellarSpace) {
            /* Find the witchpoint buoy. */
            buoys = system.filteredEntities(this, function (entity) {
                    if (!entity.isValid || entity.scanClass !== "CLASS_BUOY") {
                        /* Ignore all entities that have one of these conditions:
                         * 1) not valid
                         * 2) not CLASS_BUOY
                         */
                        return false;
                    }

                    return entity.hasRole("buoy-witchpoint");
                });

            if (buoys.length) {
                /* Closest one to the origin. */
                buoy = buoys[0];
            }
        }

        return buoy;
    };

    /* NAME
     *   $isNavyShip
     *
     * FUNCTION
     *   Checks for various Galactic Navy ships.
     *
     *   This only checks for medical ships, frigates and carriers.
     *
     * INPUT
     *   entity - entity of the ship to check
     *
     * RESULT
     *   result - true if entity is a Galactic Navy ship, false if not
     */
    this.$isNavyShip = function (entity) {
        if (!entity.isValid ||
            !entity.isShip ||
            !entity.isPiloted ||
            !entity.isPolice) {
            /* Ignore all entities that have one of these conditions:
             * 1) not valid
             * 2) not a ship
             * 3) not piloted
             * 4) not police (navy should be)
             */
            return false;
        }

        return (entity.hasRole("navy-medship") ||
            entity.hasRole("navy-frigate") ||
            entity.hasRole("patrol-frigate") ||
            entity.hasRole("picket-frigate") ||
            entity.hasRole("picket-behemoth") ||
            entity.hasRole("navy-behemoth") ||
            entity.hasRole("navy-behemoth-battlegroup") ||
            entity.hasRole("behemoth"));
    };

    /* NAME
     *   $scanForNavyShips
     *
     * FUNCTION
     *   Find any major Galactic Navy ships.
     *
     * INPUT
     *   near - entity of the search origin, will use the witchpoint if not specified
     *
     * RESULT
     *   result - array of ship entities
     */
    this.$scanForNavyShips = function (near) {
        var ships;

        if (!near || !near.isValid) {
            /* Defaults to the witchpoint as the origin. */
            ships = system.filteredEntities(this, this.$isNavyShip);
        } else {
            ships = system.filteredEntities(this, this.$isNavyShip, near);
        }

        return ships;
    };

    /* NAME
     *   $spawnJaguarCompany
     *
     * FUNCTION
     *   Spawn Jaguar Company
     *
     * INPUT
     *   state - number
     *     1 - general add
     *     2 - add because of Galactic Navy presence
     *     4 - add always
     */
    this.$spawnJaguarCompany = function (state) {
        var sysname,
        logMsg = "$spawnJaguarCompany::";

        if (!state || state <= 0 || state > 7) {
            log(this.name, logMsg + "This should NOT happen! Unknown state: " + state);

            return;
        }

        /* Reset the check flags. */
        this.$baseOK = false;
        this.$asteroidsOK = false;

        if (!this.$scriptSanityTimerReference || !this.$scriptSanityTimerReference.isRunning) {
            /* This timer will check all Jaguar Company entities for script sanity. */
            if (!this.$scriptSanityTimerReference) {
                /* Create a new timer. Checked every 5 seconds. */
                this.$scriptSanityTimerReference = new Timer(this, this.$scriptSanityTimer, 5, 5);
            } else {
                /* Start the timer if it exists and has stopped. */
                this.$scriptSanityTimerReference.start();
            }
        }

        if (player.ship.equipmentStatus("EQ_JAGUAR_COMPANY_BLACK_BOX") === "EQUIPMENT_OK" ||
            player.ship.equipmentStatus("EQ_JAGUAR_COMPANY_BLACK_BOX") === "EQUIPMENT_DAMAGED") {
            /* The player has a black box.
             * This timer will de-activate the tracker if too close to the patrol ships
             * or it will self-destruct the black box if the player is not allowed it.
             */
            if (!this.$blackBoxTimerReference || !this.$blackBoxTimerReference.isRunning) {
                if (!this.$blackBoxTimerReference) {
                    /* Create a new timer. Checked every 5 seconds. */
                    this.$blackBoxTimerReference = new Timer(this, this.$blackBoxTimer, 5, 5);
                } else {
                    /* Start the timer if it exists and has stopped. */
                    this.$blackBoxTimerReference.start();
                }
            }
        }

        /* Scan for the witchpoint buoy entity. */
        this.$witchpointBuoy = this.$scanForWitchpointBuoy();

        if (this.$logging) {
            sysname = system.name;

            if (system.isInterstellarSpace) {
                sysname = "Interstellar";
            }

            if (state & 1) {
                logMsg += "\n* Adding Jaguar Company to patrol in the " + sysname + " space lane.";
            }

            if (state & 2) {
                logMsg +=
                "\n* Adding Jaguar Company to patrol with the Galactic Navy in the " + sysname + " space lane.";
            }

            if (state & 4) {
                logMsg += "\n* Always spawn set - Adding Jaguar Company to the " + sysname + " space lane.";
            }

            log(this.name, logMsg);
        }

        if (state & 2) {
            /* Create the patrol for navy work. */
            this.$spawnJaguarCompanyNavyPatrol();
        }

        if (state & 5) {
            /* Create the base. */
            this.$spawnJaguarCompanyBase();
        }
    };

    /* NAME
     *   $spawnJaguarCompanyNavyPatrol
     *
     * FUNCTION
     *   Create the patrol for navy work.
     */
    this.$spawnJaguarCompanyNavyPatrol = function () {
        var navyShips = this.$scanForNavyShips();

        if (navyShips.length) {
            p_main.joinNavy = true;
            p_main.closestNavyShip = navyShips[0];
            /* Initialise the route list with the Navy route. */
            this.$initRoute("NAVY");

            if (!system.countShipsWithRole("jaguar_company_patrol") &&
                !system.countShipsWithRole("jaguar_company_base")) {
                /* Add the patrol ships. */
                system.addShips("jaguar_company_patrol", this.$maxPatrolShips, navyShips[0].position, 7500);
            }
        }
    };

    /* NAME
     *   $spawnJaguarCompanyBase
     *
     * FUNCTION
     *   Create the base.
     */
    this.$spawnJaguarCompanyBase = function () {
        var ratio,
        basePosition,
        baseRole,
        dot,
        mainPlanet,
        mPovUp,
        wpPosition,
        wpsunDirection,
        wpmpDirection;

        if (this.$jaguarCompanyBase && this.$jaguarCompanyBase.isValid) {
            /* Already setup. */
            return;
        }

        if (system.isInterstellarSpace) {
            /* If we are in interstellar space then the base is somewhere within
             * 7 times standard scanner range of the centre point.
             */
            basePosition = Vector3D.randomDirectionAndLength(7 * 25600);
            /* Move the base in a random direction a distance of 3 times standard scanner range. */
            basePosition = basePosition.add(Vector3D.randomDirection(3 * 25600));
        } else {
            /* Shorten some of the property names and calculations. */
            mainPlanet = system.mainPlanet;
            wpPosition = this.$witchpointBuoy.position;
            wpsunDirection = wpPosition.subtract(system.sun.position).direction();
            wpmpDirection = wpPosition.subtract(mainPlanet.position).direction();
            dot = wpsunDirection.dot(wpmpDirection);

            /* Some systems have the witchpoint, main planet and sun all in opposition/conjunction. */
            if (dot > -0.5 && dot < 0.5) {
                /* The sun is somewhere out to the right or left.
                 *  - or up or down or any variety of directions that isn't infront or behind.
                 */
                /* Pick a ratio between 0.3 and 0.5 */
                ratio = 0.3 + (system.scrambledPseudoRandomNumber(this.$salt) * 0.2);
                /* Place the base on the witchpoint -> sun route. */
                basePosition = Vector3D.interpolate(wpPosition, system.sun.position, ratio);
                /* Move it 4 to 6 times scanning range towards the main planet. */
                ratio = (4 + (system.scrambledPseudoRandomNumber(this.$salt) * 2)) * 25600;
                ratio /= basePosition.distanceTo(mainPlanet.position);
                basePosition = Vector3D.interpolate(basePosition, mainPlanet.position, ratio);
            } else {
                if (this.$logging && this.$logExtra) {
                    if (dot >= 0.5) {
                        /* Witchpoint is on the opposite side of the planet to the sun. */
                        log(this.name, "$spawnJaguarCompanyBase::Conjunction! Choosing alternate base position.");
                    } else {
                        /* Witchpoint in between the planet and the sun. */
                        log(this.name, "$spawnJaguarCompanyBase::Opposition! Choosing alternate base position.");
                    }
                }

                /* The witchpoint, main planet and sun are getting close to being in opposition/conjunction. */
                /* Pick a ratio between 0.1 and 0.3 */
                ratio = 0.1 + (system.scrambledPseudoRandomNumber(this.$salt) * 0.2);
                /* Place the base on the witchpoint -> main planet route. */
                basePosition = Vector3D.interpolate(wpPosition, mainPlanet.position, ratio);
                /* Move it 6 to 8 times scanning range upwards with respect to the main planet's surface. */
                ratio = (6 + (system.scrambledPseudoRandomNumber(this.$salt + 1) * 2)) * 25600;
                mPovUp = mainPlanet.orientation.vectorUp();
                basePosition = basePosition.add(mPovUp.multiply(mainPlanet.radius + ratio));
            }
        }

        /* Set the base role dependent on the reputation mission variable. */
        if (this.$playerVar.reputation[galaxyNumber] < this.$reputationHelper) {
            baseRole = "jaguar_company_base_no_discount";
        } else if (this.$playerVar.reputation[galaxyNumber] < this.$reputationBlackbox) {
            baseRole = "jaguar_company_base_discount";
        } else {
            baseRole = "jaguar_company_base_discount_and_docking";
        }

        /* Add the base. */
        this.$jaguarCompanyBase = system.addShips(baseRole, 1, basePosition, 0)[0];

        if (!this.$baseSwapTimerReference || !this.$baseSwapTimerReference.isRunning) {
            /* This timer will swap the base role if needed. */
            if (!this.$baseSwapTimerReference) {
                /* Create a new timer. Checked every 5 seconds. */
                this.$baseSwapTimerReference = new Timer(this, this.$baseSwapTimer, 5, 5);
            } else {
                /* Start the timer if it exists and has stopped. */
                this.$baseSwapTimerReference.start();
            }
        }

        if (!p_main.joinNavy) {
            /* Initialise the route list with the default route. */
            this.$initRoute();
        }
    };

    /* NAME
     *   $setUpCompany
     *
     * FUNCTION
     *   Check to see if we need to spawn Jaguar Company.
     *
     * RESULT
     *   result - true if Jaguar Company will be spawned, false if not
     */
    this.$setUpCompany = function () {
        var scrambledPRN,
        systemID,
        navyPresent = false,
        /* 50:50 chance of joining the Galactic Navy. */
        joinNavyProbability = 0.5,
        spawnInSystem = false,
        spawnCompany = 0;

        /* Stop and remove the timer. */
        if (this.$setUpCompanyTimerReference) {
            if (this.$setUpCompanyTimerReference.isRunning) {
                this.$setUpCompanyTimerReference.stop();
            }

            this.$setUpCompanyTimerReference = null;
        }

        if (!this.$alwaysSpawn) {
            if (system.sun && (system.sun.isGoingNova || system.sun.hasGoneNova)) {
                /* Don't setup if the system sun is going nova or has already gone nova. */
                if (this.$logging && this.$logExtra) {
                    log(this.name, "$setUpCompany::\n" +
                        "system.sun.isGoingNova: " + system.sun.isGoingNova +
                        ", system.sun.hasGoneNova: " + system.sun.hasGoneNova);
                }

                return false;
            }
        }

        if (this.$jaguarCompanyBase && this.$jaguarCompanyBase.isValid) {
            /* Already setup. */
            return true;
        }

        /* Bit pattern for spawning...
         *
         * spawnInSystem    - 001
         * joinNavy         - 010
         * alwaysSpawn      - 100
         */

        /* In interstellar space, the scrambledPRN will be for the last system you were in. */
        scrambledPRN = system.scrambledPseudoRandomNumber(this.$salt);

        if (!p_main.joinNavy) {
            navyPresent = this.$scanForNavyShips().length > 0;
        } else {
            navyPresent = true;
        }

        /* Jaguar Company are part-time reservists. */
        p_main.joinNavy = (navyPresent && scrambledPRN <= joinNavyProbability);

        if (system.isInterstellarSpace) {
            /* Use the last system ID. */
            systemID = this.$lastSystemID;
        } else {
            /* Save the current system ID. */
            systemID = system.ID;
            this.$lastSystemID = systemID;
        }

        if ((system.isInterstellarSpace && this.$jaguarCompanyInterstellar.indexOf(systemID) !== -1) ||
            (!system.isInterstellarSpace && this.$jaguarCompanySystemIDs.indexOf(systemID) !== -1)) {
            spawnInSystem = true;
        }

        /* Anarchies, Feudals and Multi-Governments or systems with Galactic Naval presence */
        spawnCompany |= (spawnInSystem ? 1 : 0);
        /* Always join the navy if we would have been created in this system. */
        spawnCompany |= ((navyPresent && spawnInSystem) || p_main.joinNavy ? 2 : 0);
        /* Always spawn no matter what. */
        spawnCompany |= (this.$alwaysSpawn ? 4 : 0);

        if (this.$logging && this.$logExtra) {
            log(this.name, "$setUpCompany::\n" +
                "* navyPresent: " + navyPresent + ", joinNavy: " + p_main.joinNavy + "\n" +
                "* spawnInSystem: " + spawnInSystem + "\n" +
                "* spawnCompany (normally): " + (spawnCompany & 3 ? "Yes" : "No"));
        }

        if (spawnCompany) {
            this.$spawnJaguarCompany(spawnCompany);

            return true;
        }

        return false;
    };

    /* NAME
     *   $uniqueShipName
     *
     * FUNCTION
     *   Create a unique ship name.
     *
     * INPUTS
     *   isBase - boolean (optional)
     *     true - is a base, will generate the same name for a system
     *     false/undefined - not a base
     *   maxNameLength - maximum length of the name (optional)
     *
     * RESULT
     *   result - unique name
     */
    this.$uniqueShipName = function (isBase, maxNameLength) {
        var index,
        salt = this.$salt,
        randf,
        prefix,
        name;

        if (typeof maxNameLength !== "number") {
            /* Empty maxNameLength. */
            maxNameLength = 0;
        }

        if (!p_main.availableShipNames || !p_main.availableShipNames.length) {
            /* Initialise the available ship names with a copy of the master list. */
            p_main.availableShipNames = p_const.shipNames.concat([]);
        } else if (p_main.availableShipNames && p_main.availableShipNames.length <= 8) {
            /* Add on a copy of the master list if the available pot gets low. */
            p_main.availableShipNames = p_main.availableShipNames.concat(p_const.shipNames);
        }

        /* Random number for the index. */
        if (isBase) {
            /* Same random number for each system. */
            randf = system.scrambledPseudoRandomNumber(salt);
        } else {
            randf = Math.random();
        }

        /* Index for the name. */
        index = Math.floor(randf * p_main.availableShipNames.length);
        /* Get a name from the available list and remove it. */
        name = p_main.availableShipNames.splice(index, 1)[0];

        /* Make sure we don't try to search for a name that is shorter than what is available. */
        if (maxNameLength) {
            /* Reset the max name length if it is shorter than what is available. */
            if (maxNameLength < this.$shortestNameLength) {
                maxNameLength = this.$shortestNameLength;
            }

            /* Keep looping until we find a name short enough. */
            while (name.length > maxNameLength) {
                /* Too long. Put the name back into the available list. */
                p_main.availableShipNames.splice(index, 0, name);

                /* Pick a new random number. */
                if (isBase) {
                    salt += 1;
                    randf = system.scrambledPseudoRandomNumber(salt);
                } else {
                    randf = Math.random();
                }

                /* Index for the new name. */
                index = Math.floor(randf * p_main.availableShipNames.length);
                /* Get a new name from the available list and remove it. */
                name = p_main.availableShipNames.splice(index, 1)[0];
            }
        }

        if (isBase) {
            prefix = "JC Base#" ;
        } else {
            prefix = "JC#";
        }

        /* Return the new name. */
        return prefix + Math.floor(randf * 10000).toString() + "-" + system.name.substring(0, 2) + ": " + name;
    };

    /* NAME
     *   $initRoute
     *
     * FUNCTION
     *   Alters the route list.
     *
     * INPUT
     *   route - route code (optional)
     *     WPWB - full route (base -> witchpoint -> planet -> witchpoint -> base (dock)) (default)
     *     I - interstellar space (base -> (fake) witchpoint -> base (dock))
     *     NAVY - patrol with the Galactic Navy
     *     WP - witchpoint <-> planet
     *     BP - base -> planet -> base (dock)
     */
    this.$initRoute = function (route) {
        if (system.isInterstellarSpace) {
            route = "I";
        } else if (typeof route !== "string" || route === "") {
            route = "WPWB";
        }

        /* Update the witchpoint buoy reference. */
        this.$witchpointBuoy = this.$scanForWitchpointBuoy();

        switch (route) {
        case "I":
            /* Alters the route list for Interstellar space. */
            p_main.routes = [{
                    /* Witchpoint. Will be a fake witchpoint. */
                    entity : this.$witchpointBuoy,
                    /* Range used in AI. */
                    range : 5000,
                    /* Message to be sent to the AI. */
                    aiMessage : "JAGUAR_COMPANY_INTERSTELLAR"
                }, {
                    /* Jaguar Company Base. */
                    entity : this.$jaguarCompanyBase,
                    /* Range used in AI. */
                    range : 8000,
                    /* Message to be sent to the AI. */
                    aiMessage : "JAGUAR_COMPANY_BASE"
                }
            ];

            break;
        case "NAVY":
            /* Alters the route list for navy patrol. */
            p_main.routes = [{
                    /* Navy ship to shadow. */
                    entity : p_main.closestNavyShip,
                    /* Range used in AI. */
                    range : 7500,
                    /* Message to be sent to the AI. */
                    aiMessage : "JAGUAR_COMPANY_NAVY_PATROL"
                }
            ];

            break;
        case "WP":
            /* Alters the route list for WP->Planet and Planet->WP. */
            p_main.routes = [{
                    /* Main planet. */
                    entity : system.mainPlanet,
                    /* Range used in AI. */
                    range : 50000,
                    /* Message to be sent to the AI. */
                    aiMessage : "JAGUAR_COMPANY_PLANET"
                }, {
                    /* Witchpoint. */
                    entity : this.$witchpointBuoy,
                    /* Range used in AI. */
                    range : 10000,
                    /* Message to be sent to the AI. */
                    aiMessage : "JAGUAR_COMPANY_WITCHPOINT"
                }
            ];

            break;
        case "BP":
            /* Alters the route list for Base->Planet and Planet->Base. */
            p_main.routes = [{
                    /* Jaguar Company Base. */
                    entity : this.$jaguarCompanyBase,
                    /* Range used in AI. */
                    range : 8000,
                    /* Message to be sent to the AI. */
                    aiMessage : "JAGUAR_COMPANY_BASE"
                }, {
                    /* Main planet. */
                    entity : system.mainPlanet,
                    /* Range used in AI. */
                    range : 50000,
                    /* Message to be sent to the AI. */
                    aiMessage : "JAGUAR_COMPANY_PLANET"
                }
            ];

            break;
        default:
            /* Full route list. */
            p_main.routes = [{
                    /* Witchpoint. */
                    entity : this.$witchpointBuoy,
                    /* Range used in AI. */
                    range : 10000,
                    /* Message to be sent to the AI. */
                    aiMessage : "JAGUAR_COMPANY_WITCHPOINT_FROM_BASE"
                }, {
                    /* Main planet. */
                    entity : system.mainPlanet,
                    /* Range used in AI. */
                    range : 50000,
                    /* Message to be sent to the AI. */
                    aiMessage : "JAGUAR_COMPANY_PLANET"
                }, {
                    /* Witchpoint. */
                    entity : this.$witchpointBuoy,
                    /* Range used in AI. */
                    range : 10000,
                    /* Message to be sent to the AI. */
                    aiMessage : "JAGUAR_COMPANY_WITCHPOINT"
                }, {
                    /* Jaguar Company Base. */
                    entity : this.$jaguarCompanyBase,
                    /* Range used in AI. */
                    range : 8000,
                    /* Message to be sent to the AI. */
                    aiMessage : "JAGUAR_COMPANY_BASE"
                }
            ];

            break;
        }

        /* Reset the current route index. */
        p_main.routeIndex = 0;
    };

    /* NAME
     *   $changeRoute
     *
     * FUNCTION
     *   Change the current route or set it to the routeNum index of the routes array.
     *
     * INPUT
     *   routeNum - index of the routes array (optional)
     *     < 0 - pick last index
     *     num - use index 'num'
     */
    this.$changeRoute = function (routeNum) {
        if (typeof routeNum !== "number") {
            p_main.routeIndex += 1;

            /* Out-of-bounds checking. */
            if (p_main.routeIndex >= p_main.routes.length) {
                p_main.routeIndex = 0;
            }
        } else {
            if (routeNum >= 0) {
                /* Out-of-bounds checking. */
                if (routeNum >= p_main.routes.length) {
                    routeNum = p_main.routes.length - 1;
                }

                p_main.routeIndex = routeNum;
            } else {
                p_main.routeIndex = p_main.routes.length - 1;
            }
        }
    };

    /* NAME
     *   $checkRoute
     *
     * FUNCTION
     *   Check the current route and send a message to the caller ship's AI.
     *
     * INPUT
     *   callerShip - caller ship
     */
    this.$checkRoute = function (callerShip) {
        var entity,
        distance;

        /* Out-of-bounds checking. */
        if (p_main.routeIndex < 0) {
            p_main.routeIndex = p_main.routes.length - 1;
        } else if (p_main.routeIndex >= p_main.routes.length) {
            p_main.routeIndex = 0;
        }

        if (this.$logging && this.$logExtra) {
            entity = p_main.routes[p_main.routeIndex].entity;

            /* Check for entities becoming invalid. */
            if (!entity || !entity.isValid) {
                /* Fake entity a distance of 10 x the required range in a random direction. */
                p_main.routes[p_main.routeIndex].entity = {
                    isValid : true,
                    position : Vector3D.randomDirection(p_main.routes[p_main.routeIndex].range * 10),
                    collisionRadius : 100
                };
                entity = p_main.routes[p_main.routeIndex].entity;
            }

            /* Calculate the surface to surface distance, not centre to centre. */
            distance = callerShip.position.distanceTo(entity.position);
            distance -= callerShip.collisionRadius;
            distance -= entity.collisionRadius;

            log(this.name, "$checkRoute::\n" +
                "* ship#" + callerShip.entityPersonality +
                " (" + callerShip.name + ": " + callerShip.displayName + ")\n" +
                "* Entity position: " + entity.position + "\n" +
                "* Distance: " + distance + "\n" +
                "* Desired range: " + p_main.routes[p_main.routeIndex].range + "\n" +
                "* Current route: " + p_main.routeIndex + " (" + p_main.routes[p_main.routeIndex].aiMessage + ")");
        }

        callerShip.reactToAIMessage(p_main.routes[p_main.routeIndex].aiMessage);
    };

    /* NAME
     *   $finishedRoute
     *
     * FUNCTION
     *   Finished the current route, change to the next one.
     *
     * INPUT
     *   callerShip - caller ship
     *   groupRole - role of our group
     *   aiResponse - AI response to send to all ships in groupRole
     */
    this.$finishedRoute = function (callerShip, groupRole, aiResponse) {
        var entity,
        distance,
        otherShips,
        otherShipsLength,
        otherShipsCounter;

        /* Calculate the surface to saved co-ordinates distance, not centre to centre. */
        distance = callerShip.position.distanceTo(callerShip.savedCoordinates);
        distance -= callerShip.collisionRadius;
        /* Take off a small fudge factor. */
        distance -= 100;

        if (distance > p_main.routes[p_main.routeIndex].range) {
            /* Don't change route if we are no where near our target. */
            return;
        }

        if (this.$logging && this.$logExtra) {
            entity = p_main.routes[p_main.routeIndex].entity;
            log(this.name, "$finishedRoute::Checking...\n" +
                "* ship#" + callerShip.entityPersonality +
                " (" + callerShip.name + ": " + callerShip.displayName + ")\n" +
                "* Entity position: " + entity.position + "\n" +
                "* Saved co-ordinates: " + callerShip.savedCoordinates + "\n" +
                "* Distance: " + distance + "\n" +
                "* Desired range: " + p_main.routes[p_main.routeIndex].range + "\n" +
                "* Current route: " + p_main.routeIndex + " (" + p_main.routes[p_main.routeIndex].aiMessage + ")");
        }

        /* Change to the next route. */
        this.$changeRoute();
        /* Find other ships in 'groupRole'. Sort by distance from the caller ship. */
        otherShips = system.shipsWithRole(groupRole, callerShip);

        if (!otherShips.length) {
            /* Return immediately if we are on our own. */
            return;
        }

        /* Cache the length. */
        otherShipsLength = otherShips.length;

        for (otherShipsCounter = 0; otherShipsCounter < otherShipsLength; otherShipsCounter += 1) {
            /* Force all other ships to regroup. The ship that called this is already regrouping. */
            otherShips[otherShipsCounter].reactToAIMessage(aiResponse);
        }
    };

    /* NAME
     *   $sendNewsToSnoopers
     *
     * FUNCTION
     *   Send news to Snoopers (if available).
     *
     * INPUTS
     *   message - news to show
     *   agency - agency to use (optional)
     */
    this.$sendNewsToSnoopers = function (message, agency) {
        var news = {},
        result,
        index;

        if (!worldScripts.snoopers) {
            /* Snoopers not installed. */
            return;
        }

        if (!agency || typeof agency !== "number") {
            /* Random agency. [1, 2 or 3] */
            agency = Math.floor(Math.random() * 3.0) + 1;
        }

        news.ID = this.name;
        news.Message = message;
        news.Agency = agency;
        result = worldScripts.snoopers.insertNews(news);
        index = result + 5;

        if (result < 0) {
            /* Save for later. Snoopers only allows one news item at a time. */
            this.$playerVar.newsForSnoopers.push(news);

            if (this.$logging && this.$logExtra) {
                log(this.name, "$sendNewsToSnoopers::Saving news for later.\n" +
                    "* ID: '" + this.name + "'\n" +
                    "* Message: '" + message + "'\n" +
                    "* Agency: " + agency + "\n" +
                    "* result: " + result + (result >= -5 ? ") " + p_const.snoopersErrorCodes[index] : ""));
            }
        } else if (result > 0) {
            /* Problem. */
            log(this.name, "$sendNewsToSnoopers::Problem with news.\n" +
                "* ID: '" + this.name + "'\n" +
                "* Message: '" + message + "'\n" +
                "* Agency: " + agency + "\n" +
                "* result: " + result + (result <= 30 ? ") " + p_const.snoopersErrorCodes[index] : ""));
        } else {
            /* News inserted. */
            if (this.$logging && this.$logExtra) {
                log(this.name, "$sendNewsToSnoopers::News inserted.\n" +
                    "* ID: '" + this.name + "'\n" +
                    "* Message: '" + message + "'\n" +
                    "* Agency: " + agency + "\n" +
                    "* result: " + result + ") " + p_const.snoopersErrorCodes[index]);
            }
        }
    };

    /* NAME
     *   newsDisplayed
     *
     * FUNCTION
     *   Called by Snoopers when the news item has been displayed.
     *   Check for any more news available and send it.
     */
    this.newsDisplayed = function () {
        var news = this.$playerVar.newsForSnoopers.shift();

        if (news) {
            /* More news available. Send it to Snoopers. */
            this.$sendNewsToSnoopers(news.Message, news.Agency);
        }
    };
}.bind(this)());
Scripts/jaguar_company_asteroid.js
/*jslint bitwise: true, es5: true, newcap: true, nomen: true, regexp: true, unparam: true, todo: true, white: true,
indent: 4, maxerr: 50, maxlen: 120 */
/*jshint boss: true, curly: true, eqeqeq: true, eqnull: true, es5: true, evil: true, forin: true, laxbreak: true,
loopfunc: true, noarg: true, noempty: true, strict: true, nonew: true, undef: true */
/*global Math, Vector3D, log, system, worldScripts */

/* Jaguar Company Asteroid
 *
 * Copyright © 2012-2013 Richard Thomas Harrison (Tricky)
 *
 * This work is licensed under the Creative Commons
 * Attribution-Noncommercial-Share Alike 3.0 Unported License.
 *
 * To view a copy of this license, visit
 * http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter
 * to Creative Commons, 171 Second Street, Suite 300, San Francisco,
 * California, 94105, USA.
 *
 * Ship related functions for the asteroids cluttering the space around the base.
 */

(function () {
    "use strict";

    /* Standard public variables for OXP scripts. */
    this.name = "jaguar_company_asteroid.js";
    this.author = "Tricky";
    this.copyright = "© 2012-2013 Richard Thomas Harrison (Tricky)";
    this.license = "CC BY-NC-SA 3.0";
    this.description = "Ship script for the asteroids cluttering the space around the base.";
    this.version = "1.2";

    /* Ship script event handlers. */

    /* NAME
     *   shipSpawned
     *
     * FUNCTION
     *   Move the asteroid to a safe distance from the base launch corridor on birth.
     */
    this.shipSpawned = function () {
        var mainScript = worldScripts["Jaguar Company"],
        asteroid = this.ship,
        base = mainScript.$jaguarCompanyBase,
        /* Increased to 12km for the first try. */
        safeDistance = 11000,
        asteroidMoved = 0,
        distance,
        totalDistanceMoved = 0,
        oldDistance,
        newDistance,
        direction,
        entities,
        ok = false;

        if (!base || !base.isValid ||
            (!asteroid.hasRole("jaguar_company_asteroid") && !asteroid.hasRole("jaguar_company_boulder"))) {
            /* Not an asteroid spawned by the base. */
            return;
        }

        /* NAME
         *   $validEntity
         *
         * FUNCTION
         *   Stop warnings about anonymous local functions within loops.
         *   Used by 'system.filteredEntities'. Returns true for any valid entity.
         *
         * INPUT
         *   entity - entity to check
         */
        function $validEntity(entity) {
            return (entity.isValid);
        }

        /* Don't drift. Just leave it rotating. */
        asteroid.velocity = new Vector3D(0, 0, 0);
        /* Work out if it is near to the base during spawning. */
        distance = asteroid.position.distanceTo(base.position);

        if (distance < safeDistance) {
            direction = asteroid.position.subtract(base.position).direction();

            /* Check the launch corridor. (0.86 from src/Core/Entities/DockEntity.m) */
            if (direction.dot(base.heading) > 0.86) {
                /* Asteroids added too close to the base can block launches.
                 * Move them to a safe distance (+/- 500m) from the base.
                 * Safe distance is altered if there is another entity nearby.
                 */
                newDistance = oldDistance = distance;

                while (!ok) {
                    asteroidMoved += 1;
                    /* Increase the safe distance by 1km. */
                    safeDistance += 1000;
                    /* Work out a new distance (varied by +/- 500m). */
                    totalDistanceMoved += distance = (safeDistance - newDistance) + (500 - (Math.random() * 1000));
                    /* Move the asteroid. */
                    asteroid.position = asteroid.position.add(direction.multiply(distance));
                    /* New distance from the base launch corridor. */
                    newDistance = oldDistance + totalDistanceMoved;
                    /* Search for any entity intersecting this asteroid (plus 500m) at the new distance. */
                    entities = system.filteredEntities(this, $validEntity, asteroid, asteroid.collisionRadius + 500);
                    /* An empty array is what we are looking for. */
                    ok = !entities.length;
                }

                if (mainScript.$logging && mainScript.$logExtra) {
                    log(this.name, "shipSpawned::Moving " + asteroid.displayName + " to " + asteroid.position + "\n" +
                        "* Moved: " + asteroidMoved + " times\n" +
                        "* Safe distance: " + safeDistance + "\n" +
                        "* Old distance: " + oldDistance + "\n" +
                        "* Total distance moved: " + totalDistanceMoved + "\n" +
                        "* New distance: " + newDistance);
                }
            }
        }

        /* No longer needed after setting up. */
        delete this.shipSpawned;
    };
}.bind(this)());
Scripts/jaguar_company_base.js
/*jslint bitwise: true, es5: true, newcap: true, nomen: true, regexp: true, unparam: true, todo: true, white: true,
indent: 4, maxerr: 50, maxlen: 120 */
/*jshint boss: true, curly: true, eqeqeq: true, eqnull: true, es5: true, evil: true, forin: true, laxbreak: true,
loopfunc: true, noarg: true, noempty: true, strict: true, nonew: true, undef: true */
/*global Array, Math, Timer, Vector3D, galaxyNumber, log, system, worldScripts */

/* Jaguar Company Base
 *
 * Copyright © 2012-2013 Richard Thomas Harrison (Tricky)
 *
 * This work is licensed under the Creative Commons
 * Attribution-Noncommercial-Share Alike 3.0 Unported License.
 *
 * To view a copy of this license, visit
 * http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter
 * to Creative Commons, 171 Second Street, Suite 300, San Francisco,
 * California, 94105, USA.
 *
 * Ship related functions for the base AI.
 */

(function () {
    "use strict";

    /* Standard public variables for OXP scripts. */
    this.name = "jaguar_company_base.js";
    this.author = "Tricky";
    this.copyright = "© 2012-2013 Richard Thomas Harrison (Tricky)";
    this.license = "CC BY-NC-SA 3.0";
    this.description = "Ship script for the Jaguar Company Base.";
    this.version = "1.5";

    /* Private variable. */
    var p_base = {};

    /* Ship script event handlers. */

    /* NAME
     *   shipSpawned
     *
     * FUNCTION
     *   Initialise various variables on ship birth.
     */
    this.shipSpawned = function () {
        var num,
        vector,
        cross,
        angle,
        loop;

        /* Initialise the p_base variable object.
         * Encapsulates all private global data.
         */
        p_base = {
            /* Cache the world scripts. */
            mainScript : worldScripts["Jaguar Company"],
            shipsScript : worldScripts["Jaguar Company Ships"],
            /* Local copies of the logging variables. */
            logging : worldScripts["Jaguar Company"].$logging,
            logExtra : worldScripts["Jaguar Company"].$logExtra,
            /* Maximum number of splinter ships around the base. */
            maxSplinterShips : 6
        };

        /* Tug object. */
        this.$tug = system.shipsWithPrimaryRole("jaguar_company_tug")[0];
        /* Buoy object. */
        this.$buoy = system.shipsWithRole("jaguar_company_base_buoy")[0];
        /* Miner object. */
        this.$miner = system.shipsWithPrimaryRole("jaguar_company_miner")[0];
        /* Reset the check flags. */
        this.$patrolShipsOK = false;
        this.$splinterShipsOK = false;
        this.$tugOK = false;
        this.$buoyOK = false;
        this.$minerOK = false;

        if (p_base.mainScript.$swapBase) {
            /* Swapping base roles. */

            /* Register this base as a friendly. */
            p_base.shipsScript.$addFriendly({
                ship : this.ship
            });
            /* How many splinter ships have launched. */
            p_base.numSplinterShips = system.shipsWithPrimaryRole("jaguar_company_ship_splinter").length;
            /* Have all splinter ships launched. */
            p_base.splinterShipsFullyLaunched = (p_base.numSplinterShips === p_base.maxSplinterShips);
            /* Has the miner launched. */
            p_base.minerLaunched = (this.$miner && this.$miner.isValid);
            /* Base fully spawned and set-up so we can reset this. */
            p_base.mainScript.$swapBase = false;
        } else {
            /* Register this base as a friendly. */
            p_base.shipsScript.$addFriendly({
                ship : this.ship,
                /* Get a unique name for the base.
                 * Maximum length for the base's name is 17 characters.
                 * Fits into the mission screen and commodities title header.
                 */
                shipName : p_base.mainScript.$uniqueShipName(true, 17)
            });

            if (!system.isInterstellarSpace) {
                /* Point the docking bay in the general direction of the sun. */
                vector = system.sun.position.subtract(this.ship.position).direction();
            } else {
                /* Point the docking bay in the general direction of the fake witchpoint. */
                vector = p_base.mainScript.$witchpointBuoy.position.subtract(this.ship.position).direction();
            }

            /* Angle to the vector from current heading + about 1/8th turn. */
            angle = this.ship.heading.angleTo(vector) + 0.707;
            /* Cross vector for rotate. */
            cross = this.ship.heading.cross(vector).direction();
            /* Rotate the base by angle. */
            this.ship.orientation = this.ship.orientation.rotate(cross, -angle);

            /* Add some clutter within 20km around the base.
             * 20 to 27 asteroids and 10 to 17 boulders
             * - gives the miners something to do. (Also apparently any Thargoids!)
             */
            num = Math.floor(system.scrambledPseudoRandomNumber(p_base.mainScript.$salt) * 8) + 20;

            for (loop = 0; loop < num; loop += 1) {
                switch (Math.floor(Math.random() * 8)) {
                case 0:
                case 1:
                case 2:
                case 3:
                case 4:
                    system.addShips("glowasteroid", 1, this.ship.position, 20000);
                    break;
                case 5:
                case 6:
                    system.addShips("glowmossasteroid", 1, this.ship.position, 20000);
                    break;
                case 7:
                    system.addShips("jaguar_company_asteroid", 1, this.ship.position, 20000);
                    break;
                default:
                    system.addShips("jaguar_company_asteroid", 1, this.ship.position, 20000);
                    break;
                }
            }

//            system.addShips("jaguar_company_asteroid", num, this.ship.position, 20000);
            num = Math.floor(system.scrambledPseudoRandomNumber(p_base.mainScript.$salt / 2) * 8) + 10;
            system.addShips("jaguar_company_boulder", num, this.ship.position, 20000);

            /* Reset the launch status of the buoy. */
            this.$buoyLaunched = false;
            /* Miner has not launched. */
            p_base.minerLaunched = false;
            /* No splinter ships have launched. */
            p_base.numSplinterShips = 0;

            if (!system.shipsWithPrimaryRole("jaguar_company_patrol").length) {
                /* No patrol ships have launched. */
                p_base.mainScript.$numPatrolShips = 0;
                /* Reset the fully launched status of the patrol ships. */
                p_base.mainScript.$patrolShipsFullyLaunched = false;
            }
        }

        /* Start up a timer to check ship script sanity. */
        this.$scriptSanityTimerReference = new Timer(this, this.$scriptSanityTimer, 5, 5);
        /* Start up a timer to do some house keeping. */
        this.$baseTimerReference = new Timer(this, this.$baseTimer, 5, 5);

        /* No longer needed after setting up. */
        delete this.shipSpawned;
    };

    /* NAME
     *   shipDied
     *
     * FUNCTION
     *   Base was destroyed.
     *   Called after the script installed by $addFriendly in jaguar_company_ships.js
     *
     * INPUTS
     *   attacker - entity that caused the death (not used)
     *   why - cause as a string
     */
    this.shipDied = function (attacker, why) {
        var mainScript,
        base = this.ship,
        basePosition,
        witchpointBuoy,
        mainPlanet,
        mPovUp,
        ratio,
        entities,
        salt,
        ok;

        /* NAME
         *   $validEntity
         *
         * FUNCTION
         *   Stop warnings about anonymous local functions within loops.
         *   Used by 'system.filteredEntities'. Returns true for any valid entity.
         *
         * INPUT
         *   entity - entity to check
         */
        function $validEntity(entity) {
            return (entity.isValid);
        }

        if (base.name === base.displayName) {
            /* Died whilst being created. The base will not have had it's display name set up. */
            mainScript = worldScripts["Jaguar Company"];
            witchpointBuoy = mainScript.$witchpointBuoy;
            mainPlanet = system.mainPlanet;
            salt = mainScript.$salt;
            ok = false;

            /* Shift the base position if it is too close to any entity.
             * If we happen to pick a new position that would collide with something already in the system
             * then the loop will pick another position and so on.
             * In practice the loop will only happen once as space is BIG.
             */
            while (!ok) {
                /* Increase the salt. */
                salt += 1;
                /* Place the base 0.1 to 0.3 units along the witchpoint -> main planet route. */
                ratio = 0.1 + (system.scrambledPseudoRandomNumber(salt) * 0.2);
                basePosition = Vector3D.interpolate(witchpointBuoy.position, mainPlanet.position, ratio);
                /* Increase the salt. */
                salt += 1;
                /* Move it 6 to 8 times scanning range upwards with respect to the main planet's surface. */
                ratio = (6 + (system.scrambledPseudoRandomNumber(salt) * 2)) * 25600;
                mPovUp = mainPlanet.orientation.vectorUp();
                base.position = basePosition.add(mPovUp.multiply(mainPlanet.radius + ratio));
                /* Search for any entity intersecting this base within scanner range. */
                entities = system.filteredEntities(this, $validEntity, base, 25600);
                /* An empty array is what we are looking for. */
                ok = !entities.length;
            }

            if (mainScript.$logging && mainScript.$logExtra) {
                log(this.name, "shipDied::\n" +
                    "Base respawning: " + why + " whilst being created.\n" +
                    "* WP-Sun dot WP-MP: " +
                    (witchpointBuoy.position.subtract(system.sun.position).direction()
                        .dot(witchpointBuoy.position.subtract(mainPlanet.position).direction())) + "\n" +
                    "* Moved: " + (salt - mainScript.$salt) + " times.");
            }

            /* Spawn a new base. Update the public variable in the main script. */
            mainScript.$jaguarCompanyBase = base.spawnOne("jaguar_company_base");

            return;
        }
    };

    /* NAME
     *   shipRemoved
     *
     * FUNCTION
     *   Base was removed by script.
     *
     * INPUT
     *   suppressDeathEvent - boolean
     *     true - shipDied() will not be called
     *     false - shipDied() will be called
     */
    this.shipRemoved = function (suppressDeathEvent) {
        if (suppressDeathEvent) {
            return;
        }

        /* Set this as no more ships will be launched. "Obvious cat is obvious!" */
        worldScripts["Jaguar Company"].$patrolShipsFullyLaunched = true;
    };

    /* NAME
     *   entityDestroyed
     *
     * FUNCTION
     *   The base has just become invalid.
     */
    this.entityDestroyed = function () {
        /* Set this as no more ships will be launched. "Obvious cat is obvious!" */
        worldScripts["Jaguar Company"].$patrolShipsFullyLaunched = true;

        /* Stop and remove the timers. */
        if (this.$baseTimerReference) {
            if (this.$baseTimerReference.isRunning) {
                this.$baseTimerReference.stop();
            }

            this.$baseTimerReference = null;
        }

        if (this.$scriptSanityTimerReference) {
            if (this.$scriptSanityTimerReference.isRunning) {
                this.$scriptSanityTimerReference.stop();
            }

            this.$scriptSanityTimerReference = null;
        }
    };

    /* NAME
     *   otherShipDocked
     *
     * FUNCTION
     *   A ship has docked.
     *
     * INPUT
     *   whom - entity of the docked ship
     */
    this.otherShipDocked = function (whom) {
        if (whom.hasRole("jaguar_company_patrol")) {
            /* Reset the script check. */
            this.$patrolShipsOK = false;
        } else if (whom.hasRole("jaguar_company_ship_splinter")) {
            /* Decrease the number of splinter ships that are launched. */
            p_base.numSplinterShips -= 1;
            /* Reset the fully launched status of the splinter ships. */
            p_base.splinterShipsFullyLaunched = false;
            /* Reset the script check. */
            this.$splinterShipsOK = false;
            /* Launch another splinter ship. */
            this.$launchJaguarCompanySplinterShip();
        } else if (whom.hasRole("jaguar_company_tug")) {
            /* Reset the script check. */
            this.$tugOK = false;
        } else if (whom.hasRole("jaguar_company_miner")) {
            /* Reset the launch status of the miner. */
            p_base.minerLaunched = false;
            /* Reset the script check. */
            this.$minerOK = false;
        }
    };

    /* NAME
     *   stationLaunchedShip
     *
     * FUNCTION
     *   A ship has launched.
     *
     * INPUT
     *   whom - entity of the launched ship
     */
    this.stationLaunchedShip = function (whom) {
        if (whom.hasRole("jaguar_company_patrol")) {
            if (p_base.mainScript.$numPatrolShips === 1) {
                /* Initialise the route list with the default route. */
                p_base.mainScript.$initRoute();
            }

            if (p_base.mainScript.$numPatrolShips !== p_base.mainScript.$maxPatrolShips) {
                /* Launch another patrol ship. */
                this.$launchJaguarCompanyPatrol();
            } else {
                /* All patrol ships are now fully launched. */
                p_base.mainScript.$patrolShipsFullyLaunched = true;

                if (!p_base.splinterShipsFullyLaunched) {
                    /* Start launching splinter ships. */
                    this.$launchJaguarCompanySplinterShip();
                }
            }
        } else if (whom.hasRole("jaguar_company_ship_splinter")) {
            if (p_base.numSplinterShips !== p_base.maxSplinterShips) {
                /* Launch another splinter ship. */
                this.$launchJaguarCompanySplinterShip();
            } else {
                /* All splinter ships are now fully launched. */
                p_base.splinterShipsFullyLaunched = true;
            }
        }
    };

    /* Other global public functions. */

    /* NAME
     *   $showProps
     *
     * FUNCTION
     *   For debugging only.
     */
    this.$showProps = function () {
        var result = "",
        prop;

        for (prop in this) {
            if (this.hasOwnProperty(prop)) {
                if (typeof this[prop] !== "function") {
                    result += "this." + prop + ": " + this[prop] + "\n";
                } else {
                    result += "this." + prop + " = function ()\n";
                }
            }
        }

        for (prop in p_base) {
            if (p_base.hasOwnProperty(prop)) {
                result += "p_base." + prop + ": " + p_base[prop] + "\n";
            }
        }

        log(this.name, "$showProps::\n" + result);
    };

    /* NAME
     *   $scriptSanityTimer
     *
     * FUNCTION
     *   Periodic function to check if Jaguar Company ships have spawned correctly on launch.
     *
     *   Checks the Patrol ships, tug, buoy and miner.
     *
     *   Called every 5 seconds.
     */
    this.$scriptSanityTimer = function () {
        var patrolShips,
        patrolShip,
        patrolShipsToCheck,
        splinterShips,
        splinterShip,
        splinterShipsToCheck,
        tug,
        buoy,
        miner,
        counter,
        length;

        /* Check the patrol ships. */
        if (!this.$patrolShipsOK) {
            /* Search for the patrol ships. */
            patrolShips = system.shipsWithPrimaryRole("jaguar_company_patrol");

            if (patrolShips.length > 0) {
                /* Set the counter to all entities found. */
                patrolShipsToCheck = patrolShips.length;
                /* Cache the length. */
                length = patrolShips.length;

                /* Iterate through each of the patrol ships. */
                for (counter = 0; counter < length; counter += 1) {
                    patrolShip = patrolShips[counter];

                    if (patrolShip.script.name !== "jaguar_company_patrol.js") {
                        /* Reload the ship script. */
                        patrolShip.setScript("jaguar_company_patrol.js");
                        patrolShip.script.shipSpawned();

                        if (p_base.logging && p_base.logExtra) {
                            log(this.name, "Script sanity check - fixed a patrol ship.");
                        }
                    } else {
                        patrolShipsToCheck -= 1;
                    }
                }

                if (!patrolShipsToCheck &&
                    p_base.mainScript.$numPatrolShips === p_base.mainScript.$maxPatrolShips) {
                    /* Don't re-check. */
                    this.$patrolShipsOK = true;
                }
            }
        }

        /* Check the splinter ships. */
        if (!this.$splinterShipsOK) {
            /* Search for the splinter ships. */
            splinterShips = system.shipsWithPrimaryRole("jaguar_company_ship_splinter");

            if (splinterShips.length > 0) {
                /* Set the counter to all entities found. */
                splinterShipsToCheck = splinterShips.length;
                /* Cache the length. */
                length = splinterShips.length;

                /* Iterate through each of the splinter ships. */
                for (counter = 0; counter < length; counter += 1) {
                    splinterShip = splinterShips[counter];

                    if (splinterShip.script.name !== "jaguar_company_ship_splinter.js") {
                        /* Reload the ship script. */
                        splinterShip.setScript("jaguar_company_ship_splinter.js");
                        splinterShip.script.shipSpawned();

                        if (p_base.logging && p_base.logExtra) {
                            log(this.name, "Script sanity check - fixed a splinter ship.");
                        }
                    } else {
                        splinterShipsToCheck -= 1;
                    }
                }

                if (!splinterShipsToCheck && p_base.numSplinterShips === p_base.maxSplinterShips) {
                    /* Don't re-check. */
                    this.$splinterShipsOK = true;
                }
            }
        }

        tug = this.$tug;

        /* Check the tug. */
        if (!this.$tugOK && tug && tug.isValid) {
            if (tug.script.name !== "jaguar_company_tug.js") {
                /* Reload the ship script. */
                tug.setScript("jaguar_company_tug.js");
                tug.script.shipSpawned();

                if (p_base.logging && p_base.logExtra) {
                    log(this.name, "Script sanity check - fixed the tug.");
                }
            } else {
                /* Don't re-check. */
                this.$tugOK = true;
            }
        }

        buoy = this.$buoy;

        /* Check the buoy. */
        if (buoy && buoy.isValid && !this.$buoyOK) {
            if (buoy.script.name !== "jaguar_company_base_buoy.js") {
                /* Reload the ship script. */
                buoy.setScript("jaguar_company_base_buoy.js");
                buoy.script.shipSpawned();

                if (p_base.logging && p_base.logExtra) {
                    log(this.name, "Script sanity check - fixed the buoy.");
                }
            } else {
                /* Don't re-check. */
                this.$buoyOK = true;
            }
        }

        /* Check the miner. Only check the miner ship script if not in interstellar space. */
        if (!system.isInterstellarSpace) {
            miner = this.$miner;

            if (miner && miner.isValid && !this.$minerOK) {
                if (miner.script.name !== "jaguar_company_miner.js") {
                    /* Reload the ship script. */
                    miner.setScript("jaguar_company_miner.js");
                    miner.script.shipSpawned();

                    if (p_base.logging && p_base.logExtra) {
                        log(this.name, "Script sanity check - fixed the miner.");
                    }
                } else {
                    /* Don't re-check. */
                    this.$minerOK = true;
                }
            }
        }
    };

    /* NAME
     *   $baseTimer
     *
     * FUNCTION
     *   Some OXP's dick around with the scanner colours. This will reset the base's scanner colour
     *   back to the station default of solid green if the player has helped out in combat with Jaguar Company.
     *
     *   Starts the launch sequence for the patrol ships if needed.
     *
     *   Checks the buoy and launches a tug if there isn't one. Resets the scanner colour as per the base.
     *   Swaps the buoy to 'no beacon' or 'beacon' dependent on the reputation mission variable.
     *
     *   Called every 5 seconds.
     */
    this.$baseTimer = function () {
        var base = this.ship,
        position,
        orientation,
        newBuoy,
        newBuoyRole;

        /* Reset the base scanner colour. */
        base.scannerDisplayColor1 = null;
        base.scannerDisplayColor2 = null;

        if (!p_base.mainScript.$numPatrolShips) {
            /* Reset the fully launched status of the patrol ships. */
            p_base.mainScript.$patrolShipsFullyLaunched = false;
            /* Start launching patrol ships. */
            this.$launchJaguarCompanyPatrol();
        }

        if ((!this.$buoy || !this.$buoy.isValid) && !this.$buoyLaunched &&
            p_base.mainScript.$patrolShipsFullyLaunched &&
            p_base.splinterShipsFullyLaunched) {
            /* No buoys. Launch the tug to drop a buoy off. */
            this.$launchJaguarCompanyTug();

            return;
        }

        if (this.$buoy && this.$buoy.isValid) {
            /* Reset the buoy scanner colour. */
            this.$buoy.scannerDisplayColor1 = null;
            this.$buoy.scannerDisplayColor2 = null;

            if (p_base.mainScript.$playerVar.reputation[galaxyNumber] < p_base.mainScript.$reputationHelper) {
                newBuoyRole = "jaguar_company_base_buoy_no_beacon";
            } else {
                newBuoyRole = "jaguar_company_base_buoy_beacon";
            }

            /* Check if the buoy already has the new role. */
            if (!this.$buoy.hasRole(newBuoyRole)) {
                /* Copy some properties. */
                position = this.$buoy.position;
                orientation = this.$buoy.orientation;
                /* Create a new buoy. */
                newBuoy = this.$buoy.spawnOne(newBuoyRole);
                /* Remove the origial buoy quietly: don't trigger 'shipDied' in the ship script. */
                this.$buoy.remove(true);
                /* Setup the new buoy with the original properties. */
                newBuoy.position = position;
                newBuoy.orientation = orientation;
                /* Update the buoy reference. */
                this.$buoy = newBuoy;
            }
        }
    };

    /* NAME
     *   $launchJaguarCompanyPatrol
     *
     * FUNCTION
     *   Launch a patrol ship.
     */
    this.$launchJaguarCompanyPatrol = function () {
        if (p_base.logging && p_base.logExtra) {
            log(this.name, "$launchJaguarCompanyPatrol::Launching patrol ship...");
        }

        p_base.mainScript.$numPatrolShips += 1;
        this.ship.launchShipWithRole("jaguar_company_patrol");
    };

    /* NAME
     *   $launchJaguarCompanySplinterShip
     *
     * FUNCTION
     *   Launch a splinter ship.
     */
    this.$launchJaguarCompanySplinterShip = function () {
        if (p_base.logging && p_base.logExtra) {
            log(this.name, "$launchJaguarCompanySplinterShip::Launching splinter ship...");
        }

        p_base.numSplinterShips += 1;
        this.ship.launchShipWithRole("jaguar_company_ship_splinter");
    };

    /* NAME
     *   $launchJaguarCompanyTug
     *
     * FUNCTION
     *   Launch the tug to push the buoy into position.
     */
    this.$launchJaguarCompanyTug = function () {
        if (!this.$buoyLaunched && (!this.$buoy || !this.$buoy.isValid) &&
            p_base.mainScript.$patrolShipsFullyLaunched &&
            p_base.splinterShipsFullyLaunched) {
            /* Only one tug dragging a buoy at a time. Also no more than 1 buoy in system at a time.
             * Also don't launch until all patrol ships and splinter ships have launched.
             */
            this.$buoyLaunched = true;
            this.ship.launchShipWithRole("jaguar_company_tug");

            if (p_base.logging && p_base.logExtra) {
                log(this.name, "$launchJaguarCompanyTug::Launching tug...");
            }
        }
    };

    /* AI functions. */

    /* NAME
     *   $launchShip
     *
     * FUNCTION
     *   Launch other ships from the base.
     *
     * INPUT
     *   arr - array of ship types to launch
     */
    this.$launchShip = function (arr) {
        var shipType,
        counter,
        length;

        if (!Array.isArray(arr)) {
            /* Default array of ship types. */
            arr = ["miner"];
        }

        /* Cache the length. */
        length = arr.length;

        for (counter = 0; counter < length; counter += 1) {
            shipType = arr[counter];

            if (p_base.mainScript.$patrolShipsFullyLaunched) {
                if (p_base.splinterShipsFullyLaunched) {
                    if (shipType === "miner") {
                        if (!system.isInterstellarSpace &&
                            Math.random() < 0.05 &&
                            !p_base.minerLaunched) {
                            /* Only 1 miner at a time. */
                            p_base.minerLaunched = true;
                            this.ship.launchShipWithRole("jaguar_company_miner");

                            if (p_base.logging && p_base.logExtra) {
                                log(this.name, "$launchShip::Launching miner...");
                            }
                        }
                    }
                }
            }
        }
    };
}.bind(this)());
Scripts/jaguar_company_base_buoy.js
/*jslint bitwise: true, es5: true, newcap: true, nomen: true, regexp: true, unparam: true, todo: true, white: true,
indent: 4, maxerr: 50, maxlen: 120 */
/*jshint boss: true, curly: true, eqeqeq: true, eqnull: true, es5: true, evil: true, forin: true, laxbreak: true,
loopfunc: true, noarg: true, noempty: true, strict: true, nonew: true, undef: true */
/*global Timer, Vector3D, addFrameCallback, isValidFrameCallback, log, removeFrameCallback, system, worldScripts */

/* Jaguar Company Base Buoy
 *
 * Copyright © 2012-2013 Richard Thomas Harrison (Tricky)
 *
 * This work is licensed under the Creative Commons
 * Attribution-Noncommercial-Share Alike 3.0 Unported License.
 *
 * To view a copy of this license, visit
 * http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter
 * to Creative Commons, 171 Second Street, Suite 300, San Francisco,
 * California, 94105, USA.
 *
 * Ship related functions for the base buoy.
 */

(function () {
    "use strict";

    /* Standard public variables for OXP scripts. */
    this.name = "jaguar_company_base_buoy.js";
    this.author = "Tricky";
    this.copyright = "© 2012-2013 Richard Thomas Harrison (Tricky)";
    this.license = "CC BY-NC-SA 3.0";
    this.description = "Ship script for the Jaguar Company Base Buoy.";
    this.version = "1.3";

    /* Private variable. */
    var p_buoy = {};

    /* Ship script event handlers. */

    /* NAME
     *   shipSpawned
     *
     * FUNCTION
     *   Initialise various variables on ship birth.
     */
    this.shipSpawned = function () {
        /* Initialise the p_buoy variable object.
         * Encapsulates all private global data.
         */
        p_buoy = {
            /* Cache the world scripts. */
            mainScript : worldScripts["Jaguar Company"],
            shipsScript : worldScripts["Jaguar Company Ships"],
            /* Local copies of the logging variables. */
            logging : worldScripts["Jaguar Company"].$logging,
            logExtra : worldScripts["Jaguar Company"].$logExtra
        };

        /* Register this buoy as a friendly. */
        p_buoy.shipsScript.$addFriendly({
            ship : this.ship
        });
        /* Wait 5 seconds then find the witchpoint. */
        p_buoy.nextTarget = "WITCHPOINT";
        this.$buoyTimerReference = new Timer(this, this.$buoyTimer, 5);

        /* No longer needed after setting up. */
        delete this.shipSpawned;
    };

    /* NAME
     *   shipRemoved
     *
     * FUNCTION
     *   Base buoy was removed by script.
     *
     * INPUT
     *   suppressDeathEvent - boolean
     *     true - shipDied() will not be called
     *     false - shipDied() will be called
     */
    this.shipRemoved = function (suppressDeathEvent) {
        var base;

        if (suppressDeathEvent) {
            return;
        }

        base = worldScripts["Jaguar Company"].$jaguarCompanyBase;

        if (base && base.isValid) {
            /* Reset the script check. */
            base.script.$buoyOK = false;
            /* Force a launch of a new buoy. */
            base.script.$buoyLaunched = false;
        }
    };

    /* NAME
     *   entityDestroyed
     *
     * FUNCTION
     *   The base buoy has just become invalid.
     */
    this.entityDestroyed = function () {
        var base = worldScripts["Jaguar Company"].$jaguarCompanyBase;

        if (base && base.isValid) {
            /* Reset the script check. */
            base.script.$buoyOK = false;
            /* Force a launch of a new buoy. */
            base.script.$buoyLaunched = false;
        }

        /* Stop and remove the frame callback and timer. */
        this.$removeBuoyTimer();
        this.$removeBuoyFCB();
    };

    /* Other global public functions. */

    /* NAME
     *   $removeBuoyTimer
     *
     * FUNCTION
     *   Stop and remove the timer.
     */
    this.$removeBuoyTimer = function () {
        if (this.$buoyTimerReference) {
            if (this.$buoyTimerReference.isRunning) {
                this.$buoyTimerReference.stop();
            }

            this.$buoyTimerReference = null;
        }
    };

    /* NAME
     *   $removeBuoyFCB
     *
     * FUNCTION
     *   Stop and remove the frame callback.
     */
    this.$removeBuoyFCB = function () {
        /* Turn the flashers on. */
        this.ship.lightsActive = true;

        if (this.$buoyFCBReference) {
            if (isValidFrameCallback(this.$buoyFCBReference)) {
                removeFrameCallback(this.$buoyFCBReference);
            }

            this.$buoyFCBReference = null;
        }
    };

    /* NAME
     *   $findJaguarCompanyPatrol
     *
     * FUNCTION
     *   Point the dish at Jaguar Company Patrol.
     */
    this.$findJaguarCompanyPatrol = function () {
        var patrolShips,
        patrolShipsLength,
        patrolShipsCounter,
        midpointPosition;

        /* Search for the patrol ships. */
        patrolShips = system.shipsWithPrimaryRole("jaguar_company_patrol");

        if (!patrolShips.length) {
            /* We are on our own. Point the dish at the witchpoint. */
            return p_buoy.mainScript.$witchpointBuoy.position;
        }

        /* Cache the length. */
        patrolShipsLength = patrolShips.length;

        /* Work out the midpoint position of all the patrol ships. */
        midpointPosition = new Vector3D(0, 0, 0);

        for (patrolShipsCounter = 0; patrolShipsCounter < patrolShipsLength; patrolShipsCounter += 1) {
            midpointPosition = midpointPosition.add(patrolShips[patrolShipsCounter].position);
        }

        midpointPosition.x /= patrolShipsLength;
        midpointPosition.y /= patrolShipsLength;
        midpointPosition.z /= patrolShipsLength;

        return midpointPosition;
    };

    /* NAME
     *   $buoyTimer
     *
     * FUNCTION
     *   Start off with a 'calibration routine' by finding the witchpoint then the planet.
     *   Once 'calibrated', track Jaguar Company patrol ships every 2 minutes.
     */
    this.$buoyTimer = function () {
        var buoy = this.ship,
        position,
        vector;

        if (p_buoy.nextTarget === "JAGUAR_COMPANY_PATROL") {
            /* Find the position of then patrol ships. */
            position = this.$findJaguarCompanyPatrol();

            if (p_buoy.logging && p_buoy.logExtra) {
                log(this.name, "$buoyTimer::Buoy tracking Jaguar Company Patrol ships...");
            }
        } else if (p_buoy.nextTarget === "PLANET") {
            this.$removeBuoyTimer();
            /* Wait 30 seconds then track Jaguar Company Patrol every 2 minutes. */
            p_buoy.nextTarget = "JAGUAR_COMPANY_PATROL";
            this.$buoyTimerReference = new Timer(this, this.$buoyTimer, 30, 120);
            /* Find the position of the main planet. */
            position = system.mainPlanet.position;

            if (p_buoy.logging && p_buoy.logExtra) {
                log(this.name, "$buoyTimer::Buoy tracking the main planet...");
            }
        } else {
            this.$removeBuoyTimer();

            if (system.isInterstellarSpace) {
                /* Wait 30 seconds then track Jaguar Company Patrol every 2 minutes. */
                p_buoy.nextTarget = "JAGUAR_COMPANY_PATROL";
                this.$buoyTimerReference = new Timer(this, this.$buoyTimer, 30, 120);
            } else {
                /* Wait 30 seconds then find the planet. */
                p_buoy.nextTarget = "PLANET";
                this.$buoyTimerReference = new Timer(this, this.$buoyTimer, 30);
            }

            /* Find the position of the witchpoint. */
            position = p_buoy.mainScript.$witchpointBuoy.position;

            if (p_buoy.logging && p_buoy.logExtra) {
                log(this.name, "$buoyTimer::Buoy tracking the witchpoint...");
            }
        }

        /* Vector pointing towards the target. */
        vector = position.subtract(buoy.position).direction();
        /* Angle to the target from current heading. */
        p_buoy.finalAngle = buoy.heading.angleTo(vector);

        if (p_buoy.finalAngle < 0.087266462599716478846184538424431) {
            /* Already pointing in the rough direction of the target.
             * Looking for a difference of greater than 5 degrees.
             */
            return;
        }

        /* Cross vector for rotate. */
        p_buoy.cross = buoy.heading.cross(vector).direction();
        /* Starting angle. */
        p_buoy.angle = 0;
        /* Should take about 5 seconds (at 60 FPS). */
        p_buoy.deltaAngle = p_buoy.finalAngle / 300;
        /* Use a frame callback to do this smoothly. */
        this.$buoyFCBReference = addFrameCallback(this.$buoyFCB.bind(this));
    };

    /* NAME
     *   $buoyFCB
     *
     * FUNCTION
     *   Frame callback to slowly rotate the buoy towards Jaguar Company Patrol.
     *
     * INPUT
     *   delta - amount of game clock time past since the last frame
     */
    this.$buoyFCB = function (delta) {
        var buoy = this.ship;

        if (!buoy || !buoy.isValid) {
            /* Buoy can be invalid for 1 frame. */
            this.$removeBuoyTimer();
            this.$removeBuoyFCB();

            return;
        }

        if (delta === 0.0) {
            /* Do nothing if paused. */
            return;
        }

        if (p_buoy.angle >= p_buoy.finalAngle) {
            /* Reached the desired orientation. */
            this.$removeBuoyFCB();

            return;
        }

        /* Rotate by delta angle. */
        buoy.orientation = buoy.orientation.rotate(p_buoy.cross, -p_buoy.deltaAngle);
        /* Update the current angle. */
        p_buoy.angle += p_buoy.deltaAngle;
    };
}.bind(this)());
Scripts/jaguar_company_blackbox.js
/*jslint bitwise: true, es5: true, newcap: true, nomen: true, regexp: true, unparam: true, todo: true, white: true,
indent: 4, maxerr: 50, maxlen: 120 */
/*jshint boss: true, curly: true, eqeqeq: true, eqnull: true, es5: true, evil: true, forin: true, laxbreak: true,
loopfunc: true, noarg: true, noempty: true, strict: true, nonew: true, undef: true */
/*global worldScripts */

/* jaguar_company_blackbox.js
 *
 * Copyright © 2012-2013 Richard Thomas Harrison (Tricky)
 *
 * This work is licensed under the Creative Commons
 * Attribution-Noncommercial-Share Alike 3.0 Unported License.
 *
 * To view a copy of this license, visit
 * http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter
 * to Creative Commons, 171 Second Street, Suite 300, San Francisco,
 * California, 94105, USA.
 *
 * Jaguar Company Black Box equipment activation script.
 */

(function () {
    "use strict";

    /* Standard public variables for OXP scripts. */
    this.name = "jaguar_company_blackbox.js";
    this.author = "Tricky";
    this.copyright = "© 2012-2013 Richard Thomas Harrison (Tricky)";
    this.license = "CC BY-NC-SA 3.0";
    this.description = "Jaguar Company Black Box equipment activation script.";
    this.version = "1.1";

    /* NAME
     *   activated
     *
     * FUNCTION
     *   Equipment activated with the 'n' key.
     */
    this.activated = function () {
        worldScripts["Jaguar Company"].$blackboxToggle();
    };

    /* NAME
     *   mode
     *
     * FUNCTION
     *   Equipment activated with the 'b' key.
     */
    this.mode = function () {
        worldScripts["Jaguar Company"].$blackboxMode();
    };

    /* NAME
     *   equipmentDamaged
     *
     * FUNCTION
     *   Equipment has become damaged.
     *
     * INPUT
     *   equipment - entity of the equipment
     */
    this.equipmentDamaged = function (equipment) {
        if (equipment === "EQ_JAGUAR_COMPANY_BLACK_BOX") {
            worldScripts["Jaguar Company"].$blackboxASCReset(true);
            worldScripts["Jaguar Company"].$blackboxHoloReset(true);
            player.commsMessage("Black Box Damaged!");
            player.commsMessage("Return to the nearest Jaguar Company Base for repairs.");
        }
    };
}.bind(this)());
Scripts/jaguar_company_eq_conditions.js
/*jslint bitwise: true, es5: true, newcap: true, nomen: true, regexp: true, unparam: true, todo: true, white: true,
indent: 4, maxerr: 50, maxlen: 120 */
/*jshint boss: true, curly: true, eqeqeq: true, eqnull: true, es5: true, evil: true, forin: true, laxbreak: true,
loopfunc: true, noarg: true, noempty: true, strict: true, nonew: true, undef: true */
/*global galaxyNumber, worldScripts */

/* Jaguar Company Equipment Conditions
 *
 * Copyright © 2013 Richard Thomas Harrison (Tricky)
 *
 * This work is licensed under the Creative Commons
 * Attribution-Noncommercial-Share Alike 3.0 Unported License.
 *
 * To view a copy of this license, visit
 * http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter
 * to Creative Commons, 171 Second Street, Suite 300, San Francisco,
 * California, 94105, USA.
 *
 * Condition script for Jaguar Company equipment.
 */

(function () {
    "use strict";

    /* Standard public variables for OXP scripts. */
    this.name = "jaguar_company_eq_conditions.js";
    this.author = "Tricky";
    this.copyright = "© 2013 Richard Thomas Harrison (Tricky)";
    this.license = "CC BY-NC-SA 3.0";
    this.description = "Condition script for Jaguar Company equipment.";
    this.version = "1.0";

    /* Equipment Condition script event handlers. */

    /* NAME
     *   allowAwardEquipment
     *
     * FUNCTION
     *   This method is called when the game engine needs to know whether a particular ship can have equipment fitted.
     *   This may be because the player is looking at possible upgrades at a station, or from a call to
     *   ship.canAwardEquipment or ship.awardEquipment, or for other similar reasons. The equipment key and a reference
     *   to the ship entity are passed as parameters. If the method does not exist, or returns a value other than false,
     *   then the equipment may be added or offered for sale (subject to other conditions, of course).
     *
     * INPUTS
     *   eqKey - equipment key
     *   ship - ship entity
     *   context - string
     *     "newShip" - equipment for a ship in a station shipyard (F3 F3)
     *     "npc" - awarding equipment to NPC on ship setup
     *     "purchase" - equipment for purchase on the F3 screen
     *     "scripted" - equipment added by JS or legacy scripts
     *
     * RESULT
     *   result - true if equipment is allowed, false if not
     */
    this.allowAwardEquipment = function (eqKey, ship, context) {
        var mainScript = worldScripts["Jaguar Company"];

        if (eqKey === "EQ_JAGUAR_COMPANY_BLACK_BOX" && ship.isPlayer) {
            if (mainScript.$playerVar.reputation[galaxyNumber] >= mainScript.$reputationBlackbox) {
                /* Only allow for players with the correct reputation level. */
                return true;
            }
        }

        if (eqKey === "EQ_JAGUAR_COMPANY_HARDENED_MISSILE_SMALL" && ship.hasRole("jaguar_company")) {
            /* Only allow Jaguar Company ships to carry this. */
            return true;
        }

        return false;
    };
}.bind(this)());
Scripts/jaguar_company_miner.js
/*jslint bitwise: true, es5: true, newcap: true, nomen: true, regexp: true, unparam: true, todo: true, white: true,
indent: 4, maxerr: 50, maxlen: 120 */
/*jshint boss: true, curly: true, eqeqeq: true, eqnull: true, es5: true, evil: true, forin: true, laxbreak: true,
loopfunc: true, noarg: true, noempty: true, strict: true, nonew: true, undef: true */
/*global expandDescription, worldScripts */

/* Jaguar Company Miner
 *
 * Copyright © 2012-2013 Richard Thomas Harrison (Tricky)
 *
 * This work is licensed under the Creative Commons
 * Attribution-Noncommercial-Share Alike 3.0 Unported License.
 *
 * To view a copy of this license, visit
 * http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter
 * to Creative Commons, 171 Second Street, Suite 300, San Francisco,
 * California, 94105, USA.
 *
 * Ship related functions for the miner.
 */

(function () {
    "use strict";

    /* Standard public variables for OXP scripts. */
    this.name = "jaguar_company_miner.js";
    this.author = "Tricky";
    this.copyright = "© 2012-2013 Richard Thomas Harrison (Tricky)";
    this.license = "CC BY-NC-SA 3.0";
    this.description = "Ship script for the Jaguar Company Miner.";
    this.version = "1.2";

    /* Private variable. */
    var p_miner = {};

    /* Ship script event handlers. */

    /* NAME
     *   shipSpawned
     *
     * FUNCTION
     *   Initialise various variables on ship birth.
     */
    this.shipSpawned = function () {
        var base;

        /* Initialise the p_miner variable object.
         * Encapsulates all private global data.
         */
        p_miner = {
            /* Cache the world scripts. */
            mainScript : worldScripts["Jaguar Company"],
            shipsScript : worldScripts["Jaguar Company Ships"],
            /* Local copies of the logging variables. */
            logging : worldScripts["Jaguar Company"].$logging,
            logExtra : worldScripts["Jaguar Company"].$logExtra,
            /* Local copy of the friendList array. */
            friendList : worldScripts["Jaguar Company Ships"].$friendList
        };

        /* Register this ship as a friendly. */
        p_miner.shipsScript.$addFriendly({
            ship : this.ship,
            /* Random name for the pilot. Used when talking about attacks and sending a report to Snoopers. */
            pilotName : expandDescription("%N [nom1]"),
            /* Get a unique name for the patrol ship. */
            shipName : p_miner.mainScript.$uniqueShipName()
        });

        base = p_miner.mainScript.$jaguarCompanyBase;

        if (base && base.isValid) {
            /* Update the base script miner references. */
            base.script.$minerOK = false;
            base.script.$miner = this.ship;
        }

        /* No longer needed after setting up. */
        delete this.shipSpawned;
    };

    /* Other global public functions. */

    /* AI functions. */

    /* NAME
     *   $setCoordsToJaguarCompanyBuoy
     *
     * FUNCTION
     *   Set the co-ordinates to the surface of the buoy or the base.
     */
    this.$setCoordsToJaguarCompanyBuoy = function () {
        var base = p_miner.mainScript.$jaguarCompanyBase;

        if (!base || !base.isValid) {
            /* If the base has gone, just go to the nearest station. */
            this.ship.reactToAIMessage("JAGUAR_COMPANY_BASE_NOT_FOUND");
        } else {
            if (base.script.$buoy && base.script.$buoy.isValid) {
                /* Set the coords to the buoy. */
                this.$setCoordsToEntity(base.script.$buoy);
                this.ship.reactToAIMessage("JAGUAR_COMPANY_BUOY_FOUND");
            } else {
                /* Set the coords to the base. */
                this.$setCoordsToEntity(base);
                this.ship.reactToAIMessage("JAGUAR_COMPANY_BASE_FOUND");
            }
        }
    };
}.bind(this)());
Scripts/jaguar_company_patrol.js
/*jslint bitwise: true, es5: true, newcap: true, nomen: true, regexp: true, unparam: true, todo: true, white: true,
indent: 4, maxerr: 50, maxlen: 120 */
/*jshint boss: true, curly: true, eqeqeq: true, eqnull: true, es5: true, evil: true, forin: true, laxbreak: true,
loopfunc: true, noarg: true, noempty: true, strict: true, nonew: true, undef: true */
/*global Array, Math, Timer, Vector3D, expandDescription, parseInt, system, worldScripts */

/* Jaguar Company Patrol
 *
 * Copyright © 2012-2013 Richard Thomas Harrison (Tricky)
 *
 * This work is licensed under the Creative Commons
 * Attribution-Noncommercial-Share Alike 3.0 Unported License.
 *
 * To view a copy of this license, visit
 * http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter
 * to Creative Commons, 171 Second Street, Suite 300, San Francisco,
 * California, 94105, USA.
 *
 * Ship related functions for the patrol and intercept AIs.
 * Missile subentity code based on tgGeneric_externalMissiles.js by Thargoid (modified)
 */

(function () {
    "use strict";

    /* Standard public variables for OXP scripts. */
    this.name = "jaguar_company_patrol.js";
    this.author = "Tricky";
    this.copyright = "© 2012-2013 Richard Thomas Harrison (Tricky)";
    this.license = "CC BY-NC-SA 3.0";
    this.description = "Ship script for the Jaguar Company Patrol ships.";
    this.version = "1.10";

    /* Private variable. */
    var p_patrol = {};

    /* Ship script event handlers. */

    /* NAME
     *   shipSpawned
     *
     * FUNCTION
     *   Initialise various variables on ship birth.
     */
    this.shipSpawned = function () {
        var counter;

        /* Initialise the p_patrol variable object.
         * Encapsulates all private global data.
         */
        p_patrol = {
            /* Cache the world scripts. */
            mainScript : worldScripts["Jaguar Company"],
            shipsScript : worldScripts["Jaguar Company Ships"],
            /* Local copies of the logging variables. */
            logging : worldScripts["Jaguar Company"].$logging,
            logExtra : worldScripts["Jaguar Company"].$logExtra,
            /* Local copy of the friendList array. */
            friendList : worldScripts["Jaguar Company Ships"].$friendList,
            /* Standard distances. */
            distance : {
                close : 10000,
                nearby : 20000,
                farAway : 40000
            },
            /* Default missile. */
            missileRole : "EQ_HARDENED_MISSILE",
            /* Default number of missiles. */
            initialMissiles : this.ship.missileCapacity,
            /* Starting amount of fuel. */
            fuel : this.ship.fuel
        };

        /* Register this ship as a friendly. */
        p_patrol.shipsScript.$addFriendly({
            ship : this.ship,
            /* Random name for the pilot. Used when talking about attacks and sending a report to Snoopers. */
            pilotName : expandDescription("%N [nom1]"),
            /* Get a unique name for the patrol ship. */
            shipName : p_patrol.mainScript.$uniqueShipName()
        });
        /* Timer reference. */
        this.$addFuelTimerReference = new Timer(this, this.$addFuelTimer, 1, 1);

        /* Thargoid's missile code.
         *
         * Just to ensure ship is fully loaded with selected missile type and nothing else.
         */
        if (this.ship.scriptInfo.missileRole) {
            /* missileRole should be defined in shipdata.plist */
            p_patrol.missileRole = this.ship.scriptInfo.missileRole;
        }

        if (this.ship.scriptInfo.initialMissiles) {
            p_patrol.initialMissiles = parseInt(this.ship.scriptInfo.initialMissiles, 10);
        }

        if (this.ship.missiles.length > 0) {
            /* Remove all spawning missiles. */
            this.ship.awardEquipment("EQ_MISSILE_REMOVAL");
        }

        /* Restock with selected ones. */
        for (counter = 0; counter < p_patrol.initialMissiles; counter += 1) {
            this.ship.awardEquipment(p_patrol.missileRole);
        }

        /* No longer needed after setting up. */
        delete this.shipSpawned;
    };

    /* NAME
     *   shipFiredMissile
     *
     * FUNCTION
     *   Thargoid's missile code. (Simplified - taken out the local function.)
     *
     * INPUT
     *   missile - missile entity
     */
    this.shipFiredMissile = function (missile) {
        var counter,
        subEntities,
        subEntity;

        subEntities = this.ship.subEntities;

        if (!subEntities || !subEntities.length) {
            /* If we've run out of sub-ents before we run out of missiles. */
            return;
        }

        /* Set counter to number of sub-ents minus 1 (as entity array goes up from zero). */
        for (counter = subEntities.length - 1; counter >= 0; counter -= 1) {
            subEntity = subEntities[counter];

            if (subEntity.hasRole(missile.primaryRole)) {
                /* If the sub-ent is the same as the missile being fired. */
                /* Move the fired missile to the sub-ent position and convert to real-world co-ordinates. */
                missile.position = this.ship.position.add(subEntity.position.rotateBy(this.ship.orientation));
                /* Point the missile in the right direction. */
                missile.orientation = subEntity.orientation.multiply(this.ship.orientation);
                /* Desired speed of missile is it's maximum speed. */
                missile.desiredSpeed = missile.maxSpeed;
                /* Remove the sub-ent version of the missile. */
                subEntity.remove();

                /* Come out of the loop, as we've done our swap. */
                break;
            }
        }
    };

    /* NAME
     *   shipRemoved
     *
     * FUNCTION
     *   Patrol ship was removed by script.
     *
     * INPUT
     *   suppressDeathEvent - boolean
     *     true - shipDied() will not be called
     *     false - shipDied() will be called
     */
    this.shipRemoved = function (suppressDeathEvent) {
        if (suppressDeathEvent) {
            return;
        }

        /* Decrease the number of patrol ships in the system. */
        worldScripts["Jaguar Company"].$numPatrolShips -= 1;
    };

    /* NAME
     *   entityDestroyed
     *
     * FUNCTION
     *   The patrol ship has just become invalid.
     */
    this.entityDestroyed = function () {
        /* Decrease the number of patrol ships in the system. */
        worldScripts["Jaguar Company"].$numPatrolShips -= 1;
        /* Stop and remove the timer. */
        this.$removeAddFuelTimer();
    };

    /* Other global public functions. */

    /* NAME
     *   $removeAddFuelTimer
     *
     * FUNCTION
     *   Stop and remove the timer.
     */
    this.$removeAddFuelTimer = function () {
        if (this.$addFuelTimerReference) {
            if (this.$addFuelTimerReference.isRunning) {
                this.$addFuelTimerReference.stop();
            }

            this.$addFuelTimerReference = null;
        }
    };

    /* NAME
     *   $addFuelTimer
     *
     * FUNCTION
     *   addFuel in the AI doesn't allow small increases.
     *
     *   This function allow us to increment the amount of fuel in tinier amounts.
     *   Called every second.
     */
    this.$addFuelTimer = function () {
        var actualFuel,
        internalFuel;

        if (this.ship.speed > this.ship.maxSpeed) {
            /* No fuel collection during Injection or Torus drive. */
            return;
        }

        /* Round off the actual fuel amount to the nearest lowest tenth.
         * The actual fuel amount can be something like 6.6000000000000005 even though
         * the system just uses the 1st decimal place.
         */
        actualFuel = Math.floor(this.ship.fuel * 10) / 10;
        /* The internal fuel amount is also rounded off in a similar manner for the following adjustment. */
        internalFuel = Math.floor(p_patrol.fuel * 10) / 10;

        /* Adjust the internal fuel amount if the actual fuel amount has changed.
         * Needed if the actual fuel amount has been reduced by Injection or Torus drive.
         */
        if (actualFuel !== internalFuel) {
            p_patrol.fuel = actualFuel;
        }

        if (p_patrol.fuel < 7) {
            /* 0.1 LY of fuel every 100 seconds. */
            p_patrol.fuel += 0.001;

            /* Cap the fuel level. */
            if (p_patrol.fuel > 7) {
                p_patrol.fuel = 7;
            }

            /* Set the actual fuel amount to the new fuel amount.
             * Adjust to the nearest lowest tenth.
             */
            this.ship.fuel = Math.floor(p_patrol.fuel * 10) / 10;
        } else {
            /* Make sure that the fuel tanks aren't over filled. */
            p_patrol.fuel = 7;
            this.ship.fuel = 7;
        }
    };

    /* NAME
     *   $queryAverageDistance
     *
     * FUNCTION
     *   Find the average distance to a set of ships from the patrol ship.
     *
     * INPUT
     *   ships - array of ships
     *
     * RESULT
     *   result - average distance to the set of ships
     */
    this.$queryAverageDistance = function (ships) {
        var averageDistance = 0,
        shipsLength,
        shipsCounter,
        ship,
        distance;

        if (!ships || !ships.length) {
            /* The array is empty. */
            return 0;
        }

        /* Cache the length. */
        shipsLength = ships.length;

        for (shipsCounter = 0; shipsCounter < shipsLength; shipsCounter += 1) {
            ship = ships[shipsCounter];
            /* Centre to centre distance. */
            distance = this.ship.position.distanceTo(ship.position);
            /* Take off the collision radius of this ship. */
            distance -= this.ship.collisionRadius;
            /* Take off the collision radius of the other ship. */
            distance -= ship.collisionRadius;
            /* Add this distance to all the other distances. */
            averageDistance += distance;
        }

        /* Average all the distances and return it. */
        return (averageDistance / shipsLength);
    };

    /* AI functions. */

    /* NAME
     *   $setCoordsToMainPlanet
     *
     * FUNCTION
     *   Set the co-ordinates to the surface of the main planet.
     */
    this.$setCoordsToMainPlanet = function () {
        this.$setCoordsToEntity(system.mainPlanet);
        this.ship.reactToAIMessage("JAGUAR_COMPANY_MAINPLANET_SET");
    };

    /* NAME
     *   $setCoordsToNavyShips
     *
     * FUNCTION
     *   Set the co-ordinates to the nearest navy ship.
     */
    this.$setCoordsToNavyShips = function () {
        var navyShips = p_patrol.mainScript.$scanForNavyShips(this.ship);

        if (navyShips.length) {
            /* Update the main script closest navy ship reference. */
            p_patrol.mainScript.$closestNavyShip = navyShips[0];
            p_patrol.mainScript.$initRoute("NAVY");
            /* Set the coords to the nearest navy ship. */
            this.$setCoordsToEntity(navyShips[0]);
            this.ship.reactToAIMessage("JAGUAR_COMPANY_NAVY_FOUND");
        } else {
            /* Navy has gone. Go back to base if possible. */
            p_patrol.mainScript.$initRoute();
            p_patrol.mainScript.$changeRoute(-1);
            this.ship.reactToAIMessage("JAGUAR_COMPANY_NAVY_NOT_FOUND");
        }
    };

    /* NAME
     *   $setCoordsToWitchpoint
     *
     * FUNCTION
     *   Set the co-ordinates to the surface of the witchpoint buoy.
     */
    this.$setCoordsToWitchpoint = function () {
        this.$setCoordsToEntity(p_patrol.mainScript.$witchpointBuoy);
        this.ship.reactToAIMessage("JAGUAR_COMPANY_WITCHPOINT_SET");
    };

    /* NAME
     *   $setCoordsToJaguarCompanyBuoy
     *
     * FUNCTION
     *   Set the co-ordinates to the surface of the buoy.
     */
    this.$setCoordsToJaguarCompanyBuoy = function () {
        var base = p_patrol.mainScript.$jaguarCompanyBase;

        if (!base || !base.isValid) {
            if (system.isInterstellarSpace) {
                this.ship.fuel = 7;
                this.ship.reactToAIMessage("JAGUAR_COMPANY_EXIT_INTERSTELLAR");
            } else {
                /* If it has gone, just patrol the witchpoint to the planet lane. */
                p_patrol.mainScript.$initRoute("WP");
                this.ship.reactToAIMessage("JAGUAR_COMPANY_BASE_NOT_FOUND");
            }
        } else {
            if (base.script.$buoy && base.script.$buoy.isValid) {
                /* Set the coords to the buoy. */
                this.$setCoordsToEntity(base.script.$buoy);
                this.ship.reactToAIMessage("JAGUAR_COMPANY_BUOY_FOUND");
            } else {
                /* Set the coords to the base. */
                this.$setCoordsToEntity(base);
                this.ship.reactToAIMessage("JAGUAR_COMPANY_BASE_FOUND");
            }
        }
    };

    /* NAME
     *   $addPatrolToNewSystem
     *
     * FUNCTION
     *   Jaguar Company were forced out of interstellar space as there was no base.
     *   Create a new base in the new system.
     */
    this.$addPatrolToNewSystem = function () {
        if (system.shipsWithPrimaryRole("jaguar_company_patrol").length === p_patrol.mainScript.$numPatrolShips) {
            /* Last ship out will create the base. */
            if (!p_patrol.mainScript.$setUpCompany()) {
                /* Base would not be created, just patrol the witchpoint to the planet lane. */
                p_patrol.mainScript.$initRoute("WP");
            } else {
                /* Base was created. Set the route. Should be a route to the base. */
                p_patrol.mainScript.$changeRoute(-1);
            }
        }
    };

    /* NAME
     *   $checkHyperspaceFollow
     *
     * FUNCTION
     *   Check to see if the patrol ship is the initiator of the wormhole or is following.
     */
    this.$checkHyperspaceFollow = function () {
        if (p_patrol.mainScript.$hyperspaceFollow) {
            /* This ship is following. */
            this.ship.savedCoordinates = p_patrol.mainScript.$hyperspaceFollow;
            this.ship.reactToAIMessage("JAGUAR_COMPANY_HYPERSPACE_FOLLOW");

            return;
        }

        /* This ship is opening the initial wormhole. */
        p_patrol.mainScript.$hyperspaceFollow = this.ship.position;
        this.ship.reactToAIMessage("JAGUAR_COMPANY_HYPERSPACE");
    };

    /* NAME
     *   $checkRoute
     *
     * FUNCTION
     *   Check current patrol route.
     */
    this.$checkRoute = function () {
        /* Call common code used by all of Jaguar Company. */
        p_patrol.mainScript.$checkRoute(this.ship);
    };

    /* NAME
     *   $finishedRoute
     *
     * FUNCTION
     *   Finished the current patrol route, change to the next one.
     */
    this.$finishedRoute = function () {
        /* Call common code used by all of Jaguar Company. */
        p_patrol.mainScript.$finishedRoute(this.ship, "jaguar_company_patrol", "JAGUAR_COMPANY_REGROUP");
    };

    /* NAME
     *   $scanForAllJaguarCompany
     *
     * FUNCTION
     *   Scan for the other ships to see if the full group is present.
     */
    this.$scanForAllJaguarCompany = function () {
        var base,
        patrolShips,
        counter,
        length,
        lurkPosition,
        variation;

        if (p_patrol.mainScript.$maxPatrolShips === 1) {
            /* We are on our own. */
            this.ship.reactToAIMessage("JAGUAR_COMPANY_NOT_FOUND");

            return;
        }

        patrolShips = system.shipsWithPrimaryRole("jaguar_company_patrol");

        if (patrolShips.length === p_patrol.mainScript.$numPatrolShips ||
            p_patrol.mainScript.$patrolShipsFullyLaunched) {
            /* Announce that we have found all of Jaguar Company to the AI. */
            this.ship.reactToAIMessage("JAGUAR_COMPANY_ALL_PRESENT");
        } else {
            base = p_patrol.mainScript.$jaguarCompanyBase;

            if (base && base.isValid) {
                /* Set the starting lurk position to the base position. */
                lurkPosition = base.position;
            } else {
                /* START OF CODE THAT SHOULD NEVER BE REACHED.
                 * This is here purely for error checking sake.
                 * If the base is destroyed it will set the patrol ships fully launched variable,
                 * therefore this code block shouldn't be reached.
                 */
                if (patrolShips.length === 1) {
                    /* We are on our own. */
                    lurkPosition = patrolShips[0].position;
                } else {
                    /* Cache the length. */
                    length = patrolShips.length;

                    /* Work out the midpoint position of all ships. */
                    lurkPosition = new Vector3D(0, 0, 0);

                    for (counter = 0; counter < length; counter += 1) {
                        lurkPosition = lurkPosition.add(patrolShips[counter].position);
                    }

                    lurkPosition.x /= length;
                    lurkPosition.y /= length;
                    lurkPosition.z /= length;

                    /* Higher variation if further away. */
                    variation = (this.ship.position.distanceTo(lurkPosition) > 51200 ? 0.5 : 0.2);

                    /* Move the vector a random amount. */
                    lurkPosition.x += variation * (Math.random() - variation);
                    lurkPosition.y += variation * (Math.random() - variation);
                    lurkPosition.z += variation * (Math.random() - variation);
                }
                /* END OF CODE THAT SHOULD NEVER BE REACHED. */
            }

            /* Move the lurk position 20km out in a random direction and save the co-ordinates for the AI. */
            this.ship.savedCoordinates = lurkPosition.add(Vector3D.randomDirection(20000));
            this.ship.reactToAIMessage("JAGUAR_COMPANY_NOT_PRESENT");
        }
    };

    /* NAME
     *   $scanForJaguarCompany
     *
     * FUNCTION
     *   Scan for the other ships and find the midpoint position of the group.
     */
    this.$scanForJaguarCompany = function () {
        var otherShips,
        otherShipsLength,
        otherShipsCounter,
        midpointPosition,
        variation;

        otherShips = system.shipsWithPrimaryRole("jaguar_company_patrol", this.ship);

        if (!otherShips.length) {
            /* We are on our own. */
            this.ship.reactToAIMessage("JAGUAR_COMPANY_NOT_FOUND");

            return;
        }

        /* Cache the length. */
        otherShipsLength = otherShips.length;

        /* Work out the midpoint position of all ships. */
        midpointPosition = this.ship.position;

        for (otherShipsCounter = 0; otherShipsCounter < otherShipsLength; otherShipsCounter += 1) {
            midpointPosition = midpointPosition.add(otherShips[otherShipsCounter].position);
        }

        midpointPosition.x /= (otherShipsLength + 1);
        midpointPosition.y /= (otherShipsLength + 1);
        midpointPosition.z /= (otherShipsLength + 1);

        /* Higher variation if further away. */
        variation = (this.ship.position.distanceTo(midpointPosition) > 51200 ? 0.5 : 0.2);

        /* Move the vector a random amount. */
        midpointPosition.x += variation * (Math.random() - variation);
        midpointPosition.y += variation * (Math.random() - variation);
        midpointPosition.z += variation * (Math.random() - variation);

        /* Save the co-ordinates for the AI. */
        this.ship.savedCoordinates = midpointPosition;
        /* Announce that we have found Jaguar Company to the AI. */
        this.ship.reactToAIMessage("JAGUAR_COMPANY_FOUND");
    };

    /* NAME
     *   $checkJaguarCompanyClosestDistance
     *
     * FUNCTION
     *   Check how close we are to other ships.
     *
     * INPUT
     *   arr - closest distance allowed (as an array) (just uses the first element in the array)
     */
    this.$checkJaguarCompanyClosestDistance = function (arr) {
        var minimumDistance,
        actualDistance,
        otherShips;

        if (!Array.isArray(arr)) {
            /* Default. */
            minimumDistance = 250.0;
        } else {
            minimumDistance = arr[0];
        }

        /* More ships will increase the minimum distance. */
        actualDistance = minimumDistance * Math.ceil(p_patrol.mainScript.$numPatrolShips / 8);
        /* Modify for surface to surface. */
        actualDistance += this.ship.collisionRadius;
        /* Check for any patrol ships within the calculated sphere. */
        otherShips = system.shipsWithPrimaryRole("jaguar_company_patrol", this.ship, actualDistance);

        if (!otherShips.length) {
            this.ship.reactToAIMessage("JAGUAR_COMPANY_DISTANCE_OK");
        } else {
            /* If we are less than the minimum distance from the closest ship then we need to move away. */
            this.ship.target = otherShips[0];
            this.ship.reactToAIMessage("JAGUAR_COMPANY_TOO_CLOSE");
        }

        return;
    };

    /* NAME
     *   $checkJaguarCompanyAverageDistance
     *
     * FUNCTION
     *   Check our average distance to all other ships.
     */
    this.$checkJaguarCompanyAverageDistance = function () {
        var otherShips,
        averageDistance,
        close,
        nearby,
        farAway;

        otherShips = system.shipsWithPrimaryRole("jaguar_company_patrol", this.ship);

        if (!otherShips.length) {
            /* Return immediately if we are on our own. */
            return;
        }

        /* Find the average distance to all the other ships. */
        averageDistance = this.$queryAverageDistance(otherShips);

        close = (p_patrol.distance.close) + ((Math.random() * 2000.0) - 1000.0);
        nearby = (p_patrol.distance.nearby) + ((Math.random() * 2000.0) - 1000.0);
        farAway = (p_patrol.distance.farAway) + ((Math.random() * 2000.0) - 1000.0);

        /* I would love to create a fuzzy logic controller for this. */
        if (averageDistance < close) {
            /* We have regrouped. */
            this.ship.sendAIMessage("JAGUAR_COMPANY_REGROUPED");
        } else if (averageDistance >= close && averageDistance < nearby) {
            /* We are close. */
            this.ship.reactToAIMessage("JAGUAR_COMPANY_CLOSE");
        } else if (averageDistance >= nearby && averageDistance < farAway) {
            /* We are nearby. */
            this.ship.reactToAIMessage("JAGUAR_COMPANY_NEARBY");
        } else {
            /* We are far away. */
            this.ship.reactToAIMessage("JAGUAR_COMPANY_FAR_AWAY");
        }
    };

    /* NAME
     *   $checkJaguarCompanyRegroup
     *
     * FUNCTION
     *   Tell everyone to regroup if the average distance to all the other ships is too great.
     *
     * INPUT
     *   arr - furthest distance allowed before a regroup message is sent out (as an array)
     *         (just uses the first element in the array)
     */
    this.$checkJaguarCompanyRegroup = function (arr) {
        var maxDistance,
        otherShips,
        otherShipsLength,
        otherShipsCounter;

        otherShips = system.shipsWithPrimaryRole("jaguar_company_patrol", this.ship);

        if (!otherShips.length) {
            /* Return immediately if we are on our own. */
            return;
        }

        if (!Array.isArray(arr)) {
            /* Default. */
            maxDistance = 15000.0;
        } else {
            maxDistance = arr[0];
        }

        /* Find the average distance to all the other ships
         * and check if this is more than the furthest distance allowed (+/- 1km).
         */
        if (this.$queryAverageDistance(otherShips) >= maxDistance + ((Math.random() * 2000.0) - 1000.0)) {
            /* Tell all ships, including ourself, to regroup. */
            this.ship.reactToAIMessage("JAGUAR_COMPANY_REGROUP");

            /* Cache the length. */
            otherShipsLength = otherShips.length;

            for (otherShipsCounter = 0; otherShipsCounter < otherShipsLength; otherShipsCounter += 1) {
                otherShips[otherShipsCounter].reactToAIMessage("JAGUAR_COMPANY_REGROUP");
            }
        }
    };
}.bind(this)());
Scripts/jaguar_company_pilot.js
/*jslint bitwise: true, es5: true, newcap: true, nomen: true, regexp: true, unparam: true, todo: true, white: true,
indent: 4, maxerr: 50, maxlen: 120 */
/*jshint boss: true, curly: true, eqeqeq: true, eqnull: true, es5: true, evil: true, forin: true, laxbreak: true,
loopfunc: true, noarg: true, noempty: true, strict: true, nonew: true, undef: true */
/*global Math, expandDescription, galaxyNumber, player, randomInhabitantsDescription, worldScripts */

/* jaguar_company_pilot.js
 *
 * Copyright © 2012-2013 Richard Thomas Harrison (Tricky)
 *
 * This work is licensed under the Creative Commons
 * Attribution-Noncommercial-Share Alike 3.0 Unported License.
 *
 * To view a copy of this license, visit
 * http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter
 * to Creative Commons, 171 Second Street, Suite 300, San Francisco,
 * California, 94105, USA.
 *
 * Jaguar Company Pilot script for delivering escape-pods to a station.
 */

(function () {
    "use strict";

    /* Standard public variables for OXP scripts. */
    this.name = "jaguar_company_pilot.js";
    this.author = "Tricky";
    this.copyright = "© 2012-2013 Richard Thomas Harrison (Tricky)";
    this.license = "CC BY-NC-SA 3.0";
    this.description = "Jaguar Company Pilot script for delivering escape-pods to a station.";
    this.version = "1.2";

    /* NAME
     *   unloadCharacter
     *
     * FUNCTION
     *   Shows a rescue message for Jaguar Company pilots you deliver back to a station.
     *   Also increases reputation.
     */
    this.unloadCharacter = function () {
        var mainScript = worldScripts["Jaguar Company"],
        insurance,
        bonus = 0,
        message,
        pilotName;

        if (mainScript.$pilotsRescued.length) {
            /* Get the name of one of the rescued pilots. */
            pilotName = mainScript.$pilotsRescued.shift();
        } else {
            /* Random name. Shouldn't be executed. */
            pilotName = expandDescription("%N [nom1]");
        }

        /* Multiple of 5 Cr for insurance. */
        insurance = 500 + (Math.floor(Math.random() * 40) * 5);
        /* Create the message for the arrival report. */
        message = "For rescuing " + pilotName + ", a " + randomInhabitantsDescription(false) +
            " and member of Jaguar Company, their insurance pays " + insurance + " ₢.";

        if (player.ship.dockedStation.hasRole("jaguar_company_base")) {
            /* Give a bonus for bringing the pilot back to one of their base's. Multiple of 5 Cr. */
            bonus = 100 + (Math.floor(Math.random() * 20) * 5);
            message += " Jaguar Company has also added a bonus of " + bonus +
            " ₢ for bringing the pilot back to their base.";
            /* Increase the reputation of the player with Jaguar Company *after* launching.
             * You don't want to be in the base when it swaps roles.
             */
            if (mainScript.$playerVar.delayedAward !== "number") {
                mainScript.$playerVar.delayedAward = 0;
            }

            mainScript.$playerVar.delayedAward += 6;
        } else {
            /* Increase the reputation of the player with Jaguar Company. */
            mainScript.$playerVar.reputation[galaxyNumber] += 4;
        }

        /* Add on the insurance and bonus to the player's credits. */
        player.credits += (insurance + bonus);
        /* Show message on arrival. */
        player.addMessageToArrivalReport(expandDescription(message));
    };
}.bind(this)());
Scripts/jaguar_company_ship_splinter.js
/*jslint bitwise: true, es5: true, newcap: true, nomen: true, regexp: true, unparam: true, todo: true, white: true,
indent: 4, maxerr: 50, maxlen: 120 */
/*jshint boss: true, curly: true, eqeqeq: true, eqnull: true, es5: true, evil: true, forin: true, laxbreak: true,
loopfunc: true, noarg: true, noempty: true, strict: true, nonew: true, undef: true */
/*global Math, Timer, Vector3D, expandDescription, galaxyNumber, log, parseInt, worldScripts */

/* Jaguar Company Splinter Ship
 *
 * Copyright © 2012-2013 Richard Thomas Harrison (Tricky)
 *
 * This work is licensed under the Creative Commons
 * Attribution-Noncommercial-Share Alike 3.0 Unported License.
 *
 * To view a copy of this license, visit
 * http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter
 * to Creative Commons, 171 Second Street, Suite 300, San Francisco,
 * California, 94105, USA.
 *
 * Ship related functions for the splinter ship AI.
 * Missile subentity code based on tgGeneric_externalMissiles.js by Thargoid (modified)
 */

(function () {
    "use strict";

    /* Standard public variables for OXP scripts. */
    this.name = "jaguar_company_ship_splinter.js";
    this.author = "Tricky";
    this.copyright = "© 2013 Richard Thomas Harrison (Tricky)";
    this.license = "CC BY-NC-SA 3.0";
    this.description = "Ship script for the Jaguar Company Splinter ships.";
    this.version = "1.0";

    /* Private variable. */
    var p_splinter = {};

    /* Ship script event handlers. */

    /* NAME
     *   shipSpawned
     *
     * FUNCTION
     *   Initialise various variables on ship birth.
     */
    this.shipSpawned = function () {
        var counter;

        /* Initialise the p_splinter variable object.
         * Encapsulates all private global data.
         */
        p_splinter = {
            /* Cache the world scripts. */
            mainScript : worldScripts["Jaguar Company"],
            shipsScript : worldScripts["Jaguar Company Ships"],
            /* Local copies of the logging variables. */
            logging : worldScripts["Jaguar Company"].$logging,
            logExtra : worldScripts["Jaguar Company"].$logExtra,
            /* Local copy of the friendList array. */
            friendList : worldScripts["Jaguar Company Ships"].$friendList,
            /* Default missile. */
            missileRole : "EQ_HARDENED_MISSILE",
            /* Default number of missiles. */
            initialMissiles : this.ship.missileCapacity,
        };

        /* Register this ship as a friendly. */
        p_splinter.shipsScript.$addFriendly({
            ship : this.ship,
            /* Random name for the pilot. Used when talking about attacks and sending a report to Snoopers. */
            pilotName : expandDescription("%N [nom1]"),
            /* Get a unique name for the patrol ship. */
            shipName : p_splinter.mainScript.$uniqueShipName()
        });
        /* Move the lurk position 15km out in a random direction from the base. */
        this.$lurkPosition = p_splinter.mainScript.$jaguarCompanyBase.position.add(Vector3D.randomDirection(15000));
        /* Reset the lurk timer. */
        this.$lurkTimerReference = null;
        /* Set the just launched flag. */
        this.$justLaunched = true;
        /* Set the timer to fire every 5 seconds. */
        this.$splinterShipTimerReference = new Timer(this, this.$splinterShipTimer, 5, 5);

        /* Thargoid's missile code.
         *
         * Just to ensure ship is fully loaded with selected missile type and nothing else.
         */
        if (this.ship.scriptInfo.missileRole) {
            /* missileRole should be defined in shipdata.plist */
            p_splinter.missileRole = this.ship.scriptInfo.missileRole;
        }

        if (this.ship.scriptInfo.initialMissiles) {
            p_splinter.initialMissiles = parseInt(this.ship.scriptInfo.initialMissiles, 10);
        }

        if (this.ship.missiles.length > 0) {
            /* Remove all spawning missiles. */
            this.ship.awardEquipment("EQ_MISSILE_REMOVAL");
        }

        /* Restock with selected ones. */
        for (counter = 0; counter < p_splinter.initialMissiles; counter += 1) {
            this.ship.awardEquipment(p_splinter.missileRole);
        }

        /* No longer needed after setting up. */
        delete this.shipSpawned;
    };

    /* NAME
     *   shipFiredMissile
     *
     * FUNCTION
     *   Thargoid's missile code. (Simplified - taken out the local function.)
     *
     * INPUT
     *   missile - missile entity
     */
    this.shipFiredMissile = function (missile) {
        var counter,
        subEntities,
        subEntity;

        subEntities = this.ship.subEntities;

        if (!subEntities || !subEntities.length) {
            /* If we've run out of sub-ents before we run out of missiles. */
            return;
        }

        /* Set counter to number of sub-ents minus 1 (as entity array goes up from zero). */
        for (counter = subEntities.length - 1; counter >= 0; counter -= 1) {
            subEntity = subEntities[counter];

            if (subEntity.hasRole(missile.primaryRole)) {
                /* If the sub-ent is the same as the missile being fired. */
                /* Move the fired missile to the sub-ent position and convert to real-world co-ordinates. */
                missile.position = this.ship.position.add(subEntity.position.rotateBy(this.ship.orientation));
                /* Point the missile in the right direction. */
                missile.orientation = subEntity.orientation.multiply(this.ship.orientation);
                /* Desired speed of missile is it's maximum speed. */
                missile.desiredSpeed = missile.maxSpeed;
                /* Remove the sub-ent version of the missile. */
                subEntity.remove();

                /* Come out of the loop, as we've done our swap. */
                break;
            }
        }
    };

    /* NAME
     *   entityDestroyed
     *
     * FUNCTION
     *   The splinter ship has just become invalid.
     */
    this.entityDestroyed = function () {
        /* Stop and remove the timers. */
        this.$removeLurkTimer();
        this.$removeSplinterShipTimer();
    };

    /* Other global public functions. */

    /* NAME
     *   $removeLurkTimer
     *
     * FUNCTION
     *   Stop and remove the timer.
     */
    this.$removeLurkTimer = function () {
        if (this.$lurkTimerReference) {
            if (this.$lurkTimerReference.isRunning) {
                this.$lurkTimerReference.stop();
            }

            this.$lurkTimerReference = null;
        }
    };

    /* NAME
     *   $removeSplinterShipTimer
     *
     * FUNCTION
     *   Stop and remove the timer.
     */
    this.$removeSplinterShipTimer = function () {
        if (this.$splinterShipTimerReference) {
            if (this.$splinterShipTimerReference.isRunning) {
                this.$splinterShipTimerReference.stop();
            }

            this.$splinterShipTimerReference = null;
        }
    };

    /* NAME
     *   $splinterShipTimer
     *
     * FUNCTION
     *   Hide the splinter ships on the scanner as rocks.
     */
    this.$splinterShipTimer = function () {
        if (p_splinter.mainScript.$playerVar.reputation[galaxyNumber] < p_splinter.mainScript.$reputationHelper) {
            /* This will set the splinter ship's scanner colour to white if the player
             * has not helped out in combat with Jaguar Company.
             */
            this.ship.scannerDisplayColor1 = "whiteColor";
            this.ship.scannerDisplayColor2 = "whiteColor";
        } else {
            /* Reset the splinter ship scanner colour. */
            this.ship.scannerDisplayColor1 = null;
            this.ship.scannerDisplayColor2 = null;
        }
    };

    /* AI functions. */

    /* NAME
     *   $setCoordsToWitchpoint
     *
     * FUNCTION
     *   Set the co-ordinates to the surface of the witchpoint buoy.
     */
    this.$setCoordsToWitchpoint = function () {
        this.$setCoordsToEntity(p_splinter.mainScript.$witchpointBuoy);
        this.ship.reactToAIMessage("JAGUAR_COMPANY_WITCHPOINT_SET");
    };

    /* NAME
     *   $setCoordsToJaguarCompanyBuoy
     *
     * FUNCTION
     *   Set the co-ordinates to the surface of the buoy.
     */
    this.$setCoordsToJaguarCompanyBuoy = function () {
        var base = p_splinter.mainScript.$jaguarCompanyBase;

        if (!base || !base.isValid) {
            /* If the base has gone, EXPLODE!. */
            this.ship.switchAI("timebombAI.plist");

            if (p_splinter.logging && p_splinter.logExtra) {
                log(this.name, "$setCoordsToJaguarCompanyBuoy::BANG!!!");
            }
        } else {
            if (base.script.$buoy && base.script.$buoy.isValid) {
                /* Set the coords to the buoy. */
                this.$setCoordsToEntity(base.script.$buoy);
                this.ship.reactToAIMessage("JAGUAR_COMPANY_BUOY_FOUND");
            } else {
                /* Set the coords to the base. */
                this.$setCoordsToEntity(base);
                this.ship.reactToAIMessage("JAGUAR_COMPANY_BASE_FOUND");
            }
        }
    };

    /* NAME
     *   $findLurkCoordinates
     *
     * FUNCTION
     *   Returns co-ordinates to lurk about.
     */
    this.$findLurkCoordinates = function () {
        var base = p_splinter.mainScript.$jaguarCompanyBase;

        if (!base || !base.isValid) {
            /* If the base has gone, EXPLODE!. */
            this.ship.switchAI("timebombAI.plist");

            if (p_splinter.logging && p_splinter.logExtra) {
                log(this.name, "$findLurkCoordinates::BANG!!!");
            }
        } else {
            /* Save the co-ordinates for the AI. */
            this.ship.savedCoordinates = this.$lurkPosition;
            this.ship.reactToAIMessage("LURK");
        }
    };

    /* NAME
     *   $addLurkTimer
     *
     * FUNCTION
     *   Restarts the lurk timer and returns co-ordinates to lurk about.
     */
    this.$addLurkTimer = function () {
        var base = p_splinter.mainScript.$jaguarCompanyBase;

        if (!base || !base.isValid) {
            /* If the base has gone, EXPLODE!. */
            this.ship.switchAI("timebombAI.plist");

            if (p_splinter.logging && p_splinter.logExtra) {
                log(this.name, "$addLurkTimer::BANG!!!");
            }
        } else {
            /* Set the timer to fire in 5-10 minutes. */
            this.$lurkTimerReference = new Timer(this, this.$addLurkTimer, ((Math.random() * 5) + 5) * 60);

            if (!this.$justLaunched && Math.random() <= 0.05) {
                /* 1 in 20 chance of docking. */
                this.ship.reactToAIMessage("JAGUAR_COMPANY_DOCK");
                this.$removeLurkTimer();
            } else {
                /* Reset the just launched flag. */
                this.$justLaunched = false;
                /* Move the lurk position 15km out in a random direction from the base. */
                this.$lurkPosition = base.position.add(Vector3D.randomDirection(15000));
            }
        }
    };
}.bind(this)());
Scripts/jaguar_company_ships.js
/*jslint bitwise: true, es5: true, newcap: true, nomen: true, regexp: true, unparam: true, todo: true, white: true,
indent: 4, maxerr: 50, maxlen: 120 */
/*jshint boss: true, curly: true, eqeqeq: true, eqnull: true, es5: true, evil: true, forin: true, laxbreak: true,
loopfunc: true, noarg: true, noempty: true, strict: true, nonew: true, undef: true */
/*global Array, Math, Timer, Vector3D, clock, expandDescription, galaxyNumber, log, missionVariables, player, system,
worldScripts */

/* Jaguar Company Ships
 *
 * Copyright © 2012-2013 Richard Thomas Harrison (Tricky)
 *
 * This work is licensed under the Creative Commons
 * Attribution-Noncommercial-Share Alike 3.0 Unported License.
 *
 * To view a copy of this license, visit
 * http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter
 * to Creative Commons, 171 Second Street, Suite 300, San Francisco,
 * California, 94105, USA.
 *
 * World script to setup Jaguar Company ships.
 */

(function () {
    "use strict";

    /* Standard public variables for OXP scripts. */
    this.name = "Jaguar Company Ships";
    this.author = "Tricky";
    this.copyright = "© 2012-2013 Richard Thomas Harrison (Tricky)";
    this.license = "CC BY-NC-SA 3.0";
    this.description = "Script to initialise the Jaguar Company ships.";
    this.version = "1.4";

    /* Private variable. */
    var p_ships = {};

    /* World script event handlers. */

    /* NAME
     *   startUp
     *
     * FUNCTION
     *   We only need to do this once.
     *   This will get redefined after a new game or loading of a new Commander.
     */
    this.startUp = function () {
        var mainScript = worldScripts["Jaguar Company"],
        cabalScript = worldScripts.Cabal_Common_Functions,
        ccl,
        cclVersion;

        if (!cabalScript || cabalScript.Cabal_Common === 'undefined') {
            this.$killSelf(" -> Cabal Common Library is missing.");

            return;
        }

        ccl = new cabalScript.Cabal_Common();
        cclVersion = ccl.internalVersion;

        if (cclVersion < 14) {
            this.$killSelf(" -> Cabal Common Library is too old for any Oolite version.");

            return;
        }

        if (cclVersion === 14 && mainScript.$gte_v1_77) {
            /* Oolite v1.77 and newer. */
            this.$killSelf(" -> Cabal Common Library is too old for Oolite v1.77 (and newer Oolite versions).");

            return;
        }

        if (cclVersion > 14 && !mainScript.$gte_v1_77) {
            /* Oolite v1.76.1 and older. */
            this.$killSelf(" -> Cabal Common Library is too new for Oolite v1.76.1 (and older Oolite versions).");

            return;
        }

        /* Setup the private ships variable + some public variables. Delay it. */
        this.$setUpTimerReference = new Timer(this, this.$setUp, 0.5, 0.5);

        log(this.name + " " + this.version + " loaded.");

        /* No longer needed after setting up. */
        delete this.startUp;
    };

    /* NAME
     *   shipWillExitWitchspace
     *
     * FUNCTION
     *   Reset everything just before exiting Witchspace.
     */
    this.shipWillExitWitchspace = function () {
        /* Setup the private ships variable + some public variables. */
        this.$setUp();
    };

    /* NAME
     *   shipAttackedOther
     *
     * FUNCTION
     *   Player fired a laser at someone and hit.
     *
     * INPUT
     *   victim - entity of the ship the player is attacking.
     */
    this.shipAttackedOther = function (victim) {
        var victimKey,
        victimsIndex,
        jaguarCompany,
        attackerIndex,
        observer,
        pilotName,
        reputation,
        helperLevel,
        blackboxLevel,
        locationsLevel,
        blackboxStatus;

        if (!this.$isHostile(victim)) {
            /* Ignore victims that are not hostile to Jaguar Company. */
            return;
        }

        if (Math.random() < 0.9) {
            /* Jaguar Company is too busy to see you helping. */
            return;
        }

        /* Unique key (entityPersonality) for the victim. */
        victimKey = victim.entityPersonality;

        /* Search for any members of Jaguar Company within maximum scanner range of the player ship. */
        jaguarCompany = system.filteredEntities(this, function (entity) {
                /* Unique key (entityPersonality) for the entity. */
                var entityKey = entity.entityPersonality;

                /* Only interested in entities that aren't the victim. */
                return (victimKey !== entityKey && this.$friendList.indexOf(entityKey) !== -1);
            }, player.ship, player.ship.scannerRange);

        if (!jaguarCompany.length) {
            /* Nobody around. */
            return;
        }

        /* Skip the next bit if the victim is a thargoid/tharglet. */
        if (!victim.isThargoid) {
            /* Find the index of the victim in the attackers index array. */
            attackerIndex = p_ships.attackersIndex.indexOf(victimKey);
            /* Get the victims index array. */
            victimsIndex = p_ships.attackers[attackerIndex].victimsIndex;
            /* Re-filter to find out if any of Jaguar Company found so far
             * are victims of the ship the player is attacking.
             */
            jaguarCompany = jaguarCompany.filter(function (entity) {
                    return (victimsIndex.indexOf(entity.entityPersonality) !== -1);
                });

            if (!jaguarCompany.length) {
                /* Nobody around. */
                return;
            }
        }

        /* Increase the reputation of the player with Jaguar Company. */
        reputation = p_ships.mainScript.$playerVar.reputation[galaxyNumber] + 1;
        p_ships.mainScript.$playerVar.reputation[galaxyNumber] = reputation;
        /* Pick a random member of Jaguar Company as the observer. */
        observer = jaguarCompany[Math.floor(Math.random() * jaguarCompany.length)];

        if (observer.isPiloted || observer.isStation) {
            if (observer.$pilotName) {
                /* Get the observer's name. */
                pilotName = observer.$pilotName;
            } else {
                /* Use displayName as the name of the observer. */
                pilotName = observer.name + ": " + observer.displayName;
            }

            /* Send a thank you message to the player. */
            player.commsMessage(pilotName + ": " + expandDescription("[jaguar_company_player_help]"));
            /* Equipment status of the black box. */
            blackboxStatus = player.ship.equipmentStatus("EQ_JAGUAR_COMPANY_BLACK_BOX");

            helperLevel = p_ships.mainScript.$reputationHelper;
            blackboxLevel = p_ships.mainScript.$reputationBlackbox;
            locationsLevel = p_ships.mainScript.$reputationLocations;

            if (reputation === helperLevel) {
                player.commsMessage(pilotName + ": " + expandDescription("[jaguar_company_player_help_buoy]"));
            } else if (reputation === blackboxLevel && blackboxStatus !== "EQUIPMENT_OK") {
                player.commsMessage(pilotName + ": " + expandDescription("[jaguar_company_player_help_blackbox]"));
            } else if (reputation === locationsLevel && blackboxStatus === "EQUIPMENT_OK") {
                player.commsMessage(pilotName + ": " + expandDescription("[jaguar_company_player_help_locations]"));
            }
        }
    };

    /* NAME
     *   shipKilledOther
     *
     * FUNCTION
     *   Player killed something.
     *
     * INPUT
     *   victim - entity of the ship the player killed
     */
    this.shipKilledOther = function (victim) {
        var victimsIndex,
        victimKey,
        jaguarCompany,
        attackerIndex,
        observer,
        pilotName,
        newsSource,
        reputation,
        helperLevel,
        blackboxLevel,
        locationsLevel,
        blackboxStatus;

        if (!this.$isHostile(victim)) {
            /* Ignore victims that are not hostile to Jaguar Company. */
            return;
        }

        /* Unique key (entityPersonality) for the victim. */
        victimKey = victim.entityPersonality;

        /* Search for any members of Jaguar Company within maximum scanner range of the player ship. */
        jaguarCompany = system.filteredEntities(this, function (entity) {
                /* Unique key (entityPersonality) for the entity. */
                var entityKey = entity.entityPersonality;

                /* Only interested in entities that aren't the victim. */
                return (victimKey !== entityKey && this.$friendList.indexOf(entityKey) !== -1);
            }, player.ship, player.ship.scannerRange);

        if (!jaguarCompany.length) {
            /* Nobody around. */
            return;
        }

        /* Skip the next bit if the victim is a thargoid/tharglet. */
        if (!victim.isThargoid) {
            /* Find the index of the victim in the attackers index array. */
            attackerIndex = p_ships.attackersIndex.indexOf(victimKey);
            /* Get the victims index array. */
            victimsIndex = p_ships.attackers[attackerIndex].victimsIndex;
            /* Re-filter to find out if any of Jaguar Company found so far
             * are victims of the ship the player has killed.
             */
            jaguarCompany = jaguarCompany.filter(function (entity) {
                    return (victimsIndex.indexOf(entity.entityPersonality) !== -1);
                });

            if (!jaguarCompany.length) {
                /* Nobody around. */
                return;
            }
        }

        /* Increase the reputation of the player with Jaguar Company. */
        reputation = p_ships.mainScript.$playerVar.reputation[galaxyNumber] + 10;
        p_ships.mainScript.$playerVar.reputation[galaxyNumber] = reputation;
        /* Pick a random member of Jaguar Company as the observer. */
        observer = jaguarCompany[Math.floor(Math.random() * jaguarCompany.length)];

        if (observer.isPiloted || observer.isStation) {
            if (observer.$pilotName) {
                /* News source is the observer. */
                newsSource = observer.$pilotName;
                pilotName = observer.$pilotName;
            } else {
                /* Random name for the news source. */
                newsSource = expandDescription("%N [nom1]");
                /* Use displayName as the name of the observer. */
                pilotName = observer.name + ": " + observer.displayName;
            }

            /* Send a thank you message to the player. */
            player.commsMessage(pilotName + ": " + expandDescription("[jaguar_company_player_help]"));
            /* Equipment status of the black box. */
            blackboxStatus = player.ship.equipmentStatus("EQ_JAGUAR_COMPANY_BLACK_BOX");

            helperLevel = p_ships.mainScript.$reputationHelper;
            blackboxLevel = p_ships.mainScript.$reputationBlackbox;
            locationsLevel = p_ships.mainScript.$reputationLocations;

            if (reputation >= helperLevel && reputation < helperLevel + 10) {
                player.commsMessage(pilotName + ": " + expandDescription("[jaguar_company_player_help_buoy]"));
            } else if (reputation >= blackboxLevel && reputation < blackboxLevel + 10 &&
                blackboxStatus !== "EQUIPMENT_OK") {
                player.commsMessage(pilotName + ": " + expandDescription("[jaguar_company_player_help_blackbox]"));
            } else if (reputation >= locationsLevel && reputation < locationsLevel + 10 &&
                blackboxStatus === "EQUIPMENT_OK") {
                player.commsMessage(pilotName + ": " + expandDescription("[jaguar_company_player_help_locations]"));
            }

            if (!p_ships.newsSent || clock.seconds - p_ships.newsSent > 30 * 60) {
                /* First kill in the current system or more than 30 minutes since the last kill. */
                p_ships.newsSent = clock.seconds;
                /* Send news to Snoopers. */
                p_ships.mainScript.$sendNewsToSnoopers(expandDescription("[jaguar_company_help_news]", {
                        jaguar_company_pilot_name : newsSource
                    }));
            }
        }
    };

    /* Other global public functions. */

    /* NAME
     *   $setUp
     *
     * FUNCTION
     *   Setup the private ships variable and clear the public friend list array.
     */
    this.$setUp = function () {
        if (!worldScripts["Jaguar Company"]) {
            /* Main script not loaded yet. */
            return;
        }

        /* Stop and remove the timer. */
        if (this.$setUpTimerReference) {
            if (this.$setUpTimerReference.isRunning) {
                this.$setUpTimerReference.stop();
            }

            this.$setUpTimerReference = null;
        }

        /* Initialise the p_ships variable object.
         * Encapsulates all private global data.
         */
        p_ships = {
            /* Cache the main world script. */
            mainScript : worldScripts["Jaguar Company"],
            /* Local copies of the logging variables. */
            logging : worldScripts["Jaguar Company"].$logging,
            logExtra : worldScripts["Jaguar Company"].$logExtra,
            /* Initialise the attackers and attackers index array. */
            attackers : [],
            attackersIndex : [],
            /* 5% probability of a message being transmitted. */
            messageProbability : 0.95
        };
        /* A list of all ship entities that are considered friendly to each other. */
        this.$friendList = [];
    };

    /* NAME
     *   $killSelf
     *
     * FUNCTION
     *   Removes all functions and variables.
     *
     * INPUT
     *   desc - description for the removal (optional)
     */
    this.$killSelf = function (desc) {
        var prop;

        if (desc && typeof desc === "string") {
            player.consoleMessage(this.name + " - Check your Latest.log", 10);
            log(this.name, this.name + " - Shutting down" + desc);
        }

        /* Delete public functions and variables. */
        for (prop in this) {
            if (this.hasOwnProperty(prop)) {
                if (prop !== 'name' && prop !== 'version') {
                    delete this[prop];
                }
            }
        }

        /* Set the deactivated flag for Cabal Common Library. */
        this.deactivated = true;

        return;
    };

    /* NAME
     *   $showProps
     *
     * FUNCTION
     *   For debugging only.
     */
    this.$showProps = function () {
        var result = "",
        prop,
        attacker,
        attackerCounter,
        attackerLength,
        victim,
        victimCounter,
        victimLength,
        counter,
        length;

        for (prop in this) {
            if (this.hasOwnProperty(prop)) {
                if (typeof this[prop] !== "function") {
                    result += "this." + prop + ": " + this[prop] + "\n";
                } else {
                    result += "this." + prop + " = function ()\n";
                }
            }
        }

        for (prop in p_ships) {
            if (p_ships.hasOwnProperty(prop)) {
                result += "p_ships." + prop + ": " + p_ships[prop] + "\n";
            }
        }

        attackerLength = p_ships.attackers.length;

        if (attackerLength) {
            result += "Attackers (" + attackerLength + ")\n";

            for (attackerCounter = 0; attackerCounter < attackerLength; attackerCounter += 1) {
                result += "#" + (attackerCounter + 1) + ") ";
                attacker = p_ships.attackers[attackerCounter];
                counter = 1;
                length = Object.keys(attacker).length;

                for (prop in attacker) {
                    if (attacker.hasOwnProperty(prop)) {
                        result += prop + ": " + attacker[prop] + (counter === length ? "\n" : ", ");
                        counter += 1;
                    }
                }

                victimLength = attacker.victims.length;

                if (victimLength) {
                    result += "* Victims (" + victimLength + ")\n";

                    for (victimCounter = 0; victimCounter < victimLength; victimCounter += 1) {
                        result += "* #" + (victimCounter + 1) + ") ";
                        victim = attacker.victims[victimCounter];
                        counter = 1;
                        length = Object.keys(victim).length;

                        for (prop in victim) {
                            if (victim.hasOwnProperty(prop)) {
                                result += prop + ": " + victim[prop] + (counter === length ? "\n" : ", ");
                                counter += 1;
                            }
                        }
                    }
                }
            }
        }

        log(this.name, "$showProps::\n" + result);
    };

    /* NAME
     *   $startAttackersTimer
     *
     * FUNCTION
     *   Start the attackers timer.
     */
    this.$startAttackersTimer = function () {
        if (!this.$attackersCleanupTimerReference || !this.$attackersCleanupTimerReference.isRunning) {
            /* Start the attack cleanup timer. */
            if (!this.$attackersCleanupTimerReference) {
                /* New timer. */
                this.$attackersCleanupTimerReference = new Timer(this, this.$attackersCleanupTimer, 60, 60);
            } else {
                /* Restart current timer. */
                this.$attackersCleanupTimerReference.start();
            }

            if (p_ships.logging && p_ships.logExtra) {
                log(this.name, "$startAttackersTimer::Started the attack cleanup timer.");
            }
        }
    };

    /* NAME
     *   $stopAttackersTimer
     *
     * FUNCTION
     *   Stop and remove the attackers timer.
     */
    this.$stopAttackersTimer = function () {
        if (this.$attackersCleanupTimerReference) {
            if (this.$attackersCleanupTimerReference.isRunning) {
                this.$attackersCleanupTimerReference.stop();
            }

            this.$attackersCleanupTimerReference = null;

            if (p_ships.logging && p_ships.logExtra) {
                log(this.name, "$stopAttackersTimer::Removed the attack cleanup timer.");
            }
        }
    };

    /* NAME
     *   $removeFriendly
     *
     * FUNCTION
     *   Remove the unique key (entityPersonality) of a ship from the friend list array.
     *
     * INPUT
     *   key - unique key (entityPersonality) of the ship
     */
    this.$removeFriendly = function (key) {
        var index = this.$friendList.indexOf(key);

        if (index !== -1) {
            /* Remove the ship from the friend list. */
            this.$friendList.splice(index, 1);
        }
    };

    /* NAME
     *   $addFriendly
     *
     * FUNCTION
     *   Add the unique key (entityPersonality) of a ship to the friend list array.
     *   Chains some new ship script event handler hooks to the originals.
     *
     * INPUT
     *   args - object
     *     .ship - entity of the ship
     *     .pilotName - name of the pilot (optional)
     *     .shipName - display name of the ship (optional)
     */
    this.$addFriendly = function (args) {
        var ship,
        shipKey;

        if (!args || !args.ship || !args.ship.isValid) {
            /* Need a valid ship as a property of args. */
            return;
        }

        ship = args.ship;
        /* Unique key (entityPersonality) for the ship. */
        shipKey = ship.entityPersonality;

        if (this.$friendList.indexOf(shipKey) !== -1) {
            /* Already setup. */
            return;
        }

        /* Save the ship key. */
        ship.script.$shipKey = shipKey;

        if (typeof args.pilotName === "string") {
            ship.$pilotName = args.pilotName;

            /* Save the original ship script event handler hook. */
            ship.script.$ships_shipLaunchedEscapePod = ship.script.shipLaunchedEscapePod;

            /* NAME
             *   shipLaunchedEscapePod
             *
             * FUNCTION
             *   The shipLaunchedEscapePod handler is called when the pilot bails out.
             *
             *   Inlined this function because it is small.
             *
             * INPUT
             *   escapepod - contains the main pod with the pilot
             */
            ship.script.shipLaunchedEscapePod = function (escapepod) {
                /* Identify this pod as containing a member of Jaguar Company. */
                escapepod.$jaguarCompany = true;
                /* Transfer pilot name to the escape pod. */
                escapepod.$pilotName = this.ship.$pilotName;

                if (this.$ships_shipLaunchedEscapePod) {
                    /* Call the original. */
                    this.$ships_shipLaunchedEscapePod.apply(this, arguments);
                }
            };
        }

        if (typeof args.shipName === "string") {
            ship.displayName = args.shipName;
        }

        /* Add the ship key to the friend list. */
        this.$friendList.push(shipKey);

        /* Save the original ship script event handler hooks. */
        ship.script.$ships_entityDestroyed = ship.script.entityDestroyed;
        ship.script.$ships_shipAttackedWithMissile = ship.script.shipAttackedWithMissile;
        ship.script.$ships_shipBeingAttacked = ship.script.shipBeingAttacked;
        ship.script.$ships_shipDied = ship.script.shipDied;
        ship.script.$ships_shipTakingDamage = ship.script.shipTakingDamage;
        ship.script.$ships_shipTargetDestroyed = ship.script.shipTargetDestroyed;

        /* New ship script event handler hooks. */

        /* NAME
         *   entityDestroyed
         *
         * FUNCTION
         *   Friendly ship has just become invalid.
         */
        ship.script.entityDestroyed = function () {
            worldScripts["Jaguar Company Ships"].$removeFriendly(this.$shipKey);

            if (this.$ships_entityDestroyed) {
                /* Call the original. */
                this.$ships_entityDestroyed.apply(this, arguments);
            }
        };

        /* NAME
         *   shipAttackedWithMissile
         *
         * FUNCTION
         *   Friendly ship is being attacked with a missile.
         *
         * INPUTS
         *   missile - entity of the missile (not used)
         *   attacker - entity of the attacker
         */
        ship.script.shipAttackedWithMissile = function (missile, attacker) {
            worldScripts["Jaguar Company Ships"].$shipIsBeingAttackedWithMissile(this.ship, attacker);

            if (this.$ships_shipAttackedWithMissile) {
                /* Call the original. */
                this.$ships_shipAttackedWithMissile.apply(this, arguments);
            }
        };

        /* NAME
         *   shipBeingAttacked
         *
         * FUNCTION
         *   Friendly ship is being attacked.
         *
         * INPUT
         *   attacker - entity of the attacker
         */
        ship.script.shipBeingAttacked = function (attacker) {
            worldScripts["Jaguar Company Ships"].$shipIsBeingAttacked(this.ship, attacker);

            if (this.$ships_shipBeingAttacked) {
                /* Call the original. */
                this.$ships_shipBeingAttacked.apply(this, arguments);
            }
        };

        /* NAME
         *   shipDied
         *
         * FUNCTION
         *   Friendly ship has died.
         *
         * INPUTS
         *   attacker - entity of the attacker
         *   why - cause as a string
         */
        ship.script.shipDied = function (attacker, why) {
            worldScripts["Jaguar Company Ships"].$shipDied(this.ship, attacker, why);

            if (this.$ships_shipDied) {
                /* Call the original. */
                this.$ships_shipDied.apply(this, arguments);
            }
        };

        /* NAME
         *   shipTakingDamage
         *
         * FUNCTION
         *   Friendly ship is taking damage.
         *
         * INPUTS
         *   amount - amount of damage
         *   attacker - entity that caused the damage
         *   type - type of damage as a string
         */
        ship.script.shipTakingDamage = function (amount, attacker, type) {
            worldScripts["Jaguar Company Ships"].$shipTakingDamage(this.ship, amount, attacker, type);

            if (this.$ships_shipTakingDamage) {
                /* Call the original. */
                this.$ships_shipTakingDamage.apply(this, arguments);
            }
        };

        /* NAME
         *   shipTargetDestroyed
         *
         * FUNCTION
         *   Friendly ship killed someone.
         *
         *   Inlined this function because it doesn't call functions within the OXP.
         *
         * INPUT
         *   target - entity of the target
         */
        ship.script.shipTargetDestroyed = function (target) {
            var conhunt = missionVariables.conhunt,
            pilotName;

            if (target.primaryRole === "constrictor" && conhunt && conhunt === "STAGE_1") {
                if (this.ship.$pilotName) {
                    /* Get the pilot's name. */
                    pilotName = this.ship.$pilotName;
                } else {
                    /* Use displayName as the name of the pilot. */
                    pilotName = this.ship.displayName;
                }

                /* Just in case the ship kills the constrictor, let's not break the mission for the player... */
                missionVariables.conhunt = "CONSTRICTOR_DESTROYED";
                player.score += 1;
                player.credits += target.bounty;
                player.consoleMessage(pilotName + " assisted in the death of " + target.name);
                player.consoleMessage(
                    pilotName + ": Commander " + player.name +
                    ", you have the kill and bounty of " + target.bounty + "₢.");

                if (p_ships.logging && p_ships.logExtra) {
                    log(this.name, "shipTargetDestroyed::" +
                        pilotName + " flying '" + this.ship.name + ": " + this.ship.displayName + "'" +
                        " killed - " + target.name + " : " + target.bounty);
                }
            }

            if (this.$ships_shipTargetDestroyed) {
                /* Call the original. */
                this.$ships_shipTargetDestroyed.apply(this, arguments);
            }
        };

        /* Common ship script functions. */

        /* NAME
         *   $setCoordsToEntity
         *
         * FUNCTION
         *   Set the co-ordinates to the surface of the entity.
         *   This borrows some code from 'src/Core/Entities/ShipEntityAI.m - setCourseToPlanet'
         *
         * INPUT
         *   entity - entity to set co-ordinates to
         */
        ship.script.$setCoordsToEntity = function (entity) {
            var position = entity.position,
            distance,
            ratio,
            variation;

            /* Calculate a vector position between the entity's surface and the ship. */
            distance = this.ship.position.distanceTo(position);
            ratio = (entity.collisionRadius + this.ship.collisionRadius + 100) / distance;
            position = Vector3D.interpolate(position, this.ship.position, ratio);

            /* Higher variation if further away. */
            variation = (distance > 51200 ? 0.5 : 0.2);

            /* Move the vector a random amount. */
            position.x += variation * (Math.random() - variation);
            position.y += variation * (Math.random() - variation);
            position.z += variation * (Math.random() - variation);

            /* Save this position for 'setDestinationFromCoordinates' in the AI. */
            this.ship.savedCoordinates = position;
        };

        /* Common AI sendScriptMessage functions. */

        /* NAME
         *   $checkTargetIsValid
         *
         * FUNCTION
         *   Checks the current target to make sure it is still valid.
         *
         *   Responds to the caller ship with a 'TARGET_LOST' AI message.
         *
         *   Inlined this function because it is small.
         */
        ship.script.$checkTargetIsValid = function () {
            if (!this.ship.target || !this.ship.target.isValid) {
                /* Target was lost or became invalid. */
                this.ship.reactToAIMessage("TARGET_LOST");
            }
        };

        /* NAME
         *   $performJaguarCompanyAttackTarget
         *
         * FUNCTION
         *   This does something similar to a mix between the deployEscorts and groupAttackTarget AI commands.
         */
        ship.script.$performJaguarCompanyAttackTarget = function () {
            worldScripts["Jaguar Company Ships"].$performAttackTarget(this.ship);
        };

        /* NAME
         *   $recallAIState
         *
         * FUNCTION
         *   Recall the saved AI state.
         *
         *   Inlined this function because it is small.
         */
        ship.script.$recallAIState = function () {
            var mainScript = worldScripts["Jaguar Company"];

            if (mainScript.$logging && mainScript.$logExtra) {
                log(this.name,
                    "[" + this.ship.AI + "::" + this.ship.AIState + "] $recallAIState::" +
                    this.ship.name + ": " + this.ship.displayName + " - state: " + this.$savedAIState);
            }

            this.ship.AIState = this.$savedAIState;
        };

        /* NAME
         *   $saveAIState
         *
         * FUNCTION
         *   Save the current AI state.
         *
         * INPUT
         *   arr - alternative AI state (as an array) (optional) (just uses the first element in the array)
         *
         *   Inlined this function because it is small.
         */
        ship.script.$saveAIState = function (arr) {
            var mainScript = worldScripts["Jaguar Company"],
            state;

            if (!Array.isArray(arr)) {
                /* Default. */
                state = this.ship.AIState;
            } else {
                state = arr[0];
            }

            this.$savedAIState = state;

            if (mainScript.$logging && mainScript.$logExtra) {
                log(this.name,
                    "[" + this.ship.AI + "::" + this.ship.AIState + "] $saveAIState::" +
                    this.ship.name + ": " + this.ship.displayName + " - state: " + this.$savedAIState);
            }
        };

        /* NAME
         *   $scanForAttackers
         *
         * FUNCTION
         *   Scan for current ships or players from the past that have attacked us.
         *   Also scan for potential attackers.
         */
        ship.script.$scanForAttackers = function () {
            worldScripts["Jaguar Company Ships"].$scanForAttackers(this.ship);
        };

        if (p_ships.mainScript.$gte_v1_77) {
            /* Oolite v1.77 and newer. */

            /* NAME
             *   $scanForCascadeWeapon
             *
             * FUNCTION
             *   Do nothing. The real magic is done in the 'cascadeWeaponDetected' ship event function.
             */
            ship.script.$scanForCascadeWeapon = function () {
                return;
            };

            /* Save the original ship event hooks. */
            ship.script.$ships_cascadeWeaponDetected = ship.script.cascadeWeaponDetected;
            ship.script.$ships_shipBeingAttackedUnsuccessfully = ship.script.shipBeingAttackedUnsuccessfully;

            /* NAME
             *   cascadeWeaponDetected
             *
             * FUNCTION
             *   The cascadeWeaponDetected handler fires when a Q-bomb (or equivalent device) detonates within
             *   scanner range of the player. The stock Q-mine (and potentially OXP equivalents) will also send
             *   this handler at the start of the countdown, giving ships more time to react.
             *
             *   Reacts with a 'CASCADE_WEAPON_FOUND' AI message rather than 'CASCADE_WEAPON_DETECTED'
             *   used by Oolite v1.77 and newer.
             *
             *   Inlined this function because it doesn't call functions within the OXP.
             *
             * INPUT
             *   weapon - entity of the cascade weapon
             */
            ship.script.cascadeWeaponDetected = function (weapon) {
                /* Set the target and send a CASCADE_WEAPON_FOUND message to the AI. */
                this.ship.target = weapon;
                this.ship.reactToAIMessage("CASCADE_WEAPON_FOUND");

                if (this.$ships_cascadeWeaponDetected) {
                    /* Call the original. */
                    this.$ships_cascadeWeaponDetected.apply(this, arguments);
                }
            };

            /* NAME
             *   shipBeingAttackedUnsuccessfully
             *
             * FUNCTION
             *   A ship is being unsuccessfully attacked.
             *
             * INPUT
             *   attacker - entity of the attacker
             */
            ship.script.shipBeingAttackedUnsuccessfully = function (attacker) {
                worldScripts["Jaguar Company Ships"].$shipIsBeingAttackedUnsuccessfully(this.ship, attacker);

                if (this.$ships_shipBeingAttackedUnsuccessfully) {
                    /* Call the original. */
                    this.$ships_shipBeingAttackedUnsuccessfully.apply(this, arguments);
                }
            };
        } else {
            /* Oolite v1.76.1 and older. */

            /* NAME
             *   $scanForCascadeWeapon
             *
             * FUNCTION
             *   Scan for cascade weapons. Won't be needed when v1.78 comes out.
             *   Reacts with a 'CASCADE_WEAPON_FOUND' AI message rather than 'CASCADE_WEAPON_DETECTED'
             *   used by Oolite v1.77 and newer.
             *
             *   Inlined this function because it doesn't call functions within the OXP.
             */
            ship.script.$scanForCascadeWeapon = function () {
                /* This is modified from some code in Random Hits spacebar ship script. */
                var cascadeWeaponRoles = [
                    "EQ_QC_MINE",
                    "EQ_CASCADE_MISSILE",
                    "EQ_LAW_MISSILE",
                    "EQ_OVERRIDE_MISSILE",
                    "energy-bomb",
                    "RANDOM_HITS_MINE"
                ],
                cascadeWeapons;

                /* Search for any cascade weapons within maximum scanner range of the caller ship. */
                cascadeWeapons = system.filteredEntities(this, function (entity) {
                        return (cascadeWeaponRoles.indexOf(entity.primaryRole) !== -1);
                    }, this.ship, this.ship.scannerRange);

                if (cascadeWeapons.length) {
                    /* Found at least one. First one in the cascadeWeapons array is the closest.
                     * Set the target and send a CASCADE_WEAPON_FOUND message to the AI.
                     */
                    this.ship.target = cascadeWeapons[0];
                    this.ship.reactToAIMessage("CASCADE_WEAPON_FOUND");
                }
            };
        }
    };

    /* NAME
     *   $addAttacker
     *
     * FUNCTION
     *   Add an attacker and victim to the attackers array.
     *
     * INPUTS
     *   attacker - entity of the attacker
     *   victim - entity of the victim (optional)
     *
     * RESULT
     *   result - object containing attacker and victim indexes, -1 if not added.
     */
    this.$addAttacker = function (attacker, victim) {
        var attackerKey,
        attackerIndex,
        victimKey,
        victimIndex,
        logMsg = "";

        if (!attacker || !attacker.isValid) {
            /* The attacker is no longer valid. */
            return -1;
        }

        if (this.$friendList.indexOf(attacker.entityPersonality) !== -1) {
            /* Don't add members of Jaguar Company. */
            return -1;
        }

        if (attacker.isPolice) {
            /* Don't add police ships. */
            return -1;
        }

        /* Start the attackers timer if not started already. */
        this.$startAttackersTimer();
        /* Get the attacker's key. */
        attackerKey = attacker.entityPersonality;
        /* Get the attacker's index. */
        attackerIndex = p_ships.attackersIndex.indexOf(attackerKey);

        if (attackerIndex === -1) {
            if (p_ships.logging && p_ships.logExtra) {
                logMsg += "\nAdding attacker#" + attackerKey + " (" + attacker.displayName + ")";
            }

            /* Create an entry for the attacker if it doesn't exist.
             * push() returns the new length. The attacker index will be 1 less than this.
             */
            attackerIndex = p_ships.attackers.push({
                    hostile : false,
                    ship : attacker,
                    shipKey : attackerKey,
                    victims : [],
                    victimsIndex : []
                }) - 1;
            /* Create the index entry for the attacker. */
            p_ships.attackersIndex[attackerIndex] = attackerKey;
        }

        if (!victim || !victim.isValid) {
            victimIndex = -1;
        } else {
            /* Get the victim's key. */
            victimKey = victim.entityPersonality;
            /* Get the victim's index. */
            victimIndex = p_ships.attackers[attackerIndex].victimsIndex.indexOf(victimKey);

            if (victimIndex === -1) {
                if (p_ships.logging && p_ships.logExtra) {
                    logMsg += "\nAdding victim#" + victimKey + " (" + victim.name + ": " + victim.displayName + ")" +
                    " attacked by attacker#" + attackerKey + " (" + attacker.displayName + ")";
                }

                /* Create an entry for the victim if it doesn't exist.
                 * push() returns the new length. The victim index will be 1 less than this.
                 */
                victimIndex = p_ships.attackers[attackerIndex].victims.push({
                        attackCounter : 0,
                        attackTime : clock.seconds,
                        missCounter : 0,
                        missTime : clock.seconds,
                        ship : victim,
                        shipKey : victimKey
                    }) - 1;
                /* Create the index entry for the victim. */
                p_ships.attackers[attackerIndex].victimsIndex[victimIndex] = victimKey;
            }
        }

        if (p_ships.logging && p_ships.logExtra && logMsg.length) {
            log(this.name, "$addAttacker::" + logMsg);
        }

        return {
            attackerIndex : attackerIndex,
            victimIndex : victimIndex
        };
    };

    /* NAME
     *   $removeAttacker
     *
     * FUNCTION
     *   Remove an attacker from the attackers array.
     *
     * INPUT
     *   attackerKey - unique key (entityPersonality) of the attacker
     */
    this.$removeAttacker = function (attackerKey) {
        var attackersIndex,
        attackerIndex;

        attackersIndex = p_ships.attackersIndex;
        /* Get the attacker's index. */
        attackerIndex = attackersIndex.indexOf(attackerKey);

        if (!attackersIndex.length || attackerIndex === -1) {
            /* No such attacker. */
            return;
        }

        /* Remove the attacker from the attackers array. */
        p_ships.attackers.splice(attackerIndex, 1);
        /* Remove the attacker from the attackers index array. */
        p_ships.attackersIndex.splice(attackerIndex, 1);
    };

    /* NAME
     *   $removeVictimFromAttacker
     *
     * FUNCTION
     *   Remove a victim from the victims array of the attacker.
     *
     * INPUTS
     *   victimKey - unique key (entityPersonality) of the victim
     *   attackerKey - unique key (entityPersonality) of the attacker
     */
    this.$removeVictimFromAttacker = function (victimKey, attackerKey) {
        var attackersIndex,
        attackerIndex,
        victimsIndex,
        victimIndex;

        attackersIndex = p_ships.attackersIndex;
        /* Get the attacker's index. */
        attackerIndex = attackersIndex.indexOf(attackerKey);

        if (!attackersIndex.length || attackerIndex === -1) {
            /* No such attacker. */
            return;
        }

        victimsIndex = p_ships.attackers[attackerIndex].victimsIndex;
        /* Get the victim's index. */
        victimIndex = victimsIndex.indexOf(victimKey);

        if (!victimsIndex.length || victimIndex === -1) {
            /* No such victim. */
            return;
        }

        /* Remove the victim from the victims array. */
        p_ships.attackers[attackerIndex].victims.splice(victimIndex, 1);
        /* Remove the victim from the victims index array. */
        p_ships.attackers[attackerIndex].victimsIndex.splice(victimIndex, 1);
    };

    /* NAME
     *   $attackersCleanupTimer
     *
     * FUNCTION
     *   Periodic timer to clean up the attackers array.
     *
     *   Called every 1 minute.
     */
    this.$attackersCleanupTimer = function () {
        var attackers,
        attackerIndex,
        attackerKey,
        attackersCounter,
        attackersLength,
        attacker,
        victimKey,
        victimsCounter,
        victimsLength,
        victim,
        mainScript,
        logMsg;

        if (!p_ships.attackers.length) {
            /* No attackers, stop and remove this timer. */
            this.$stopAttackersTimer();

            /* Reset the attackers and index array. Pointless, but done for potential error avoidance. */
            p_ships.attackersIndex = [];
            p_ships.attackers = [];

            return;
        }

        if (p_ships.logging && p_ships.logExtra) {
            logMsg = "$attackersCleanupTimer::Checking attackers...";
        }

        /* Cache the length. */
        attackersLength = p_ships.attackers.length;
        /* Empty array to copy the attackers into. */
        attackers = [];

        /* Create a copy of the attackers array. The index should follow the original */
        for (attackersCounter = 0; attackersCounter < attackersLength; attackersCounter += 1) {
            /* Iterate over each attacker. */
            attacker = p_ships.attackers[attackersCounter];
            /* No need to copy the victimsIndex property as we don't use it in the cleanup. */
            attackers.push({
                hostile : attacker.hostile,
                ship : attacker.ship,
                shipKey : attacker.shipKey,
                /* Empty array to copy the victims into. */
                victims : []
            });

            /* Cache the length. */
            victimsLength = attacker.victims.length;

            /* Create a copy of the victims array for the attacker. The index should follow the original. */
            for (victimsCounter = 0; victimsCounter < victimsLength; victimsCounter += 1) {
                /* Iterate over each victim. */
                victim = attacker.victims[victimsCounter];
                /* No need to copy the attackCounter and missCounter properties as we don't use them in the cleanup. */
                attackers[attackersCounter].victims.push({
                    attackTime : victim.attackTime,
                    missTime : victim.missTime,
                    ship : victim.ship,
                    shipKey : victim.shipKey
                });
            }
        }

        mainScript = p_ships.mainScript;

        /* attackersLength already set-up. */
        for (attackersCounter = 0; attackersCounter < attackersLength; attackersCounter += 1) {
            /* Iterate over each attacker. */
            attacker = attackers[attackersCounter];
            attackerKey = attacker.shipKey;

            if (p_ships.logging && p_ships.logExtra) {
                logMsg += "\n* attacker#" + attackerKey;

                if (attacker.ship.displayName !== undefined) {
                    logMsg += " (" + attacker.ship.displayName + ")";
                }

                logMsg += ", " + attacker.victims.length + " victims";
            }

            if (!attacker.ship.isValid) {
                if (p_ships.logging && p_ships.logExtra) {
                    logMsg += ", removing (dead/not valid)";
                }

                /* Remove the invalid attacker from the real attackers array. */
                this.$removeAttacker(attackerKey);
            } else if (attacker.ship.hasRole("tharglet") && attacker.ship.isCargo) {
                if (p_ships.logging && p_ships.logExtra) {
                    logMsg += ", removing (inactive tharglet)";
                }

                /* Remove the inactive tharglet from the real attackers array. */
                this.$removeAttacker(attackerKey);
            } else if (attacker.ship.isDerelict) {
                if (p_ships.logging && p_ships.logExtra) {
                    logMsg += ", removing (derelict)";
                }

                /* Remove the derelict attacker from the real attackers array. */
                this.$removeAttacker(attackerKey);
            } else if (attacker.ship.isPlayer &&
                mainScript.$playerVar.reputation[galaxyNumber] >= mainScript.$reputationHelper) {
                if (p_ships.logging && p_ships.logExtra) {
                    logMsg += ", removing (player turned from the dark side)";
                }

                /* Remove the player from the real attackers array. */
                this.$removeAttacker(attackerKey);
            } else if (attacker.victims.length) {
                if (p_ships.logging && p_ships.logExtra) {
                    logMsg += ", checking victims...";
                }

                /* Cache the length. */
                victimsLength = attacker.victims.length;

                for (victimsCounter = 0; victimsCounter < victimsLength; victimsCounter += 1) {
                    /* Iterate over each victim. */
                    victim = attacker.victims[victimsCounter];
                    victimKey = victim.shipKey;

                    if (p_ships.logging && p_ships.logExtra) {
                        logMsg += "\n** victim#" + victimKey;

                        if (victim.ship.displayName !== undefined) {
                            logMsg += " (" + victim.ship.name + ": " + victim.ship.displayName + ")";
                        }
                    }

                    if (!victim.ship.isValid) {
                        if (p_ships.logging && p_ships.logExtra) {
                            logMsg += ", removing (dead/not valid)";
                        }

                        /* Remove the invalid victim from the real victims array. */
                        this.$removeVictimFromAttacker(victimKey, attackerKey);
                    } else if (victim.ship.isDerelict) {
                        if (p_ships.logging && p_ships.logExtra) {
                            logMsg += ", removing (derelict)";
                        }

                        /* Remove the derelict victim from the real victims array. */
                        this.$removeVictimFromAttacker(victimKey, attackerKey);
                    } else if (!attacker.hostile &&
                        (clock.seconds - victim.attackTime > 5 || clock.seconds - victim.missTime > 5)) {
                        if (p_ships.logging && p_ships.logExtra) {
                            logMsg += ", removing (no longer attacked)";
                        }

                        /* More than 5 seconds have passed since the first attack.
                         * Remove the old victim from the real victims array.
                         */
                        this.$removeVictimFromAttacker(victimKey, attackerKey);
                    }
                }

                if (p_ships.logging && p_ships.logExtra) {
                    logMsg += "\n" + "$attackersCleanupTimer::Finished checking victims.";
                }
            }

            if (!attacker.hostile) {
                /* Find the real index of the attacker. May have changed. */
                attackerIndex = p_ships.attackersIndex.indexOf(attackerKey);

                if (attackerIndex !== -1 && !p_ships.attackers[attackerIndex].victims.length) {
                    if (p_ships.logging && p_ships.logExtra) {
                        logMsg += "\n** attacker#" + attackerKey + " (" + attacker.ship.displayName + ")" +
                        ", removing (no victims)";
                    }

                    /* Has no victims so no longer an attacker.
                     * Remove the attacker from the real attackers array.
                     */
                    this.$removeAttacker(attackerKey);
                }
            }
        }

        if (p_ships.logging && p_ships.logExtra) {
            logMsg += "\n" + "$attackersCleanupTimer::Finished checking attackers.";
            log(this.name, logMsg);
        }

        if (!p_ships.attackers.length) {
            /* No attackers, stop and remove this timer. */
            this.$stopAttackersTimer();

            /* Reset the attackers and index array. Pointless, but done for potential error avoidance. */
            p_ships.attackersIndex = [];
            p_ships.attackers = [];
        }
    };

    /* NAME
     *   $makeHostile
     *
     * FUNCTION
     *   Makes the attacking ship hostile to the victim's group.
     *
     * INPUTS
     *   attacker - entity of the attacker
     *   victim - entity of the victim
     */
    this.$makeHostile = function (attacker, victim) {
        var index;

        if (!attacker || !attacker.isValid || !victim || !victim.isValid) {
            /* The attacker and/or victim is no longer valid. */
            return;
        }

        if (this.$friendList.indexOf(attacker.entityPersonality) !== -1) {
            /* The attacker is a member of Jaguar Company. */
            return;
        }

        if (this.$isHostile(attacker)) {
            /* Already hostile. */
            return;
        }

        if (p_ships.logging && p_ships.logExtra) {
            log(this.name, "$makeHostile::Make hostile:" +
                " attacker#" + attacker.entityPersonality + " (" + attacker.displayName + ")" +
                ", bounty: " + attacker.bounty +
                ", victim#" + victim.entityPersonality + " (" + victim.name + ": " + victim.displayName + ")");
        }

        /* Add the attacker (if needed) and get the attacker and victim index. */
        index = this.$addAttacker(attacker, victim);

        if (index !== -1) {
            /* Set the hostile property. */
            p_ships.attackers[index.attackerIndex].hostile = true;
        }
    };

    /* NAME
     *   $isHostile
     *
     * FUNCTION
     *   Check if the ship is hostile.
     *
     * INPUT
     *   ship - entity of the ship to check
     *
     * RESULT
     *   result - return true if ship is hostile, otherwise return false
     */
    this.$isHostile = function (ship) {
        var index,
        attackerIndex,
        counter,
        length;

        if (!ship || !ship.isValid || this.$friendList.indexOf(ship.entityPersonality) !== -1) {
            /* The ship is no longer valid or is a member of Jaguar Company. */
            return false;
        }

        if (ship.isThargoid) {
            /* Cache the length. */
            length = ship.escortGroup.length;

            for (counter = 0; counter < length; counter += 1) {
                /* Add the thargoid and tharglets (if needed) and get the attacker index. */
                index = this.$addAttacker(ship.escortGroup[counter]);

                if (index !== -1) {
                    /* Set the hostile property. */
                    p_ships.attackers[index.attackerIndex].hostile = true;
                }
            }

            /* Always true for Thargoids/tharglets. */
            return true;
        }

        if (!p_ships.attackers.length) {
            /* No attackers. */
            return false;
        }

        /* Find the index of the ship in the attackers index. */
        attackerIndex = p_ships.attackersIndex.indexOf(ship.entityPersonality);

        if (attackerIndex === -1) {
            /* No such attacker. */
            return false;
        }

        /* Return hostile status. */
        return p_ships.attackers[attackerIndex].hostile;
    };

    /* NAME
     *   $increaseAttackCounter
     *
     * FUNCTION
     *   Increase the ship's attack counter for the victim.
     *
     * INPUTS
     *   attacker - entity of the attacker
     *   victim - entity of the attacked ship
     */
    this.$increaseAttackCounter = function (attacker, victim) {
        var index;

        if (!attacker || !attacker.isValid || !victim || !victim.isValid) {
            /* The attacker and/or victim is no longer valid. */
            return;
        }

        /* Setup the attacker and victim. */
        index = this.$addAttacker(attacker, victim);

        if (index !== -1) {
            /* Increase the attack counter for the victim. */
            p_ships.attackers[index.attackerIndex].victims[index.victimIndex].attackCounter += 1;
        }
    };

    /* NAME
     *   $attackCounter
     *
     * FUNCTION
     *   Return the attack counter for the victim.
     *
     * INPUTS
     *   attacker - entity of the attacker
     *   victim - entity of the attacked ship
     *
     * RESULT
     *   result - return attackCounter if available, otherwise return -1
     */
    this.$attackCounter = function (attacker, victim) {
        var attackerIndex,
        victimIndex;

        if (!attacker || !attacker.isValid || !victim || !victim.isValid) {
            /* The attacker and/or victim is no longer valid. */
            return -1;
        }

        if (!p_ships.attackers.length) {
            /* No attackers. */
            return -1;
        }

        /* Find the index of the attacker. */
        attackerIndex = p_ships.attackersIndex.indexOf(attacker.entityPersonality);

        if (attackerIndex === -1 || !p_ships.attackers[attackerIndex].victims.length) {
            /* No such attacker or victims. */
            return -1;
        }

        /* Find the index of the victim. */
        victimIndex = p_ships.attackers[attackerIndex].victimsIndex.indexOf(victim.entityPersonality);

        if (victimIndex === -1) {
            /* No such victim. */
            return -1;
        }

        /* Return attack counter. */
        return p_ships.attackers[attackerIndex].victims[victimIndex].attackCounter;
    };

    /* NAME
     *   $resetAttackCounter
     *
     * FUNCTION
     *   Reset the attack counter to 1.
     *
     * INPUTS
     *   attacker - entity of the attacker
     *   victim - entity of the attacked ship
     */
    this.$resetAttackCounter = function (attacker, victim) {
        var attackerIndex,
        victimIndex;

        if (!attacker || !attacker.isValid || !victim || !victim.isValid) {
            /* The attacker and/or victim is no longer valid. */
            return;
        }

        if (!p_ships.attackers.length) {
            /* No attackers. */
            return;
        }

        /* Find the index of the attacker. */
        attackerIndex = p_ships.attackersIndex.indexOf(attacker.entityPersonality);

        if (attackerIndex === -1 || !p_ships.attackers[attackerIndex].victims.length) {
            /* No such attacker or victims. */
            return;
        }

        /* Find the index of the victim. */
        victimIndex = p_ships.attackers[attackerIndex].victimsIndex.indexOf(victim.entityPersonality);

        if (victimIndex === -1) {
            /* No such victim. */
            return;
        }

        /* Reset the attack counter to 1. */
        p_ships.attackers[attackerIndex].victims[victimIndex].attackCounter = 1;
    };

    /* NAME
     *   $attackTime
     *
     * FUNCTION
     *   Return the attack time for the victim or 'clock.seconds' if not available.
     *
     * INPUTS
     *   attacker - entity of the attacker
     *   victim - entity of the attacked ship
     *
     * RESULT
     *   result - return attackTime if available, otherwise return 'clock.seconds'
     */
    this.$attackTime = function (attacker, victim) {
        var attackerIndex,
        victimIndex;

        if (!attacker || !attacker.isValid || !victim || !victim.isValid) {
            /* The attacker and/or victim is no longer valid. */
            return clock.seconds;
        }

        if (!p_ships.attackers.length) {
            /* No attackers. */
            return clock.seconds;
        }

        /* Find the index of the attacker. */
        attackerIndex = p_ships.attackersIndex.indexOf(attacker.entityPersonality);

        if (attackerIndex === -1 || !p_ships.attackers[attackerIndex].victims.length) {
            /* No such attacker or victims. */
            return clock.seconds;
        }

        /* Find the index of the victim. */
        victimIndex = p_ships.attackers[attackerIndex].victimsIndex.indexOf(victim.entityPersonality);

        if (victimIndex === -1) {
            /* No such victim. */
            return clock.seconds;
        }

        /* Return attack time. */
        return p_ships.attackers[attackerIndex].victims[victimIndex].attackTime;
    };

    /* NAME
     *   $increaseMissCounter
     *
     * FUNCTION
     *   Increase the ship's miss counter for the victim.
     *
     * INPUTS
     *   attacker - entity of the attacker
     *   victim - entity of the attacked ship
     */
    this.$increaseMissCounter = function (attacker, victim) {
        var index;

        if (!attacker || !attacker.isValid || !victim || !victim.isValid) {
            /* The attacker and/or victim is no longer valid. */
            return;
        }

        /* Setup the attacker and victim. */
        index = this.$addAttacker(attacker, victim);

        if (index !== -1) {
            /* Increase the miss counter for the victim. */
            p_ships.attackers[index.attackerIndex].victims[index.victimIndex].missCounter += 1;
        }
    };

    /* NAME
     *   $missCounter
     *
     * FUNCTION
     *   Return the miss counter for the victim.
     *
     * INPUTS
     *   attacker - entity of the attacker
     *   victim - entity of the attacked ship
     *
     * RESULT
     *   result - return missCounter if available, otherwise return -1
     */
    this.$missCounter = function (attacker, victim) {
        var attackerIndex,
        victimIndex;

        if (!attacker || !attacker.isValid || !victim || !victim.isValid) {
            /* The attacker and/or victim is no longer valid. */
            return -1;
        }

        if (!p_ships.attackers.length) {
            /* No attackers. */
            return -1;
        }

        /* Find the index of the attacker. */
        attackerIndex = p_ships.attackersIndex.indexOf(attacker.entityPersonality);

        if (attackerIndex === -1 || !p_ships.attackers[attackerIndex].victims.length) {
            /* No such attacker or victims. */
            return -1;
        }

        /* Find the index of the victim. */
        victimIndex = p_ships.attackers[attackerIndex].victimsIndex.indexOf(victim.entityPersonality);

        if (victimIndex === -1) {
            /* No such victim. */
            return -1;
        }

        /* Return miss counter. */
        return p_ships.attackers[attackerIndex].victims[victimIndex].missCounter;
    };

    /* NAME
     *   $resetMissCounter
     *
     * FUNCTION
     *   Reset the miss counter to 1.
     *
     * INPUTS
     *   attacker - entity of the attacker
     *   victim - entity of the attacked ship
     */
    this.$resetMissCounter = function (attacker, victim) {
        var attackerIndex,
        victimIndex;

        if (!attacker || !attacker.isValid || !victim || !victim.isValid) {
            /* The attacker and/or victim is no longer valid. */
            return;
        }

        if (!p_ships.attackers.length) {
            /* No attackers. */
            return;
        }

        /* Find the index of the attacker. */
        attackerIndex = p_ships.attackersIndex.indexOf(attacker.entityPersonality);

        if (attackerIndex === -1 || !p_ships.attackers[attackerIndex].victims.length) {
            /* No such attacker or victims. */
            return;
        }

        /* Find the index of the victim. */
        victimIndex = p_ships.attackers[attackerIndex].victimsIndex.indexOf(victim.entityPersonality);

        if (victimIndex === -1) {
            /* No such victim. */
            return;
        }

        /* Reset the miss counter to 1. */
        p_ships.attackers[attackerIndex].victims[victimIndex].missCounter = 1;
    };

    /* NAME
     *   $missTime
     *
     * FUNCTION
     *   Return the miss time for the victim or 'clock.seconds' if not available.
     *
     * INPUTS
     *   attacker - entity of the attacker
     *   victim - entity of the attacked ship
     *
     * RESULT
     *   result - return missTime if available, otherwise return 'clock.seconds'
     */
    this.$missTime = function (attacker, victim) {
        var attackerIndex,
        victimIndex;

        if (!attacker || !attacker.isValid || !victim || !victim.isValid) {
            /* The attacker and/or victim is no longer valid. */
            return clock.seconds;
        }

        if (!p_ships.attackers.length) {
            /* No attackers. */
            return clock.seconds;
        }

        /* Find the index of the attacker. */
        attackerIndex = p_ships.attackersIndex.indexOf(attacker.entityPersonality);

        if (attackerIndex === -1 || !p_ships.attackers[attackerIndex].victims.length) {
            /* No such attacker or victims. */
            return clock.seconds;
        }

        /* Find the index of the victim. */
        victimIndex = p_ships.attackers[attackerIndex].victimsIndex.indexOf(victim.entityPersonality);

        if (victimIndex === -1) {
            /* No such victim. */
            return clock.seconds;
        }

        /* Return miss time. */
        return p_ships.attackers[attackerIndex].victims[victimIndex].missTime;
    };

    /* New ship script event handler hooks. */

    /* NAME
     *   $shipDied
     *
     * FUNCTION
     *   A ship has died.
     *
     *   Not to be confused with the world script event function 'shipDied',
     *   although it should be called from the ship script event function 'shipDied'.
     *
     * INPUTS
     *   victim - entity that died
     *   attacker - entity that caused the death
     *   why - cause as a string
     */
    this.$shipDied = function (victim, attacker, why) {
        var destroyedBy = attacker,
        pilotName;

        if (attacker && attacker.isValid && !victim.isDerelict) {
            destroyedBy = "ship#" + attacker.entityPersonality + " (" + attacker.displayName + ")";

            if (why === "energy damage" || why === "cascade weapon") {
                /* Check for piloted ships that aren't hostile.
                 * Generally this will pick up death by a surprise/instant kill, i.e. cascade weapon.
                 */
                if (attacker.isPiloted && !this.$isHostile(attacker)) {
                    /* Make the attacker a hostile for future checking. */
                    this.$makeHostile(attacker, victim);

                    if (attacker.isPlayer) {
                        /* Remember the player, even if they jump system. */
                        p_ships.mainScript.$playerVar.attacker = true;
                        /* Clear the reputation of the player. */
                        p_ships.mainScript.$playerVar.reputation[galaxyNumber] = 0;
                    }
                }
            }

            if ((victim.isPiloted || victim.isStation) && Math.random() > p_ships.messageProbability) {
                if (victim.$pilotName) {
                    /* Get the victims's name. */
                    pilotName = victim.$pilotName;
                } else {
                    /* Use displayName as the name of the victim. */
                    pilotName = victim.name + ": " + victim.displayName;
                }

                if (player.ship && player.ship.isValid &&
                    victim.position.distanceTo(player.ship.position) < victim.scannerRange) {
                    /* Death message. */
                    player.consoleMessage(pilotName + ": " + expandDescription("[jaguar_company_death]"));
                }
            }
        }

        if (p_ships.logging && p_ships.logExtra) {
            log(this.name,
                "$shipDied::" +
                "ship#" + victim.entityPersonality + " (" + victim.name + ": " + victim.displayName + ")" +
                " was destroyed by " + destroyedBy +
                ", reason: " + why);
        }
    };

    /* NAME
     *   $shipIsBeingAttacked
     *
     * FUNCTION
     *   Remember who is attacking us. Pay particular attention to players.
     *
     *   The AI will send an ATTACKED message to this ship.
     *   Since we check for occurences of "friendly fire" we can not use or respond to that message,
     *   so we send out a new message of HOSTILE_FIRE if it really is an attack.
     *
     *   Not to be confused with the world script event function 'shipBeingAttacked',
     *   although it should be called from the ship script event function 'shipBeingAttacked'.
     *
     * INPUTS
     *   victim - caller ship
     *   attacker - entity of the attacker
     */
    this.$shipIsBeingAttacked = function (victim, attacker) {
        var attackCounter,
        piloted,
        pilotName,
        psInRange;

        if (!attacker || !attacker.isValid ||
            !victim || !victim.isValid ||
            victim.isDerelict) {
            /* The attacker is no longer valid
             * or the victim is no longer valid
             * or the victim is a derelict
             */
            return;
        }

        piloted = (victim.isPiloted || victim.isStation);
        psInRange = (player.ship && player.ship.isValid &&
            victim.position.distanceTo(player.ship.position) < victim.scannerRange);

        if (victim.$pilotName) {
            /* Get the victims's name. */
            pilotName = victim.$pilotName;
        } else {
            /* Use displayName as the name of the victim. */
            pilotName = victim.name + ": " + victim.displayName;
        }

        /* Check if the attacker is a friend of the victim. */
        if (this.$friendList.indexOf(attacker.entityPersonality) !== -1) {
            if (piloted && Math.random() > p_ships.messageProbability && psInRange) {
                /* Broadcast a "friendly fire" message. */
                player.consoleMessage(pilotName + ": " + expandDescription("[jaguar_company_friendly_fire]"));
            }

            /* Tell the attacker that we are a friend. */
            attacker.reactToAIMessage("FRIENDLY_FIRE");

            return;
        }

        /* Setup the attacker and victim if needed and increase the attack counter. */
        this.$increaseAttackCounter(attacker, victim);

        if (this.$isHostile(attacker)) {
            /* Already been marked as hostile. */
            if (piloted && Math.random() > p_ships.messageProbability) {
                /* Show hostile message. */
                if (attacker.isPlayer) {
                    /* Player hostile message. */
                    player.consoleMessage(pilotName + ": " + expandDescription("[jaguar_company_player_hostile_fire]"));
                } else if (psInRange) {
                    /* Other ship hostile message. */
                    player.consoleMessage(pilotName + ": " + expandDescription("[jaguar_company_hostile_fire]"));
                }
            }

            /* Send back a custom AI message. */
            victim.reactToAIMessage("HOSTILE_FIRE");

            return;
        }

        /* Thargoids/tharglets and pirates don't get warnings. */
        if (!attacker.isThargoid && !attacker.isPirate) {
            if (clock.seconds - this.$attackTime(attacker, victim) > 5) {
                /* More than 5 seconds since the last "friendly fire" hit. */
                this.$resetAttackCounter(attacker, victim);
            }

            attackCounter = this.$attackCounter(attacker, victim);

            if (attackCounter === -1) {
                /* Not an attacker or there are no victims. */
                return;
            }

            if (attackCounter < 5) {
                /* We've only hit this ship less than 5 times. Assume "friendly fire". */
                if (attackCounter === 1) {
                    /* Only show "friendly fire" message on the first hit. */
                    if (attacker.isPlayer) {
                        /* Decrease reputation. */
                        p_ships.mainScript.$playerVar.reputation[galaxyNumber] -= 1;

                        if (piloted) {
                            /* Player warning. */
                            player.consoleMessage(pilotName + ": " +
                                expandDescription("[jaguar_company_player_friendly_fire]"));
                        }
                    } else if (piloted && psInRange) {
                        /* Other ship warning. */
                        player.consoleMessage(pilotName + ": " + expandDescription("[jaguar_company_friendly_fire]"));
                    }
                }

                return;
            }
        }

        /* Everybody has had all the warnings they are going to get once we have reached this point.
         * This section is only executed once. Hostiles are caught above after this.
         */

        /* Make the attacker a hostile for future checking. */
        this.$makeHostile(attacker, victim);

        if (attacker.isPlayer) {
            /* Remember the player, even if they jump system. */
            p_ships.mainScript.$playerVar.attacker = true;
            /* Clear the reputation of the player. */
            p_ships.mainScript.$playerVar.reputation[galaxyNumber] = 0;

            if (piloted) {
                /* Player hostile message. */
                player.consoleMessage(pilotName + ": " + expandDescription("[jaguar_company_player_hostile_fire]"));
            }
        } else if (piloted && psInRange) {
            /* Other ship hostile message. */
            player.consoleMessage(pilotName + ": " + expandDescription("[jaguar_company_hostile_fire]"));
        }

        /* Send back a custom AI message. */
        victim.reactToAIMessage("HOSTILE_FIRE");
    };

    /* NAME
     *   $shipIsBeingAttackedUnsuccessfully
     *
     * FUNCTION
     *   Oolite v1.77 and newer.
     *
     *   A ship is being unsuccessfully attacked.
     *
     *   The AI will send an ATTACKER_MISSED message to this ship.
     *   Since we check for occurences of "friendly fire" we can not use or respond to that message,
     *   so we send out a new message of HOSTILE_FIRE if it really is an attack.
     *
     *   Not to be confused with the world script event function 'shipBeingAttackedUnsuccessfully',
     *   although it should be called from the ship script event function 'shipBeingAttackedUnsuccessfully'.
     *
     * INPUTS
     *   victim - caller ship
     *   attacker - entity of the unsuccessful attacker
     */
    this.$shipIsBeingAttackedUnsuccessfully = function (victim, attacker) {
        var missCounter,
        piloted,
        pilotName,
        psInRange;

        if (!attacker || !attacker.isValid ||
            !victim || !victim.isValid ||
            victim.isDerelict) {
            /* The attacker is no longer valid
             * or the victim is no longer valid
             * or the victim is a derelict
             */
            return;
        }

        /* Check if the attacker is a friend of the victim. */
        if (this.$friendList.indexOf(attacker.entityPersonality) !== -1) {
            /* Tell the attacker that we are a friend. */
            attacker.reactToAIMessage("FRIENDLY_FIRE");

            return;
        }

        /* Setup the attacker and victim if needed and increase the miss counter. */
        this.$increaseMissCounter(attacker, victim);

        if (this.$isHostile(attacker)) {
            /* Already been marked as hostile. Treat it as though the attacker hit. */
            victim.reactToAIMessage("HOSTILE_FIRE");

            return;
        }

        /* Thargoids/tharglets and pirates don't get warnings. */
        if (!attacker.isThargoid && !attacker.isPirate) {
            if (clock.seconds - this.$missTime(attacker, victim) > 5) {
                /* More than 5 seconds since the last "friendly fire" miss. */
                this.$resetMissCounter(attacker, victim);
            }

            missCounter = this.$missCounter(attacker, victim);

            if (missCounter === -1) {
                /* Not an attacker or there are no victims. */
                return;
            }

            if (missCounter < 5) {
                /* We've only missed this ship less than 5 times. Assume ineptitude. */
                return;
            }
        }

        /* Everybody has had all the chances they are going to get once we have reached this point.
         * This section is only executed once. Hostiles are caught above after this.
         */

        /* Make the attacker a hostile for future checking. */
        this.$makeHostile(attacker, victim);

        piloted = (victim.isPiloted || victim.isStation);
        psInRange = (player.ship && player.ship.isValid &&
            victim.position.distanceTo(player.ship.position) < victim.scannerRange);

        if (victim.$pilotName) {
            /* Get the victims's name. */
            pilotName = victim.$pilotName;
        } else {
            /* Use displayName as the name of the victim. */
            pilotName = victim.name + ": " + victim.displayName;
        }

        if (attacker.isPlayer) {
            /* Remember the player, even if they jump system. */
            p_ships.mainScript.$playerVar.attacker = true;
            /* Clear the reputation of the player. */
            p_ships.mainScript.$playerVar.reputation[galaxyNumber] = 0;

            if (piloted) {
                /* Player hostile message. */
                player.consoleMessage(pilotName + ": " + expandDescription("[jaguar_company_player_hostile_fire]"));
            }
        } else if (piloted && psInRange) {
            /* Other ship hostile message. */
            player.consoleMessage(pilotName + ": " + expandDescription("[jaguar_company_hostile_fire]"));
        }

        /* Send back a custom AI message. */
        victim.reactToAIMessage("HOSTILE_FIRE");
    };

    /* NAME
     *   $shipIsBeingAttackedWithMissile
     *
     * FUNCTION
     *   The AI will automatically send an INCOMING_MISSILE message to this ship.
     *   Since we don't have to do anything fancy that would confuse the AI state system
     *   we don't have to send a special message like the general attack system below.
     *
     *   No such thing as "friendly fire" with a missile.
     *
     *   Not to be confused with the world script event function 'shipAttackedWithMissile',
     *   although it should be called from the ship script event function 'shipAttackedWithMissile'.
     *
     * INPUTS
     *   victim - caller ship
     *   attacker - entity of the attacker
     */
    this.$shipIsBeingAttackedWithMissile = function (victim, attacker) {
        var piloted,
        pilotName;

        if (!attacker || !attacker.isValid ||
            !victim || !victim.isValid ||
            victim.isDerelict) {
            /* The attacker is no longer valid
             * or the victim is no longer valid
             * or the victim is a derelict
             */
            return;
        }

        /* Setup the attacker and victim if needed and increase the attack counter. */
        this.$increaseAttackCounter(attacker, victim);
        /* Make the attacker a hostile for future checking. */
        this.$makeHostile(attacker, victim);

        piloted = (victim.isPiloted || victim.isStation);

        if (victim.$pilotName) {
            /* Get the victims's name. */
            pilotName = victim.$pilotName;
        } else {
            /* Use displayName as the name of the victim. */
            pilotName = victim.name + ": " + victim.displayName;
        }

        if (attacker.isPlayer) {
            /* Remember the player, even if they jump system. */
            p_ships.mainScript.$playerVar.attacker = true;
            /* Clear the reputation of the player. */
            p_ships.mainScript.$playerVar.reputation[galaxyNumber] = 0;

            if (piloted) {
                /* Player hostile message. */
                player.consoleMessage(pilotName + ": " + expandDescription("[jaguar_company_player_hostile_fire]"));
            }
        } else if (piloted && player.ship && player.ship.isValid &&
            victim.position.distanceTo(player.ship.position) < victim.scannerRange) {
            /* Other ship hostile message. */
            player.consoleMessage(pilotName + ": " + expandDescription("[jaguar_company_hostile_fire]"));
        }
    };

    /* NAME
     *   $shipTakingDamage
     *
     * FUNCTION
     *   Taking damage. Check attacker and what type.
     *
     *   Not to be confused with the world script event function 'shipTakingDamage',
     *   although it should be called from the ship script event function 'shipTakingDamage'.
     *
     * INPUTS
     *   victim - entity that is being damaged
     *   amount - amount of damage
     *   attacker - entity that caused the damage
     *   type - type of damage as a string
     */
    this.$shipTakingDamage = function (victim, amount, attacker, type) {
        if (attacker && attacker.isValid && attacker.isShip && type === "scrape damage") {
            /* Make sure it is a ship dealing scrape damage. */
            if (this.$friendList.indexOf(attacker.entityPersonality) !== -1) {
                /* Cancel damage from collision with Jaguar Company ships. */
                victim.energy += amount;
                /* Target the ship we are colliding with. */
                victim.target = attacker;

                if (victim.AI === "jaguar_company_interceptAI.plist") {
                    /* Force an exit of the intercept AI. */
                    victim.lightsActive = false;
                    victim.exitAI();
                }

                /* Move away from the ship we are colliding with. */
                victim.reactToAIMessage("JAGUAR_COMPANY_TAKING_DAMAGE");
            }
        }
    };

    /* Internal AI functions. Called by ship script. */

    /* NAME
     *   $performAttackTarget
     *
     * FUNCTION
     *   This does something similar to a mix between the deployEscorts and groupAttackTarget AI commands.
     *
     * INPUT
     *   callerShip - entity of the caller ship
     */
    this.$performAttackTarget = function (callerShip) {
        var target = callerShip.target,
        otherShips,
        idleShips = [],
        idleShip,
        counter,
        length;

        if (this.$friendList.indexOf(callerShip.entityPersonality) === -1) {
            /* Caller ship is not a friend of Jaguar Company. */
            return;
        }

        if (target === null) {
            /* Return immediately if we have no target. */
            return;
        }

        if (this.$friendList.indexOf(target.entityPersonality) !== -1 || !this.$isHostile(target)) {
            /* Clear the target and return for one of the following 2 states...
             * 1. Target is a friend.
             * 2. Target is not a hostile.
             */
            callerShip.target = null;

            return;
        }

        /* Force attacker to hostile status. */
        this.$makeHostile(target, callerShip);
        /* React to our own attack call. */
        callerShip.reactToAIMessage("JAGUAR_COMPANY_ATTACK_TARGET");

        /* NAME
         *   $identifyFriends
         *
         * FUNCTION
         *   Stop warnings about anonymous local functions within loops.
         *   Used by 'system.filteredEntities'. Returns true for any friend of the caller ship.
         *
         * INPUT
         *   entity - entity to check
         */
        function $identifyFriends(entity) {
            if (!entity.isValid || entity.isCloaked || entity.isDerelict) {
                /* Ignore all entities that have one of these conditions:
                 * 1) not valid
                 * 2) cloaked
                 * 3) is a derelict
                 */
                return false;
            }

            /* Is a friend of the caller ship. */
            return (this.$friendList.indexOf(entity.entityPersonality) !== -1);
        }

        /* Limit range of check to scanner range of caller ship. */
        otherShips = system.filteredEntities(this, $identifyFriends, callerShip, callerShip.scannerRange);

        if (!otherShips.length) {
            /* Return immediately if we are on our own. */
            return;
        }

        /* Cache the length. */
        length = otherShips.length;

        for (counter = 0; counter < length; counter += 1) {
            if (!otherShips[counter].hasHostileTarget) {
                /* Other ship not in attack mode. Put it on the idle list. */
                idleShips.push(otherShips[counter]);
            }
        }

        if (!idleShips.length) {
            /* Return immediately if there are no idle ships. */
            return;
        }

        /* Get a random number of idle ships to deploy. */
        length = Math.ceil(Math.random() * idleShips.length);

        for (counter = 0; counter < length; counter += 1) {
            idleShip = idleShips[counter];

            /* The idle ship is not currently in attack mode. Give it a target. */
            idleShip.target = target;
            idleShip.reactToAIMessage("JAGUAR_COMPANY_ATTACK_TARGET");
        }
    };

    /* NAME
     *   $scanForAttackers
     *
     * FUNCTION
     *   Scan for ships from the past that have attacked the caller ship.
     *   Also scan for potential attackers.
     *
     * INPUT
     *   callerShip - entity of the caller ship
     */
    this.$scanForAttackers = function (callerShip) {
        var target = null,
        attackersWithinRange,
        pilotName,
        counter,
        length;

        if (this.$friendList.indexOf(callerShip.entityPersonality) === -1) {
            /* Caller ship is not a friend of Jaguar Company. */
            return;
        }

        /* NAME
         *   $identifyAttacker
         *
         * FUNCTION
         *   Stop warnings about anonymous local functions within loops.
         *   Used by 'system.filteredEntities'. Returns true for attackers or potential attackers.
         *
         * INPUT
         *   entity - entity to check
         */
        function $identifyAttacker(entity) {
            if (!entity.isValid || entity.isCloaked || entity.isDerelict) {
                /* Ignore all entities that have one of these conditions:
                 * 1) not valid
                 * 2) cloaked
                 * 3) is a derelict
                 */
                return false;
            }

            if (this.$isHostile(entity)) {
                /* The entity is a previous hostile for the caller ship. */
                return true;
            }

            if (entity.isPlayer && p_ships.mainScript.$playerVar.attacker) {
                /* Player has attacked us in the past. */
                return true;
            }

            if (p_ships.mainScript.$jaguarCompanyBase && p_ships.mainScript.$jaguarCompanyBase.isValid &&
                entity.position.distanceTo(p_ships.mainScript.$jaguarCompanyBase.position) < 30000) {
                /* All ships not identified as hostile so far are safe within 30km of the base. */
                return false;
            }

            if (entity.isPirate) {
                /* The entity is a pirate. */
                return true;
            }

            if (entity.bounty > 20 || Math.random() < ((entity.bounty - 10) / 40)) {
                /* Entity has a bounty greater than 20Cr.
                 * o Entities with a low bounty (minimum 11Cr) have a very small chance of being picked on.
                 *   o Bounty of 11Cr: 1 in 40 chance.
                 *   o Bounty of 20Cr: 1 in 4 chance.
                 */
                return true;
            }

            /* Everything else is ignored. */
            return false;
        }

        /* Find past attackers and potential attackers within range of the caller ship. */
        attackersWithinRange = system.filteredEntities(this, $identifyAttacker, callerShip, callerShip.scannerRange);

        if (!attackersWithinRange.length) {
            /* No attackers. */
            callerShip.reactToAIMessage("ATTACKERS_NOT_FOUND");
        } else {
            /* Cache the length. */
            length = attackersWithinRange.length;

            for (counter = 0; counter < length; counter += 1) {
                /* Force all attackers within range to hostile status. */
                this.$makeHostile(attackersWithinRange[counter], callerShip);
            }

            /* Set target to the closest attacker. */
            target = attackersWithinRange[0];

            if ((callerShip.isPiloted || callerShip.isStation) && Math.random() > p_ships.messageProbability) {
                if (callerShip.$pilotName) {
                    /* Get the callerShip's name. */
                    pilotName = callerShip.$pilotName;
                } else {
                    /* Use displayName as the name of the callerShip. */
                    pilotName = callerShip.name + ": " + callerShip.displayName;
                }

                /* Show hostile message. */
                if (target.isPlayer) {
                    /* Player hostile message. */
                    player.consoleMessage(pilotName + ": " + expandDescription("[jaguar_company_player_hostile_fire]"));
                } else {
                    if (player.ship && player.ship.isValid &&
                        callerShip.position.distanceTo(player.ship.position) < callerShip.scannerRange) {
                        /* Other ship hostile message. */
                        player.consoleMessage(pilotName + ": " + expandDescription("[jaguar_company_hostile_fire]"));
                    }
                }
            }

            /* Set the target. */
            callerShip.target = target;
            callerShip.reactToAIMessage("ATTACKERS_FOUND");
        }
    };
}.bind(this)());
Scripts/jaguar_company_tracker.js
/*jslint bitwise: true, es5: true, newcap: true, nomen: true, regexp: true, unparam: true, todo: true, white: true,
indent: 4, maxerr: 50, maxlen: 120 */
/*jshint boss: true, curly: true, eqeqeq: true, eqnull: true, es5: true, evil: true, forin: true, laxbreak: true,
loopfunc: true, noarg: true, noempty: true, strict: true, nonew: true, undef: true */
/*global Timer, addFrameCallback, isValidFrameCallback, log, player, removeFrameCallback, system, worldScripts */

/* Jaguar Company Tracker
 *
 * Copyright © 2012-2013 Richard Thomas Harrison (Tricky)
 *
 * This work is licensed under the Creative Commons
 * Attribution-Noncommercial-Share Alike 3.0 Unported License.
 *
 * To view a copy of this license, visit
 * http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter
 * to Creative Commons, 171 Second Street, Suite 300, San Francisco,
 * California, 94105, USA.
 *
 * Ship/Effect related functions for the patrol tracker.
 */

(function () {
    "use strict";

    /* Standard public variables for OXP scripts. */
    this.name = "jaguar_company_tracker.js";
    this.author = "Tricky";
    this.copyright = "© 2012-2013 Richard Thomas Harrison (Tricky)";
    this.license = "CC BY-NC-SA 3.0";
    this.description = "Ship script for the Jaguar Company Tracker.";
    this.version = "1.2";

    /* Private variable. */
    var p_tracker = {};

    /* Ship script event handlers. */

    /* NAME
     *   shipSpawned
     *
     * FUNCTION
     *   Initialise various variables on ship birth. Oolite v1.76.1 and older.
     */
    this.shipSpawned = function () {
        /* Common setup. */
        this.$setUp();
        /* Use a frame callback to keep the position constant. */
        this.$trackerFCBReference = addFrameCallback(this.$invisibleTrackerFCB.bind(this));

        /* No longer needed after setting up. */
        delete this.shipSpawned;
        delete this.effectSpawned;
        delete this.$visualTrackerFCB;
    };

    /* NAME
     *   effectSpawned
     *
     * FUNCTION
     *   Initialise various variables on effect birth. Oolite v1.77 and newer.
     */
    this.effectSpawned = function () {
        /* Common setup. */
        this.$setUp();
        /* Use a frame callback to keep the position constant. */
        this.$trackerFCBReference = addFrameCallback(this.$visualTrackerFCB.bind(this));

        /* No longer needed after setting up. */
        delete this.shipSpawned;
        delete this.effectSpawned;
        delete this.$invisibleTrackerFCB;
    };

    /* NAME
     *   shipDied
     *
     * FUNCTION
     *   Patrol tracker was destroyed.
     *
     *   Not triggered for Oolite v1.77 and newer visual effects.
     *
     * INPUTS
     *   whom - entity that caused the death
     *   why - cause as a string
     */
    this.shipDied = function (whom, why) {
        var destroyedBy = whom,
        tracker = this.ship,
        patrolShips;

        if (whom && whom.isValid) {
            destroyedBy = "ship#" + whom.entityPersonality + " (" + whom.displayName + ")";
            patrolShips = system.shipsWithPrimaryRole("jaguar_company_patrol");

            if (patrolShips.length > 0) {
                /* Patrol still around. Re-spawn. */
                worldScripts["Jaguar Company"].$tracker = patrolShips[0].spawnOne("jaguar_company_tracker");
            }
        }

        if (p_tracker.logging && p_tracker.logExtra) {
            log(this.name, "shipDied::" +
                "ship#" + tracker.entityPersonality + " (" + tracker.displayName + ")" +
                " was destroyed by " + destroyedBy +
                ", reason: " + why);
        }
    };

    /* NAME
     *   shipRemoved, effectRemoved, entityDestroyed and $removeTrackerRefs
     *
     * FUNCTION
     *   The patrol tracker has just become invalid or was removed.
     */
    this.shipRemoved = this.effectRemoved = this.entityDestroyed = this.$removeTrackerRefs = function () {
        /* Stop and remove the timer. */
        if (this.$trackerTimerReference) {
            if (this.$trackerTimerReference.isRunning) {
                this.$trackerTimerReference.stop();
            }

            this.$trackerTimerReference = null;
        }

        /* Stop and remove the frame callback. */
        if (this.$trackerFCBReference) {
            if (isValidFrameCallback(this.$trackerFCBReference)) {
                removeFrameCallback(this.$trackerFCBReference);
            }

            this.$trackerFCBReference = null;
        }
    };

    /* Other global functions. */

    /* NAME
     *   $setUp
     *
     * FUNCTION
     *   Setup the private main variable + some public variables.
     */
    this.$setUp = function () {
        /* Initialise the p_tracker variable object.
         * Encapsulates all private global data.
         */
        p_tracker = {
            /* Cache the main world script. */
            mainScript : worldScripts["Jaguar Company"],
            /* Local copies of the logging variables. */
            logging : worldScripts["Jaguar Company"].$logging,
            logExtra : worldScripts["Jaguar Company"].$logExtra,
            /* Updated by the timer. */
            closestPatrolShip : null,
            /* Material used by the visual effect. */
            material : "none"
        };

        /* Track the patrol ships every 0.25 seconds. */
        this.$trackerTimerReference = new Timer(this, this.$trackerTimer, 0.25, 0.25);
    };

    /* NAME
     *   $trackerTimer
     *
     * FUNCTION
     *   Tracker timer. Updates the closest patrol ship position.
     *
     *   Called every 0.25 seconds.
     */
    this.$trackerTimer = function () {
        var tracker = this.ship || this.visualEffect,
        playerShip,
        patrolShips;

        if (!tracker || !tracker.isValid) {
            /* Tracker no longer valid. */
            this.$removeTrackerRefs();

            if (p_tracker.logging && p_tracker.logExtra) {
                log(this.name, "$trackerTimer::Tracker not valid");
            }

            return;
        }

        /* Player ship object. */
        playerShip = player.ship;

        if (!playerShip || !playerShip.isValid) {
            /* If the player has died, reset the tracker. */
            p_tracker.mainScript.$blackboxASCReset(false);
            p_tracker.mainScript.$blackboxHoloReset(false);

            return;
        }

        /* Search for the patrol ships. Sort by distance from the player. */
        patrolShips = system.shipsWithPrimaryRole("jaguar_company_patrol", playerShip);

        if (!patrolShips.length) {
            /* We are on our own. Deactivate the black box. */
            p_tracker.mainScript.$blackboxASCReset(true);
            p_tracker.mainScript.$blackboxHoloReset(true);

            if (p_tracker.logging && p_tracker.logExtra) {
                log(this.name, "$trackerTimer::Tracker removed - no patrol ships");
            }

            return;
        }

        /* Update the closest patrol ship reference. */
        p_tracker.closestPatrolShip = patrolShips[0];
    };

    /* NAME
     *   $invisibleTrackerFCB
     *
     * FUNCTION
     *   Tracker frame callback.
     *
     *   Used by Oolite v1.76.1 and older.
     *
     * INPUT
     *   delta - amount of game clock time past since the last frame
     */
    this.$invisibleTrackerFCB = function (delta) {
        var tracker = this.ship,
        closestPatrolShip = p_tracker.closestPatrolShip,
        playerShip,
        distance;

        if (!tracker || !tracker.isValid) {
            /* Tracker can be invalid for 1 frame. */
            this.$removeTrackerRefs();

            return;
        }

        if (delta === 0.0 || !closestPatrolShip || !closestPatrolShip.isValid) {
            /* Do nothing if paused or the position of the closest patrol ship has not been setup. */
            return;
        }

        /* Player ship object. */
        playerShip = player.ship;

        if (!playerShip || !playerShip.isValid) {
            /* If the player has died, reset the tracker. */
            p_tracker.mainScript.$blackboxASCReset(false);

            return;
        }

        /* Distance above the closest patrol ship. */
        distance = 5 + closestPatrolShip.collisionRadius;
        /* Keep the tracker above the closest patrol ship. */
        tracker.position = closestPatrolShip.position.add(closestPatrolShip.orientation.vectorUp().multiply(distance));
    };

    /* NAME
     *   $visualTrackerFCB
     *
     * FUNCTION
     *   Tracker frame callback.
     *
     *   Used by Oolite v1.77 and newer for visual effects.
     *
     * INPUT
     *   delta - amount of game clock time past since the last frame
     */
    this.$visualTrackerFCB = function (delta) {
        var tracker = this.visualEffect,
        closestPatrolShip = p_tracker.closestPatrolShip,
        playerShip,
        distance,
        vector,
        angle,
        cross;

        if (!tracker || !tracker.isValid) {
            /* Tracker can be invalid for 1 frame. */
            this.$removeTrackerRefs();

            return;
        }

        if (delta === 0.0 || !closestPatrolShip || !closestPatrolShip.isValid) {
            /* Do nothing if paused or the closest patrol ship has not been setup. */
            return;
        }

        /* Player ship object. */
        playerShip = player.ship;

        if (!playerShip || !playerShip.isValid) {
            /* If the player has died, reset the tracker. */
            p_tracker.mainScript.$blackboxHoloReset(false);

            return;
        }

        if (playerShip.viewDirection !== "VIEW_FORWARD" && p_tracker.material !== "off") {
            p_tracker.material = "off";
            /* Make the tracker small. */
            tracker.scale(0.001);
            /* Move the tracker so it can't be seen. Centre of the player ship should do it. */
            tracker.position = playerShip.position;
        } else if (playerShip.viewDirection === "VIEW_FORWARD") {
            /* Scale the tracker to it's original size. */
            tracker.scale(1.0);
            /* Vector pointing towards the target. */
            vector = closestPatrolShip.position.subtract(playerShip.position).direction();

            if (vector.dot(playerShip.heading) >= 0 && p_tracker.material !== "green") {
                p_tracker.material = "green";
                /* Change the tracker colour to be green. */
                tracker.setMaterials({
                    "jaguar_company_tracker" : {
                        diffuse_color : ["0", "0.667", "0", "1"],
                        diffuse_map : "jaguar_company_tracker_diffuse.png",
                        emission_color : ["0", "0.05", "0", "1"],
                        shininess : "5",
                        specular_color : ["0", "0.2", "0", "1"]
                    }
                });
            } else if (vector.dot(playerShip.heading) < 0 && p_tracker.material !== "red") {
                p_tracker.material = "red";
                /* Change the tracker colour to be red. */
                tracker.setMaterials({
                    "jaguar_company_tracker" : {
                        diffuse_color : ["0.667", "0", "0", "1"],
                        diffuse_map : "jaguar_company_tracker_diffuse.png",
                        emission_color : ["0.05", "0", "0", "1"],
                        shininess : "5",
                        specular_color : ["0.2", "0", "0", "1"]
                    }
                });
            }

            /* Distance in front of the player. */
            distance = 100 + playerShip.collisionRadius;
            /* Keep the tracker in front of the player. */
            tracker.position = playerShip.position.add(playerShip.heading.multiply(distance));
            /* Angle to the target from current heading. */
            angle = playerShip.heading.angleTo(vector);
            /* Cross vector for rotate. */
            cross = playerShip.heading.cross(vector).direction();
            /* Rotate the tracker by the angle. */
            tracker.orientation = playerShip.orientation.rotate(cross, -angle);
        }
    };
}.bind(this)());
Scripts/jaguar_company_tug.js
/*jslint bitwise: true, es5: true, newcap: true, nomen: true, regexp: true, unparam: true, todo: true, white: true,
indent: 4, maxerr: 50, maxlen: 120 */
/*jshint boss: true, curly: true, eqeqeq: true, eqnull: true, es5: true, evil: true, forin: true, laxbreak: true,
loopfunc: true, noarg: true, noempty: true, strict: true, nonew: true, undef: true */
/*global Vector3D, expandDescription, galaxyNumber, worldScripts */

/* Jaguar Company Tug
 *
 * Copyright © 2012-2013 Richard Thomas Harrison (Tricky)
 *
 * This work is licensed under the Creative Commons
 * Attribution-Noncommercial-Share Alike 3.0 Unported License.
 *
 * To view a copy of this license, visit
 * http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter
 * to Creative Commons, 171 Second Street, Suite 300, San Francisco,
 * California, 94105, USA.
 *
 * Ship related functions for the tug.
 */

(function () {
    "use strict";

    /* Standard public variables for OXP scripts. */
    this.name = "jaguar_company_tug.js";
    this.author = "Tricky";
    this.copyright = "© 2012-2013 Richard Thomas Harrison (Tricky)";
    this.license = "CC BY-NC-SA 3.0";
    this.description = "Ship script for the Jaguar Company Tug.";
    this.version = "1.2";

    /* Private variable. */
    var p_tug = {};

    /* Ship script event handlers. */

    /* NAME
     *   shipSpawned
     *
     * FUNCTION
     *   Initialise various variables on ship birth.
     */
    this.shipSpawned = function () {
        var base;

        /* Initialise the p_tug variable object.
         * Encapsulates all private global data.
         */
        p_tug = {
            /* Cache the world scripts. */
            mainScript : worldScripts["Jaguar Company"],
            shipsScript : worldScripts["Jaguar Company Ships"],
            /* Local copies of the logging variables. */
            logging : worldScripts["Jaguar Company"].$logging,
            logExtra : worldScripts["Jaguar Company"].$logExtra,
            /* Local copy of the friendList array. */
            friendList : worldScripts["Jaguar Company Ships"].$friendList
        };

        /* Register this ship as a friendly. */
        p_tug.shipsScript.$addFriendly({
            ship : this.ship,
            /* Random name for the pilot. Used when talking about attacks and sending a report to Snoopers. */
            pilotName : expandDescription("%N [nom1]"),
            /* Get a unique name for the patrol ship. */
            shipName : p_tug.mainScript.$uniqueShipName()
        });

        base = p_tug.mainScript.$jaguarCompanyBase;

        if (base && base.isValid) {
            /* Update the base script tug references. */
            base.script.$tugOK = false;
            base.script.$tug = this.ship;
        }

        /* No longer needed after setting up. */
        delete this.shipSpawned;
    };

    /* NAME
     *   shipRemoved
     *
     * FUNCTION
     *   Tug was removed by script.
     *
     * INPUT
     *   suppressDeathEvent - boolean
     *     true - shipDied() will not be called
     *     false - shipDied() will be called
     */
    this.shipRemoved = function (suppressDeathEvent) {
        var base;

        if (suppressDeathEvent) {
            return;
        }

        base = worldScripts["Jaguar Company"].$jaguarCompanyBase;

        if (base && base.isValid) {
            /* Reset the script check. */
            base.script.$buoyOK = false;

            if (!base.script.$buoy || !base.script.$buoy.isValid) {
                /* Not released the buoy yet, reset the launch status of the buoy. */
                base.script.$buoyLaunched = false;
            }
        }
    };

    /* NAME
     *   entityDestroyed
     *
     * FUNCTION
     *   The tug has just become invalid.
     */
    this.entityDestroyed = function () {
        var base = worldScripts["Jaguar Company"].$jaguarCompanyBase;

        if (base && base.isValid) {
            /* Reset the script check. */
            base.script.$buoyOK = false;

            if (!base.script.$buoy || !base.script.$buoy.isValid) {
                /* Not released the buoy yet, reset the launch status of the buoy. */
                base.script.$buoyLaunched = false;
            }
        }
    };

    /* Other global public functions. */

    /* AI functions. */

    /* NAME
     *   $setCoordsToJaguarCompanyBuoy
     *
     * FUNCTION
     *   Set the co-ordinates to the surface of the buoy or the base.
     */
    this.$setCoordsToJaguarCompanyBuoy = function () {
        var base = p_tug.mainScript.$jaguarCompanyBase;

        if (!base || !base.isValid) {
            /* If the base has gone, just go to the nearest station. */
            this.ship.reactToAIMessage("JAGUAR_COMPANY_BASE_NOT_FOUND");
        } else {
            if (base.script.$buoy && base.script.$buoy.isValid) {
                /* Set the coords to the buoy. */
                this.$setCoordsToEntity(base.script.$buoy);
                this.ship.reactToAIMessage("JAGUAR_COMPANY_BUOY_FOUND");
            } else {
                /* Set the coords to the base. */
                this.$setCoordsToEntity(base);
                this.ship.reactToAIMessage("JAGUAR_COMPANY_BASE_FOUND");
            }
        }
    };

    /* NAME
     *   $setCoordsForBuoyDropOff
     *
     * FUNCTION
     *   Set the co-ordinates for the buoy drop-off position.
     */
    this.$setCoordsForBuoyDropOff = function () {
        var base = p_tug.mainScript.$jaguarCompanyBase,
        distance = 10000;

        if (!base || !base.isValid) {
            /* If it has gone, just go to the nearest station. */
            this.ship.reactToAIMessage("JAGUAR_COMPANY_BUOY_DROP_OFF_NOT_FOUND");

            return;
        }

        /* Calculate the base surface to buoy centre distance, not centre to centre. */
        distance += base.collisionRadius;
        /* Add on desired range. */
        distance += 20;

        /* Set the ending position for the tug in front of the base. */
        this.ship.savedCoordinates = base.position.add(base.heading.multiply(distance));
        this.ship.reactToAIMessage("JAGUAR_COMPANY_BUOY_DROP_OFF_FOUND");
    };

    /* NAME
     *   $releaseBuoy
     *
     * FUNCTION
     *   Release the buoy by removing the sub-entity and replacing with a real buoy.
     */
    this.$releaseBuoy = function () {
        var tug = this.ship,
        subEntities = tug.subEntities,
        base = p_tug.mainScript.$jaguarCompanyBase,
        buoyPosition,
        buoyRole,
        buoy;

        /* We make the assumption that the buoy is the 1st sub-entity. */
        if (!subEntities.length || !subEntities[0].hasRole("jaguar_company_base_buoy_subent")) {
            /* The buoy isn't there??? */
            return;
        }

        /* Calculate the real-world position for the buoy. */
        buoyPosition = tug.position.add(subEntities[0].position.rotateBy(tug.orientation));
        /* Remove the buoy sub-entity quietly: don't trigger 'shipDied' in the ship script. */
        subEntities[0].remove(true);

        if (p_tug.mainScript.$playerVar.reputation[galaxyNumber] < p_tug.mainScript.$reputationHelper) {
            /* No beacon. Scanner colour is solid white. */
            buoyRole = "jaguar_company_base_buoy_no_beacon";
        } else {
            /* Beacon. Standard scanner colour for a buoy. */
            buoyRole = "jaguar_company_base_buoy_beacon";
        }

        /* Create the real buoy and add it to the system. */
        buoy = tug.spawnOne(buoyRole);
        buoy.position = buoyPosition;
        /* Keep the original orientation. */
        buoy.orientation = tug.orientation;

        if (base && base.isValid) {
            /* Update the base script buoy reference. */
            base.script.$buoy = buoy;
        }

        /* Stop the kick in velocity from spawning the buoy and it colliding with the tug.
         * In effect this will put the tug into reverse.
         */
        tug.velocity = new Vector3D(0, 0, 0).subtract(tug.vectorForward.multiply(tug.maxSpeed));
    };
}.bind(this)());