Back to Index Page generated: May 8, 2024, 6:16:03 AM

Expansion Ship Repurchase

Content

Warnings

  1. Information URL mismatch between OXP Manifest and Expansion Manager string length at character position 0

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Implements a 'repurchase' methodology when the player ship is destroyed. Implements a 'repurchase' methodology when the player ship is destroyed.
Identifier oolite.oxp.phkb.ShipRepurchase oolite.oxp.phkb.ShipRepurchase
Title Ship Repurchase Ship Repurchase
Category Mechanics Mechanics
Author phkb phkb
Version 0.6.2 0.6.2
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
  • oolite.oxp.CaptMurphy.ShipStorageHelper:0.37
  • oolite.oxp.CaptMurphy.ShipStorageHelper:0.37
  • Optional Expansions
    Conflict Expansions
    Information URL n/a
    Download URL https://wiki.alioth.net/img_auth.php/e/ef/ShipRepurchase.oxz n/a
    License CC-BY-NC-SA 4.0 CC-BY-NC-SA 4.0
    File Size n/a
    Upload date 1657067260

    Documentation

    Also read http://wiki.alioth.net/index.php/Ship%20Repurchase

    readme.txt

    Ship Repurchase
    By Nick Rogers
    
    Overview
    ========
    This OXP aims to make the experience of using an escape pod a bit more interesting and realistic. The first change is that the player will be auto-ejected from their ship just before it is destroyed, in all cases, even if they don't have an escape pod. If the player has not fitted an escape pod, they will eject from their ship in a life-support suit only, which provides a means of keeping the pilot in stasis until they can be recovered. If an escape pod is fitted, it will auto-eject the player when it is clear the ship can no longer support life. Players can also choose when they eject if they have an escape pod, rather than waiting for the auto-eject process to kick in. Also, any parcels or passengers currently on board, and any gold, platinum or gem-stones will not fit into the life-support suit, and so will be lost with the rest of the cargo and ship. If an escape pod is fitted, these items will be preserved.
    
    Once the player has been recovered, they then need to decide what to do about replacing their ship. They will be given (at most) three options: 
       (1) to replace their previous ship plus all the equipment on it; 
       (2) to just replace their existing ship, leaving out any equipment; or 
       (3) choosing a lower cost ship as a replacement.
    
    For options (1) and (2) there is a cost involved: 10% of the original cost. If, however, you have purchased an escape pod, there is an insurance component to the purchase that reduces the amount to be only 5% of the original cost. A more expensive escape pod option, named "Escape Pod Plus", will reduce the amount further, to 2.5% of the original cost. 
    
    Option (3) is always free. Potentially two ships will be offered, depending on the base value of your current ship. The following table outlines the ships that will be offered (Offer 1 ships are generally trader-type ships, while Offer 2 ships are generally hunter-type ships):
    
        Current ship
        Base Value      Offer 1             Offer 2
        -------------   ------------------  ---------------
        650001+         Anaconda            Fer-de-Lance
        495001-650000   Boa Class Cruiser   Fer-de-Lance
        485001-495000   Boa                 Fer-de-Lance
        450001-485000   Boa                 Asp Mark II
        375001-450000   Python              Asp Mark II
        200001-375000   Python              Cobra Mark III
        150001-200000   Cobra Mark III
        145001-150000   Moray Medical Boat  Moray Star Boat
        125001-145000   Moray Star Boat
        100001-125000   Cobra Mark I
        0-100000        Adder
    
    If a full ship plus equipment replacement is chosen, the ship will arrive in a fully working condition. That is, any equipment that was damaged at the time of destruction will be in a working order on the replacement(*), and the ship will not be in need of an overhaul. 
    
    If the player has insufficient funds to purchase either the full replacement, or the stock replacement ship, they will be forced to accept the lower value ship as their free ship to continue their galactic journey.
    
    Note that, if you have passengers on board, choosing option (2) or (3) will result in those passengers being abandoned at the station and the contracts terminated.
    
    (*) Note: The one exception to this rule is the cloaking device, which, because of the nature of the device and how it came to be in your posession, constitutes a real problem for insurance repair coverage. However, if you are rescued at a station that has a suitably high tech level, the device will be repaired.
    
    Frequent Ejecting Penalty
    =========================
    In order to curb misuse (or instance, ejecting from a badly damaged ship to avoid repair bills), players who eject frequently will suffer a repurchase penalty. The calculation is (1 + ((monthly_eject_count - 1) x 2) / 10) x original_percentage. So, ejecting 2 times in a month will increase the percentage to 12%. Ejecting 3 times in a month will increase the percentages to 14%. If you were to eject 16 times in a month, that would change the 10% of original cost of the ship to be 40%.
    
    As a player progresses through the game, the length of time used for picking up ejecting events in the calculation will increase. Players with less than 32 kills will stay at the 30 day mark. After 32 kills, the period increases to 60 days (2 months). At 64 kills, the period is 90 days (3 months); at 128 kills, the period is 4 months; at 512 kills the period is 6 months; at 2560 kills, the period is 9 months; and at 6400+ kills, the period is 1 year.
    
    *News Flash*: GalCop intervenes in the Insurance sector, ordering the suspension of any penalty for frequency of ejecting. The Head of Pilot Relations noted: "The life as a pilot in the galaxy is already hard. To penalise the majority of pilots based on the actions of a few cases is unfair." Given the lack of any formal notification by the insurance sector, speculation has arisen that GalCop is funding the insurance premiums through other means.
    
    Insurance
    =========
    Due to the high number of total-loss payments insurance providers have been forced to make over the last decade, no-cost replacement insurance is no longer a complimentary part of an escape pod purchase. Instead, as noted above, any escape pod purchase will instead reduce the cost of any repurchase event. 
    
    However, many clients still find even the reduced costs difficult to bear. So insurance providers have introduced "Ship Repurchase Insurance", which covers the holder of the policy for a maximum of 60 days from date of purchase, and will reduce the cost of any ship plus equipment replacement to just 1000cr. A ship-only replacement will cost just 500cr. This insurance can be purchased at any GalCop-aligned station in all systems. The insurance is a once-only policy - if the policy is invoked, the terms state that the policy is then terminated.
    
    New Jamesons
    ============
    Insurance providers understand that new pilots need more protection than experienced ones. To help new pilots on their way, these payments will only be required for pilots who have attained an Elite rank of "Poor". If a "Harmless" or "Mostly Harmless" pilot ejects, the cost of a full replacement ship will be greatly reduced: 200cr for a full ship and equipment replacement, 100cr for a ship-only replacement.
    
    Interstellar space
    ==================
    Ejecting in interstellar space is generally fatal - there is usually no one close enough to your location to render assistance, and the limited fuel on the escape pod is insufficient to enable a return to normal space. 
    
    However, occasionally there are rescue options even in interstellar space. If one of these options is close by (like a naval station or carrier), there is a chance a rescue will take place. However, insurance operators cannot provide replacement ships in this situation. Instead, the player will be given a used ship and will have to make their way back to normal space in this. When they dock at a GalCop station they will at that time be presented with options for ship replacement.
    
    History
    =======
    The standard escape pod in the core game operates in a strange way. While the description indicates there is some sort of insurance policy involved in purchasing the pod, if you eject with a ship full of damaged equipment, you will be given a replacement ship with the same damaged equipment in it. This reflects what happened in the 8-bit versions of Elite, and was likely a result of limited resources to do anything more complex.
    
    Required OXP's
    ==============
    This OXP uses the "Ship Storage Helper" OXP to store and recover the players ship.
    
    Compatibility
    =============
    This OXP should be fully compatible with the "Auto-Eject" OXP by Commander McLane. The "Auto-Eject" equipment will eject the player ahead of the auto-eject function of this pack, and the repurchasing process will apply in both situations.
    
    It should also be fully compatible with the "Interstellar Tweaks" OXP by UK Eliter. When ejecting in interstellar space, the recovery options of this OXP will ceed control to Interstellar Tweaks, allowing it to choose the destination. The repurchasing process will apply after recovery.
    
    License
    =======
    This work is licensed under the Creative Commons Attribution-Noncommercial-Share Alike 4.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/
    
    Rocket image from http://simpleicon.com/rocket.html
    Shield image from https://www.iconfinder.com/icons/175318/shield_icon#size=512
    
    Version History
    ===============
    0.6.2
    - Preventing recovery from a Generation ship.
    
    0.6.1
    - Increased required version level for Ship Storage Helper.
    
    0.6
    - Adjusted the method for determining when the ship is about to die, to allow for other OXP's to potentially change the player ship energy before ejecting the player.
    - Changed insurance premium coverage period to 60 days.
    - If insurance lapses while in transit, it will still be valid at next dock if the player needs to use it.
    - Reduced cost of Escape Pod Plus to 3000 credits, and set techlevel requirement to same as for a normal escape pod.
    - If the Escape Pod Plus is installed, rescue time will be reduced to less than 12 hours. Also, 50% of cargo will be recovered.
    - Cost of Repurchase Insurance reduced to 2500 credits.
    - Removed the frequent ejection penalty calculation (although it could be returned based on feedback).
    - Raised minimum Oolite version to 1.89 (now utilising equipment-overrides.plist file and new escape pod rescue time property).
    
    0.5
    - Better handling of multiple damage events taking place at the same time.
    
    0.4
    - Better integration with Ship Configuration OXP armour settings.
    - Better integration with Battle Damage OXP.
    - Code refactoring.
    
    0.3
    - Added integration with Email System, so contract termination emails are sent if the player loses parcel or passenger contracts due to using the life-support suit when ejecting.
    
    0.2
    - Now considering player score when determining how far back to include ejecting events in the frequent ejecting penalty calculation.
    - Free ship offer can be one of 2 types: a trader ship, or a hunter/assassin ship.
    - Fixed issue where equipment items that can be multiple were not all being repaired after an eject event.
    - Enforced consistency with Cobra Mark III and Mark I base prices.
    - Fixed issue where interstellar space mode was being activated incorrectly.
    
    0.1
    - Initial version.
    

    Equipment

    Name Visible Cost [deci-credits] Tech-Level
    Escape Pod Plus yes 30000 7+
    Ship Repurchase Insurance yes 25000 1+

    Ships

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

    Models

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

    Scripts

    Path
    Config/script.js
    "use strict";
    this.name = "ShipRepurchase";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Implements a ship repurchase process for when the player's ship is destroyed.";
    this.licence = "CC BY-NC-SA 4.0";
    
    this._debug = true;
    this._doRepurchase = false;
    this._repurchasePercent = 0.1;
    this._baseShipCost = 0;
    this._fullShipCost = 0;
    this._jamesonPoint = 16;
    this._portable = [];
    this._roles = [];
    this._lastEjectDates = [];
    this._ejectDate = 0;
    this._simulator = false;
    this._insuranceDate = 0;
    this._scTimer = null;
    this._cargoRecovery = [];
    this._cargoRecoverySummary = "";
    this._applyPenalty = false;
    this._interstellarRecovery = false;
    this._interstellarMode = false;
    this._interstellarFreeShip = {
        key: "cobramk1-player",
        name: "Cobra Mark I"
    };
    this._storedShip = "";
    
    // dictionary of equipment key/min techlevel items to apply as an override when auto-repairing equipment
    // additional equipment items could be added here for further restrictions on auto-repair
    this._autoRepairMinTL = {
        "EQ_CLOAKING_DEVICE": 14,
        "EQ_MILITARY_JAMMER": 99,
        "EQ_MILITARY_SCANNER_FILTER": 99
    };
    // used to calculate how far back the premium calculation will look for repurchase events. based on player.score
    this._periodLevels = {
        "0": 30,
        "8": 30,
        "16": 30,
        "32": 60,
        "64": 90,
        "128": 120,
        "512": 180,
        "2560": 270,
        "6400": 365
    };
    
    // dictionary of min values and key/name dictionary
    // used to determine which free ship is available based on the original cost of the players current ship
    this._freeShipTrader = {
        "650000": {
            key: "anaconda-player",
            name: "Anaconda"
        },
        "495000": {
            key: "boa-mk2-player",
            name: "Boa Class Cruiser"
        },
        "450000": {
            key: "boa-player",
            name: "Boa"
        },
        "200000": {
            key: "python-player",
            name: "Python"
        },
        "150000": {
            key: "cobra3-player",
            name: "Cobra Mark III"
        },
        "145000": {
            key: "morayMED-player",
            name: "Moray Medical Boat"
        },
        "125000": {
            key: "moray-player",
            name: "Moray Star Boat"
        },
        "100000": {
            key: "cobramk1-player",
            name: "Cobra Mark I"
        },
        "65000": {
            key: "adder-player",
            name: "Adder"
        }
    };
    this._freeShipHunter = {
        "485000": {
            key: "ferdelance-player",
            name: "Fer-de-Lance"
        },
        "375000": {
            key: "asp-player",
            name: "Asp Mark II"
        },
        "150000": {
            key: "cobra3-player",
            name: "Cobra Mark III"
        },
        "125000": {
            key: "moray-player",
            name: "Moray Star Boat"
        },
    };
    this._freePick1 = null; // ship key of the free ship the player chose
    this._freePick2 = null;
    
    // some OXP ship packs change the default price of Cobra Mark III's and Mark I's. 
    // We're putting overrides in here so we can be consistent.
    this._overrideBasePrice = {
        "Cobra Mark III": 150000,
        "Cobra Mark I": 100000
    };
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function () {
        var trueValues = ["yes", "1", 1, "true", true];
        if (missionVariables.ShipRepurchase_LastEjectDates) this._lastEjectDates = JSON.parse(missionVariables.ShipRepurchase_LastEjectDates);
        if (missionVariables.ShipRepurchase_InsuranceDate) this._insuranceDate = missionVariables.ShipRepurchase_InsuranceDate;
        if (missionVariables.ShipRepurchase_StoredShip) this._storedShip = missionVariables.ShipRepurchase_StoredShip;
        if (missionVariables.ShipRepurchase_InterstellarMode) this._interstellarMode = (trueValues.indexOf(missionVariables.ShipRepurchase_InterstellarMode) >= 0 ? true : false);
    
        /*if (worldScripts.ShipConfiguration_Armour) {
            // monkey patch shipconfig armour so we can make sure things get done in the right sequence
            var sca = worldScripts.ShipConfiguration_Armour;
            sca.$sr_shipLaunchedFromStation = sca.shipLaunchedFromStation;
            delete sca.shipLaunchedFromStation;
        }*/
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerWillSaveGame = function () {
        if (this._lastEjectDates.length > 0) {
            missionVariables.ShipRepurchase_LastEjectDates = JSON.stringify(this._lastEjectDates);
        } else {
            delete missionVariables.ShipRepurchase_LastEjectDates;
        }
        missionVariables.ShipRepurchase_InsuranceDate = this._insuranceDate;
        if (this._storedShip != "") {
            missionVariables.ShipRepurchase_StoredShip = this._storedShip;
        } else {
            delete missionVariables.ShipRepurchase_StoredShip;
        }
        missionVariables.ShipRepurchase_InterstellarMode = this._interstellarMode;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipLaunchedEscapePod = function (pod) {
        if (this._simulator) return;
        this._ejectDate = clock.adjustedSeconds;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // todo: what happens if multiple events turn up simultaneously?
    this.shipTakingDamage = function (amount, whom, type) {
        if (this._simulator) return;
        // are we just about to die?
        // award the escape pod and eject
        if (this._debug) log(this.name, "shipTakingDamage " + amount + ", " + type + " : current ship energy " + (player.ship ? player.ship.energy : "no player ship!"));
        if (player.ship && player.ship.energy <= 0) {
            // turn off ship config functions
            var sc = worldScripts.ShipConfiguration_Core;
            if (sc) {
                sc._repairing = true;
                sc._adding = true;
            }
            var p = player.ship;
            if (this._debug) log(this.name, "attempting to award escape pod");
            var result = p.awardEquipment("EQ_ESCAPE_POD");
            if (this._debug) log(this.name, "award escape pod result " + result);
            // if the result of adding an escape pod was false, this means the player already had a functional one
            this._cargoRecoverySummary = "";
            if (result === false) {
                var eq;
                if (p.equipmentStatus("EQ_ESCAPE_POD") == "EQUIPMENT_OK") {
                    eq = EquipmentInfo.infoForKey("EQ_ESCAPE_POD");
                    this._cargoRecoverySummary = "some";
                }
                if (p.equipmentStatus("EQ_ESCAPE_POD_PLUS") == "EQUIPMENT_OK") {
                    eq = EquipmentInfo.infoForKey("EQ_ESCAPE_POD_PLUS")
                    this._cargoRecoverySummary = "most";
                }
                var tm = parseInt(eq.scriptInfo["max_rescue_time"]);
                // set the amount of time for the rescue
                if (p.hasOwnProperty("escapePodRescueTime")) p.escapePodRescueTime = ((tm / 2) + (Math.random() * (tm / 2))) * 3600;
                this._repurchasePercent = parseFloat(eq.scriptInfo["repurchase_percent"]);
    
                // grab a copy of all the cargo on board
                this._cargoRecovery.length = 0;
                var cr = parseFloat(eq.scriptInfo["cargo_recovery"]);
                if (cr > 0) {
                    var t = 0;
                    var m = p.manifest;
                    if (m.list && m.list.length > 0) {
                        for (var i = 0; i < m.list.length; i++) {
                            if (m.list[i].unit == "t" && m.list[i].quantity > 0) {
                                this._cargoRecovery.push(m.list[i]);
                                t += m.list[i].quantity;
                            }
                        }
                    }
                    var rm = parseInt((1 - cr) * t); // amount of cargo to remove
                    if (rm > 0) {
                        var pt = 0;
                        var end = false;
                        do {
                            if (this._cargoRecovery[pt].quantity > 0) {
                                this._cargoRecovery[pt].quantity -= 1;
                                rm -= 1;
                            }
                            pt += 1;
                            if (pt >= this._cargoRecovery.length) pt = 0;
                            if (rm <= 0) end = true;
                        } while (end == false);
                    } else {
                        this._cargoRecoverySummary = "";
                        this._cargoRecovery.length = 0;
                    }
                } else {
                    this._cargoRecoverySummary = "";
                }
            } else {
                // null out any gold, platinum and gems - they won't fit in the lifesuit
                this._cargoRecovery = [];
                p.manifest["gold"] = 0;
                p.manifest["platinum"] = 0;
                p.manifest["gem_stones"] = 0;
                // manually remove passengers and parcels
                // remove parcels - these won't fit either
                for (var i = p.parcels.length - 1; i >= 0; i--) {
                    // tell player about this via an arrival report
                    player.addMessageToArrivalReport(expandDescription("[parcel-lost]", {
                        name: p.parcels[i].name
                    }));
                    // send an email message for our special case
                    var w = worldScripts.EmailSystem;
                    if (w) {
                        var sndr = expandDescription("[parcel-contract-sender]");
                        var msg = expandDescription("[sr-parcel-contract-terminated]", {
                            contractname: p.parcels[i].name,
                            systemname: System.systemNameForID(p.parcels[i].destination),
                            time: global.clock.clockStringForTime(clock.seconds + p.parcels[i].eta)
                        });
                        var subj = expandDescription("[sr-parcel-contract-terminated-subject]");
    
                        w.$createEmail({
                            sender: sndr,
                            subject: subj,
                            date: global.clock.seconds,
                            message: msg,
                            expiryDays: worldScripts.GalCopAdminServices._defaultExpiryDays
                        });
                    }
    
                    var bb = worldScripts.BulletinBoardSystem;
                    var cobb = worldScripts.ContractsOnBB;
                    if (bb && cobb) {
                        for (var j = 0; j < bb._data.length; j++) {
                            var itm = bb._data[j];
                            if (itm != null && itm.accepted === true) {
                                // is this item the one we're dealing with?
                                if (itm.destination === p.parcels[i].destination && itm.payment === p.parcels[i].fee && itm.source === p.parcels[i].start) {
                                    bb.$removeBBMission(itm.ID);
                                    break;
                                }
                            }
                        }
                    }
    
                    p.removeParcel(p.parcels[i].name);
                    player.decreaseParcelReputation();
                }
                // remove passengers - they won't fit either
                var list = "";
                var count = p.passengers.length;
                if (count >= 1) {
                    for (var i = count - 1; i >= 0; i--) {
                        list += (list === "" ? "" : (i === count - 1 ? " and " : ", ")) + p.passengers[i].name;
                        // send an email message for our special case
                        var w = worldScripts.EmailSystem;
                        if (w) {
                            var sndr = expandDescription("[passenger-contract-sender]");
                            var msg = expandDescription("[sr-passenger-contract-terminated]", {
                                contractname: p.passengers[i].name,
                                systemname: System.systemNameForID(p.passengers[i].destination),
                                time: clock.clockStringForTime(clock.seconds + p.passengers[i].eta)
                            });
                            var subj = expandDescription("[sr-passenger-contract-terminated-subject]");
    
                            w.$createEmail({
                                sender: sndr,
                                subject: subj,
                                date: global.clock.seconds,
                                message: msg,
                                expiryDays: worldScripts.GalCopAdminServices._defaultExpiryDays
                            });
                        }
    
                        var bb = worldScripts.BulletinBoardSystem;
                        var cobb = worldScripts.ContractsOnBB;
                        if (bb && cobb) {
                            for (var j = 0; j < bb._data.length; j++) {
                                var itm = bb._data[j];
                                if (itm != null && itm.accepted === true) {
                                    // is this item the one we're dealing with?
                                    if (itm.destination === p.passengers[i].destination && itm.payment === p.passengers[i].fee && itm.source === p.passengers[i].start) {
                                        bb.$removeBBMission(itm.ID);
                                        break;
                                    }
                                }
                            }
                        }
    
                        p.removePassenger(p.passengers[i].name);
                        player.decreasePassengerReputation();
                    }
                    if (count > 1) {
                        player.addMessageToArrivalReport(expandDescription("[passengers-lost]", {
                            names: list
                        }));
                    } else {
                        player.addMessageToArrivalReport(expandDescription("[passenger-lost]", {
                            name: list
                        }));
                    }
                }
            }
            if (this._debug) log(this.name, "auto-ejecting...");
            p.abandonShip();
            if (sc) {
                sc._repairing = false;
                sc._adding = false;
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.escapePodSequenceOver = function () {
        if (this._simulator) return;
        this._doRepurchase = true;
        // stop hull repairs from showing up
        if (missionVariables.BattleDamage_status != "OK") missionVariables.BattleDamage_status = "OK";
        if (system.isInterstellarSpace) {
            this._interstellarRecovery = true;
            this._cargoRecovery.length = 0; // no cargo recovery in interstellar space
            this._cargoRecoverySummary = "";
            // only continue if IST is not installed - it will control the recovery chance and end-point
            if (!worldScripts["IST_masterScript"]) {
                if (system.stations.length > 0) {
                    // similar logic as for IST
                    // naval rescue - near
                    if (this._debug === true || Math.random() <= 0.8) {
                        var navalCarriers = system.filteredEntities(this, this.$isNavalCarrier, player.ship, 30000);
                        if (navalCarriers.length > 0) {
                            player.setEscapePodDestination(navalCarriers[0]);
                            return;
                        }
                    }
                    // naval rescue - further away
                    if (this._debug === true || Math.random() <= 0.4) {
                        navalCarriers = system.filteredEntities(this, this.$isNavalCarrier, player.ship, 100000);
                        if (navalCarriers.length > 0) {
                            player.setEscapePodDestination(navalCarriers[0]);
                            return;
                        }
                    }
                    // other station types (eg Erehwon)
                    if (this._debug === true || Math.random() <= 0.4) {
                        var nonNavalCarriers = system.filteredEntities(this, this.$isNotNavalCarrier, player.ship, 200000);
                        if (nonNavalCarriers.length > 0) player.setEscapePodDestination(nonNavalCarriers[0]);
                    }
                }
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerBoughtEquipment = function (equipmentKey) {
        if (equipmentKey === "EQ_REPURCHASE_INSURANCE") this._insuranceDate = clock.adjustedSeconds;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.missionScreenOpportunity = function () {
        // check if our insurance has lapsed
        if (this._insuranceDate != 0 && (clock.adjustedSeconds - this._insuranceDate) > (86400 * 60) && this._doRepurchase === false) {
            player.ship.removeEquipment("EQ_REPURCHASE_INSURANCE");
            this._insuranceDate = 0;
            mission.runScreen({
                screenID: "oolite-insurance-expiry-map",
                title: "Repurchase Insurance",
                overlay: {
                    name: "shiprepurchase_logo.png",
                    height: 546
                },
                message: expandDescription("[insurance_lapsed]"),
                exitScreen: "GUI_SCREEN_STATUS"
            });
            return;
        }
        if (this._passengerList && this._passengerList.length > 0) {
            if (this._passengerList.length > 1) {
                // more than 1 passenger
                var list = "";
                for (var i = 0; i < this._passengerList.length; i++) {
                    list += (list === "" ? "" : (i === this._passengerList.length - 1 ? " and " : ", ")) + this._passengerList[i];
                    player.decreasePassengerReputation();
                }
                mission.runScreen({
                    screenID: "oolite-passengers-stranded-map",
                    title: "Passengers",
                    message: expandDescription("[passengers-stranded]", {
                        names: list,
                        location: system.name
                    }),
                    exitScreen: "GUI_SCREEN_STATUS"
                });
            } else {
                // just 1 passenger
                player.decreasePassengerReputation();
                mission.runScreen({
                    screenID: "oolite-passenger-stranded-map",
                    title: "Passengers",
                    message: expandDescription("[passenger-stranded]", {
                        name: this._passengerList[0],
                        location: system.name
                    }),
                    exitScreen: "GUI_SCREEN_STATUS"
                });
            }
            this._passengerList.length = 0;
            return;
        }
        // have we been rescued in interstellar space?
        if (this._interstellarRecovery === true) {
            this._interstellarRecovery = false;
            // turn on the interstellar mode
            this._interstellarMode = true;
            // turn off the standard repurchase screen
            this._doRepurchase = false;
            var p = player.ship;
            var hud = p.hud;
            var eq = p.equipment;
            // populate the portable list
            this._portable.length = 0;
            for (var i = 0; i < eq.length; i++) {
                var item = eq[i];
                if (item.isPortableBetweenShips === true) this._portable.push(item.equipmentKey);
            }
            this.$rememberPassengers();
            // switch to the "loaner"
            player.replaceShip(this._interstellarFreeShip.key);
            // portable equipment is not automatically transferred if the shipkey changes
            this.$addPortable();
            p.serviceLevel = 75;
            p.hud = hud;
            if (p.dockedStation.isPolice) {
                mission.runScreen({
                    screenID: "oolite-naval-recovery-map",
                    title: "Naval Command",
                    message: expandDescription("[interstellar_rescue_navy]", {
                        shiptype: this._interstellarFreeShip.name
                    }),
                    exitScreen: "GUI_SCREEN_STATUS"
                });
            } else {
                // a non-police/navy station in interstellar space
                mission.runScreen({
                    screenID: "oolite-interstellar-recovery-map",
                    title: "Station Control",
                    message: expandDescription("[interstellar_rescue_normal]", {
                        shiptype: this._interstellarFreeShip.name
                    }),
                    exitScreen: "GUI_SCREEN_STATUS"
                });
            }
            return;
        }
        // are we doing the repurchase process now?
        if (this._doRepurchase === true) {
            this._doRepurchase = false;
            this.$showRepurchaseScreen();
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipLaunchedFromStation = function (station) {
        if (this.$simulatorRunning()) {
            this._simulator = true;
            return;
        }
        this._simulator = false;
    
        // what's our repurchase percentage? make an initial setting here but we'll recheck before an auto-eject
        var p = player.ship;
        this._repurchasePercent = 0.1;
        if (p.equipmentStatus("EQ_ESCAPE_POD") == "EQUIPMENT_OK") this._repurchasePercent = parseFloat(EquipmentInfo.infoForKey("EQ_ESCAPE_POD").scriptInfo["repurchase_percent"]);
        if (p.equipmentStatus("EQ_ESCAPE_POD_PLUS") == "EQUIPMENT_OK") this._repurchasePercent = parseFloat(EquipmentInfo.infoForKey("EQ_ESCAPE_POD_PLUS").scriptInfo["repurchase_percent"]);;
        if (worldScripts.ShipConfiguration_Armour) {
            this._scTimer = new Timer(this, this.$getSCArmour, 0.5, 0);
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$getSCArmour = function $getSCArmour() {
        var p = player.ship;
        this._holdSCAArmourFrontType = p.script._frontArmourType;
        this._holdSCAArmourAftType = p.script._aftArmourType;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipExitedWitchspace = function () {
        // check if we've been recovered in interstellar space, and if so, turn on the repurchase screen
        if (this._interstellarMode === true) {
            // in case we have a misjump in interstellar space
            if (system.isInterstellarSpace === false) this._doRepurchase = true;
            return;
        }
    
        // store ship and equipment (but not cargo) in case we need it later
        // but only if we're not already in interstellar recovery mode
        var shipinfo = worldScripts["Ship_Storage_Helper.js"].storeCurrentShip();
        var data = JSON.parse(shipinfo);
        data[8].length = 0; // remove any cargo
        data[10].length = 0; // remove any passengers
        this._storedShip = JSON.stringify(data);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$showRepurchaseScreen = function $showRepurchaseScreen() {
        // at this point we should have a new ship + equipment (damaged or otherwise) to work with
        // calculate cost of replacement ship + equipment
        var p = player.ship;
        var col1 = 22;
        var col2 = 32 - col1;
        p.hudHidden = true;
    
        // store the current list of player roles
        this._roles = [];
        for (var i = 0; i < p.roleWeights.length; i++) {
            this._roles.push(p.roleWeights[i]);
        }
    
        // check for an interstellar mode recovery
        if (system.isInterstellarSpace === false && this._interstellarMode === true && this._storedShip != "") {
            var lmss = worldScripts.LMSS_Core;
            if (lmss) lmss._switching = true;
            // restore the players original ship
            worldScripts["Ship_Storage_Helper.js"].restoreStoredShip(this._storedShip);
            this._storedShip = "";
            if (lmss) lmss._switching = false;
        }
    
        // first, repair everything, so the ship price will be accurate
        var eq = p.equipment;
        this._portable.length = 0;
        for (var i = 0; i < eq.length; i++) {
            var item = eq[i];
            var sts = "";
            var count = 0;
            var m_sts = p.equipmentStatus(item.equipmentKey, true);
            if (m_sts["EQUIPMENT_DAMAGED"] > 0) {
                sts = "EQUIPMENT_DAMAGED";
                count = m_sts["EQUIPMENT_DAMAGED"];
            }
            if (sts === "EQUIPMENT_DAMAGED") {
                var autoRepair = this._autoRepairMinTL[item.equipmentKey];
                if (!autoRepair || parseInt(autoRepair) <= p.dockedStation.equivalentTechLevel) {
                    for (var j = 1; j <= count; j++) {
                        p.setEquipmentStatus(item.equipmentKey, "EQUIPMENT_OK");
                    }
                }
            }
            if (item.isPortableBetweenShips === true) this._portable.push(item.equipmentKey);
        }
        // repair any ship config armour
        if (worldScripts.ShipConfiguration_Armour) {
            if (this._holdSCAArmourFrontType != "") {
                if (p.equipmentStatus(this._holdSCAArmourFrontType) != "EQUIPMENT_OK") p.awardEquipment(this._holdSCAArmourFrontType);
                p.script._armourFront = 100;
            }
            if (this._holdSCAArmourAftType != "") {
                if (p.equipmentStatus(this._holdSCAArmourAftType) != "EQUIPMENT_OK") p.awardEquipment(this._holdSCAArmourAftType);
                p.script._armourAft = 100;
            }
        }
        // battle damage OXP
        if (worldScripts["Battle Damage"]) {
            p.awardEquipment("EQ_HULL_REPAIR");
            missionVariables.BattleDamage_status = "OK";
        }
    
        // get the cost
        this._fullShipCost = p.price;
    
        if (this._overrideBasePrice[p.name]) {
            this._baseShipCost = this._overrideBasePrice[p.name];
            // if we needed to override the price, we should update the full ship/equipment price as well
            var sd = Ship.shipDataForKey(p.dataKey);
            if (sd._oo_shipyard) {
                var base = sd._oo_shipyard.price;
                this._fullShipCost = (this._fullShipCost - base) + this._baseShipCost;
            }
        } else {
            var sd = Ship.shipDataForKey(p.dataKey);
            if (sd._oo_shipyard) {
                this._baseShipCost = sd._oo_shipyard.price;
            } else {
                sd = this.$getShipDataEntryWithShipyard(p.name);
                if (sd) {
                    this._baseShipCost = sd._oo_shipyard.price;
                } else {
                    // this should never happen, but, you know...
                    log(this.name, "!!ERROR: Unable to find valid shipyard price value for shipkey " + p.dataKey);
                    this._baseShipCost = p.price;
                }
            }
        }
    
        var ins = false;
        if (this._repurchasePercent < 0.1) {
            if (p.equipmentStatus("EQ_REPURCHASE_INSURANCE") === "EQUIPMENT_OK") ins = true;
        }
    
        var repurchase = this._fullShipCost * (this._repurchasePercent * this.$calculatePremium());
        if (ins && player.score >= this._jamesonPoint) repurchase = 1000;
        if (player.score < this._jamesonPoint) repurchase = 200;
    
        var curChoices = {};
        var text = "Current credit balance: " + formatCredits(player.credits, true, true) + "\n\n";
    
        if (this._interstellarMode === false) {
            text += "Your ship has been destroyed. ";
        } else {
            text += "We have received word that your ship was destroyed in interstellar space. ";
        }
        text += "Please select your preferred ship repurchase option:";
        if (this._cargoRecoverySummary != "") {
            text += "\n\nNote: All repurchase options include the recovery of " + this._cargoRecoverySummary + " of your cargo.";
        }
        var label = "Current ship";
        if (this._interstellarMode === true) label = "Original ship";
    
        curChoices["01_CURRENT"] = {
            text: this.$padTextRight(label + " (" + p.shipClassName + ") with all equipment", col1) +
                this.$padTextLeft((repurchase === 0 ? "free" : formatCredits(repurchase, true, true)), col2),
            unselectable: (player.credits >= repurchase ? false : true)
        };
    
        this._freePick1 = null;
        this._freePick2 = null;
        var types = Object.keys(this._freeShipTrader);
        for (var i = 0; i < types.length; i++) {
            if (parseInt(types[i]) < this._baseShipCost && this._freePick1 == null && this._freeShipTrader[types[i]].name != p.shipClassName) {
                this._freePick1 = this._freeShipTrader[types[i]];
            }
        }
        // if we didn't get a hit, just choose the lowest value one (Adder)
        if (this._freePick1 == null) {
            this._freePick1 = this._freeShipTrader[types[types.length - 1]];
        } else {
            var types = Object.keys(this._freeShipHunter);
            for (var i = 0; i < types.length; i++) {
                if (parseInt(types[i]) < this._baseShipCost && this._freePick2 == null && this._freeShipHunter[types[i]].name != p.shipClassName) {
                    this._freePick2 = this._freeShipHunter[types[i]];
                }
            }
            // if they're the same, just show first one
            if (this._freePick2 != null && this._freePick2.name == this._freePick1.name) this._freePick2 = null;
        }
    
        // if the player is already in an adder, we have to show a free Adder as a replacement
        // so don't show a "ship only" repurchase option if we're going to offer a free one anyway
        if (p.shipClassName != "Adder") {
            var baseRepurchase = this._baseShipCost * (this._repurchasePercent * this.$calculatePremium());
            if (ins && player.score >= this._jamesonPoint) baseRepurchase = 500;
            if (player.score < this._jamesonPoint) baseRepurchase = 100;
    
            curChoices["01_CURRENT_BASE"] = {
                text: this.$padTextRight("Stock model of " + label.toLowerCase() + " (" + p.shipClassName + ")", col1) +
                    this.$padTextLeft((baseRepurchase === 0 ? "free" : formatCredits(baseRepurchase, true, true)), col2),
                unselectable: (player.credits >= baseRepurchase ? false : true)
            }
        }
    
        // a stock stepdown is always free
        curChoices["04_BASE_SHIP"] = {
            text: this.$padTextRight("Stock " + this._freePick1.name, col1) +
                this.$padTextLeft("free", col2),
            unselectable: false
        };
        if (this._freePick2 != null) {
            curChoices["04_BASE_SHIP2"] = {
                text: this.$padTextRight("Stock " + this._freePick2.name, col1) +
                    this.$padTextLeft("free", col2),
                unselectable: false
            }
        }
    
        var opts = {
            screenID: "oolite-shiprepurchase-details-map",
            title: "Ship Repurchase Options",
            overlay: {
                name: "shiprepurchase_logo.png",
                height: 546
            },
            allowInterrupt: false,
            exitScreen: "GUI_SCREEN_STATUS",
            choices: curChoices,
            initialChoicesKey: "01_CURRENT",
            message: text
        };
    
        mission.runScreen(opts, this.$screenHandler, this);
    
        this._interstellarMode = false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$screenHandler = function $screenHandler(choice) {
        var p = player.ship;
        var hud = p.hud;
        p.hudHidden = false;
    
        var ins = false;
        if (this._repurchasePercent < 0.1) {
            if (p.equipmentStatus("EQ_REPURCHASE_INSURANCE") === "EQUIPMENT_OK") ins = true;
        }
    
        if (choice === "01_CURRENT") {
            // nothing to do for this option: current ship and equipment should already be present.
            var repurchase = (this._fullShipCost * (this._repurchasePercent * this.$calculatePremium()));
            if (ins && player.score >= this._jamesonPoint) {
                repurchase = 1000;
                p.removeEquipment("EQ_REPURCHASE_INSURANCE");
            }
            if (player.score < this._jamesonPoint) repurchase = 200;
            player.credits -= repurchase;
            p.serviceLevel = 95; // replacement ship shouldn't need to be serviced any time soon.
            player.consoleMessage(formatCredits(repurchase, true, true) + " has been deducted from your account.");
        } else {
            // make a note of any passengers who will be getting dumped here.
            this.$rememberPassengers();
    
            // remove any secondary lasers
            if (worldScripts.LMSS_Core) {
                var lmss = worldScripts.LMSS_Core;
                if (lmss._forwardAltKey && lmss._forwardAltKey != "EQ_WEAPON_NONE") lmss._forwardAltKey = "EQ_WEAPON_NONE";
                if (lmss._aftAltKey && lmss._aftAltKey != "EQ_WEAPON_NONE") lmss._aftAltKey = "EQ_WEAPON_NONE";
                if (lmss._portAltKey && lmss._portAltKey != "EQ_WEAPON_NONE") lmss._portAltKey = "EQ_WEAPON_NONE";
                if (lmss._starboardAltKey && lmss._starboardAltKey != "EQ_WEAPON_NONE") lmss._starboardAltKey = "EQ_WEAPON_NONE";
            }
            // do we need to clean up any other 3rd party packs that have data stored in mission or local variables?
            // note: some base models come with an escape pod - should we remove it after changing the player ship?
    
            var cost = this._baseShipCost;
            if (choice === "01_CURRENT_BASE") {
                player.replaceShip(p.dataKey, p.entityPersonality);
                var deduct = (cost * (this._repurchasePercent * this.$calculatePremium()));
                if (ins && player.score >= this._jamesonPoint) {
                    deduct = 500;
                    p.removeEquipment("EQ_REPURCHASE_INSURANCE");
                }
                if (player.score < this._jamesonPoint) deduct = 100;
                player.credits -= deduct;
                p.serviceLevel = 95; // replacement ship shouldn't need to be serviced any time soon.
                player.consoleMessage(formatCredits(deduct, true, true) + " has been deducted from your account.");
            }
    
            if (choice === "04_BASE_SHIP") {
                player.replaceShip(this._freePick1.key);
                p.serviceLevel = 95; // replacement ship shouldn't need to be serviced any time soon.
                // portable equipment is not automatically transferred if the shipkey changes
                this.$addPortable();
            }
            if (choice === "04_BASE_SHIP2") {
                player.replaceShip(this._freePick2.key);
                p.serviceLevel = 95; // replacement ship shouldn't need to be serviced any time soon.
                // portable equipment is not automatically transferred if the shipkey changes
                this.$addPortable();
            }
    
            p.hud = hud;
        }
    
        // restore any recovered cargo
        this.$restoreCargo();
        // make sure the player has a complimentary fuel load
        p.fuel = 7;
    
        // add the date to our array here, so that it's only amended after the replacement is bought
        if (player.score >= this._jamesonPoint) this._lastEjectDates.push(this._ejectDate);
    
        // restore the previous list of roles
        for (var i = 0; i < p.roleWeights.length; i++) {
            if (this._roles.length - 1 >= i) p.setPlayerRole(this._roles[i], i);
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$addPortable = function $addPortable() {
        if (this._portable.length > 0) {
            var p = player.ship;
            for (var i = 0; i < this._portable.length; i++) {
                p.awardEquipment(this._portable[i]);
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$restoreCargo = function $restoreCargo() {
        if (this._cargoRecovery.length > 0) {
            var p = player.ship;
            var m = p.manifest;
            for (var i = 0; i < this._cargoRecovery.length; i++) {
                m[this._cargoRecovery[i].commodity] += this._cargoRecovery[i].quantity;
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$calculatePremium = function $calculatePremium() {
        if (this._applyPenalty == false) {
            return 1;
        } else {
            // how many times has the player ejected in period?
            var period = 365;
            // get a period based on the player score.
            var keys = Object.keys(this._periodLevels);
            for (var i = 0; i < keys.length; i++) {
                if (player.score >= parseInt(keys[i])) {
                    period = parseInt(this._periodLevels[keys[i]]);
                }
            }
            var mth = clock.adjustedSeconds - (86400 * period);
            var count = 0;
            if (this._lastEjectDates.length > 0) {
                for (var i = 0; i > this._lastEjectDates.length; i++) {
                    if (this._lastEjectDates[i] > mth) count += 1;
                }
            }
            // return a (1.n) value as a multiplier
            var result = 1 + ((count * 2) / 10);
            return result;
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // appends space to currentText to the specified length in 'em'
    this.$padTextRight = function $padTextRight(currentText, desiredLength, leftSwitch) {
        if (currentText == null) currentText = "";
        var hairSpace = String.fromCharCode(31);
        var ellip = "…";
        var currentLength = defaultFont.measureString(currentText.replace(/%%/g, "%"));
        var hairSpaceLength = defaultFont.measureString(hairSpace);
        // calculate number needed to fill remaining length
        var padsNeeded = Math.floor((desiredLength - currentLength) / hairSpaceLength);
        if (padsNeeded < 1) {
            // text is too long for column, so start pulling characters off
            var tmp = currentText;
            do {
                tmp = tmp.substring(0, tmp.length - 2) + ellip;
                if (tmp === ellip) break;
            } while (defaultFont.measureString(tmp.replace(/%%/g, "%")) > desiredLength);
            currentLength = defaultFont.measureString(tmp.replace(/%%/g, "%"));
            padsNeeded = Math.floor((desiredLength - currentLength) / hairSpaceLength);
            currentText = tmp;
        }
        // quick way of generating a repeated string of that number
        if (!leftSwitch || leftSwitch === false) {
            return currentText + new Array(padsNeeded).join(hairSpace);
        } else {
            return new Array(padsNeeded).join(hairSpace) + currentText;
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // appends space to currentText to the specified length in 'em'
    this.$padTextLeft = function $padTextLeft(currentText, desiredLength) {
        return this.$padTextRight(currentText, desiredLength, true);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // routine to check the combat simulator worldscript, to see if it's running or not
    this.$simulatorRunning = function $simulatorRunning() {
        var w = worldScripts["Combat Simulator"];
        if (w && w.$checkFight && w.$checkFight.isRunning) return true;
        return false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$isNavalCarrier = function $isNavalCarrier(e) {
        return e.isShip && e.isPolice && e.isStation;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$isNotNavalCarrier = function $isNotNavalCarrier(e) {
        return e.isShip && !e.isPolice && e.isStation && !e.hasRole("generationship") && (e.allegiance == null || e.allegiance != "thargoid");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // gets a list of all the current passenger names
    this.$rememberPassengers = function $rememberPassengers() {
        var p = player.ship;
        if (p.passengers.length > 0) {
            this._passengerList = [];
            for (var i = 0; i < p.passengers.length; i++) {
                this._passengerList.push(p.passengers[i].name);
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // finds a shipdata entry that has a corresponding shipyard value, so we can get an accurate base ship cost
    this.$getShipDataEntryWithShipyard = function $getShipDataEntryWithShipyard(shipName) {
        var keys = Ship.keysForRole("player");
        for (var i = 0; i < keys.length; i++) {
            var sd = Ship.shipDataForKey(keys[i]);
            if (sd.name === shipName) {
                // did we find a match that has a shipyard entry?
                if (sd._oo_shipyard) return sd;
            }
        }
        return null;
    }
    Scripts/repurchase_conditions.js
    "use strict";
    this.name = "ShipRepurchase_Conditions";
    this.author = "phkb";
    this.copyright = "2017 phkb";
    this.description = "Condition script for determining whether to include the 'Repurchase Insurance' item on the F3 screen";
    this.licence = "CC BY-NC-SA 3.0";
    
    //-------------------------------------------------------------------------------------------------------------
    this.allowAwardEquipment = function (equipment, ship, context) {
    	if (context === "scripted") return true;
    	if (equipment == "EQ_REPURCHASE_INSURANCE") {
    		if (player.ship.dockedStation.allegiance != "galcop") return false;
    	}
    	// otherwise allowed
    	return true;
    }