tanks.gg小工具

改进tanks.gg页面,实现显示瞄准、曲线等信息,并易于使用。加QQ交流群:628613664 了解或获取更多工具

// ==UserScript==
// @name         tanks.gg小工具
// @namespace    Eruru
// @version      1.2.3
// @description  改进tanks.gg页面,实现显示瞄准、曲线等信息,并易于使用。加QQ交流群:628613664 了解或获取更多工具
// @author       Eruru
// @match        https://tanks.gg/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=csdn.net
// @grant        unsafeWindow
// @require      https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.6.0.slim.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // Your code here...
    window.curveFrameCount = 30;
    window.curveCanvasHeight = "450vh";
    window.decimalPlaces = 4;
    window.checkInterval = 1000;
    window.color = {
        lightGreen: "rgb(144, 238, 144)",
        lightCoral: "rgb(240, 128, 128)"
    };
    window.language = {
        aimInformation: "瞄准信息",
        terrain: "地形",
        terrainHard: "硬地",
        terrainMedium: "中地",
        terrainSoft: "软地",
        isMoving: "移动",
        isTankTraversing: "旋转车体",
        isTurretTraversing: "旋转炮塔",
        isFiring: "开火",
        isGunDamaged: "炮受损",
        aimTime: "瞄准时间",
        dispersion: "精度",
        moveSpeed: "移动速度",
        tankTraverseSpeed: "车体旋转速度",
        turretTraverseSpeed: "炮塔旋转速度",
        curveType: "类型",
        speedAndDispersion: "速度/精度",
        traverseAndDispersion: "转速/精度",
        aimTimeAndDispersion: "缩圈时间/精度",
        speedAndAimTime: "速度/瞄准时间",
        traverseAndAimTime: "转速/瞄准时间",
        second: "秒",
        curve: "曲线",
        animation: "动画",
        play: "播放",
        selectAll: "全选",
        deselectAll: "全部取消选择",
        oneKeySelectAllUnselectedSkill: "一键选择所有未被选中的技能",
        oneKeyDeselectAllselectedSkill: "一键取消选择所有已选中的技能"
    };
    window.tankPanel = null;
    window.aimInformationPanel = new AimInformationPanel (window.language.aimInformation, onValueChanged);
    window.aimInformationCurvePanel = new AimInformationCurvePanel ();
    //window.animationPanel = new AnimationPanel (window.language.animation);
    setInterval (checkPage, window.checkInterval);

    function checkPage () {
        //checkSkill ();
        checkCopy ();
        checkPanel ();
    }

    function checkSkill () {
        var moduleListElements = $ (".module.skill .dropdown-menu");
        if (moduleListElements.length > 0) {
            for (var i = 0; i < moduleListElements.length; i++) {
                var moduleListElement = moduleListElements.eq (i);
                var dividerElement = moduleListElement.find (".divider");
                if (dividerElement.length > 0 && moduleListElement.find (".EruruSkillPanel").length == 0) {
                    var selectAllElement = $ ("<a class='EruruSkillPanel dropdown-item'><b>" + window.language.selectAll + "</b><div><span>" + window.language.oneKeySelectAllUnselectedSkill + "</span></div></a>");
                    var deselectAllElement = $ ("<a class='EruruSkillPanel dropdown-item'><b>" + window.language.deselectAll + "</b><div><span>" + window.language.oneKeyDeselectAllselectedSkill + "</span></div></a>");
                    dividerElement.before (selectAllElement);
                    dividerElement.before (deselectAllElement);
                    selectAllElement.bind ("click", function () {
                        var skillElements = $ (this).parent ().find ("a:not(.EruruSkillPanel)");
                        for (var i = 0; i < skillElements.length; i++) {
                            var skillElement = skillElements.eq (i);
                            if (skillElement.hasClass ("active")) {
                                continue;
                            }
                            skillElement.get (0).click ();
                        }
                    });
                    deselectAllElement.bind ("click", function () {
                        var skillElements = $ (this).parent ().find ("a:not(.EruruSkillPanel)");
                        for (var i = 0; i < skillElements.length; i++) {
                            var skillElement = skillElements.eq (i);
                            if (!skillElement.hasClass ("active")) {
                                continue;
                            }
                            skillElement.get (0).click ();
                        }
                    });
                }
            }
        }
    }

    function checkPanel () {
        var everythingElseElement = $ (".card-header:contains('Everything Else')").parent ();
        if (everythingElseElement.length > 0) {
            if ($ (".EruruAimInformationPanel").length > 0) {
                window.aimInformationPanel.panel.element.css ("width", everythingElseElement.width ());
                window.aimInformationCurvePanel.getPanel ().element.css ("width", everythingElseElement.width ());
                onValueChanged ();
                return;
            }
            everythingElseElement.addClass ("EruruAimInformationPanel");
            everythingElseElement.addClass ("mb-3");
            window.tankPanel = new TankPanel (onValueChanged);
            var adsElement = $ (".wrecker:first");
            var gunConstraintsElement = $ (".card-header:contains('Gun constraints')").parent ();
            window.aimInformationPanel.panel.element.insertBefore (everythingElseElement);
            window.aimInformationPanel.panel.element.css ("width", everythingElseElement.width ());
            window.aimInformationCurvePanel.getPanel ().element.insertBefore (adsElement);
            window.aimInformationCurvePanel.getPanel ().element.css ("width", adsElement.width ());
            onValueChanged ();
            /*
			adsElement.before (window.animationPanel.panel.element);
			window.animationPanel.setSize (gunConstraintsElement.width (), gunConstraintsElement.width ());
			window.animationPanel.play ();
			*/
            return;
        }
        window.tankPanel = null;
    }

    function checkCopy () {
        var actionsElement = $ (".justify-content-end");
        if (actionsElement.length > 0) {
            if (actionsElement.find (".EruruComparePanelAction").length > 0) {
                return;
            }
            var copyElement = $ ("<button class='EruruComparePanelAction btn btn-lg btn-primary me-3'>复制配装</button>");
            var swapElement = $ ("<button class='EruruComparePanelAction btn btn-lg btn-primary me-3'>交换配装</button>");
            var clearElement = $ ("<button class='EruruComparePanelAction btn btn-lg btn-dark me-3'>清空配装</button>");
            actionsElement.append (copyElement);
            actionsElement.append (swapElement);
            actionsElement.append (clearElement);
            copyElement.prev ().addClass ("me-3");
            copyElement.bind ("click", function () {
                clickSaveButton ();
                var url = window.location.href;
                var f = getUrlParameterValue (url, "f");
                var e = getUrlParameterValue (url, "e");
                var l = getUrlParameterValue (url, "l");
                var c = getUrlParameterValue (url, "c");
                var k = getUrlParameterValue (url, "k");
                var d = getUrlParameterValue (url, "d");
                var m = getUrlParameterValue (url, "m");
                window.location.href = setTankParameters (url, f, e, l, c, k, d, m, true);
            });
            swapElement.bind ("click", function () {
                clickSaveButton ();
                var url = window.location.href;
                var f = getUrlParameterValue (url, "f");
                var e = getUrlParameterValue (url, "e");
                var l = getUrlParameterValue (url, "l");
                var c = getUrlParameterValue (url, "c");
                var k = getUrlParameterValue (url, "k");
                var d = getUrlParameterValue (url, "d");
                var m = getUrlParameterValue (url, "m");
                var cf = getUrlParameterValue (url, "cf");
                var ce = getUrlParameterValue (url, "ce");
                var cl = getUrlParameterValue (url, "cl");
                var cc = getUrlParameterValue (url, "cc");
                var ck = getUrlParameterValue (url, "ck");
                var cd = getUrlParameterValue (url, "cd");
                var cm = getUrlParameterValue (url, "cm");
                url = setTankParameters (url, cf, ce, cl, cc, ck, cd, cm, false);
                url = setTankParameters (url, f, e, l, c, k, d, m, true);
                window.location.href = url;
            });
            clearElement.bind ("click", function () {
                clickSaveButton ();
                window.location.href = setTankParameters (window.location.href, "", "", "", "", "", "", "", true);
            });
        }
    }

    function clickSaveButton () {
        $(".btn.btn-lg.btn-primary:not(.EruruComparePanelAction)").trigger ("click");
    }

    function setTankParameters (url, f, e, l, c, k, d, m, isCompareTank) {
        var header = isCompareTank ? "c" : "";
        url = setUrlParameterValue (url, header + "f", f);
        url = setUrlParameterValue (url, header + "e", e);
        url = setUrlParameterValue (url, header + "l", l);
        url = setUrlParameterValue (url, header + "c", c);
        url = setUrlParameterValue (url, header + "k", k);
        url = setUrlParameterValue (url, header + "d", d);
        url = setUrlParameterValue (url, header + "m", m);
        return url;
    }

    function formatDifference (label, value, baseValue, direction) {
        var difference = (value - baseValue) * direction;
        if(difference == 0) {
            return label;
        }
        var text = label + " (";
        if (difference >= 0) {
            text += "+";
        }
        text += difference.toFixed (window.decimalPlaces) + ")";
        return text;
    }

    function onValueChanged () {
        var tank = window.tankPanel.toTank ();
        var tankState = tank.calculateState (window.aimInformationPanel);
        var tankResult = tank.calculateAimTime (tankState.moveSpeed, tankState.tankTraverseSpeed, tankState.turretTraverseSpeed, tankState.isFiring, tankState.isGunDamaged);
        var baseTank = window.tankPanel.toBaseTank ();
        var baseTankState = baseTank.calculateState (window.aimInformationPanel);
        var baseTankResult = baseTank.calculateAimTime (baseTankState.moveSpeed, baseTankState.tankTraverseSpeed, baseTankState.turretTraverseSpeed, baseTankState.isFiring, baseTankState.isGunDamaged);
        window.aimInformationPanel.aimTimeItem.setValue (tankResult.aimTime.toFixed (window.decimalPlaces));
        window.aimInformationPanel.aimTimeItem.setBaseValue (baseTankResult.aimTime.toFixed (window.decimalPlaces));
        window.aimInformationPanel.aimTimeItem.setCompareColor (compare (tankResult.aimTime, baseTankResult.aimTime) * -1);
        window.aimInformationPanel.aimTimeItem.setLabel (formatDifference (window.language.aimTime, tankResult.aimTime, baseTankResult.aimTime, 1));
        window.aimInformationPanel.dispersionItem.setValue (tankResult.dispersion.toFixed (window.decimalPlaces));
        window.aimInformationPanel.dispersionItem.setBaseValue (baseTankResult.dispersion.toFixed (window.decimalPlaces));
        window.aimInformationPanel.dispersionItem.setCompareColor (compare (tankResult.dispersion, baseTankResult.dispersion) * -1);
        window.aimInformationPanel.dispersionItem.setLabel (formatDifference (window.language.dispersion, tankResult.dispersion, baseTankResult.dispersion, 1));
        window.aimInformationPanel.moveSpeedItem.setValue (tankState.moveSpeed);
        window.aimInformationPanel.moveSpeedItem.setBaseValue (baseTankState.moveSpeed);
        window.aimInformationPanel.moveSpeedItem.setCompareColor (compare (tankState.moveSpeed, baseTankState.moveSpeed));
        window.aimInformationPanel.moveSpeedItem.setVisible (tankState.isMoving);
        window.aimInformationPanel.moveSpeedItem.setLabel (formatDifference (window.language.moveSpeed, tankState.moveSpeed, baseTankState.moveSpeed, 1));
        window.aimInformationPanel.tankTraverseSpeedItem.setValue (tankState.tankTraverseSpeed);
        window.aimInformationPanel.tankTraverseSpeedItem.setBaseValue (baseTankState.tankTraverseSpeed);
        window.aimInformationPanel.tankTraverseSpeedItem.setCompareColor (compare (tankState.tankTraverseSpeed, baseTankState.tankTraverseSpeed));
        window.aimInformationPanel.tankTraverseSpeedItem.setVisible (tankState.isTankTraverse);
        window.aimInformationPanel.tankTraverseSpeedItem.setLabel (formatDifference (window.language.tankTraverseSpeed, tankState.tankTraverseSpeed, baseTankState.tankTraverseSpeed, 1));
        window.aimInformationPanel.turretTraverseSpeedItem.setValue (tankState.turretTraverseSpeed);
        window.aimInformationPanel.turretTraverseSpeedItem.setBaseValue (baseTankState.turretTraverseSpeed);
        window.aimInformationPanel.turretTraverseSpeedItem.setCompareColor (compare (tankState.turretTraverseSpeed, baseTankState.turretTraverseSpeed));
        window.aimInformationPanel.turretTraverseSpeedItem.setVisible (tankState.isTurretTraverse);
        window.aimInformationPanel.turretTraverseSpeedItem.setLabel (formatDifference (window.language.turretTraverseSpeed, tankState.turretTraverseSpeed, baseTankState.turretTraverseSpeed, 1));
        window.aimInformationCurvePanel.tank = tank;
        window.aimInformationCurvePanel.tankResult = tankResult;
        window.aimInformationCurvePanel.baseTank = baseTank;
        window.aimInformationCurvePanel.baseTankResult = baseTankResult;
        window.aimInformationCurvePanel.refresh ();
        /*
		window.animationPanel.tank = tank;
		window.animationPanel.baseTank = baseTank;
		*/
    }

    function compare (a, b) {
        if (a == b) {
            return -0;
        }
        if (a > b) {
            return 1;
        }
        return -1;
    }

    function clamp (value, max, min) {
        if (value > max) {
            return max;
        }
        if (value < min) {
            return min;
        }
        return value;
    }

    function Deg2Rad (degree) {
        return degree / 180 * Math.PI;
    }

    function Rad2Deg (radian) {
        return radian * 180 / Math.PI;
    }

    function getUrlParameterValue (url, parameter) {
        var str = url.substr(url.indexOf('?') + 1);
        var arr = str.split('&');
        for (var i in arr) {
            var paired = arr[i].split('=');
            if (paired[0] == parameter) {
                return paired[1];
            }
        }
        return "";
    }

    function setUrlParameterValue (url, parameter, value) {
        if (value == null || value == "") {
            return removeUrlParameter (url, parameter);
        }
        if (url.indexOf('?') == -1){
            return url + "?" + parameter + "=" + value;
        }
        if (!new RegExp ("[/?&]" + parameter + "=").test (url)){
            return url + "&" + parameter + "=" + value;
        }
        var arr_url = url.split('?');
        var base = arr_url[0];
        var arr_param = arr_url[1].split('&');
        for (var i = 0; i < arr_param.length; i++) {
            var paired = arr_param[i].split('=');
            if (paired[0] == parameter) {
                paired[1] = value;
                arr_param[i] = paired.join('=');
                break;
            }
        }
        return base + "?" + arr_param.join('&');
    }

    function removeUrlParameter (url, parameter) {
        var arr_url = url.split('?');
        var base = arr_url[0];
        var arr_param = arr_url[1].split('&');
        var index = -1;
        for (var i = 0; i < arr_param.length; i++) {
            var paired = arr_param[i].split('=');
            if (paired[0] == parameter) {
                index = i;
                break;
            }
        }
        if (index == -1) {
            return url;
        } else {
            arr_param.splice(index, 1);
            return base + "?" + arr_param.join('&');
        }
    }

    function TankPanel (onValueChanged) {
        this.nameElement = $ (".tank h1").get (0).firstChild;
        this.compareElement = $ (".nav.nav-tabs .nav-item").eq (2).find ("a");
        this.aimTimeElement = $ ("label:contains('Aim time (sec)'):first+");
        this.dispersionElement = $ ("label:contains('Dispersion'):first+");
        this.dispersionFactorMovingElement = $ ("label:contains('… moving (+)'):first+");
        this.dispersionFactorTankTraverseElement = $ ("label:contains('… tank traverse (+)'):first+");
        this.dispersionFactorTurretTraverseElement = $ ("label:contains('… turret traverse (+)'):first+");
        this.dispersionFactorAfterFiringElement = $ ("label:contains('… after firing (+)'):first+");
        this.dispersionFactorDamagedElement = $ ("label:contains('… damaged (×)'):first+");
        this.topSpeedElement = $ ("label:contains('Top speed (km/h)'):first+");
        this.effectiveTopSpeedHardElement = $ ("label:contains('… hard (km/h)'):first+");
        this.effectiveTopSpeedMediumElement = $ ("label:contains('… medium (km/h)'):first+");
        this.effectiveTopSpeedSoftElement = $ ("label:contains('… soft (km/h)'):first+");
        this.effectiveTraverseHardElement = $ ("label:contains('… hard (°/sec)'):first+");
        this.effectiveTraverseMediumElement = $ ("label:contains('… medium (°/sec)'):first+");
        this.effectiveTraverseSoftElement = $ ("label:contains('… soft (°/sec)'):first+");
        this.turretTraverseElement = $ ("label:contains('Turret traverse (°/sec)'):first+");
        this.getName = function () {
            return this.nameElement.nodeValue;
        };
        this.getAimTime = function () {
            return parseFloat (this.aimTimeElement.text ());
        };
        this.getDispersion = function () {
            return parseFloat (this.dispersionElement.text ());
        };
        this.getDispersionFactorMoving = function () {
            return parseFloat (this.dispersionFactorMovingElement.text ());
        };
        this.getDispersionFactorTankTraverse = function () {
            return parseFloat (this.dispersionFactorTankTraverseElement.text ());
        };
        this.getDispersionFactorTurretTraverse = function () {
            return parseFloat (this.dispersionFactorTurretTraverseElement.text ());
        };
        this.getDispersionFactorAfterFiring = function () {
            return parseFloat (this.dispersionFactorAfterFiringElement.text ());
        };
        this.getDispersionFactorDamaged = function () {
            return parseFloat (this.dispersionFactorDamagedElement.text ());
        };
        this.getTopSpeed = function () {
            return parseFloat (this.topSpeedElement.text ());
        };
        this.getEffectiveTopSpeedHard = function () {
            return parseFloat (this.effectiveTopSpeedHardElement.text ());
        };
        this.getEffectiveTopSpeedMedium = function () {
            return parseFloat (this.effectiveTopSpeedMediumElement.text ());
        };
        this.getEffectiveTopSpeedSoft = function () {
            return parseFloat (this.effectiveTopSpeedSoftElement.text ());
        };
        this.getEffectiveTraverseHard = function () {
            return parseFloat (this.effectiveTraverseHardElement.text ());
        };
        this.getEffectiveTraverseMedium = function () {
            return parseFloat (this.effectiveTraverseMediumElement.text ());
        };
        this.getEffectiveTraverseSoft = function () {
            return parseFloat (this.effectiveTraverseSoftElement.text ());
        };
        this.getTurretTraverse = function () {
            return parseFloat (this.turretTraverseElement.text ());
        };
        this.getBaseName = function () {
            return this.compareElement.text ();
        };
        this.getBaseAimTime = function () {
            return getBaseValue.call (this, this.aimTimeElement, this.getAimTime);
        };
        this.getBaseDispersion = function () {
            return getBaseValue.call (this, this.dispersionElement, this.getDispersion);
        };
        this.getBaseDispersionFactorMoving = function () {
            return getBaseValue.call (this, this.dispersionFactorMovingElement, this.getDispersionFactorMoving);
        };
        this.getBaseDispersionFactorTankTraverse = function () {
            return getBaseValue.call (this, this.dispersionFactorTankTraverseElement, this.getDispersionFactorTankTraverse);
        };
        this.getBaseDispersionFactorTurretTraverse = function () {
            return getBaseValue.call (this, this.dispersionFactorTurretTraverseElement, this.getDispersionFactorTurretTraverse);
        };
        this.getBaseDispersionFactorAfterFiring = function () {
            return getBaseValue.call (this, this.dispersionFactorAfterFiringElement, this.getDispersionFactorAfterFiring);
        };
        this.getBaseDispersionFactorDamaged = function () {
            return getBaseValue.call (this, this.dispersionFactorDamagedElement, this.getDispersionFactorDamaged);
        };
        this.getBaseTopSpeed = function () {
            return getBaseValue.call (this, this.topSpeedElement, this.getTopSpeed);
        };
        this.getBaseEffectiveTopSpeedHard = function () {
            return getBaseValue.call (this, this.effectiveTopSpeedHardElement, this.getEffectiveTopSpeedHard);
        };
        this.getBaseEffectiveTopSpeedMedium = function () {
            return getBaseValue.call (this, this.effectiveTopSpeedMediumElement, this.getEffectiveTopSpeedMedium);
        };
        this.getBaseEffectiveTopSpeedSoft = function () {
            return getBaseValue.call (this, this.effectiveTopSpeedSoftElement, this.getEffectiveTopSpeedSoft);
        };
        this.getBaseEffectiveTraverseHard = function () {
            return getBaseValue.call (this, this.effectiveTraverseHardElement, this.getEffectiveTraverseHard);
        };
        this.getBaseEffectiveTraverseMedium = function () {
            return getBaseValue.call (this, this.effectiveTraverseMediumElement, this.getEffectiveTraverseMedium);
        };
        this.getBaseEffectiveTraverseSoft = function () {
            return getBaseValue.call (this, this.effectiveTraverseSoftElement, this.getEffectiveTraverseSoft);
        };
        this.getBaseTurretTraverse = function () {
            return getBaseValue.call (this, this.turretTraverseElement, this.getTurretTraverse);
        };
        this.toTank = function () {
            return toTank (this, false);
        };
        this.toBaseTank = function () {
            return toTank (this, true);
        };

        function getBaseValue (element, onGetValue) {
            var baseElement = element.next ();
            return baseElement.length > 0 ? parseFloat (baseElement.text ()) : onGetValue.call (this);
        }

        function toTank (tankPanel, isBase) {
            var header = isBase ? "getBase" : "get";
            var tank = new Tank ();
            tank.name = tankPanel[header + "Name"] ();
            tank.aimTime = tankPanel[header + "AimTime"] ();
            tank.dispersion = tankPanel[header + "Dispersion"] ();
            tank.dispersionFactorMoving = tankPanel[header + "DispersionFactorMoving"] ();
            tank.dispersionFactorTankTraverse = tankPanel[header + "DispersionFactorTankTraverse"] ();
            tank.dispersionFactorTurretTraverse = tankPanel[header + "DispersionFactorTurretTraverse"] ();
            tank.dispersionFactorAfterFiring = tankPanel[header + "DispersionFactorAfterFiring"] ();
            tank.dispersionFactorDamaged = tankPanel[header + "DispersionFactorDamaged"] ();
            tank.topSpeed = tankPanel[header + "TopSpeed"] ();
            tank.effectiveTopSpeedHard = tankPanel[header + "EffectiveTopSpeedHard"] ();
            tank.effectiveTopSpeedMedium = tankPanel[header + "EffectiveTopSpeedMedium"] ();
            tank.effectiveTopSpeedSoft = tankPanel[header + "EffectiveTopSpeedSoft"] ();
            tank.effectiveTraverseHard = tankPanel[header + "EffectiveTraverseHard"] ();
            tank.effectiveTraverseMedium = tankPanel[header + "EffectiveTraverseMedium"] ();
            tank.effectiveTraverseSoft = tankPanel[header + "EffectiveTraverseSoft"] ();
            tank.turretTraverse = tankPanel[header + "TurretTraverse"] ();
            return tank;
        }

    }

    function Panel (header) {
        this.element = $ ("<div class='card mb-3'>");
        this.headerElement = $ ("<div class='bg-primary fs-5 card-header'>");
        this.bodyElement = $ ("<div class='card-body'>");
        this.element.append (this.headerElement);
        this.element.append (this.bodyElement);
        this.items = [];

        this.setHeader = function (header) {
            this.headerElement.html (header);
        };

        this.addItem = function (label, value) {
            var item = new PanelItem (label, value);
            this.items.push (item);
            this.bodyElement.append (item.element);
            return item;
        };

        this.setHeader (header);
    }

    function PanelItem (label, value) {
        this.element = $ ("<div class='stat-line'>");
        this.labelElement = $ ("<label>");
        this.valueElement = $ ("<span>");
        this.baseValueElement = null;
        this.element.append (this.labelElement);
        this.element.append (this.valueElement);

        this.setLabel = function (label) {
            this.labelElement.html (label);
        };

        this.setValue = function (value) {
            this.valueElement.html (value);
        };

        this.getValue = function () {
            return this.valueElement.text ();
        };

        this.setBaseValue = function (value) {
            if (value == this.getValue ()) {
                if (this.baseValueElement != null) {
                    this.baseValueElement.remove ();
                    this.baseValueElement = null;
                }
                return;
            }
            if (this.baseValueElement == null) {
                this.baseValueElement = $ ("<div>");
                this.element.append (this.baseValueElement);
            }
            this.baseValueElement.html (value);
        };

        this.getBaseValue = function () {
            return this.baseValueElement == null ? this.getValue () : this.baseValueElement.text ();
        };

        this.getValueElement = function () {
            return this.valueElement.children (":first");
        };

        this.getValueElementValue = function () {
            return this.getValueElement ().val ();
        };

        this.getValueElementChecked = function () {
            return this.getValueElement ().prop ("checked");
        };

        this.getValueElementOptionText = function () {
            return this.getValueElement ().find ("option:selected").text ();
        };

        this.setVisible = function (visible) {
            if (visible) {
                this.element.show ();
                return;
            }
            this.element.hide ();
        };

        this.setHighlight = function (highlight) {
            if (highlight) {
                this.element.addClass ("highlight");
                return;
            }
            this.element.removeClass ("highlight");
        };

        this.setCompareColor = function (compare) {
            if (compare == 0) {
                this.valueElement.parent ().removeAttr ("style");
                return;
            }
            if (compare > 0) {
                this.valueElement.parent ().attr ("style", "background-color: " + window.color.lightGreen);
                return;
            }
            this.valueElement.parent ().attr ("style", "background-color: " + window.color.lightCoral);
        };

        this.setLabel (label);
        this.setValue (value);
    }

    function AimInformationPanel (header, onValueChanged) {
        this.panel = new Panel (header);
        this.terrainItem = this.panel.addItem (window.language.terrain, "<select>" +
                                               "<option value='Hard'>" + window.language.terrainHard + "</option>" +
                                               "<option value='Medium' selected>" + window.language.terrainMedium + "</option>" +
                                               "<option value='Soft'>" + window.language.terrainSoft + "</option>" +
                                               "</select>");
        this.isMovingItem = this.panel.addItem (window.language.isMoving, "<input type='checkbox' />");
        //this.moveSpeedSliderItem = this.panel.addItem (window.language.moveSpeed, "<input type='range' />");
        this.isTankTraversingItem = this.panel.addItem (window.language.isTankTraversing, "<input type='checkbox' checked />");
        this.isTurretTraversingItem = this.panel.addItem (window.language.isTurretTraversing, "<input type='checkbox' checked />");
        this.isFiringItem = this.panel.addItem (window.language.isFiring, "<input type='checkbox' />");
        this.isGunDamagedItem = this.panel.addItem (window.language.isGunDamaged, "<input type='checkbox' />");
        this.aimTimeItem = this.panel.addItem (window.language.aimTime, 0);
        this.dispersionItem = this.panel.addItem (window.language.dispersion, 0);
        this.moveSpeedItem = this.panel.addItem (window.language.moveSpeed, 0);
        this.tankTraverseSpeedItem = this.panel.addItem (window.language.tankTraverseSpeed, 0);
        this.turretTraverseSpeedItem = this.panel.addItem (window.language.turretTraverseSpeed, 0);

        this.getTerrain = function () {
            return this.terrainItem.getValueElementValue ();
        };

        this.getIsMoving = function () {
            return this.isMovingItem.getValueElementChecked ();
        };

        this.getMoveSpeed = function () {
            return this.moveSpeedSliderItem.getValueElementValue ();
        };

        this.getIsTankTraversing = function () {
            return this.isTankTraversingItem.getValueElementChecked ();
        };

        this.getIsTurretTraversing = function () {
            return this.isTurretTraversingItem.getValueElementChecked ();
        };

        this.getIsFiring = function () {
            return this.isFiringItem.getValueElementChecked ();
        };

        this.getIsGunDamaged = function () {
            return this.isGunDamagedItem.getValueElementChecked ();
        };

        this.toString = function () {
            return "Terrain: " + this.getTerrain () +
                "\nIsMoving: " + this.getIsMoving () +
                "\nIsTankTraverse: " + this.getIsTankTraversing () +
                "\nTraversingTurret: " + this.getIsTurretTraversing () +
                "\nFiring: " + this.getIsFiring () +
                "\nGunDamaged: " + this.getIsGunDamaged ();
        };

        this.aimTimeItem.setHighlight (true);
        this.dispersionItem.setHighlight (true);
        this.terrainItem.getValueElement ().bind ("input propertychange", onValueChanged);
        this.isMovingItem.getValueElement ().bind ("input propertychange", onValueChanged);
        //this.moveSpeedSliderItem.getValueElement ().bind ("input propertychange", onValueChanged);
        this.isTankTraversingItem.getValueElement ().bind ("input propertychange", onValueChanged);
        this.isTurretTraversingItem.getValueElement ().bind ("input propertychange", onValueChanged);
        this.isFiringItem.getValueElement ().bind ("input propertychange", onValueChanged);
        this.isGunDamagedItem.getValueElement ().bind ("input propertychange", onValueChanged);
    }

    function CurvePanel (header) {
        this.panel = new Panel (header);
        this.canvasElement = $ ("<canvas>");
        this.panel.element.append (this.canvasElement);
        this.canvasElement.attr ("height", window.curveCanvasHeight);
        this.chart = new Chart (this.canvasElement.get (0).getContext ("2d"), {
            type: "line",
            data: null,
            options: {
                animation: false
            }
        });

        this.setData = function (data) {
            this.chart.data = data;
            this.chart.update (0);
        };

    }

    function AimInformationCurvePanel () {

        this.getPanel = function () {
            return this.canvasPanel.panel;
        };

        this.canvasPanel = new CurvePanel ();
        this.tank = null;
        this.baseTank = null;
        this.tankResult = null;
        this.baseTankResult = null;
        this.typeItem = this.getPanel ().addItem (window.language.curveType, $ ("<select>" +
                                                                                "<option value='Speed/Dispersion' selected>" + window.language.speedAndDispersion + "</option>" +
                                                                                "<option value='Traverse/Dispersion'>" + window.language.traverseAndDispersion + "</option>" +
                                                                                "<option value='Time/Dispersion' selected>" + window.language.aimTimeAndDispersion + "</option>" +
                                                                                "<option value='Speed/Time'>" + window.language.speedAndAimTime + "</option>" +
                                                                                "<option value='Traverse/Time'>" + window.language.traverseAndAimTime + "</option>" +
                                                                                "</select>"));
        this.typeItem.element.after (this.canvasPanel.canvasElement);
        this.typeItem.element.bind ("change", function () {
            var type = this.typeItem.getValueElementValue ();
            var data = {
                labels: [],
                datasets: [
                    {
                        label: this.tank.name,
                        borderColor: window.color.lightGreen,
                        data: []
                    },
                    {
                        label: this.baseTank.name,
                        borderColor: window.color.lightCoral,
                        data: []
                    }
                ]
            };
            var frameCount = window.curveFrameCount;
            var current = 0;
            var result = null;
            var baseResult = null;
            for (var i = 0; i <= frameCount; i++) {
                current = i / frameCount;
                var x = 0;
                var y = 0;
                var baseY = 0;
                switch (type) {
                    case "Time/Dispersion":
                        var time = Math.max (this.tankResult.aimTime, this.baseTankResult.aimTime) * current;
                        result = this.tank.calculateAimTimeByTime (this.tankResult.aimTime, this.tankResult.dispersion, time);
                        baseResult = this.baseTank.calculateAimTimeByTime (this.baseTankResult.aimTime, this.baseTankResult.dispersion, time);
                        x = time.toFixed (window.decimalPlaces) + " " + window.language.second;
                        y = result.dispersion;
                        baseY = baseResult.dispersion;
                        break;
                    case "Speed/Time":
                    case "Speed/Dispersion":
                        var speed = Math.max (this.tank.topSpeed, this.baseTank.topSpeed) * current;
                        result = this.tank.calculateAimTime (speed, 0, 0, false, false);
                        baseResult = this.baseTank.calculateAimTime (speed, 0, 0, false, false);
                        x = speed.toFixed (window.decimalPlaces) + " KM/H";
                        if (type == "Speed/Time") {
                            y = result.aimTime;
                            baseY = baseResult.aimTime;
                        } else {
                            y = result.dispersion;
                            baseY = baseResult.dispersion;
                        }
                        break;
                    case "Traverse/Time":
                    case "Traverse/Dispersion":
                        var tankTraverse = Math.max (this.tank.effectiveTraverseHard, this.baseTank.effectiveTraverseHard) * current;
                        var turretTraverse = Math.max (this.tank.turretTraverse, this.baseTank.turretTraverse) * current;
                        result = this.tank.calculateAimTime (0, tankTraverse, turretTraverse, false, false);
                        baseResult = this.baseTank.calculateAimTime (0, tankTraverse, turretTraverse, false, false);
                        x = tankTraverse.toFixed (window.decimalPlaces) + " + " + turretTraverse.toFixed (window.decimalPlaces);
                        if (type == "Traverse/Time") {
                            y = result.aimTime;
                            baseY = baseResult.aimTime;
                        } else {
                            y = result.dispersion;
                            baseY = baseResult.dispersion;
                        }
                        break;
                }
                data.labels.push (x);
                data.datasets[0].data.push (y.toFixed (window.decimalPlaces));
                data.datasets[1].data.push (baseY.toFixed (window.decimalPlaces));
            }
            this.getPanel ().setHeader (this.typeItem.getValueElementOptionText () + " " + window.language.curve);
            this.canvasPanel.setData (data);
        }.bind (this));

        this.refresh = function () {
            this.typeItem.element.trigger ("change");
        };

    }

    function AnimationPanel (header) {
        this.panel = new Panel (header);
        this.tank = null;
        this.tankBase = null;
        this.currentTurretAngle = 0;
        this.currentBaseTurretAngle = 0;
        this.drawId = null;
        this.canvasElement = $ ("<canvas>");
        this.panel.element.append (this.canvasElement);
        this.canvasContext = this.canvasElement.get (0).getContext ("2d");
        this.playButton = $ ("<button>" + window.language.play + "</button>");
        this.panel.element.append (this.playButton);

        this.setSize = function (width, height) {
            this.width = width;
            this.height = height;
            this.halfWidth = this.width / 2.0;
            this.halfHeight = this.height / 2.0;
            this.turretRadius = this.height * 0.1;
            this.baseTurretRadius = this.turretRadius + 5;
            this.gunLength = this.height;
            this.canvasElement.attr ("width", width);
            this.canvasElement.attr ("height", height);
        };

        this.playButton.bind ("click", function () {
            this.play ();
        }.bind (this));

        this.play = function () {
            this.currentTurretAngle = 0;
            this.currentBaseTurretAngle = 0;
            clearInterval (this.drawId);
            this.drawId = setInterval (drawFrame, 16, this, 0.016);
        };

        function drawFrame (animationPanel, deltaTime) {
            if (animationPanel.tank == null) {
                return;
            }
            animationPanel.canvasContext.clearRect (0, 0, animationPanel.width, animationPanel.height);
            animationPanel.canvasContext.beginPath ();
            animationPanel.canvasContext.strokeStyle = "#000000";
            animationPanel.canvasContext.moveTo (animationPanel.halfWidth, animationPanel.halfHeight);
            animationPanel.canvasContext.lineTo (animationPanel.halfWidth, animationPanel.halfHeight - animationPanel.gunLength);
            animationPanel.canvasContext.stroke ();
            draw (animationPanel.canvasContext, animationPanel.halfWidth, animationPanel.halfHeight, animationPanel.turretRadius, animationPanel.currentTurretAngle, animationPanel.gunLength, window.color.lightGreen);
            draw (animationPanel.canvasContext, animationPanel.halfWidth, animationPanel.halfHeight, animationPanel.baseTurretRadius, animationPanel.currentBaseTurretAngle, animationPanel.gunLength, window.color.lightCoral);
            animationPanel.currentTurretAngle += animationPanel.tank.turretTraverse * deltaTime;
            animationPanel.currentBaseTurretAngle += animationPanel.baseTank.turretTraverse * deltaTime;
        }

        function draw (canvasContext, turretX, turretY, turretRadius, turretAngle, gunLength, color) {
            canvasContext.beginPath ();
            canvasContext.strokeStyle = color;
            canvasContext.arc (turretX, turretY, turretRadius, 0, 2 * Math.PI);
            canvasContext.moveTo (turretX, turretY);
            var turretRadian = Deg2Rad (turretAngle - 90);
            var x = turretX + gunLength * Math.cos (turretRadian);
            var y = turretY + gunLength * Math.sin (turretRadian);
            canvasContext.lineTo (x, y);
            canvasContext.stroke ();
        }

        this.play ();

    }

    function Tank () {
        this.name = null;
        this.aimTime = 0;
        this.dispersion = 0;
        this.dispersionFactorMoving = 0;
        this.dispersionFactorTankTraverse = 0;
        this.dispersionFactorTurretTraverse = 0;
        this.dispersionFactorAfterFiring = 0;
        this.dispersionFactorDamaged = 0;
        this.effectiveTopSpeedHard = 0;
        this.effectiveTopSpeedMedium = 0;
        this.effectiveTopSpeedSoft = 0;
        this.effectiveTraverseHard = 0;
        this.effectiveTraverseMedium = 0;
        this.effectiveTraverseSoft = 0;
        this.TurretTraverse = 0;

        this.toString = function () {
            return "Name: " + this.name +
                "\nAimTime: " + this.aimTime +
                "\nDispersion: " + this.dispersion +
                "\nDispersionFactorMoving: " + this.dispersionFactorMoving +
                "\nDispersionFactorTankTraverse: " + this.dispersionFactorTankTraverse +
                "\nDispersionFactorTurretTraverse: " + this.dispersionFactorTurretTraverse +
                "\nDispersionFactorAfterFiring: " + this.dispersionFactorAfterFiring +
                "\nDispersionFactorDamaged: " + this.dispersionFactorDamaged +
                "\nEffectiveTopSpeedHard: " + this.effectiveTopSpeedHard +
                "\nEffectiveTopSpeedMedium: " + this.effectiveTopSpeedMedium +
                "\nEffectiveTopSpeedSoft: " + this.effectiveTopSpeedSoft +
                "\nEffectiveTraverseHard: " + this.effectiveTraverseHard +
                "\nEffectiveTraverseMedium: " + this.effectiveTraverseMedium +
                "\nEffectiveTraverseSoft: " + this.effectiveTraverseSoft +
                "\nTurretTraverse: " + this.TurretTraverse;
        };

        this.calculateState = function (aimInformationPanel) {
            var terrain = aimInformationPanel.getTerrain ();
            var isMoving = aimInformationPanel.getIsMoving ();
            var isTankTraverse = aimInformationPanel.getIsTankTraversing ();
            var isTurretTraverse = aimInformationPanel.getIsTurretTraversing ();
            var isFiring = aimInformationPanel.getIsFiring ();
            var isGunDamaged = aimInformationPanel.getIsGunDamaged ();
            var moveSpeed = 0;
            var tankTraverseSpeed = 0;
            var turretTraverseSpeed = this.turretTraverse;
            switch (terrain) {
                case "Hard":
                    moveSpeed = this.effectiveTopSpeedHard;
                    tankTraverseSpeed = this.effectiveTraverseHard;
                    break;
                case "Medium":
                    moveSpeed = this.effectiveTopSpeedMedium;
                    tankTraverseSpeed = this.effectiveTraverseMedium;
                    break;
                case "Soft":
                    moveSpeed = this.effectiveTopSpeedSoft;
                    tankTraverseSpeed = this.effectiveTraverseSoft;
                    break;
            }
            if (!isMoving) {
                moveSpeed = 0;
            }
            if (!isTankTraverse) {
                tankTraverseSpeed = 0;
            }
            if (!isTurretTraverse) {
                turretTraverseSpeed = 0;
            }
            return {
                terrain: terrain,
                isMoving: isMoving,
                isTankTraverse: isTankTraverse,
                isTurretTraverse: isTurretTraverse,
                isFiring: isFiring,
                isGunDamaged: isGunDamaged,
                moveSpeed: moveSpeed,
                tankTraverseSpeed: tankTraverseSpeed,
                turretTraverseSpeed: turretTraverseSpeed
            };
        };

        this.calculateAimTime = function (moveSpeed, tankTraverseSpeed, turretTraverseSpeed, isFiring, isGunDamaged) {
            var movePenalty = Math.pow (moveSpeed * this.dispersionFactorMoving, 2);
            var tankTraversePenalty = Math.pow (tankTraverseSpeed * this.dispersionFactorTankTraverse, 2);
            var turretTraversePenalty = Math.pow (turretTraverseSpeed * this.dispersionFactorTurretTraverse, 2);
            var firePenalty = isFiring ? Math.pow (this.dispersionFactorAfterFiring, 2) : 0;
            var dispersionFactor = Math.sqrt (1 + movePenalty + tankTraversePenalty + turretTraversePenalty + firePenalty);
            var aimTime = Math.log (dispersionFactor) * this.aimTime;
            var dispersion = (isGunDamaged ? (this.dispersion * this.dispersionFactorDamaged) : this.dispersion) * dispersionFactor;
            return {
                aimTime: aimTime,
                dispersion: dispersion
            };
        };

        this.calculateAimTimeByTime = function (actualAimTime, actualDispersion, time) {
            var current = clamp (time / actualAimTime, 1, 0);
            var aimTime = time;
            var dispersion = actualDispersion / (Math.pow (Math.E, current * (actualAimTime / this.aimTime)));
            return {
                aimTime: aimTime,
                dispersion: dispersion
            };
        };

    }

}) ();