Planets.nu - Ship List Plugin

Planets.NU add-on to automatically keep track of other players' fleets.

目前为 2020-06-26 提交的版本。查看 最新版本

// ==UserScript==
// @name          Planets.nu - Ship List Plugin
// @namespace     vgap.plugins.shipList
// @version       1.0.10
// @date          2020-06-26
// @author        Space Pirate Harlock
// @description   Planets.NU add-on to automatically keep track of other players' fleets.
// @homepage      https://planets.nu/
// @license       GPL
// @include       https://planets.nu/*
// @include       https://play.planets.nu/*
// @include       http://play.planets.nu/*
// @include       https://test.planets.nu/*
// @include       https://mobile.planets.nu/*
// @resource      userscript https://greasyfork.org/en/scripts/405728-planets-nu-ship-list-plugin
// ==/UserScript==

/*
 Changelog:
 1.0.10     Feature: map highlight when selecting a ship
            Bug fix: ship intercepts
            Bug fix: buildShipfromVcr (heading)
            Bug fix: ship edit form (dropdowns)
 1.0.9      Bug fix: ship intercepts
            Bug fix: ship history notes formatting
 1.0.8      Bug fix: showScan again
 1.0.7      Bug fix: showScan in mobile client
 1.0.6      Feature: Support for non-mobile client
 1.0.5      Feature: Ships destroyed by mine hits get deleted
 Bug fix: Ghost ships made updateShips crash
 1.0.4      Feature: Notes saved from ship list
 Feature: Ship history saved to ship notes
 Bug fix: Ghost ships made showShips crash
 Bug fix: Not visible ships from allies weren't deleted
 1.0.3      Feature: Added toggle for compact tables
 Feature: Added hull name to ship scan
 Feature: Added turns ago to ship scan
 Feature: Added min-max mass to ship scan
 1.0.2      Bug fix: Loading stale notes after finishing time machine loop
 1.0.1      Feature: UI improvements for Firefox and mobile
 Bug fix: Notes weren't read in correctly (vgap.nowTurn)
 1.0.0      Initial release
 */

/*
 Known Issues:

 -

 Roadmap:

 - Add visual to star map when selecting a ship
 - Track intercepts - sphere map
 - Overview table showing fleet strength of opposing player
 - Add info about towing ships and ships being towed
 - Make it possible to add ships without knowing their exact ID
 - Tooltip over ship showing notes
 */

// no globals (vgap)
/**
 *
 * @param {vgaPlanets}  vgap
 * @constructor
 */
const ShipList = function (vgap)
{

    if (vgap.version < 3.0)
    {
        console.log("Ship List: [3000] NU version < 3.0. Plugin Disabled.");
        return;
    }

    /** PROPERTIES */

    // views
    this.view = 1;

    // internal
    this.doImport = false;
    this.doLoop = false;
    this.doReset = false;
    this.editMode = false;
    this.enabled = false;
    this.maxShipId = 0;
    this.pluginName = "Ship List";
    this.noteType = -1;
    this.shipIds = [];
    this.storagePath = '';

    // serialized
    this.firstTurn = 0;
    this.intercepts = [];
    this.lastTurn = 0;
    this.ships = [];
    this.version = '1.0.10';

    // view preferences
    this.settings = {
        addShipHistory: false,
        debugMode: true,
        deleteAfterImport: false,
        showFullAllies: true,
        showSafePassage: true,
        showShareIntel: true,
        showOwnShips: false,
        showUnknown: false,
        showColoredText: false,
        showCompactTable: false,
        showLocationHistory: true,
        showVerticalButtons: true,
        acceptShipData: {}
    };
    this.hideWarningPane = false;

    console.log('Ship List v' + this.version);

    /**
     * Initialize the plugin and its helpers
     * @returns {ShipList}
     */
    this.init = function()
    {
        // quick hack to hide features in development
        if (vgap.player.username == 'space pirate harlock') this.devMode = true;

        this.gameName = vgap.game.name;
        this.importPrefix = 'EnemyShipListPlugin.';
        // https://greasyfork.org/en/scripts/6685-planets-nu-plugin-toolkit/code
        this.noteType = -parseInt(this.pluginName.replace(/[_\W]/g, ''), 36) % 2147483648;
        this.playerName = vgap.player.username;
        this.raceName = (vgap.getRace(vgap.player.raceid)).shortname;
        this.storagePrefix = 'ShipList.';
        this.storagePath = this.storagePrefix + vgap.gameId + '.' + vgap.player.id + '.';

        this.drawer = new DrawHelper(vgap, this.settings).init();
        this.templater = new Templater();

        return this;
    }

    /** VGAP Hooks */

    /**
     * Called when redrawing the map
     * @namespace vgap.plugins.shipList
     */
    this.draw = function ()
    {
        const plugin = vgap.plugins.shipList;

        try
        {
            plugin.drawer.drawShips(plugin.ships);
            plugin.drawer.shipsDrawn = [];
        } catch (e)
        {
            console.log('[5003] (draw)');
            if (plugin.settings.debugMode)
            {
                console.log(e.name + ' ' + e.message);
                console.trace();
            }
        }
    };

    /** Called when building the dashboard */
    this.loaddashboard = function ()
    {
        const plugin = vgap.plugins.shipList;

        try
        {
            let menu = document.getElementById("DashboardMenu").childNodes[2];
            $("<li>Ship List »</li>").tclick(function ()
            {
                plugin.showShips(1);
            }).appendTo(menu);
        } catch (e)
        {
            console.log('[5004] (loaddashboard)');
            if (plugin.settings.debugMode)
            {
                console.log(e.name + ' ' + e.message);
                console.trace();
            }
        }
    };

    /** Called after loading the first turn to create the map */
    this.loadmap = function ()
    {
        const plugin = vgap.plugins.shipList;

        try
        {
            let css = [
                '<style id="shipListCss" type="text/css">',
                '#MessageInbox, #ShipPane {margin:0px 0px 50px 0px}',
                '#playerToggles {position:absolute;right:40px;top:0px;}',
                '#playerToggles.hori {display:flex;}',
                '#playerToggles .mapbutton {relative;margin:0px 0px 10px 10px;}',
                '#playerToggles .mapbutton::before {font-family:"Arial" !important;font-weight:600}',
                '#playerToggles .hideToggles::before {font-family:"Font Awesome 5 Free" !important;font-weight:900;content:"\\f105"}',
                '#playerToggles .player0::before{content:"All";color:#fff}#playerToggles .player0.active::before{content:"All";color:#ff0}',
                '.shipListTool::before {content:"\\f135"}',
                '#ShipTable, #FreighterTable, #PlayerSelectionTable, #ConfigTable {width:100%;border-spacing:0;margin-bottom:20px}',
                '#ShipTable th, #FreighterTable th {white-space:nowrap;padding:10px 5px}',
                '#ShipTable td, #FreighterTable td {white-space:nowrap;padding:10px 5px}',
                '#ShipTable.compact td, #FreighterTable.compact td {padding:3px 3px}',
                '#ShipTable th, #FreighterTable th, .BasicFlatButton:hover {background-color:#666}',
                '#ShipTable tr:hover, #FreighterTable tr:hover, #ConfigTable tr:hover {background-color:#333;cursor:pointer}',
                '#SettingsTable{border-spacing:0}#SettingsTable tr:hover{background-color:#111}',
                '#WarningPane .BasicFlatButton, #ConfigTable .BasicFlatButton, #newShipForm .BasicFlatButton {float:left;margin:0px 20px 0px 0px;display:inline-block;width:80px;text-align:center}',
                '#newShipForm{padding:0px}#newShipForm td,#newShipForm th{text-align:center}',
                '#newShipForm label, .ConfigForm label {display:inline-block; cursor:pointer}',
                '#newShipForm input, #newShipForm select, .ConfigForm input, .ConfigForm select {-webkit-appearance:auto;font-size:13px;height:16px;border-radius:8px;padding:8px 15px;width:-webkit-fill-available;text-align:center;border:none;background-color:#111 !important;color:#fff !important;box-sizing:content-box}',
                '#newShipForm input[type=checkbox], .ConfigForm input[type=checkbox] {height:16px;width:16px;cursor:pointer}',
                '#hullImg {width:90px;height:90px;margin:0px 5px;background-color:#000}',
                '#ShipEditPane, #PlayerSelectionPane, #WarningPane {display:table;background-image:url(https://mobile.planets.nu/img/game/dashboardbg.png);border-radius:10px;width:calc(100% - 32px);margin-bottom:10px;padding:10px}',
                '#WarningTable {display:inline-block;width:calc(100% - 20px)}',
                '#ShipRows .BasicFlatButton {border-radius:6px;padding:2px 15px;margin:3px}',
                '#ShipRows form, #FreighterRows form {padding:0px 20px}',
                '.EnemyItem .lval, .AllyItem .lval, .MyItem .lval {font-size:11px !important;color:#ccc !important;line-height:15px !important}',
                '.ItemSelection.ShipSeen img {top:60px}',
                '.warning{color:#f90}.center{text-align:center}.capitalize {text-transform:capitalize}',
                '.noteIcon::before {content:"\\f15c";font-family:"Font Awesome 5 Free";display:inline-block;width:20px;font-weight:900;color:#00ffff;-webkit-font-smoothing:antialiased;text-align:center;}',
                '.noteIcon.down::before{content:"\\f0d8"}.noteRow{display:none}',
                '.heading::before {color: #339999;content: "\\f14e";}',
                '.mass::before {color: #333399;content: "\\f5cd";}',
                'textarea.note {margin:0 0 0 5px;width:75%;height:120px;background-color:#333;border:solid 2px #000;color:#fff}',
                '.closeIcon {float:right;}',
                '.closeIcon::before {content:"\\f00d";font-family:"Font Awesome 5 Free";display:inline-block;width:16px;font-weight:900;color:#00ffff;-webkit-font-smoothing:antialiased;text-align:center;}',
                'section.popup: {max-width:max-content}',
                'form.modalForm {padding:0px}',
                'form.modalForm .table {margin:0px}',
                'form.modalForm .td {border:0;padding:10px 15px 10px 0px}',
                'form.modalForm label {white-space:nowrap;margin:0}'
            ];

            for (let i = plugin.drawer.playerToggles.length - 1; i > 0; i--)
            {
                css.push(
                    '.player' + i + '::before {content:"' + i + '";color:#fff}',
                    '.player' + i + '.active::before {content:"' + i + '";color:' + plugin.drawer.playerColors[i] + '}',
                    '.shipRow' + i + ' {background-color:' + colorToRGBA(plugin.drawer.playerColors[i], 0.5) + '}',
                    '.shipRow' + i + '.alt {color:' + plugin.drawer.playerColors[i] + ';background-color:transparent}'
                );
            }

            if (plugin.settings.addShipHistory)
            {
                css.push('.GoodTextNote {font: .9em "Roboto Mono", "Lucida Console", monospace;white-space:pre-wrap}');
            }

            // SimpTip CSS - (c) Arash Manteghi under MIT license
            css.push("[data-tooltip]{position:relative;display:inline-block;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}[data-tooltip]:before,[data-tooltip]:after{position:absolute;visibility:hidden;opacity:0;z-index:999999;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-transform:translate3d(0, 0, 0);-moz-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0)}[data-tooltip]:before{content:'';border:6px solid transparent}[data-tooltip]:after{height:22px;padding:11px 11px 0 11px;font-size:13px;line-height:11px;content:attr(data-tooltip);white-space:nowrap}[data-tooltip].simptip-position-top:before{border-top-color:#323232}[data-tooltip].simptip-position-top:after{background-color:#323232;color:#ecf0f1}[data-tooltip].simptip-position-top.half-arrow:before{border-right:7px solid #323232}[data-tooltip]:hover,[data-tooltip]:focus{background-color:transparent}[data-tooltip]:hover:before,[data-tooltip]:hover:after,[data-tooltip]:focus:before,[data-tooltip]:focus:after{opacity:1;visibility:visible}.simptip-multiline.simptip-position-top:before,.simptip-position-top:after{left:50%;-webkit-transform:translateX(-50%);-moz-transform:translateX(-50%);-ms-transform:translateX(-50%);-o-transform:translateX(-50%);transform:translateX(-50%)}.simptip-position-top:after{width:auto}.half-arrow.simptip-position-top:before{border-style:none;border-right:7px solid #323232}.simptip-position-top:before,.simptip-position-top:after{bottom:100%}.simptip-position-top:before{margin-bottom:-5px}.simptip-position-top:after{margin-bottom:7px}.simptip-position-top:hover:before,.simptip-position-top:hover:after{-webkit-transform:translate(-50%, 0px);-moz-transform:translate(-50%, 0px);-ms-transform:translate(-50%, 0px);-o-transform:translate(-50%, 0px);transform:translate(-50%, 0px)}.simptip-smooth:after{-webkit-border-radius:4px;border-radius:4px}.simptip-fade:before,.simptip-fade:after{-webkit-transition:opacity 0.2s linear,visibility 0.2s linear;-moz-transition:opacity 0.2s linear,visibility 0.2s linear;-o-transition:opacity 0.2s linear,visibility 0.2s linear;-ms-transition:opacity 0.2s linear,visibility 0.2s linear;transition:opacity 0.2s linear,visibility 0.2s linear}.simptip-multiline:after{height:auto;width:150px;padding:11px;line-height:19px;white-space:normal;text-align:left}.simptip-success.simptip-position-top:before{border-top-color:#62c462}.simptip-success.simptip-position-top:after{background-color:#62c462;color:#ecf0f1}.simptip-success.simptip-position-top.half-arrow:before{border-right:7px solid #62c462}.simptip-info.simptip-position-top:before{border-top-color:#5bc0de}.simptip-info.simptip-position-top:after{background-color:#5bc0de;color:#ecf0f1}.simptip-info.simptip-position-top.half-arrow:before{border-right:7px solid #5bc0de}.simptip-danger.simptip-position-top:before{border-top-color:#e74c3c}.simptip-danger.simptip-position-top:after{background-color:#e74c3c;color:#ecf0f1}.simptip-danger.simptip-position-top.half-arrow:before{border-right:7px solid #e74c3c}.simptip-warning.simptip-position-top:before{border-top-color:#e67e22}.simptip-warning.simptip-position-top:after{background-color:#e67e22;color:#ecf0f1}.simptip-warning.simptip-position-top.half-arrow:before{border-right:7px solid #e67e22}");

            css.push('</style>');
            if (!$('#shipListCss').length)
            {
                $(css.join('')).appendTo($('head:first'));
            }

            /** @todo fix direct call to drawer */
            plugin.drawer.addMapTool();

            console.log('Ship List: [1001] (loadmap) T' + vgap.game.turn + '.');
        } catch (e)
        {
            console.log('Ship List: [5001] (loadmap)');
            if (plugin.settings.debugMode)
            {
                console.log(e.name + ' ' + e.message);
                console.trace();
            }
        }
    };

    /** Called when loading a turn (current turn or time machine) */
    this.processload = function ()
    {
        const plugin = vgap.plugins.shipList;

        try
        {
            console.log('Ship List: [1000] (processload) T' + vgap.game.turn + '.');

            plugin.init();

            if (plugin.doReset)
            {
                plugin.doReset = false;
                plugin.ships = [];
                plugin.firstTurn = vgap.game.turn;
                plugin.lastTurn = vgap.game.turn - 1;
            } else
            {
                // load ships from storage
                plugin.load();

                // disable looping here instead of in processLoadHistory - doesn't get called on current turn
                // don't use vgap.nowTurn - hasn't been set at this point
                if (plugin.doLoop && !vgap.inHistory && vgap.game.turn == vgap.settings.turn)
                {
                    plugin.doLoop = false;
                }
            }

            if (!plugin.enabled) return;

            if (vgap.game.turn <= plugin.lastTurn)
            {
                console.log('Ship List: [2012] (processload) Turn ' + vgap.game.turn + ' already processed, skipping.');
                return;
            }

            if (vgap.game.turn != (plugin.lastTurn + 1))
            {
                console.log('Ship List: [2013] (processload) Turn ' + vgap.game.turn + '. Turn for update required: ' + (plugin.lastTurn + 1) + ', skipping.');
                return;
            }

            // get meat on bones
            plugin.updateShips();

        } catch (e)
        {
            console.log('Ship List: [5000] (processload)');
            if (plugin.settings.debugMode)
            {
                console.log(e.name + ' ' + e.message);
                console.trace();
            }
        }
    };

    /** Called when viewing the home screen of the dashboard */
    this.showsummary = function ()
    {
        const plugin = vgap.plugins.shipList;

        try
        {
            let ships = plugin.ships;
            let shipCount = 0;
            plugin.maxShipId = vgap.settings.shiplimit;

            for (let i = plugin.ships.length - 1; i >= 0; i--)
            {
                if (ships[i].id > plugin.maxShipId) plugin.maxShipId = ships[i].id;
                if (ships[i].ownerid == vgap.player.id) continue;
                shipCount++;
            }

            let icon = $([
                '<span><div class="iconholder">',
                '<img src="https://planets.nu/_library/2013/7/enemy_ships.png"/>',
                '</div>', shipCount, ' Enemy Ships</span>',
            ]
                .join(''))
                .click(function ()
                {
                    plugin.showShips(1);
                });

            if (vgap.isMobileVersion())
            {
                $('<span></span>').append(icon).insertBefore($('#TurnSummary').find(':nth-child(5)'));
            } else
            {
                $('<li></li>').append(icon).insertBefore($('#TurnSummary').find(':first :nth-child(5)'));
            }

            console.log('Ship List: [1002] (showsummary)');
        } catch (e)
        {
            console.log('Ship List: [5002] (showsummary)');
            if (plugin.settings.debugMode)
            {
                console.log(e.name + ' ' + e.message);
                console.trace();
            }
        }
    };

    /** loadplanet: Called when selecting a planet */

    /** loadstarbase: Called when selecting a planet */

    /** loadship: Called when selecting a ship */

    /** showdashboard: Called when switching to dashboard */

    /** showmap: Called when switching to starmap */

    /** MEAT AND BONES: SHIP LIST UPDATE FUNCTIONS */

    /**
     * Update the ship list with data from current turn if appropriate
     * @returns {ShipList}
     */

    this.updateShips = function ()
    {
        console.log('Ship List: [2014] (updateShips) Updating Ships for Turn ' + vgap.game.turn + '.');

        this.buildShipsFromIntercepts();

        // make vcrPlayer
        const vcr = new vcrPlayer();

        //first process all VCRs
        for (let i = 0; i < vgap.vcrs.length; i++)
        {

            let ship;
            let shipIdx;
            let shipIds = this.ships.map(function (ship)
            {
                return ship.id;
            });

            vcr.runReport(vgap.vcrs[i]);

            /** @namespace x.objectid */
            shipIdx = shipIds.indexOf(vgap.vcrs[i].left.objectid);

            if (vcr.results[0] == 'Left Destroyed')
            {
                // delete ship
                if (shipIdx != -1) this.ships.splice(shipIdx, 1);
            } else
            {
                // build ship
                ship = this.buildShipFromVcr(vcr, vgap.vcrs[i], 0, shipIdx);
            }

            // ignore planets
            if (vgap.vcrs[i].battletype == 1) continue;

            // refresh ship ids
            shipIds = this.ships.map(function (ship)
            {
                return ship.id;
            });

            shipIdx = shipIds.indexOf(vgap.vcrs[i].right.objectid);

            for (let j = 0; j < vcr.results.length; j++)
            {
                if (vcr.results[j] == 'Right Destroyed')
                {
                    if (shipIdx != -1) this.ships.splice(shipIdx, 1);
                    break;
                } else
                {
                    ship = this.buildShipFromVcr(vcr, vgap.vcrs[i], 1, shipIdx);
                }
            }
        }

        // before adding vgap ships
        this.checkMinehitReports();

        // visible ships
        this.buildShipsFromVgap();

        this.ships.sort(function (a, b)
        {
            return (a.id - b.id);
        });
        this.lastTurn = vgap.game.turn;
        this.save();
        return this;
    };

    this.buildShipsFromIntercepts = function ()
    {
        const shipIds = this.ships.map(function (ship)
        {
            return ship.id;
        });

        const vgapShipIds = vgap.ships.map(function (ship)
        {
            return ship.id;
        });

        for (let i = this.intercepts.length - 1; i >= 0; i--)
        {
            const intercept = this.intercepts[i];

            // find source ship in vgap ship list
            const srcShipIdx = vgapShipIds.indexOf(intercept.srcId);

            // ship could have been destroyed - so check
            if (srcShipIdx != -1 )
            {
                const srcShip = vgap.ships[srcShipIdx];

                // find target ship in plugin ship list
                const tgtShipIdx = shipIds.indexOf(intercept.tgtId)

                if (tgtShipIdx != -1)
                {
                    const tgtShip = this.ships[tgtShipIdx];

                    tgtShip.heading = Math.round((
                        90 - Math.atan2(srcShip.targety - tgtShip.y, srcShip.targetx - tgtShip.x)
                        * 180 / Math.PI + 360) % 360);
                    tgtShip.x = srcShip.targetx;
                    tgtShip.y = srcShip.targety;
                    tgtShip.infoturn = vgap.game.turn;
                }
            }
        }
    };

    /**
     * Checks for ships destroyed by mine hits
     * @returns {ShipList}
     */
    this.checkMinehitReports = function ()
    {
        let ships = [];
        let shipIds = this.ships.map(function (ship)
        {
            return ship.id;
        });

        for (let i = vgap.messages.length - 1; i >= 0; i--)
        {

            const message = vgap.messages[i];

            if (message.messagetype != 16) continue;

            const match = message.body.match(/#(\d+) has struck a mine!.*Damage is at: (\d+)/)

            if (match && match[2] >= 100)
            {
                ships.push([parseInt(match[1]), parseInt(match[2])]);
            }
        }

        for (let i = ships.length - 1; i >= 0; i--)
        {
            let shipIdx = shipIds.indexOf(ships[i][0]);

            if (shipIdx != -1 &&
                // Lizards
                ( vgap.players[this.ships[shipIdx].ownerid].raceid != 2 ||
                ships[i][1] >= 150 )
            ) this.ships.splice(shipIdx, 1);
        }

        return this;
    };

    /**
     * Update ship list from VGAP ship list
     * @returns {ShipList}
     */
    this.buildShipsFromVgap = function ()
    {
        // mark all ships in list invisible
        for (let i = this.ships.length - 1; i >= 0; i--)
        {
            this.ships[i].visible = false;
        }

        let shipIds = this.ships.map(function (ship)
        {
            return ship.id;
        });

        // get ids of vgap ships
        const vgapShipIds = vgap.ships.map(function (ship)
        {
            return ship.id;
        });

        // loop through vgap ship list
        for (let i = vgap.ships.length - 1; i >= 0; i--)
        {
            const vgapShip = vgap.ships[i];
            //support for sphere add-on
            if (vgapShip.id < 0) continue;

            let ship = {
                id: vgapShip.id,
                name: vgapShip.name,
                heading: vgapShip.heading,
                hullid: vgapShip.hullid,
                infoturn: vgapShip.infoturn,
                mass: vgapShip.mass,
                ownerid: vgapShip.ownerid,
                targetx: vgapShip.targetx,
                targety: vgapShip.targety,
                visible: true,
                warp: vgapShip.warp,
                x: vgapShip.x,
                y: vgapShip.y
            };

            let shipIdx = shipIds.indexOf(vgapShip.id);

            if (shipIdx == -1)
            {
                // add missing properties
                $.extend(ship, {
                    ammo: vgapShip.ammo,
                    beamid: vgapShip.beamid,
                    beams: vgapShip.beams,
                    crew: vgapShip.crew,
                    damage: vgapShip.damage,
                    engineid: vgapShip.engineid,
                    heading: -1,
                    history: [],
                    torpedoid: vgapShip.torpedoid,
                    torps: vgapShip.torps
                });

                this.updateShipHistory(ship, ship);
                this.ships.push(ship);
                if (this.settings.addShipHistory) this.addShipHistoryToNote(ship);
                if (this.settings.debugMode)
                {
                    console.log('Ship List: [1007] (buildShipsFromVgap) Adding New Ship.');
                    console.log(ship);
                }
            } else
            {
                if (vgapShip.ammo) ship.ammo = vgapShip.ammo;
                if (vgapShip.beamid) ship.beamid = vgapShip.beamid;
                if (vgapShip.beams) ship.beams = vgapShip.beams;
                if (vgapShip.crew != -1) ship.crew = vgapShip.crew;
                if (vgapShip.damage != -1) ship.damage = vgapShip.damage;
                if (vgapShip.engineid) ship.engineid = vgapShip.engineid;
                if (vgapShip.torpedoid) ship.torpedoid = vgapShip.torpedoid;
                if (vgapShip.torps) ship.torps = vgapShip.torps;

                let oldShip = this.ships[shipIdx];
                this.updateShipHistory(oldShip, ship);
                $.extend(true, oldShip, ship);
                if (this.settings.addShipHistory) this.addShipHistoryToNote(oldShip);
                if (this.settings.debugMode)
                {
                    console.log('Ship List: [1008] (buildShipsFromVgap) Updating Ship.');
                    console.log(oldShip);
                }
            }
        }

        // delete own ships / from share intel / full allies that aren't visible
        for (let i = this.ships.length - 1; i >= 0; i--)
        {
            if (vgapShipIds.indexOf(this.ships[i].id) == -1
                && vgap.getRelationFromForShip(this.ships[i].ownerid) >= 3
            ) this.ships.splice(i, 1);
        }

        this.shipIds = this.ships.map(function (ship)
        {
            return ship.id;
        });

        return this;
    };

    /**
     * Builds a ship object from VCR player and VCR report
     * @param {vcrPlayer}      vcr
     * @param {Object}         report   vcr Report object
     * @param {number}         ix       [0-1]
     * @param {number}         shipIdx
     * @returns {*}
     */
    this.buildShipFromVcr = function (vcr, report, ix, shipIdx)
    {

        const combatInfo = vcr.Objects[ix];
        const side = ix == 0 ? 'left' : 'right';
        const shipInfo = report[side];

        let ownerId = report[side + 'ownerid'];

        for (let j = 0; j < vcr.results.length; j++)
        {
            if (vcr.results[j].toLowerCase() == side + ' captured')
            {
                ownerId = ix == 0 ? /** @type int */ report.rightownerid : /** @type int */ report.leftownerid;
            }
        }

        let ship = {
            id: shipInfo.objectid,
            name: shipInfo.name,
            ammo: combatInfo.BayCount ? combatInfo.Fighters : combatInfo.Torpedos,
            beams: combatInfo.BeamCount,
            beamid: combatInfo.BeamId,
            crew: combatInfo.Crew,
            damage: combatInfo.Damage,
            hullid: shipInfo.hullid,
            infoturn: vgap.game.turn,
            mass: combatInfo.Mass,
            ownerid: ownerId,
            torpedoid: combatInfo.TorpedoId,
            torps: combatInfo.LauncherCount,
            x: report.x,
            y: report.y,
        };

        if (shipIdx == -1)
        {
            // add missing properties
            $.extend(ship, {
                engineid: 0,
                heading: -1,
                history: [],
                warp: -1
            });
            this.updateShipHistory(ship, ship);
            this.ships.push(ship);
        } else
        {
            let oldShip = this.ships[shipIdx];
            this.updateShipHistory(oldShip, ship);
            $.extend(true, oldShip, ship);
        }

        if (this.settings.addShipHistory) this.addShipHistoryToNote(ship);

        if (this.settings.debugMode)
        {
            console.log('Ship List: [1009] (buildShipFromVcr) Adding Ship.');
            console.log(ship);
        }

        return ship;
    };

    /**
     * Add ship history to ship note
     * @param {Object}      ship
     * @returns {ShipList}
     */
    this.addShipHistoryToNote = function (ship)
    {
        const start = '\n---SHIP HISTORY---';
        const note = vgap.getNote(ship.id, 2);
        const body = note.body.split(start);
        const rows = [];

        rows.push(body[0]);
        rows.push(start);
        rows.push('\nT', ship.infoturn.toString().padEnd(3, ' '), '', ship.x, ',', ship.y, ' W', (ship.warp == -1 ? '?' : ship.warp),
            ' H', ship.heading ? ship.heading.toString().padStart(3, ' ') : '   ', ship.mass.toString().padStart(5, ' '), 'kt');

        if (body.length > 1)
        {
            const turns = body[1].split('\n');
            // remove empty strings
            for (let i = turns.length - 1; i >= 0; i--)
            {
                if (turns[i] == "") turns.splice(i, 1);
            }
            // remove gibberish
            for (let i = turns.length - 1; i >= 0; i--)
            {
                if (turns[i].search(/^T\d+ /) == -1) turns.splice(i, 1);
            }
            // remove first row if same as current turn
            const match = turns[0].match(/^T(\d+)/);
            if (match && match[1] == vgap.game.turn) turns.shift();
            // remove last row if more than 5
            if (turns.length > 5) turns.pop();
            // add newline before first row
            if (turns.length > 0) turns[0] = '\n' + turns[0];
            rows.push(rows, turns.join('\n'));
        }

        note.body = rows.join('');
        note.changed = 1;
        return this;
    };

    /**
     * Update ship history
     * @param {Object}      oldShip     ship info to be updated
     * @param {Object}      newShip     ship info to update with
     * @returns {Object} oldShip
     */
    this.updateShipHistory = function (oldShip, newShip)
    {
        if (oldShip.history.length == 0 || oldShip.history[0].turn < newShip.infoturn)
        {
            oldShip.history.unshift({
                turn: newShip.infoturn,
                x: newShip.x,
                y: newShip.y,
                mass: newShip.mass
            });
        }
        if (oldShip.history.length > 5) oldShip.history.pop();
        oldShip.history.sort(function (a, b)
        {
            return (b.turn - a.turn);
        });

        return oldShip;
    };

    /** HTML */

    /**
     * HTML for Ship List tabs
     * @param {number}  view
     * @param {number}  playerId [1]
     * @returns {ShipList}
     */
    this.showShips = function (view, playerId)
    {

        if (!playerId) playerId = 1;

        vgap.playSound("button");
        vgap.closeSecond();
        vgap.dash.content.empty();

        // disable hotkeys in edit mode
        if (this.editMode) vgap.hotkeysOn = false;

        $([
            '<ul class="FilterMenu">',
            '<li onclick="vgap.plugins.shipList.showShips(1);" ' + (view == 1 ? 'class="SelectedFilter"' : '') + '>Summary</li>',
            '<li onclick="vgap.plugins.shipList.showShips(2);" ' + (view == 2 ? 'class="SelectedFilter"' : '') + '>All Ships</li>',
            '<li onclick="vgap.plugins.shipList.showShips(3);" ' + (view == 3 ? 'class="SelectedFilter"' : '') + '>Other Players</li>',
            '<li onclick="vgap.plugins.shipList.showShips(4);" ' + (view == 4 ? 'class="SelectedFilter"' : '') + '>Allies</li>',
            '<li onclick="vgap.plugins.shipList.showShips(5);" ' + (view == 5 ? 'class="SelectedFilter"' : '') + '>Enemies</li>',
            '<li onclick="vgap.plugins.shipList.showShips(6);" ' + (view == 6 ? 'class="SelectedFilter"' : '') + '>Single Player</li>',
            '<li onclick="vgap.plugins.shipList.showShips(7);" ' + (view == 7 ? 'class="SelectedFilter"' : '') + '>Settings</li>',
            '<li onclick="vgap.plugins.shipList.showShips(8);" ' + (view == 8 ? 'class="SelectedFilter"' : '') + '>Storage Stats</li>',
            '</ul>'
        ].join('')).appendTo(vgap.dash.content);


        let pane = $('<div id="dashPane" class="DashPane" style="height:' + ($('#DashboardContent').height() - 30) + 'px;"></div>').appendTo(vgap.dash.content);

        if (view == 8) return this.showStorage(pane, view);
        this.showWarningPane(pane, view);
        if (view == 7) return this.showSettings(pane);
        if (view == 1) return this.showOverview(pane);
        console.time('showShips');
        if (view == 2) this.showSelectionPaneComplete(pane, view, playerId);
        if (view == 4) this.showSelectionPaneAllied(pane, view, playerId);
        if (view == 6)
        {
            this.showSelectionPaneSingle(pane, view, playerId);
            this.showShipForm($('#newShipForm'), view, playerId);
        }
        if (this.ships.length) this.showShipTableHeader(pane, view, playerId);

        this.shipIds = this.ships.map(function (ship)
        {
            return ship.id;
        });

        const shipRows = $('#ShipRows');
        const freighterRows = $('#FreighterRows');
        const shortEngineNames = ['?', 'SD1', 'SD2', 'SD3', 'SSD4', 'ND5', 'HND6', 'QD7', 'HD8', 'TW'];
        const shortBeamNames = ['?', 'Las', 'X-Ray', 'Pla', 'Bla', 'Posi', 'Dis', 'HB', 'PH', 'HD', 'HP'];
        const shortTorpNames = ['?', 'Mk1', 'Prot', 'Mk2', 'GaB', 'Mk3', 'Mk4', 'Mk5', 'Mk6', 'Mk7', 'Mk8', 'QT'];


        // loop through all ship ids
        for (let id = 1; id <= this.maxShipId; id++)
        {

            const shipIdx = this.shipIds.indexOf(id);

            if (shipIdx == -1)
            {
                // no unknown ships or not complete view - continue
                if (!this.settings.showUnknown || view != 2) continue;
                shipRows.append('<tr><td>' + id + '</td><td>-</td><td>-</td><td>-</td><td>-</td><td></td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>');
                continue;
            }

            let ship = this.ships[shipIdx];

            const relation = vgap.getRelationFromForShip(ship.ownerid);

            // thanks to Jellyfishspam for this one
            // ghost ship - continue loop
            // training levels - relation is 0
            if (!relation &&
                !(vgap.game.gametype == 1 && vgap.game.shortdescription == "Storyline")) continue;

            if (
                //ships of other players
            (view == 3 && ship.ownerid == vgap.player.id) ||
            //allies (>= safe passage)
            (view == 4 && (
                relation == 1 ||
                (relation == 2 && !this.settings.showSafePassage) ||
                (relation == 3 && !this.settings.showShareIntel) ||
                (relation == 4 && !this.settings.showFullAllies) ||
                (ship.ownerid == vgap.player.id && !this.settings.showOwnShips)
            )) ||
            //non-allies
            (view == 5 &&
                (ship.ownerid == vgap.player.id || vgap.alliedTo(ship.ownerid))
            ) ||
            //single player
            (view == 6 && ship.ownerid != playerId)
            ) continue;

            const hull = vgap.getHull(ship.hullid);
            const player = vgap.getPlayer(ship.ownerid);

            let beam_weapons = '?';

            if (hull.beams == 0)
            {
                beam_weapons = "---";
            } else if (ship.beams != 0 && ship.beamid != 0)
            {
                beam_weapons = (ship.beams ? ship.beams + ' ' : '') + shortBeamNames[ship.beamid];
            }

            let secondary_weapons = '?';

            if (hull.fighterbays > 0)
            {
                secondary_weapons = hull.fighterbays + (hull.fighterbays == 1 ? ' Bay' : ' Bays');
            } else if (hull.launchers > 0)
            {
                if (ship.torps == -1 || ship.torpedoid == -1) secondary_weapons = "?";
                else if (ship.torps == 0 || ship.torpedoid == 0) secondary_weapons = "---";
                else secondary_weapons = ( ship.torps ? ship.torps + ' ' : '') + shortTorpNames[ship.torpedoid];
            } else secondary_weapons = "---";

            let row = [
                '<tr class="shipRow' + player.id + (this.settings.showColoredText ? ' alt' : '') + '">',
                !this.editMode ? '' : '<td><div class="BasicFlatButton" onclick="vgap.plugins.shipList.deleteShip(' + ship.id + ').showShips(' + view + ',' + playerId + ');">Delete</div></td>',
                '<td>' + ship.id + '</td>',
                '<td>' + player.username + '</td>',
                '<td>' + vgap.getRace(player.raceid).shortname.substr(3) + '</td>',
                '<td title="' + hull.name + '"><img class="TinyIcon" src="' + hullImg(ship.hullid) + '"/><div style="display: none;">' + hull.id + '</div></td>',
                '<td>' + ship.name + '</td>',
                '<td class="noteIcon"></td>',
                '<td>(' + ship.x + "," + ship.y + ')</td>',
                '<td>' + ship.infoturn + '</td>',
                '<td>' + (ship.heading > -1 ? ship.heading : '?') + '</td>',
                '<td>' + ship.warp + '</td>',
                '<td>' + (ship.engineid > 0 ? shortEngineNames[ship.engineid] : '?') + '</td>',
                '<td>' + beam_weapons + '</td>',
                '<td>' + secondary_weapons + '</td>',
                '<td>' + ((ship.ammo == -1) ? '?' : ship.ammo) + '</td>',
                '<td>' + ship.mass + '</td>',
                '<td>' + ((ship.crew == -1) ? '?' : ship.crew) + '</td>',
                '<td>' + ((ship.damage == -1) ? '?' : ship.damage) + '</td>',
                '</tr>'
            ].join('');

            const note = vgap.getNote(ship.id, 2).body;

            const noteRow = [
                '<tr class="shipRow' + player.id + (this.settings.showColoredText ? ' alt' : '') + ' noteRow tablesorter-childRow">',
                '<td colspan="' + (this.editMode ? '18' : '17') + '">',
                '<form>',
                '<label for="note' + ship.id + '">Ship Notes</label>',
                '<textarea class="note" id="note' + ship.id + '" name="body">' + note + '</textarea>',
                '</form>',
                '</td>',
                '</tr>'
            ].join('');

            if (view == 6 && hull.beams == 0 && hull.launchers == 0 && hull.fighterbays == 0)
            {
                row = $(row).appendTo(freighterRows);
                freighterRows.append(noteRow);
            } else
            {
                row = $(row).appendTo(shipRows);
                shipRows.append(noteRow);
            }

            if (view == 6 && this.editMode)
            {
                row.on('fill-form', this.fillShipForm.bind(this));
                row.click(function ()
                {
                    $(this).trigger('fill-form', [ship.id, playerId]);
                    pane.data('jsp').scrollToElement($('#ShipEditTable'));
                });
            } else
            {
                row.children().eq(6).hover((function (ship)
                {
                    this.showScan(ship);
                }).bind(this.drawer, ship), this.drawer.hideScan.bind(this.drawer));

                row.click((function (x, y, id, ships)
                {
                    vgap.showMap();
                    vgap.map.centerMap(x, y, true);
                    this.selectPlayer(id)
                        .drawShips(ships);
                }).bind(this.drawer, ship.x, ship.y, ship.ownerid, this.ships));
            }

            // add slide toggle for notes row
            row.children('.noteIcon').click(function (e)
            {
                $(this)
                    .toggleClass('down')
                    .parent().next().slideToggle();
                e.stopPropagation();
            });

            // save note on blur
            row.next().find('textarea').blur(function ()
            {
                const note = vgap.getNote(ship.id, 2);
                note.body = $(this).val();
                note.changed = 1;
            });

        }

        //add freighters and warships to end of list
        this.showUnknownShipsSingle(view, playerId, shipRows, freighterRows);

        $('#ShipTable').tablesorter({cssChildRow: 'tablesorter-childRow'});
        shipRows.find('td:nth-child(2)').addClass('capitalize');

        if (view == 6)
        {
            $("#FreighterTable").tablesorter();
            freighterRows.find('td:nth-child(2)').addClass('capitalize');
        }

        pane.jScrollPane({animateScroll: true});
        // fix annoying scroll up behavior
        $('#newShipForm *').off('focus');
        // remove focus handler on textareas
        shipRows.find('textarea').off('focus');
        freighterRows.find('textarea').off('focus');

        // vgap.action added for the assistant
        vgap.showShipsViewed = 1;
        vgap.action();

        console.timeEnd('showShips');

        return this;
    };

    /** Unknown Ships for "Single Player" view */
    this.showUnknownShipsSingle = function (view, playerId, shipRows, freighterRows)
    {
        if (view == 6 && this.settings.showUnknown)
        {

            const player = vgap.getPlayer(playerId);
            const race = vgap.getRace(player.raceid).shortname.substr(3);
            const warshipTotal = this.playerInfo(player.id, "capitalships");
            const freighterTotal = this.playerInfo(player.id, "freighters");
            let warshipCount = 0;
            let freighterCount = 0;
            let row = '<tr class="shipRow"' + playerId + '>' + (this.editMode ? '<td></td>' : '') + '<td>-</td><td>' + player.username + '</td><td>' + race + '</td><td>-</td><td>-</td><td></td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>';
            let rows = [];

            for (let i = this.ships.length - 1; i >= 0; i--)
            {
                let ship = this.ships[i];

                if (ship.ownerid != player.id) continue;

                let hull = vgap.getHull(ship.hullid);
                if (hull.beams == 0 && hull.launchers == 0 && hull.fighterbays == 0)
                {
                    freighterCount++;
                } else
                {
                    warshipCount++;
                }
            }

            for (let i = warshipCount; i < warshipTotal; i++)
            {
                rows.push(row);
            }
            shipRows.append(rows.join(''));

            for (let i = freighterCount; i < freighterTotal; i++)
            {
                rows.push(row);
            }
            freighterRows.append(rows.join(''));
        }
        return this;
    };

    /** Selection pane for "Allies" view */
    this.showSelectionPaneAllied = function (target, view, playerId)
    {

        target.append([
            '<form id="newShipForm"><div id="PlayerSelectionPane">',
            '<table id="PlayerSelectionTable">',
            '<tr><th><label for="showFullAllies">Show Full Allies</label></th><th><label for="showShareIntel">Show Share Intel</label></th><th><label for="showSafePassage">Show Safe Passage</label></th><th><label for="showOwnShips">Show Own Ships</label></th></tr>',
            '<tr><td><input type="checkbox" ' + (this.settings.showFullAllies ? 'checked="yes" ' : '') + 'id="showFullAllies"/></td>',
            '<td><input type="checkbox" ' + (this.settings.showShareIntel ? 'checked="yes" ' : '') + 'id="showShareIntel"/></td>',
            '<td><input type="checkbox" ' + (this.settings.showSafePassage ? 'checked="yes" ' : '') + 'id="showSafePassage"/></td>',
            '<td><input type="checkbox" ' + (this.settings.showOwnShips ? 'checked="yes" ' : '') + 'id="showOwnShips"/></td>',
            '</tr></table>',
            '</div></form>'
        ].join(''));

        /** @this ShipList */
        $('#showFullAllies').click((function (view, playerid)
        {
            this.toggleFullAllies().showShips(view, playerid);
        }).bind(this, view, playerId));

        /** @this ShipList */
        $('#showShareIntel').click((function (view, playerid)
        {
            this.toggleShareIntel().showShips(view, playerid);
        }).bind(this, view, playerId));

        /** @this ShipList */
        $('#showSafePassage').click((function (view, playerid)
        {
            this.toggleSafePassage().showShips(view, playerid);
        }).bind(this, view, playerId));

        /** @this ShipList */
        $('#showOwnShips').click((function (view, playerid)
        {
            this.toggleOwnShips().showShips(view, playerid);
        }).bind(this, view, playerId));

        return this;
    };

    /** Selection pane for "Complete" view */
    this.showSelectionPaneComplete = function (target, view, playerId)
    {

        target.append([
            '<form id="newShipForm">',
            '<div id="PlayerSelectionPane">',
            '<table id="PlayerSelectionTable">',
            '<tr><th><label for="showUnknown">Show Unknown</label></th></tr>',
            '<tr><td>',
            this.templater.get('checkBox', {id: 'showUnknown', checked: this.settings.showUnknown}),
            //'<input type="checkbox" ' + (this.settings.showUnknown ? 'checked="yes" ' : '') + 'id="showUnknown"/>',
            '</td></tr>',
            '</table>',
            '</div>',
            '</form>'
        ].join(''));

        /** @this ShipList */
        $('#showUnknown').click((function (view, playerid)
        {
            this.toggleUnknown().showShips(view, playerid);
        }).bind(this, view, playerId));

        return this;
    };

    /** Selection pane for "Single Player" view */
    this.showSelectionPaneSingle = function (target, view, playerId)
    {

        target.append([
            '<script>var newShip = { overwrite: false, ship: {} };</script>',
            '<form id="newShipForm">',
            '<div id="PlayerSelectionPane">',
            '<table id="PlayerSelectionTable">',
            '<tr><th><label for="shipListPlayer">Select Player</label></th><th><label for="showUnknown">Show Unknown</label></th><th><label for="enableEdit">Enable Edit</label></th></tr>',
            '<tr><td><select id="shipListPlayer" class="capitalize" onchange="vgap.plugins.shipList.showShips(6,$(this).val());"></select></td>',
            '<td><input type="checkbox" ' + (this.settings.showUnknown ? 'checked="yes" ' : '') + 'id="showUnknown"/></td>',
            '<td><input type="checkbox" ' + (this.editMode ? 'checked="yes" ' : '') + 'id="enableEdit"/></td></tr>',
            '</table>',
            '</div>',
            '</form>'
        ].join(''));

        $.each(vgap.players, function (k, v)
        {
            $('#shipListPlayer').append('<option value="' + v.id + '">' + v.id + ' - ' + v.fullname + '</option>');
        });
        $('#shipListPlayer').val(playerId);

        /** @this ShipList */
        $('#showUnknown').click((function (view, playerId)
        {
            this.toggleUnknown().showShips(view, playerId);
        }).bind(this, view, playerId));

        /** @this ShipList */
        $('#enableEdit').click((function (view, playerId)
        {
            this.toggleEdit().showShips(view, playerId);
            $('#dashPane').data('jsp').reinitialise();
        }).bind(this, view, playerId));

        return this;
    };

    /** Overview tab */
    this.showOverview = function (target)
    {

        let table = target
                .append('<div id="MessageInbox"></div>').children(':last-child')
                .append('<table></table>').children(':last-child')
                .append('<tr><td><strong>Overview</strong></td></tr>')
            ;

        $('#MessageInbox').append([
            '<table id="ShipTable"><thead>',
            '<th>ID</th><th>Player</th><th>Race</th>',
            '<th>Warships</th>',
            '<th>Freighters</th>',
            '<th>Total</th>',
            '</thead><tbody id="ShipRows">',
            '</tbody></table>'
        ].join(''));

        for (let i = 0; i < vgap.players.length; i++)
        {

            let warshipsKnown = 0;
            let freightersKnown = 0;

            let player = vgap.players[i];
            let warshipsTotal = this.playerInfo(player.id, "capitalships");
            let freightersTotal = this.playerInfo(player.id, "freighters");

            for (let i = this.ships.length - 1; i >= 0; i--)
            {
                let ship = this.ships[i];

                if (ship.ownerid != player.id) continue;

                //support for sphere addon: don't count ships with negative id
                if (ship.id < 0) continue;

                let hull = vgap.getHull(ship.hullid);
                if ((hull.beams == 0 || (ship.beamid == 0 && ship.ownerid == vgap.player.id)) && (hull.launchers == 0 || (ship.torpedoid == 0 && ship.ownerid == vgap.player.id)) && hull.fighterbays == 0)
                {
                    freightersKnown++;
                } else
                {
                    warshipsKnown++;
                }
            }

            /** @this ShipList */
            $(['<tr class="shipRow' + player.id + '">',
                '<td>' + player.id + '</td>',
                '<td>' + player.username + '</td>',
                '<td>' + vgap.getRace(player.raceid).shortname + '</td>',
                '<td>' + warshipsKnown + ' / ' + warshipsTotal + '</td>',
                '<td>' + freightersKnown + ' / ' + freightersTotal + '</td>',
                '<td>' + (freightersTotal + warshipsTotal) + '</td>',
                '</tr>'
            ].join('')).appendTo("#ShipRows")
                .click((function (playerId)
                {
                    this.showShips(6, playerId);
                }).bind(this, player.id))
            ;
            $('#ShipRows').find('td:nth-child(2)').addClass('capitalize');
        }

        $("#ShipTable").tablesorter();
        target.jScrollPane();

        vgap.action();
        return this;
    };

    /** Settings tab */
    this.showSettings = function (target)
    {

        let rows = [
            '<tr><td><p><strong>Plugin Preferences</strong></p></td></tr>',
            '<tr><td><form class="ConfigForm"><table id="SettingsTable">',
            '<tr><td><input type="checkbox" ' + (this.settings.showLocationHistory ? 'checked="yes" ' : '') + 'id="showLocationHistory"/></td><td><label for="showLocationHistory"><span>Show ship location history.</span></label></td></tr>',
            '<tr><td><input type="checkbox" ' + (this.settings.showVerticalButtons ? 'checked="yes" ' : '') + 'id="showVerticalButtons"/></td><td><label for="showVerticalButtons"><span>Vertical player buttons.</span></label></td></tr>',
            '<tr><td><input type="checkbox" ' + (this.settings.showColoredText ? 'checked="yes" ' : '') + 'id="showColoredText"/></td><td><label for="showColoredText"><span>Colored text instead of colored rows.</span></label></td></tr>',
            '<tr><td><input type="checkbox" ' + (this.settings.showCompactTable ? 'checked="yes" ' : '') + 'id="showCompactTable"/></td><td><label for="showCompactTable"><span>Compact ship tables.</span></label></td></tr>',
            '<tr><td><input type="checkbox" ' + (this.settings.addShipHistory ? 'checked="yes" ' : '') + 'id="addShipHistory"/></td><td><label for="addShipHistory"><span>Add ship history to notes.</span></label></td></tr>',
            '<tr><td><input type="checkbox" ' + (this.settings.deleteAfterImport ? 'checked="yes" ' : '') + 'id="deleteAfterImport"/></td><td><label for="deleteAfterImport"><span>Delete EnemyShipList plugin data after import.</span></label></td></tr>',
            '<tr><td><input type="checkbox" ' + (this.settings.debugMode ? 'checked="yes" ' : '') + 'id="toggleDebugMode"/></td><td><label for="toggleDebugMode"><span>Debug Mode (verbose logging).</span></label></td></tr>',
            '</table></form></td></tr>',

            '<tr><td><p><strong>Tools</strong></p></td></tr>',
            '<tr><td><form class="ConfigForm"><table id="SettingsTable">',
            '<tr><td><input type="checkbox" id="sendShipData"/></td><td><label for="sendShipData"><span class="simptip-position-top simptip-smooth simptip-multiline simptip-info" data-tooltip="Send your ship data you have on a specific player to another player. Data will be integrated in their Ship List if they accept."></span>Send Ship Data to another player.</label></td></tr>',
            '</table></form></td></tr>',

            '<tr><td><p><strong>Update Options</strong></p></td></tr>',
            '<tr><td><form class="ConfigForm"><table id="SettingsTable">',
            '<tr><td><input type="checkbox" ' + (this.lastTurn == vgap.nowTurn ? 'disabled ' : '') + 'id="updateMissingTurns"/></td><td><label for="updateMissingTurns"><span class="simptip-position-top simptip-smooth simptip-multiline simptip-info" data-tooltip="Add all data from missing turns to the ship list. ' +
            '(Your list contains data up to turn ' + this.lastTurn + '.) ' +
            'More recent turns will be automatically processed.">Update all missing turns.</span></label></td></tr>',
            '<tr><td><input type="checkbox" id="rebuildFromCurrent"/></td><td><label id="ttReset" for="rebuildFromCurrent"><span class="simptip-position-top simptip-smooth simptip-multiline simptip-warning" data-tooltip="Empty the ship list and rebuild it with information from this turn. ' +
            'More recent turns will not be processed. All custom entries will be deleted.">Rebuild ship list from current turn.</span></label></td></tr>',
            '<tr><td><input type="checkbox" id="rebuildFromScratch"/></td><td><label id="ttRebuild" for="rebuildFromScratch"><span class="simptip-position-top simptip-smooth simptip-multiline simptip-warning" data-tooltip="Empty the ship list and rebuild it starting from the ' +
            'first turn. More recent turns will be automatically processed. All custom entries will be deleted.">Rebuild ship list from the first turn.</span></label></td></tr>',
            '</table></form></td></tr>',
            '<tr><td><p><strong>Usage Notes</strong></p></td></tr>',
            '<tr><td><p>The plugin records all ships seen throughout the game. A starmap tool allows you to draw circles on the map where ships were last seen. The plugin needs to be activated for each game/player combination, and on each computer on which you use it.</p>',
            '<p>Plugin data is saved both to local storage and to the server, meaning it is shared between computers.</p></td></tr>',
            '<tr><td><p><strong>Limitations</strong></p></td></tr><tr><td><p>Ships destroyed by mine hits or just listed in "Explosion" reports are not removed since ship id cannot be determined.</p>' +
            '<p>Warships without beam weapons, technically freighters, will be shown as warships, since NU internally stores unknown beams as 0. </p>' +
            '<p>To make a backup of your data, use the "Export" function found under "Storage".</p></td></tr>',
            '<tr><td><p><strong>Credits</strong></p></td></tr>',
            '<tr><td>',
            '<p><a href="http://planets.nu/#/account/kedalion" target="_blank" style="color:#00F7FF">Kedalion</a> - the writer of the original Enemy Ship List plugin.</p>' +
            '<p><a href="http://planets.nu/#/account/mcnimble" target="_blank" style="color:#00F7FF">McNimble</a> - for the help in saving ship info to the server.</p>' +
            '</td></tr>',
            '<tr><td><p><strong>Bugs / Feature Requests:</strong></p></td></tr>',
            '<tr><td><p>Drop me a line anytime on NU: <a href="http://planets.nu/#/account/space+pirate+harlock" target="_blank" style="color:#00F7FF">http://planets.nu/#/account/space+pirate+harlock</a>.</p></td></tr>'
        ];
        /*
         rows.push('<tr><td><strong>Turn:</strong><select id="rebuildStartTurn" style="width:80px"></select></td></tr>');
         rows.push('<tr><td><div class="BasicFlatButton" onclick="' +
         'vgap.plugins.shipList.enable().rebuildShips($(\'#rebuildStartTurn\').val());">' +
         'Rebuild</div></td></tr>');
         const firstTurn = (vgap.settings.acceleratedturns > 0 ? vgap.settings.acceleratedturns : 1);
         const nowTurn = vgap.nowTurn;

         for (let i = firstTurn; i <= nowTurn; i++) {
         $('#rebuildStartTurn').append('<option value="' + i + '">' + i + '</option>')
         }
         */
        target.append(
            '<div id="MessageInbox"><table id="ConfigTable">' +
            rows.join('') +
            '</table></div>'
        );

        const accel = vgap.settings.acceleratedturns;

        $('#showLocationHistory').click((function ()
        {
            this.toggleLocationHistory();
        }).bind(this));
        $('#showVerticalButtons').click((function ()
        {
            this.toggleVerticalButtons();
        }).bind(this));
        $('#showColoredText').click((function ()
        {
            this.toggleColoredText();
        }).bind(this));
        $('#showCompactTable').click((function ()
        {
            this.toggleCompactTable();
        }).bind(this));
        $('#addShipHistory').click((function ()
        {
            this.toggleAddShipHistory();
        }).bind(this));
        $('#deleteAfterImport').click((function ()
        {
            this.toggleDeleteAfterImport();
        }).bind(this));
        $('#toggleDebugMode').click((function ()
        {
            this.toggleDebugMode();
        }).bind(this));

        $('#sendShipData').click((function()
        {
            const dialog = $(this.templater.get('dataSend', {list: vgap.players}));
            nu.modal(dialog, 'Send Ship Data', 450);

            dialog.find('#cancel').tclick(nu.closemodal);
            dialog.find('#yes').tclick((function ()
            {
                this.sendShipData(dialog.find('#on').val(), dialog.find('#to').val(), vgap.player.id, nu.closemodal);
            }).bind(this));
/*            dialog.find('#yes').tclick((function (on, to)
            {
                this.sendShipData(dialog.find('#on').val(), dialog.find('#to').val(), vgap.player.id, nu.closemodal);
            }).bind(this, dialog.find('#on').val(), dialog.find('#to').val()));
*/        }).bind(this));

        $('#updateMissingTurns').click((function ()
        {
            this.initLoop();
        }).bind(this));
        $('#rebuildFromCurrent').click((function ()
        {
            this.enable().resetShips().showShips(7);
        }).bind(this));
        $('#rebuildFromScratch').click((function ()
        {
            this.enable().rebuildShips(accel > 0 ? accel : 1);
        }).bind(this));

        if (vgap.game.turn < accel)
        {
            $('#rebuildFromCurrent').prop('disabled', true);
            $('#rebuildFromScratch').prop('disabled', true);
            const txt = 'This game has "Accelerated Start" enabled. The plugin can only be enabled once you reach turn ' + accel + '.'
            $('#ttReset').prop('data-tooltip', txt);
            $('#ttRebuild').prop('data-tooltip', txt);
        }

        target.jScrollPane();
        // fix annoying scroll up behavior
//                $('#rebuildStartTurn').off('focus');
        $('.ConfigForm *').off('focus');
        vgap.action();
        return this;
    };

    this.processShipData = function(data)
    {
        /** @todo add version check and abort if incompatible */

        const ships = data.ships;

        for (let i = ships.length - 1; i >= 0; i--)
        {
            let ship = ships[i];
            let shipIdx = this.shipIds.indexOf(ship.id);

            // nasty stuff - what to merge?
            if (shipIdx != -1)
            {
                let listShip = this.ships[shipIdx];

                // ship.infoturn >= listShip.infoturn - update with everything not unknown

                if (ship.infoturn >= listShip.infoturn)
                {
                    // ammo could be 0 after vcr, so keep new value
                    //ship.ammo
                    ship.beamid = ship.beamid ? ship.beamid : listShip.beamId;
                    ship.beams = ship.beams ? ship.beams : listShip.beams;
                    ship.crew = ship.crew ? ship.crew : listShip.crew;
                    ship.damage = ship.damage != -1 ? ship.damage : listShip.damage;
                    ship.engineid = ship.engineid ? ship.engineid : listShip.engineid;
                    //ship.heading
                    //ship.mass
                    //ship.name
                    //ship.ownerid
                    //ship.targetx
                    //ship.targety
                    ship.torpedoid = ship.torpedoid ? ship.torpedoid : listShip.torpedoid;
                    ship.torps = ship.torps ? ship.torps : listShip.torps;
                    ship.visible = false;
                    //ship.warp
                    //ship.x
                    //ship.y
                } else {
                    ship.ammo = listShip.ammo ? listShip.ammo : ship.ammo;
                    ship.beamid = listShip.beamId ? listShip.beamId : ship.beamid;
                    ship.beams = listShip.beams ? listShip.beams : ship.beams;
                    ship.crew = listShip.crew ? listShip.crew : ship.crew;
                    ship.damage = listShip.damage != -1? listShip.damage : ship.damage;
                    ship.engineid = listShip.engineid ? listShip.engineid : ship.engineid;
                    ship.heading = listShip.heading;
                    ship.mass = listShip.mass;
                    ship.name = listShip.name;
                    ship.ownerid = listShip.ownerid;
                    ship.targetx = listShip.targetx;
                    ship.targety = listShip.targety
                    ship.torpedoid = listShip.torpedoid ? listShip.torpedoid : ship.torpedoid;
                    ship.torps = listShip.torps ? listShip.torps : ship.torps;
                    ship.visible = listShip.visible;
                    ship.warp = listShip.warp;
                    ship.x = listShip.x;
                    ship.y = listShip.y;
                }

                // merge history

                ship.history = ship.history.concat(listShip.history);

                ship.history.sort(function (a, b)
                {
                    return (b.turn - a.turn);
                });

                let prev = 0;

                for (let j = ship.history.length - 1; j >= 0; j-- )
                {
                    let turn = ship.history[j].turn;

                    if (ship.history[j].turn == prev)
                        ship.history.splice(j, 1);

                    prev = turn;
                }

                /** @todo update ship notes from history every turn */
            }

            this.saveShip(ship, true);
        }
    };

    this.checkShipData = function()
    {
        for (let i = vgap.activity.length - 1; i >= 0; i--)
        {
            let match = vgap.activity[i].message.exec(/^Ship Data on (\w+) from (\w+) JSON_START(.+)JSON_END/);
            let data;
            let from;
            let on;

            if (match) {
                try
                {
                    data = JSON.parse(atob(match[3]));
                } catch(e) {
                    console.log('Ship List: [5010] (checkShipData) Invalid JSON.');
                    continue;
                }
            }

            on = match[1];
            from = match[2];

            // security check - no spoofing

            if (vgap.activity[i].sourceid != data.from)
            {
                let spoofer;
                for (let j = vgap.players.length - 1; j >= 0; j--)
                {
                    if (vgap.activity[i].sourceid == vgap.players[j].accountid)
                    {
                        spoofer = vgap.players[j];
                        this.settings.acceptShipData[spoofer.id] = false;
                        nu.info([
                            'Player ', spoofer.username, ' tried to send you ship data posing as ', from,
                            '. They have been blacklisted from sending you ship data again.',
                            'Draw your own conclusions...'
                        ].join(''), 'Ship List: [4003] Cheating Alert!', 400);
                        break;
                    }

                }
            }

            if (this.settings.acceptShipData[data.from] == false) continue;
            if (this.settings.acceptShipData[data.from] == true)
            {
                this.processShipData(data);
                continue;
            }

            const dialog = $(this.templater.get('dataAccept', {on: match[1], from: match[2]}));
            nu.modal(dialog, 'Accept Ship Data', 450);

            dialog.find('#cancel').tclick((function () {
                if (dialog.find('#ignore').prop('checked'))
                {
                    this.settings.acceptShipData[data.from] = false;
                }
                nu.closemodal();
            }).bind(this));
            dialog.find('#yes').tclick((function (on, to)
            {
                if (dialog.find('#accept').prop('checked'))
                {
                    this.settings.acceptShipData[data.from] = true;
                }
                this.processShipData(data);
                nu.closemodal();
            }).bind(this, dialog.find('#on').val(), dialog.find('#to').val()));


        }
    };

    this.sendShipData = function(on, to, from, callback)
    {
        const ships = [];

        for (let i = this.ships.length - 1; i >= 0; i--)
        {
            if (this.ships[i].ownerid == on) ships.push(this.ships[i]);
        }

        let json = {
            from: from,
            version: this.version,
            ships: ships
        };

        vgap.postGameMessage([
            'Ship Data on ', vgap.players[on-1].username, ' from ', vgap.players[from-1].username, '\n\n',
            'JSON_START',
            btoa(JSON.stringify(json)),
            'JSON_END'
        ].join(''), null, to);

        if(callback) callback();
    }

    this.showShipForm = function (target, view, playerId)
    {
        if (!(view == 6) || !this.editMode) return this;

        let engineTypes = [ ...vgap.engines];
        engineTypes.unshift({id: 0, name: 'Unknown'});
        let beamTypes = [ ...vgap.beams];
        beamTypes.unshift({id: 0, name: 'Unknown'});
        let torpTypes = [ ...vgap.torpedos];
        torpTypes.unshift({id: 0, name: 'Unknown'});

        target.append([
            '<div id="ShipEditPane">',
            '<table id="ShipEditTable">',
            '<tr><th>Id</th><th>Hull</th><th>Name</th></tr>',
            '<tr><td><select id="shipId"><optgroup id="optGroupOwn" label="Owned Ships"/><optgroup id="optGroupNew" label="Unknown Ships"/></select></td><td><select id="shipHullId"/></td><td><input id="shipName" size="30"></td></tr>',
            '<tr><th></th><th>Engine(s)</th></tr>',
            '<tr><td rowspan="3"><input id="shipVisible" type="hidden" value="false"><input id="shipHistory" type="hidden" value="[]"><input id="shipOwnerId" type="hidden" value="' + playerId + '"><img id="hullImg"/></td><td>',
            this.templater.get('selectBox',{id: 'shipEngineId', options: engineTypes}),
            '</td></tr>',
            '<tr><th>Beam Type</th><th>Beam #</th></tr>',
            '<tr><td>',
            this.templater.get('selectBox',{id: 'shipBeamId', options: beamTypes}),
            '</td><td><input id="shipBeams" size="4"></td></tr>',
            '<tr><th></th><th>Torp Type</th><th>Tube #</th></tr>',
            '<tr><td></td><td>',
            this.templater.get('selectBox',{id: 'shipTorpedoId', options: torpTypes}),
            '</td><td><input id="shipTorps" size="4"></td></tr>',
            '<tr><th rowspan="2"></th><td colspan="2"><table id="ShipMiscTable">',
            '<tr><th>Turn</th><th>X</th><th>Y</th><th>Heading</th><th>Warp</th><th>Ammo</th><th>Mass</th><th>Crew</th><th>Dmg</th></tr>',
            '<tr><td><input id="shipInfoTurn" size="4" value="' + vgap.game.turn + '"></td><td><input id="shipX" size="4" value="2000"></td><td><input id="shipY" size="4" value="2000"></td>',
            '<td><input id="shipHeading" size="4" value="0"></td><td><input id="shipWarp" size="4" value="0"></td><td><input id="shipAmmo" size="4" value="0"></td>',
            '<td><input id="shipMass" size="4" value="0"></td><td><input id="shipCrew" size="4" value="0"></td><td><input id="shipDamage" size="4" value="0"></td></tr>',
            '</table></td></tr>',
            '<tr><td colspan="3"><div id="shipSaveButton" class="BasicFlatButton">Save</div>',
            '<div id="shipResetButton" class="BasicFlatButton">Reset</div></td></tr>',
            '</table></div>'
        ].join(''));

        /*
         * Select Box Fillers
         */

        let player = vgap.getPlayer(playerId);
        let race = vgap.getRace(player.raceid);
        let shipForm = $('#newShipForm');
        let shipIdSelect = $('#shipId');
        let shipHullSelect = $('#shipHullId');
        let shipEngineSelect = $('#shipEngineId');
        let shipBeamSelect = $('#shipBeamId');
        let shipTorpSelect = $('#shipTorpedoId');
        let optGroupNew = $('#optGroupNew');
        let optGroupOwn = $('#optGroupOwn');
        let options;

        let id = 1;
        for (let i = 0; i < this.ships.length; i++)
        {
            while (id < this.ships[i].id)
            {
                optGroupNew.append('<option value="' + id + '">' + id + '</option>');
                id++;
            }
            if (this.ships[i].ownerid == playerId)
            {
                optGroupOwn.append('<option value="' + id + '">' + id + '</option>');
            }
            id++;
        }

        while (id <= this.maxShipId)
        {
            optGroupNew.append('<option value="' + id + '">' + id + '</option>');
            id++;
        }
        shipIdSelect.val((optGroupNew).find(':first').val());

        options = [];
        let hulls = race.hulls.split(',');
        for (let i = 0; i < hulls.length; i++)
        {
            options.push('<option value="' + hulls[i] + '">' + vgap.getHull(hulls[i]).name + '</option>');
        }

        options.push('<option value="0">-------</option>');
        hulls = vgap.hulls;
        for (let i = 0; i < hulls.length; i++)
        {
            options.push('<option value="' + hulls[i].id + '">' + hulls[i].name + '</option>');
        }
        shipHullSelect.append(options.join(''));

        /*
         * Event Handlers
         */

        shipForm.change(this.changeShipForm);

        shipIdSelect.on('fill-form', this.fillShipForm.bind(this));
        shipIdSelect.change(function ()
        {
            $(this).trigger('fill-form', [$(this).val(), playerId]);
        });

        shipHullSelect.change(function ()
        {
            const val = $(this).val();
            // new ships - add name of hull
            if (val != 0 && !newShip.overwrite)
            {
                const hull = vgap.getHull(val);
                $('#shipName').val(hull.name);
                $('#shipBeams').val(hull.beams);
                $('#shipTorps').val(hull.launchers);
                $('#shipCrew').val(hull.crew);
                $('#hullImg')[0].src = hullImg(hull.id);
            }
        });

        // fill form when loading the page
        shipHullSelect.trigger('change');

        /** @this ShipList */
        $('#shipSaveButton').click((function (view, playerId)
        {
            shipForm.trigger('change');
            console.log('Ship List: [2017] (newShipForm) Saving Ship ' + newShip.ship.id + '.');
            console.log(newShip);
            this.saveShip(newShip.ship, newShip.overwrite).showShips(view, playerId);
        }).bind(this, view, playerId));

        $('#shipResetButton').click(function ()
        {
            shipForm.trigger('reset');
            // DUPLICATE CODE - REFACTOR
            $('#shipListPlayer').val(playerId);
            shipIdSelect.val(optGroupNew.find(':first').val());
            // hidden fields don't reset
            $('#shipHistory').val('[]');
            newShip = {overwrite: false, ship: {}};
            // update hull and name
            shipHullSelect.trigger('change');
        });

        return this;
    };

    this.fillShipForm = function (e, shipId, playerId)
    {

        shipId = parseInt(shipId);

        let shipIdx = this.shipIds.indexOf(shipId);

        if (shipIdx != -1)
        {
            let ship = this.ships[shipIdx];
            newShip.overwrite = true;
            $('#shipId').val(ship.id);
            $('#shipHullId').val(ship.hullid);
            $('#hullImg')[0].src = hullImg(ship.hullid);
            $('#shipName').val(ship.name);
            $('#shipHistory').val(JSON.stringify(ship.history));
            $('#shipEngineId').val(ship.engineid);
            $('#shipBeamId').val(ship.beamid);
            $('#shipBeams').val(ship.beams);
            $('#shipTorpedoId').val(ship.torpedoid);
            $('#shipTorps').val(ship.torps);
            $('#shipInfoTurn').val(ship.infoturn);
            $('#shipX').val(ship.x);
            $('#shipY').val(ship.y);
            $('#shipHeading').val(ship.heading > -1 ? ship.heading : '');
            $('#shipWarp').val(ship.warp > -1 ? ship.warp : '');
            $('#shipAmmo').val(ship.ammo);
            $('#shipMass').val(ship.mass);
            $('#shipCrew').val(ship.crew > -1 ? ship.crew : '');
            $('#shipDamage').val(ship.damage > -1 ? ship.damage : '');
            $('#shipVisible').val(ship.visible);
        } else
        {
            newShip.overwrite = false;
            $('#newShipForm').trigger('reset');
            $('#shipListPlayer').val(playerId);
            $('#shipId').val(shipId);
            // hidden fields don't reset
            $('#shipHistory').val('[]');
            $('#shipVisible').val('false');
            $('#shipHullId').trigger('change');
        }
        return this;
    };

    /**
     * Event handler for ship form change
     * @returns {ShipList}
     */
    this.changeShipForm = function ()
    {
        let ship = newShip.ship;

        ship.id = parseInt($('#shipId').val());
        ship.ownerid = parseInt($('#shipOwnerId').val());
        ship.hullid = parseInt($('#shipHullId').val());
        ship.engineid = parseInt($('#shipEngineId').val());
        ship.beamid = parseInt($('#shipBeamId').val());
        ship.beams = parseInt($('#shipBeams').val());
        ship.torpedoid = parseInt($('#shipTorpedoId').val());
        ship.torps = parseInt($('#shipTorps').val());
        ship.infoturn = parseInt($('#shipInfoTurn').val());
        ship.x = parseInt($('#shipX').val());
        ship.y = parseInt($('#shipY').val());
        ship.heading = parseInt($('#shipHeading').val());
        ship.warp = parseInt($('#shipWarp').val());
        ship.ammo = parseInt($('#shipAmmo').val());
        ship.mass = parseInt($('#shipMass').val());
        ship.crew = parseInt($('#shipCrew').val());
        ship.damage = parseInt($('#shipDamage').val());
        ship.visible = ($('#shipVisible').val() == 'true');

        for (let key in ship)
        {
            if (isNaN(ship[key])) ship[key] = -1;
        }

        ship.name = $('#shipName').val();
        ship.history = JSON.parse($('#shipHistory').val());
        console.log(newShip);

        return this;
    };

    /** Ship table header */
    this.showShipTableHeader = function (target, view, playerId)
    {

        let warshipsTotal = this.playerInfo(playerId, "capitalships");
        let freightersTotal = this.playerInfo(playerId, "freighters");
        let warshipsKnown = 0;
        let freightersKnown = 0;
        let rows = ['<div id="ShipPane">'];

        if (view == 6)
        {
            for (let i = this.ships.length - 1; i >= 0; i--)
            {
                let ship = this.ships[i];

                if (ship.ownerid != playerId) continue;

                let hull = vgap.getHull(ship.hullid);
                if (hull.beams == 0 && hull.launchers == 0 && hull.fighterbays == 0)
                {
                    freightersKnown++;
                } else
                {
                    warshipsKnown++;
                }
            }
        }

        // warship table header
        if (view == 6)
        {
            rows.push('<table><tr><td><p><strong>Warships (' + warshipsKnown + ' / ' + warshipsTotal + ')</strong></p></td></tr></table>');
        }

        rows.push(
            '<table id="ShipTable"' + (this.settings.showCompactTable ? ' class="compact"' : '') + '>',
            '<thead><tr>' + (this.editMode ? '<th></th>' : '') + '<th>ID</th><th>Player</th><th>Race</th><th>Hull</th><th>Name</th>',
            '<th></th><th>Location</th><th>Turn</th><th>Heading</th><th>Warp</th><th>Engine</th><th>Beams</th><th>Torps/Bays</th><th>Ammo</th>',
            '<th>Mass</th><th>Crew</th><th>Dmg</th></tr></thead>',
            '<tbody id="ShipRows"></tbody>',
            '</table>'
        );

        // freighter table header
        if (view == 6)
        {
            rows.push(
                '<table><tr><td><p><strong>Freighters (' + freightersKnown + ' / ' + freightersTotal + ')</strong></p></td></tr></table>',
                '<table id="FreighterTable"' + (this.settings.showCompactTable ? ' class="compact"' : '') + '>',
                '<thead><tr>' + (this.editMode ? '<th></th>' : '') + '<th>Id</th><th>Player</th><th>Race</th><th>Hull</th><th>Name</th><th></th>',
                '<th>Location</th><th>Turn</th><th>Heading</th><th>Warp</th><th>Engine</th><th>Beams</th><th>Torps/Bays</th><th>Ammo</th><th>Mass</th>',
                '<th>Crew</th><th>Dmg</th></tr></thead>',
                '<tbody id="FreighterRows"></tbody>',
                '</table>'
            );
        }

        rows.push('</div>');
        target.append(rows.join(''));

        return this;
    };

    /** Storage pane */
    this.showStorage = function (target, view)
    {

        target.append([
            '<div id="MessageInbox">',
            '<table id="ConfigTable"><tr><td><p><strong>Storage</strong></p></td></tr>',
            '<tr><td><p>The ', this.pluginName, ' plugin stores ship data locally on your machine. Since the amount of available storage varies, you may need to delete some stale game data to free up space.</p></td></tr>',
            '<tr><td><p><strong>Games</strong></p></td></tr><tr><td><table id="ShipTable"><tr>' +
            '<th>Game Id</th><th>Name</th><th>Slot</th><th>Player</th><th>Race</th><th>#1 Turn</th><th>Last Turn</th><th>Memory</th><th>Version</th><th></th>' +
            '</tr><tbody id="ShipRows"></tbody></table></td></tr><tr><td><p><strong>Import / Export</strong></p></td></tr>',
            '<tr><td><p>This allows you to export all game data on this computer, and to import it to another. Note that importing will delete all prior game data on the target machine. It is highly recommended to make regular backups, especially if using the bleeding edge versions of the plugin, which I highly encourage.</p></td></tr>',
            '<tr><td><div class="center"><div class="BasicFlatButton" onclick="vgap.plugins.shipList.exportGameData();">Export</div>',
            '<label for="fileInput"><div class="BasicFlatButton">Import</div></label><input id="fileInput" type="file" onchange="',
            'vgap.plugins.shipList.importGameData();" style="display:none"/></div></td></tr>',
            '</table>',
            '</div>'
        ].join(''));

        let rows = [];
        let totalSize = 0;

        for (let key in localStorage)
        {

            let prefix = this.storagePrefix;
            let re = new RegExp(prefix + '(\\d+)\\.(\\d+)\\.data');
            let match = re.exec(key);
            if (match)
            {
                let gameId = match[1];
                let playerId = match[2];
                let gameData = localStorage.getItem(prefix + match[1] + '.' + match[2] + '.data');
                let gameSize = gameData.length * 2;
                gameData = JSON.parse(gameData);
                totalSize += gameSize;

                rows.push('<tr>',
                    '<td>' + gameId + '</td>',
                    '<td>' + gameData.gameName + '</td>',
                    '<td>' + playerId + '</td>',
                    '<td>' + gameData.playerName + '</td>',
                    '<td>' + gameData.raceName + '</td>',
                    '<td>' + gameData.firstTurn + '</td>',
                    '<td>' + gameData.lastTurn + '</td>',
                    '<td>' + (gameSize / 1024).toFixed(2) + ' kB</td>',
                    '<td>' + gameData.version + "</td>",
                    '<td><div class="BasicFlatButton" onclick="vgap.plugins.shipList.deleteGame(' + gameId + ',' + playerId + ').showShips(' + view + ');">Delete</div></td>',
                    '</tr>'
                );
            }
        }

        rows.push('<tr><td>All games</td><td></td><td>All players</td><td></td><td></td><td></td><td></td>',
            '<td>' + (totalSize / 1024).toFixed(2) + ' kB</td><td></td>',
            '<td><div class="BasicFlatButton" onclick="vgap.plugins.shipList.deleteAllGames().showShips(' + view + ');">Delete&nbsp;All</div></td>',
            '</tr>'
        );

        const shipRows = $('#ShipRows').append(rows.join(''));
        shipRows.find('td:nth-child(4)').addClass('capitalize');
        $("#ShipTable").tablesorter();
        target.jScrollPane();

        vgap.action();
        return this;
    };

    /** Warning pane */
    this.showWarningPane = function (target, view)
    {

        if (this.hideWarningPane) return this;

        let table = target
                .append('<div id="WarningPane"></div>').children(':last-child')
                .append('<table id="WarningTable"></table>').children(':last-child')
            ;

        $('#WarningPane').append('<div id="closeWarningPane" class="closeIcon"></div>');

        $('#closeWarningPane').click((function ()
        {
            $('#WarningPane').hide();
            this.hideWarningPane = true;
        }).bind(this));

        if (this.enabled)
        {
            table.append('<tr><th><p>The ship list contains data from turn ' + this.firstTurn + ' to turn ' + this.lastTurn + '</p></th></tr>');

            if (this.lastTurn > vgap.game.turn)
            {
                table.append('<tr><th><p class="warning">Total ships from turn (' + vgap.game.turn + '), known ships from turn (' + this.lastTurn + ')! More ships can be shown than the player had this turn.</p></th></tr>');
            }

            if (this.lastTurn < vgap.nowTurn)
            {
                table.append('<tr><th><p class="warning">This list does not include the most recent turns yet. ' +
                    'You will need to either manually go through all turns starting at turn ' + ((this.lastTurn) + 1) + ' or use "Auto Update". ' +
                    'More options are available under "Settings".</p><div class="center"><div class="BasicFlatButton" onclick="vgap.plugins.shipList.initLoop();"> Auto Update</div></div></th></tr>'
                );
            }
        } else
        {
            table.append('<tr><th><p class="warning">Plugin is not enabled for this game/player combination on this machine.</p></th></tr>');

            if (view != 7)
            {

                if (vgap.nowTurn < vgap.settings.acceleratedturns)
                {
                    table.append('<tr><th>This game has "Accelerated Start" enabled. The plugin can only be enabled once you reach turn ' + vgap.settings.acceleratedturns + '.</tr></th>');
                } else
                {
                    table.append('<tr><td><p>To enable the plugin, select one of the options below.</p></td></tr>');

                    if (!vgap.settings.acceleratedturns || vgap.game.turn >= vgap.settings.acceleratedturns)
                    {
                        table.append('<tr><th><div class="BasicFlatButton" onclick="' +
                            /** @namespace shipList.resetShips */
                            'vgap.plugins.shipList.enable().resetShips().showShips(' + view + ');">' +
                            'Initialize</div><span>Initialize list from the current turn only.</span></th></tr>');
                    }
                    table.append('<tr><th><div class="BasicFlatButton" onclick="' +
                        /** @namespace plugin.rebuildShips */
                        'vgap.plugins.shipList.enable().rebuildShips(' +
                        (vgap.settings.acceleratedturns > 0 ? vgap.settings.acceleratedturns : 1) + ');"> ' +
                        'Build</div><span>Build list starting from turn ' + (vgap.settings.acceleratedturns > 0 ? vgap.settings.acceleratedturns : 1) +
                        '</span></th></tr>');
                }
            }
        }

        return this;
    };

    /** MAP DRAWING */

    /** PERSISTENCE */

    /**
     * Load plugin data from storage or initialize empty list
     * @returns {ShipList}
     */
    this.load = function ()
    {

        /**
         * get from VGAP note
         * will not work when looping - notes in time machine don't get written
         * so disabled in earlier turns
         */
        if (vgap.game.turn == vgap.settings.turn && !this.doLoop)
        {
            let data = this.fromNote('data');

            if (data)
            {
                this.ships = data.ships;
                this.settings = data.settings;
                this.firstTurn = data.firstTurn;
                this.lastTurn = data.lastTurn;
                this.enabled = true;
                return this;
            }
        } else
        {
            console.log('Ship List: [2022] (load) Time Machine: Notes Disabled.')
        }

        // get from Local Storage
        let str = this.fromStorage('data');

        if (str)
        {
            try
            {
                if (this.settings.debugMode) console.log(str);
                let data = JSON.parse(str);
                this.ships = data.ships;
                this.settings = data.settings;
                this.firstTurn = data.firstTurn;
                this.lastTurn = data.lastTurn;
                this.enabled = true;
                console.log('Ship List: [2001] (load) Loaded Game Data from Local Storage.')
                this.save(); // toNote
            } catch (e)
            {
                console.log('Ship List: [4001] (load) Corrupt Local Storage Game Data.');
                console.log(str);
            }
            if (this.settings.debugMode) console.log(this);
            return this;
        }

        // compatibility mode
        console.log('Ship List: [2002] (load) Compatibility Mode.')
        this.storagePath = this.importPrefix + vgap.gameId + '.' + vgap.player.id + '.';

        const stored_version = this.fromStorage('version');
        const stored_list = this.fromStorage('shiplist');
        const stored_turn = this.fromStorage('turn');
        const first_turn = this.fromStorage('firstturn');

        if (stored_version)
        {
            this.enabled = true;
            this.ships = JSON.parse(stored_list);
            this.lastTurn = parseInt(stored_turn);
            this.firstTurn = parseInt(first_turn);

            if (stored_version < 1.40)
            {
                for (let i = this.ships.length - 1; i >= 0; i--)
                {
                    this.ships[i].history = [];
                }
            }
            this.doImport = true;
            console.log('Ship List: [2003] (load) Converting Game Data.');
            if (this.settings.deleteAfterImport) this.deleteGame(vgap.gameId, vgap.player.id);
            this.storagePath = this.storagePrefix + vgap.gameId + '.' + vgap.player.id + '.';
            this.save();
            this.doImport = false;
        } else
        {
            this.storagePath = this.storagePrefix + vgap.gameId + '.' + vgap.player.id + '.';
            console.log('Ship List: [2004] (load) No Game Data Found.');
            this.reInitialize();
        }

        if (this.settings.debugMode) console.log(this);
        return this;
    };

    /**
     * Save plugin data to local storage
     * @returns {ShipList}
     */
    this.save = function ()
    {

        if (!this.enabled) return this;

        console.log('Ship List: [2018] (save) Saving Game Data.')

        const data = {
            version: this.version,
            ships: this.ships,
            firstTurn: this.firstTurn,
            lastTurn: this.lastTurn,
            gameName: this.gameName,
            playerName: this.playerName,
            raceName: this.raceName,
            settings: this.settings
        };

        if (this.settings.debugMode) console.log(data);

        // saving notes in time machine does f*all
        if (vgap.game.turn == vgap.nowTurn)
        {
            this.toNote('data', data);
        } else
        {
            console.log('Ship List: [2021] (save) Time Machine: Notes Disabled.')
        }

        const old = this.fromStorage('data');

        try
        {
            this.toStorage('data', JSON.stringify(data));
        } catch (e)
        {
            this.toStorage(old);

            // abort loop
            if (this.doLoop)
            {
                this.doLoop = false;
            }
            nu.info([
                'Your browser has run out of Local Storage space. ',
                'Please go to the "Storage" tab to delete data, ',
                'then try re-enabling the plugin.'
            ].join(''), this.pluginName + ' Error 4002: No Storage Available.', 400);
        }

        return this;
    };

    /**
     * Get from local storage
     * @param key
     * @returns {*}
     */
    this.fromStorage = function (key)
    {
        if ($.isPlainObject(key))
        {
            const self = this;
            $.each(key, function (k, v)
            {
                v = localStorage.getItem(self.storagePath + k);
            });
            return key;
        }
        return localStorage.getItem(this.storagePath + key);
    };

    /**
     * Save to local storage
     * @param key
     * @param data
     * @returns {*}
     */
    this.toStorage = function (key, data)
    {
        // hash
        if ($.isPlainObject(key))
        {
            const self = this;
            $.each(key, function (k, v)
            {
                localStorage.setItem(self.storagePath + k, v);
            });
            return key;
        }
        // 2 arguments
        localStorage.setItem(this.storagePath + key, data);

        return data;
    };

    /** Thanks to McNimble for the pointers to using notes. */

    /** Get data from VGAP notes */
    this.fromNote = function (key)
    {
        console.log('Ship List: [1005] (fromNote) Loading VGAP Note.');

        try
        {
            let noteId = 0;
            let arr = [];

            let chunks = JSON.parse(vgap.getNote(noteId--, this.noteType).body).chunks;

            for (let i = 0; i < chunks; i++)
            {
                arr.push(vgap.getNote(noteId--, this.noteType).body);
            }

            let obj = JSON.parse(arr.join(''));

            console.log('Ship List: [2005] (fromNote) VGAP Note Read Success.');

            return obj[key];
        } catch (e)
        {
            console.log('Ship List: [5005] (fromNote) VGAP Note Read Error.');
            console.log(e.name + ' ' + e.message);
            console.trace();
        }
    };

    /** Save data to VGAP notes
     * @param key
     * @param value
     * @returns {ShipList}
     */
    this.toNote = function (key, value)
    {
        console.log('Ship List: [1006] (toNote) Saving VGAP Note.');
        try
        {
            let noteId = 0;
            let obj = {};
            obj[key] = value;

            let match = JSON.stringify(obj).match(/.{1,16384}/g);
            let chunks = match.length;

            let note = vgap.getNote(noteId--, this.noteType);
            note.body = JSON.stringify({chunks: chunks});
            note.changed = 1;

            for (let i = 0; i < chunks; i++)
            {
                note = vgap.getNote(noteId--, this.noteType);
                note.body = match[i];
                note.changed = 1;
            }

            vgap.save();
            console.log('Ship List: [2006] (toNote) VGAP Note Save Success.');
        } catch (e)
        {
            console.log('Ship List: [5006] (toNote) VGAP Note Save Error.');
            console.log(e.name + ' ' + e.message);
            console.trace();
        }
        return this;
    };

    /** UI HANDLERS / UTILITIES */

    /**
     * Delete data for a given game and player
     * @param   gameId         int
     * @param   playerId       int
     * @returns {ShipList}
     */
    this.deleteGame = function (gameId, playerId)
    {

        console.log('Ship List: [2019] (deleteGame) Deleting Game Data.');

        const path = this.storagePrefix + '.' + gameId + '.' + playerId + '.';

        if (this.doImport)
        {
            let keys = ['version', 'shiplist', 'turn', 'firstturn', 'player', 'race', 'game_name'];
            for (let i = keys.length - 1; i >= 0; i--)
            {
                localStorage.removeItem(path + keys[i]);
            }
        } else
        {
            localStorage.removeItem(path + data);
        }

        if (gameId == vgap.gameId && playerId == vgap.player.id)
        {
            this.reInitialize();
            this.enabled = false;
        }

        return this;
    };

    /**
     * Delete all game data from local storage
     * @returns {ShipList}
     */
    this.deleteAllGames = function ()
    {

        console.log('Ship List: [2020] (deleteAllGames) Deleting Game Data.');

        let prefix = this.storagePrefix;
        let re = new RegExp(prefix + '\\d+\\.\\d+\\.data');

        for (let key in localStorage)
        {
            if (re.test(key)) localStorage.removeItem(key);
        }
        return this;
    };

    /**
     * Delete a ship (ship form)
     * @param id    id of ship to remove
     * @returns {ShipList}
     */
    this.deleteShip = function (id)
    {
        for (let i = this.ships.length - 1; i >= 0; i--)
        {
            if (this.ships[i].id == id)
            {
                this.ships.splice(i, 1);
                break;
            }
        }
        this.shipIds = this.ships.map(function (ship)
        {
            return ship.id;
        });
        this.save();
        return this;
    };

    /**
     * Enable plugin
     * @returns {ShipList}
     */
    this.enable = function ()
    {
        /** @todo Remove Local Storage check once we switch entirely to notes */
        try
        {
            localStorage.setItem(this.storagePrefix, this.version);
        } catch (err)
        {
            nu.info([
                'Your browser has run out of Local Storage space. ',
                'Please go to the "Storage" tab to delete data, ',
                'then try re-enabling the plugin.'
            ].join(''), this.pluginName + ' Error 4001: No Storage Available.', 400);
        } finally
        {
            localStorage.removeItem(this.storagePrefix);
        }
        this.enabled = true;
        return this;
    };

    /** Exports all game data to JSON */
    this.exportGameData = function ()
    {

        const pluginData = {};

        for (let key in localStorage)
        {
            if (key.indexOf(this.storagePrefix) != -1)
            {
                pluginData[key] = localStorage.getItem(key);
            }
        }

        let blob = new Blob([JSON.stringify(pluginData)], {type: 'text/json;charset=utf-8'});
        /** @namespace window.URL.createObjectURL */
        let url = window.URL.createObjectURL(blob);
        let a = $('<a style="display:none" href="' + url + '" download="ShipList.json"></a>')
                .appendTo($('#MessageInbox'))
            ;
        // jQuery click doesn't work for default event
        a[0].click();
        a.remove();
        /** @namespace window.URL.revokeObjectURL */
        window.URL.revokeObjectURL(url);
    };

    /** Import JSON game data */
    this.importGameData = function ()
    {

        console.log('Ship List: [2007] (importGameData) Starting Import.');

        let fileInput = $('#fileInput');

        if (fileInput[0].files.length == 0) return;

        const file = fileInput[0].files[0];
        const read = new FileReader();
        const self = this;

        // reset fileInput
        fileInput.val('');

        read.onload = function ()
        {
            let obj;

            try
            {
                obj = JSON.parse(read.result.toString())
            }
            catch (err)
            {
                nu.info([
                    file.name + ' does not contain valid JSON data. ',
                    'Please try again with a correct game export file.'
                ].join(''), 'Ship List: [4002] Invalid JSON.', 400);
                return;
            }
            console.log(obj);

            // load saved plugin data
            const pluginData = {};
            for (let key in localStorage)
            {
                if (key.indexOf(self.storagePrefix) != -1)
                {
                    pluginData[key] = localStorage.getItem(key);
                }
            }

            // remove all plugin data
            self.deleteAllGames();

            // save uploaded data
            let err;
            $.each(obj, function (key, val)
            {
                if (key.indexOf(self.storagePrefix) != 0)
                {
                    err = true;
                    return false;
                }
                localStorage.setItem(key, val);
                return true;
            });

            // revert if an error occurred

            if (err)
            {
                self.deleteAllGames();
                self.enabled = true;
                $.each(pluginData, function (key, val)
                {
                    localStorage.setItem(key, val);
                });

                nu.info([
                    file.name + ' does not contain valid Ship List data. ',
                    'Please try again with a correct game export file.'
                ].join(''), 'Ship List: [4003] Invalid Game Data.', 400);
                return;
            }

            self.enabled = true;
            self.showShips(8);
        };
        read.readAsText(file);
    };

    /**
     * Start looping through turns
     * @param turn
     * @returns {ShipList}
     */
    this.initLoop = function (turn)
    {

        if (!turn) turn = this.lastTurn + 1;
        if (turn < 1 || turn > vgap.nowTurn) return this;

        if (turn == vgap.nowTurn)
        {
            vgap.loadNow();
        } else
        {
            this.doLoop = true;
            console.log('Ship List: [2015] (initLoop) Looping from turn ' + turn + ' to ' + vgap.nowTurn + '.');
            vgap.loadHistory(turn);
        }

        return this;
    };

    /**
     * get scoreboard info for player
     * @param id
     * @param type
     * @returns {*}
     */
    this.playerInfo = function (id, type)
    {
        for (let i = vgap.scores.length - 1; i >= 0; i--)
        {
            if (vgap.scores[i].ownerid == id)
            {
                return vgap.scores[i][type];
            }
        }
        return 0;
    };

    /**
     * Empty ship list and update from given turn
     * @param turn  int
     * @returns     {ShipList}
     */
    this.rebuildShips = function (turn)
    {

        console.log('Ship List: [2009] (rebuildShips) Updating from turn ' + turn + '.');

        if (!turn) turn = vgap.game.turn;

        if (turn > vgap.nowTurn || turn < 1) return this;

        this.doReset = true;
        this.initLoop(turn);

        return this;
    };

    /**
     * Reset game defaults
     * @returns {ShipList}
     */
    this.reInitialize = function ()
    {
        console.log('Ship List: [2016] (reInitialize) Reinitializing.');
        this.ships = [];
        this.firstTurn = vgap.game.turn;
        this.lastTurn = vgap.game.turn - 1;
        this.maxShipId = vgap.settings.shiplimit;
        if (this.settings.debugMode) console.log(this);
        return this;
    };

    /**
     * Empty ship list and update from current turn
     * @returns {ShipList}
     */
    this.resetShips = function ()
    {

        console.log('Ship List: [2008] (resetShips) Updating from current turn.');

        this.ships = [];
        this.firstTurn = vgap.game.turn;
        this.lastTurn = vgap.game.turn - 1;

        this.updateShips();

        vgap.showDashboard();
        return this;
    };

    /**
     * Save a ship (ship form)
     * @param ship        {*} ship
     * @param update        boolean
     * @returns {ShipList}
     */
    this.saveShip = function (ship, update)
    {

        const shipIdx = this.shipIds.indexOf(ship.id);

        if (shipIdx != -1 && update)
        {
            this.ships.splice(shipIdx, 1);
        }

        this.ships.push(ship);

        this.ships.sort(function (a, b)
        {
            return (a.id - b.id);
        });

        this.shipIds = this.ships.map(function (ship)
        {
            return ship.id;
        });

        this.save();

        return this;
    };

    /** TOGGLES */

    /** Toggles adding ship history to notes
     * @returns {ShipList}
     */
    this.toggleAddShipHistory = function ()
    {
        this.settings.addShipHistory = !this.settings.addShipHistory;
        return this.save();
    };

    /** Toggles between colored text and background
     * @returns {ShipList}
     */
    this.toggleColoredText = function ()
    {
        this.settings.showColoredText = !this.settings.showColoredText;
        return this.save();
    };

    /** Toggles compact tables */
    this.toggleCompactTable = function ()
    {
        this.settings.showCompactTable = !this.settings.showCompactTable;
        return this.save();
    };

    /** Toggles debug mode */
    this.toggleDebugMode = function ()
    {
        this.settings.debugMode = !this.settings.debugMode;
        return this.save();
    };

    /** Toggles removal of older data */
    this.toggleDeleteAfterImport = function ()
    {
        this.settings.deleteAfterImport = !this.settings.deleteAfterImport;
        return this.save();
    };

    /** Toggles edit in single player view */
    this.toggleEdit = function ()
    {
        this.editMode = !this.editMode;
        return this.save();
    };

    /** Toggles full allies */
    this.toggleFullAllies = function ()
    {
        this.settings.showFullAllies = !this.settings.showFullAllies;
        return this.save();
    };

    /** Toggles between colored text and background */
    this.toggleLocationHistory = function ()
    {
        this.settings.showLocationHistory = !this.settings.showLocationHistory;
        return this.save();
    };

    /** Toggles own ships */
    this.toggleOwnShips = function ()
    {
        this.settings.showOwnShips = !this.settings.showOwnShips;
        return this.save();
    };

    /** Toggles safe passage */
    this.toggleSafePassage = function ()
    {
        this.settings.showSafePassage = !this.settings.showSafePassage;
        return this.save();
    };

    /** Toggles share intel */
    this.toggleShareIntel = function ()
    {
        this.settings.showShareIntel = !this.settings.showShareIntel;
        return this.save();
    };

    /** Toggles unknown ships */
    this.toggleUnknown = function ()
    {
        this.settings.showUnknown = !this.settings.showUnknown;
        return this.save();
    };

    /** Toggles between horizontal and vertical buttons */
    this.toggleVerticalButtons = function ()
    {
        this.settings.showVerticalButtons = !this.settings.showVerticalButtons;
        this.drawer.settings.showVerticalButtons = this.settings.showVerticalButtons;
        return this.save();
    };

}; // end ShipList

const Templater = function()
{
    this.get = function(template, data)
    {
       return Mustache.render(this.templates[template], data);
    }

    this.templates = {
        dataAccept: [
            '<form><table>',
            '<tr>',
            '<td><label>Accept ship data on {{on}}<br/>',
            'Sent by {{from.name}}</label></td>',
            '<td></td>',
            '</tr><tr>',
            '<td><input type="checkbox" id="accept" value="{{from.id}}"></td>',
            '<td><label for="eaccept">Always accept data from this player</label></td>',
            '</tr><tr>',
            '<td><input type="checkbox" id="ignore" value="{{from.id}}"></td>',
            '<td><label for="eignore">Always ignore data from this player</label></td>',
            '</tr>',
            '</table></form>' +
            '<div class="center">',
            '<span id="yes" class="button eadd">Yes</span>',
            '<span id="cancel" class="button enav">Cancel</span>',
            '</div>'
        ].join(''),
        dataSend: [
            '<form class="modalForm"><table>',
            '<tr>',
            '<td><label>Send Data on:</label></td>',
            '<td><div class="select"><select id="on">' +
            '{{#list}}<option value="{{id}}">{{fullname}}</option>{{/list}}</select></div></td>',
            '</tr><tr>',
            '<td><label>To Recipient:</label></td>',
            '<td><div class="select"><select id="to">',
            '{{#list}}<option value="{{id}}">{{fullname}}</option>{{/list}}',
            '</select></div></td>',
            '</table></form>' +
            '<div class="center">',
            '<span id="yes" class="button eadd">Yes</span>',
            '<span id="cancel" class="button enav">Cancel</span>',
            '</div>'
        ].join(''),
        checkBox:
            '<input id="{{id}}" type="checkbox"{{#checked}} checked{{/checked}} {{#click}} onclick="{{click}}"{{/click}}/>'
        ,
        selectBox: [
            '<select id="{{id}}"{{#change}} onchange="{{change}}"{{/change}}>',
            '{{#options}}<option value="{{id}}">{{name}}</option>{{/options}}',
            '</select>'
        ].join('')
    };
};

/**
 * @param {vgaPlanets}      vgap
 * @param {*}                settings
 * @type {DrawHelper}
 */
const DrawHelper = function (vgap, settings)
{
    this.drawRadius = 10;
    this.playerColors = ["#ffff00", "#0000ff", "#00ff00", "#ff0000", "#ffff00", "#ff00ff", "#0000ff", "#ff9900", "#00ff99", "#9900ff", "#cccc00", "#00cccc", "#cc00cc", "#ff9999", "#99ff99", "#9999ff", "#cc0044", "#44cc00", "#0044cc", "#999900", "#009999", "#990099", "#ee3300", "#00ee33", "#3300ee", "#ee0033", "#33ee00", "#0033ee", "#bb4444", "#44bb44", "#4444bb"];
    this.playerToggles = [];
    this.shipsDrawn = [];
    this.togglesShown = false;

    this.settings = settings;

    this.init = function ()
    {
        for (let i = 0; i <= vgap.players.length; i++)
        {
            this.playerToggles[i] = false;
        }

        // load colors from diplomacy and configuration settings

        for (let i = vgap.players.length - 1; i >= 0; i--)
        {
            let color = this.playerColors[i + 1];
            let id = vgap.players[i].id;
            if (id == vgap.player.id)
            {
                if (vgap.accountsettings.myshipto) color = vgap.accountsettings.myshipto;
            } else
            {
                let relation = vgap.getRelation(id);
                if (relation != null && relation.color && relation.color != "") color = '#' + relation.color;
            }
            this.playerColors[i + 1] = color;
        }

        return this;
    }

    this.addMapTool = function()
    {
        // add map tool

        if (vgap.isMobileVersion())
        {
            vgap.map.addMapTool('Ship List', 'shipListTool', this.toggleMapTool.bind(this));
        } else
        {
            vgap.map.addMapTool('Ship List', 'ShowMinerals', this.showSelectionPane.bind(this));
        }

        return this;
    }

    /**
     * Draw last location of ships
     * @param {[]}  ships
     */
    this.drawShips = function (ships)
    {

        let doDraw = false;

        for (let i = 0; i <= vgap.players.length; i++)
        {
            if (this.playerToggles[i])
            {
                doDraw = true;
                break;
            }
        }

        if (!doDraw) return;

        for (let i = ships.length - 1; i >= 0; i--)
        {
            const ship = ships[i];

            if (vgap.game.turn < ship.infoturn) continue;

            if (this.playerToggles[0] || this.playerToggles[ship.ownerid])
            {
                this.drawShipCircle(ship.x, ship.y, this.drawRadius, {stroke: this.playerColors[ship.ownerid]}, null);
            }
        }

        // mark visible ships in time machine
        if (vgap.game.turn < vgap.nowTurn)
        {
            for (let i = vgap.ships.length - 1; i >= 0; i--)
            {
                const ship = vgap.ships[i];

                if (this.playerToggles[0] || this.playerToggles[ship.ownerid])
                {
                    this.drawShipCircle(ship.x, ship.y, this.drawRadius, {stroke: this.playerColors[ship.ownerid]}, null);
                }
            }
        }

        return this;
    };

    /**
     * Select player ships on map
     * @param id
     */
    this.selectPlayer = function (id)
    {
        for (let i = this.playerToggles.length - 1; i >= 0; i--)
        {
            if (!this.playerToggles[i])
            {
                this.playerToggles[0] = false;
            }
        }
        this.playerToggles[id] = true;
        return this;
    };

    /**
     * Draw ship circle on map
     * @param x            int
     * @param y            int
     * @param radius       int
     * @param attr         {*}
     * @param ctx          context
     */
    this.drawShipCircle = function (x, y, radius, attr, ctx)
    {
        if (!vgap.map.isVisible(x, y, radius)) return;
        radius *= vgap.map.zoom;
        if (radius <= 1) radius = 1;
        if (ctx == null) ctx = vgap.map.ctx;
        ctx.strokeStyle = attr.stroke;
        ctx.lineWidth = 3;
        ctx.beginPath();
        ctx.arc(vgap.map.screenX(x), vgap.map.screenY(y), radius, 0, Math.PI * 2, false);
        ctx.stroke();
    };

    /**
     * Get ships in radius
     * @param {[]}              ships
     * @param {number}          x
     * @param {number}          y
     * @param {boolean}         includeVisible  include visible ships from the list
     * @returns {[]}
     */
    this.shipsAt = function (ships, x, y, includeVisible)
    {

        let at = [];

        for (let i = 0; i < ships.length; i++)
        {
            let ship = ships[i];

            if ((this.playerToggles[0] || this.playerToggles[ship.ownerid]) && (!ship.visible || includeVisible) && (Math.dist(x, y, ship.x, ship.y) <= this.drawRadius))
            {
                at.push(ship);
            }
        }
        return at;
    };

    /** Show ships from previous turns in pane (hover) */
    this.shipScan = function (ship)
    {
        const note = vgap.getNote(ship.id, 2);
        /** @todo Clean up nu.js code */

        if (vgap.isMobileVersion())
        {
            const hull = vgap.getHull(ship.hullid);
            const player = vgap.getPlayer(ship.ownerid);
            const race = vgap.getRace(player.raceid);

            let cls = "";
            if (ship.ownerid == vgap.player.id) cls = "MyItem";
            else if (vgap.allied(ship.ownerid)) cls = "AllyItem";
            else if (ship.ownerid != vgap.player.id) cls = "EnemyItem";

            let html = "<div class='ItemSelection ShipSeen " + cls + "' data-id='" + ship.id + "'>";
            html += "<img " + (ship.iscloaked ? "class='imgcloaked'" : "") + " src='" + hullImg(ship.hullid) + "'/>";

            let cloaked = "";
            if (ship.iscloaked) cloaked = "<div class='sval cloak' style='margin-right:3px;'></div>";

            html += "<div class='ItemTitle'><div class='sval warp'>" + ( ship.warp == -1 ? '?' : ship.warp ) + "</div>" + cloaked + Math.abs(ship.id) + ": " + ship.name + "</div>";
            html += "<div class='ItemTitle'>" + hull.name + "</div>";

            //if (ship.iscloaked)
            //    html += "<div class='sval cloak' style='margin-right: 50px;'></div>"

            //html += "<span class='" + cls + "'>" + hull.name + "</span>";
            // var tower = vgap.isTowTarget(ship.id);

            if (ship.ownerid != vgap.player.id && !vgap.fullallied(ship.ownerid) && !vgap.editmode)
            {
                const ago = vgap.game.turn - ship.infoturn;
                html += "<div><strong>Seen: Turn " + ship.infoturn + " (" + ago + " turn" + (ago > 1 ? "s" : "") + " ago)</strong></div>";
                html += "<hr/><div>" + race.shortname + "<br/>(" + player.username + ")</div>";
                if (ship.heading > 0)
                    html += "<div class='lval heading'>" + ship.heading + "</div>";
                html += "<div class='lval crew'>" + ( ship.crew == -1 ? '?' : ship.crew ) + "/" + hull.crew + "</div>";
                if (ship.damage > 0)
                    html += "<div class='lval damage'>" + ship.damage + "%</div>";
                if (hull.launchers > 0)
                    html += "<div class='lval torpedo'>" + ship.ammo + "</div>";
                if (hull.fighterbays > 0)
                    html += "<div class='lval fighters'>" + ship.ammo + "</div>";
                let minMass = ship.mass;
                let maxMass = minMass;
                for (let i = ship.history.length - 1; i >= 0; i--)
                {
                    if (ship.history[i].mass < minMass) minMass = ship.history[i].mass;
                    if (ship.history[i].mass > maxMass) maxMass = ship.history[i].mass;
                }
                html += "<div class='lval mass'>" + ship.mass + " kt" +
                    (minMass != ship.mass || maxMass != ship.mass ? ' (' + minMass + '-' + maxMass + ')' : '') + "</div>";
            }
            /*
             html += "<div class='lval crew'>" + ship.crew + "/" + hull.crew + "</div>";
             if (ship.damage > 0)
             html += "<div class='lval damage'>" + ship.damage + "%</div>";
             }
             if (vgap.gameUsesFuel())
             html += "<div class='lval neu'>" + ship.neutronium + "</div>";
             if (vgap.gameUsesSupplies())
             html += "<div class='lval supplies' " + (ship.supplies == 0 ? "style='display:none;'" : "") + ">" + ship.supplies + "</div>";


             html += "<div class='lval mc' " + (ship.megacredits == 0 ? "style='display:none;'" : "") + ">" + ship.megacredits + "</div>";
             html += "<div class='lval dur' " + (ship.duranium == 0 ? "style='display:none;'" : "") + ">" + ship.duranium + "</div>";
             html += "<div class='lval tri' " + (ship.tritanium == 0 ? "style='display:none;'" : "") + ">" + ship.tritanium + "</div>";
             html += "<div class='lval mol' " + (ship.molybdenum == 0 ? "style='display:none;'" : "") + ">" + ship.molybdenum + "</div>";
             html += "<div class='lval clans' " + (ship.clans == 0 ? "style='display:none;'" : "") + ">" + ship.clans + "</div>";
             */
            /*
             if (vgap.gameUsesAmmo()) {
             if (hull.launchers > 0)
             html += "<div class='lval torpedo'>" + ship.ammo + "</div>";
             if (hull.fighterbays > 0)
             html += "<div class='lval fighters'>" + ship.ammo + "</div>";
             }
             */

            //                if (tower != null)
            //                    html += "<div style='color:#990099;margin-top:10px;'>" + nu.t.towedbyship + " s" + tower.id + "</div>";

            if (note != null) html += "<hr/><div class='GoodTextNote'>" + note.body.replace(/\n/g, "<br/>") + "</div>";

            html += "</div>";

            return html;
        } else
        {
            const ago = vgap.game.turn - ship.infoturn;
            const hullName = vgap.getHull(ship.hullid).name;
            const name = vgap.players[ship.ownerid].fullname;

            return [
                '<div class="ItemSelectionBox">',
                '<span>', ship.id, ': ', ship.name, '</span>',
                '<div>', hullName, '</div>',
                '<table class="CleanTable">',
                '<tr><td colspan="2"><strong>Seen: Turn ', ship.infoturn,
                ' (', ago, ' turn' + (ago > 1 ? 's' : '') + ' ago)</strong></td></tr>',
                '<tr><td>Heading:</td><td>', (ship.heading >= 0 ? ship.heading : '?' ),
                ' at Warp: ', (ship.warp >= 0 ? ship.warp : '?' ), '</td></tr>',
                '<tr><td>Mass:</td><td>', ship.mass, '</td></tr>',
                '<tr><td colspan="2">', name, '</td></tr>',
                (note != null ? '<tr><td colspan="4" class="GoodTextNote">' +
                    note.body.replace(/\n/g, '<br/>') + '</td></tr>' : ''),
                '</table>',
                '</div>'
            ].join('');

        }
    };

    /**
     * Show planet pane when hovering over ship location
     * @param ship
     */
    this.showScan = function (ship)
    {

        if (vgap.isMobileVersion())
        {
            vgap.list.empty();
            vgap.list.show();

            const pane = $('<div class="childpane"></div>').appendTo(vgap.list);
            let planet = vgap.planetAt(ship.x, ship.y);
            const titleBar = $("<div id='ScanTitle'></div>").appendTo(pane);

            if (planet)
            {
                titleBar.html('<div>' + Math.abs(planet.id) + ': ' + planet.name + '</div>');
            } else
            {
                let dist = 1000;
                let planetB;
                let distB = 1000;

                for (let i = 0; i < vgap.planets.length; i++)
                {
                    planetB = vgap.planets[i];
                    distB = Math.dist(planetB.x, planetB.y, ship.x, ship.y);
                    if (distB <= dist)
                    {
                        dist = distB;
                        planet = planetB;
                    }
                }

                if (dist <= 3)
                {
                    titleBar.html('<div>' + Math.abs(planet.id) + ': ' + planet.name + '<span>(Warp Well)</span></div>');
                } else
                {
                    titleBar.html('<div>' + Math.abs(planet.id) + ': ' + planet.name + '<span>(' + dist.toFixed(1) + ' ly away)</span></div>');
                }
            }

            pane.append(shtml.planetScan(planet, true));

            const starBase = vgap.getStarbase(planet.id);
            if (starBase != null) pane.append(shtml.starbaseScan(starBase));
        } else
        {

        }
    };

    /** Show player toggles */
    this.showMapTool = function ()
    {

        const map = vgap.map;

        $("#playerToggles").remove();
        let playerToggles = $([
            '<div id="playerToggles"',
            (this.settings.showVerticalButtons ? '' : ' class="hori"'),
            '></div>'].join(''))
            .appendTo('#MapControls');

        /*
         map.addMapTool("Close Ship Tool", "hideToggles", function () {
         vgap.plugins["shipList"].drawer.toggleMapTool();
         }, "#playerToggles");

         map.addMapTool('All Ships', 'player0' + (this.playerToggles[0] ? ' active' : ''), function () {
         vgap.plugins.shipList.togglePlayer(0)
         }, "#playerToggles");
         */

        map.addMapTool("Close Ship Tool", "hideToggles", this.toggleMapTool.bind(this), "#playerToggles");

        map.addMapTool('All Ships', 'player0 capitalize' + (this.playerToggles[0] ? ' active' : ''), (function ()
        {
            this.togglePlayer(0)
        }).bind(this), "#playerToggles");

        for (let i = 1; i < this.playerToggles.length; i++)
        {
            let name = '';
            for (let j = 0; j < vgap.players.length; j++)
            {
                if (vgap.players[j].id == i)
                {
                    name = vgap.players[j].fullname;
                    break;
                }
            }
            map.addMapTool(name, 'player' + i + (this.playerToggles[i] ? ' active' : ''), (function ()
            {
                this.togglePlayer(i);
            }).bind(this), "#playerToggles");
        }

    };

    /** Show player selection pane for old client */
    this.showSelectionPane = function()
    {
        // empty left content
        vgap.lc.empty();

        let pane = $([
            '<div id="#shipListContainer">',
            '<div class="TitleBar">',
            '<div class="TopTitle">Show ships for:</div>',
            '<div class="CloseLeftScreen" onclick="vgap.closeLeft();vgap.lc.empty();"></div>',
            '</div></div>',
        ].join('')).appendTo(vgap.lc);

        for (let i = 1; i < this.playerToggles.length; i++)
        {
            let name = '';
            let race = '';
            for (let j = 0; j < vgap.players.length; j++)
            {
                if (vgap.players[j].id == i)
                {
                    name = vgap.players[j].username;
                    race = vgap.getRace(vgap.players[j].raceid).shortname;
                    break;
                }
            }

            let row = $([
                '<div>',
                '<label class="capitalize" style="cursor:pointer">',
                '<input type="checkbox" style="cursor:pointer"', (this.playerToggles[i] ? ' checked' : ''), '/>',
                ' ', name, ' (', race, ')</label>',
                '</div>'
            ].join('')).appendTo(pane);

            row.click((function ()
            {
                this.togglePlayer(i);
            }).bind(this));
        }

        vgap.openLeft();
    };

    /** Toggles player buttons */
    this.toggleMapTool = function ()
    {

        if (this.togglesShown) $("#playerToggles").remove();
        else this.showMapTool();
        this.togglesShown = !this.togglesShown;

    };

    /** hide ship scan pane */
    this.hideScan = function ()
    {
        vgap.list.hide();
    };

    /**
     * Toggle player ships on starmap
     * @param id        int player id
     */
    this.togglePlayer = function (id)
    {

        if (id < 0 || id > vgap.players.length) return;

        this.playerToggles[id] = !this.playerToggles[id];

        if (id == 0)
        { // all
            for (let i = this.playerToggles.length - 1; i >= 1; i--)
            {
                this.playerToggles[i] = this.playerToggles[id];
            }
        } else
        { // set state of "all" toggle
            this.playerToggles[0] = true;
            for (let i = this.playerToggles.length - 1; i >= 1; i--)
            {
                if (!this.playerToggles[i])
                {
                    this.playerToggles[0] = false;
                }
            }
        }

        if (vgap.isMobileVersion()) this.showMapTool();
        vgap.map.draw();
    };

};

/** make instance */
const shipList = new ShipList(vgap);

/** register the plugin */
vgap.registerPlugin(shipList, "shipList");

/** OVERLOADED nu.js FUNCTIONS */

/** Check for mobile version */
vgaPlanets.prototype.isMobileVersion = function ()
{
    return (this.version >= 4.0);
};

/** Overload to clear player toggles */
let fn1 = vgapMap.prototype.clearTools;

vgapMap.prototype.clearTools = function (result)
{

    /** @type {ShipList} */
    const plugin = vgap.plugins.shipList;

    for (let i = 0; i < plugin.drawer.playerToggles.length; i++)
    {
        plugin.drawer.playerToggles[i] = false;
    }
    plugin.drawer.togglesShown = false;
    $("#playerToggles").remove();

    // call nu.js clearTools
    /** @type Function */
    fn1.apply(this, arguments);

};

/** Overload to loop through turns */
let fn2 = vgaPlanets.prototype.processLoadHistory;

vgaPlanets.prototype.processLoadHistory = function (result)
{

    /** @type {ShipList} */
    const plugin = vgap.plugins.shipList;

    //call nu.js processLoadHistory
    /** @type {Function} */
    fn2.apply(this, arguments);

    if (plugin.doLoop)
    {

        if (!result.success)
        {
            console.log('Ship List: [5010] (processLoadHistory) Error loading turn. Aborting rebuild.');
            console.trace();
            plugin.doReset = false;
            plugin.doLoop = false;
            return;
        }

        console.log('Ship List: [2010] (processLoadHistory) Next Turn: ' + (this.settings.turn + 1) + '/' + this.nowTurn + '.');

        if (this.settings.turn + 1 == this.nowTurn)
        {
            this.loadNow();
        } else
        {
            this.loadHistory(this.settings.turn + 1);
        }
    }

};

/**
 * Overload functions to show ships seen in previous turns
 * showScan for mobile, showInfo for non-mobile
 */
let fn3 = sharedContent.prototype.showScan;

sharedContent.prototype.showScan = function (x, y, target, lock)
{
    /** @type {ShipList} */
    const plugin = vgap.plugins.shipList;
    let ships;

    /** @type {Function} */
    fn3.apply(this, arguments);

    const pane = vgap.list.first();
    ships = plugin.drawer.shipsAt(plugin.ships, x, y, false);

    for (let i = ships.length - 1; i >= 0; i--)
    {
        $(plugin.drawer.shipScan(ships[i])).appendTo(pane);
    }

    ships = plugin.drawer.shipsAt(plugin.ships, x, y, true);

    for (let i = ships.length - 1; i >= 0; i--)
    {
        const ship = ships[i];

        if (!ship.history || !plugin.settings.showLocationHistory ||
            plugin.drawer.shipsDrawn.indexOf(ship.id) != -1
        ) continue;

        let x1 = ship.x;
        let y1 = ship.y;
        let opacity = 1;
        const ctx = vgap.map.ctx;

        for (let i = 0; i < ship.history.length; i++)
        {
            let loc = ship.history[i];
            let x2 = loc.x;
            let y2 = loc.y;
            opacity *= 0.75;
            if (opacity < 0.25) opacity = 0.25;

            ctx.strokeStyle = colorToRGBA("#ADD8E6", opacity);
            ctx.lineWidth = 1;
            ctx.beginPath();
            ctx.dashedLine(vgap.map.screenX(x1), vgap.map.screenY(y1),
                vgap.map.screenX(x2), vgap.map.screenY(y2), [5, 3]);
            ctx.closePath();
            ctx.stroke();
            if (loc.turn != vgap.game.turn && loc.turn != ship.infoturn)
                vgap.map.drawOffsetText(x2, y2, 'T' + loc.turn, 0, -10);
            x1 = x2;
            y1 = y2;
        }

        plugin.drawer.shipsDrawn.push(ship.id);
    }
};

if (!vgap.isMobileVersion())
{
    let fn4 = vgapMap.prototype.showInfo;

    vgapMap.prototype.showInfo = function (x, y)
    {
        /** @type {ShipList} */
        const plugin = vgap.plugins.shipList;
        let ships;

        /** @type {Function} */
        fn4.apply(this, arguments);

        const pane = $('#PlanetsLoc').find(':first :last');
        ships = plugin.drawer.shipsAt(plugin.ships, vgap.map.x, vgap.map.y, false);

        for (let i = ships.length - 1; i >= 0; i--)
        {
            $(plugin.drawer.shipScan(ships[i])).insertBefore(pane);
        }

        ships = plugin.drawer.shipsAt(plugin.ships, vgap.map.x, vgap.map.y, true);

        for (let i = ships.length - 1; i >= 0; i--)
        {
            const ship = ships[i];

            if (!ship.history || !plugin.settings.showLocationHistory ||
                plugin.drawer.shipsDrawn.indexOf(ship.id) != -1
            ) continue;

            let x1 = ship.x;
            let y1 = ship.y;
            let opacity = 1;
            const ctx = vgap.map.ctx;

            for (let i = 0; i < ship.history.length; i++)
            {
                let loc = ship.history[i];
                let x2 = loc.x;
                let y2 = loc.y;
                opacity *= 0.75;
                if (opacity < 0.25) opacity = 0.25;

                ctx.strokeStyle = colorToRGBA("#ADD8E6", opacity);
                ctx.lineWidth = 1;
                ctx.beginPath();
                ctx.dashedLine(vgap.map.screenX(x1), vgap.map.screenY(y1),
                    vgap.map.screenX(x2), vgap.map.screenY(y2), [5, 3]);
                ctx.closePath();
                ctx.stroke();
                if (loc.turn != vgap.game.turn && loc.turn != ship.infoturn)
                    vgap.map.drawOffsetText(x2, y2, 'T' + loc.turn, 0, -10);
                x1 = x2;
                y1 = y2;
            }

            plugin.drawer.shipsDrawn.push(ship.id);
        }
    };
}

/** Get intercept missions after succesful save */
let fn5 = vgaPlanets.prototype.processSave;

vgaPlanets.prototype.processSave = function(result)
{
    /** @type {Function} */
    fn5.apply(this, arguments);

    if (result && result.success)
    {
        const plugin = vgap.plugins.shipList;

        // clear list
        plugin.intercepts = [];

        // loop through own ships with mission intercept / cloak & intercept
        for (let i = vgap.ships.length - 1; i >= 0; i--)
        {
            let ship = vgap.ships[i];

            if (ship.id > 0 && //support for sphere add-on - thanks to Maberi for pointing this out
                ship.ownerid == vgap.player.id &&
                (ship.mission == 7 || ship.mission == 20) &&
                ship.mission1target != 0 // intercept without valid target
            )
            {
                plugin.intercepts.push(
                    {
                        srcId: ship.id,
                        tgtId: ship.mission1target
                    }
                );
            }
        }

    }
}

/** Prime the VCR player with combat data */
vcrPlayer.prototype.runReport = function (report)
{

    let left = new combatObject();
    let right = new combatObject();

    left.setObject(report.left);
    right.setObject(report.right);

    this.init(left, right, report.battletype, report.seed);
    this.finished = function ()
    {
    };
    this.run(-1);

};

/** Get relationFrom for ship */
vgaPlanets.prototype.getRelationFromForShip = function (ownerId)
{

    const relation = this.getRelation(ownerId);

    if (relation)
    {
        return relation.relationfrom;
    } else
    {
        return 0;
    }

};

/** Missing function in non-mobile */
if (!vgap.isMobileVersion())
{
    vgapMap.prototype.drawOffsetText = function (x, y, text, dx, dy, attr, ctx)
    {
        if (!ctx)
            ctx = this.ctx;
        if (!vgap.map.isVisible(x, y, Math.max(10, ctx.measureText(text).width) / 2 / this.zoom)) return;
        var color = "#ffffff";
        if (attr)
        {
            if (attr.color)
                color = attr.color;
            else if (attr.fill)
                color = attr.fill;
        }
        ctx.fillStyle = color;
        ctx.lineWidth = 1;
        ctx.font = "10px Arial";
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";
        ctx.fillText(text, this.screenX(x) + dx, this.screenY(y) + dy);
    };
}