Gats.io-BuildOutliner

Tool for aligning build in gats.io

// ==UserScript==
// @name         Gats.io-BuildOutliner
// @namespace    http://tampermonkey.net/
// @version      1.4.1
// @description  Tool for aligning build in gats.io
// @author       Medbay3medic
// @match        https://gats.io
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Settings
    const KeyToggleOutline = 'Q';
    const KeyToggleGUI = 'E';
    const KeyToggleBinocular = 'F';
    const KeyToggleExGUI = 'Z';
    const KeyToggleExMode = 'X';
    const KeyIncreaseSize = 'O';
    const KeyDecreaseSize = 'P';
    const KeyIncreasePos = 'K';
    const KeyDecreasePos = 'L';
    const KeyIncreaseAngle = 'I';
    const KeyDecreaseAngle = 'U';
    const KeyToggleOverlap = 'I';
    const KeyToggleGradient = 'I';
    const KeyBuild = ' '
    const BlockSize = 45;
    const BlockColorPrimary = '#00ffff';
    const BlockColorSecondary = '#9400d3';
    const BlockColorTertiary = '#f3b3c3';
    const BlockOutlineWidth = 1;
    const BlockDistanceFromCharacter = 55;
    const CharacterSize = 24;
    const ScreenMouseOffsetRatio = 1/12.5;
    const CharacterLeftHandInset = 5;
    const BinocularScaleRatio = 0.95;
    const CrosshairSize = 10;
    const CrosshairColor = '#00ffff';
    const CrosshairColorCircumcentre = '#9400d3';
    const SlantedDirectionStep = 15;
    const ModeNone = 0;
    const ModeGrid = 1;
    const ModeSlanted = 2;
    const ModeCircle = 3;
    const ModeCustom = 4;
    const GridSizeMax = 5;
    const SlantedSizeMin = 1;
    const SlantedSizeMax = 9;
    let SlantedSize = 5;
    let SlantedPosition = Math.floor(SlantedSize / 2);
    const CircleSizeMin = 1;
    const CircleSizeMax = 9;
    let CircleSize = 2;
    let CirclePos = 1;
    let CircleSquares = [];
    let IsCircleOverlapping = false;
    let CustomPos = 1;
    let CustomSquares = [];
    let IsGradientActive = true;
    let IsOutline = false;
    let IsBinocular = false;
    let Mode = ModeNone;
    let AnimationID = null;
    let IsGUIActive = true;
    let IsExGUIActive = false;
    let SlantedDirection = 0;
    let GridSizeIndex = 2;
    let GridSize = GridSizeIndex * 2 - 1;
    let GridCentrePos = (GridSize - 1) / 2;
    let GridXPos = GridCentrePos;
    let GridYPos = GridCentrePos;
    let IsKeysActive = true;
    let ScreenXPos = window.innerWidth / 2;
    let ScreenYPos = window.innerHeight / 2;
    let MouseXPos = window.innerWidth / 2;
    let MouseYPos = window.innerHeight / 2;
    document.addEventListener("mousemove", function(e) {
        MouseXPos = e.clientX;
        MouseYPos = e.clientY;
    });
    let IsChatLoopActive = false;
    const ChatLoopTextLength = 28;
    const ChatLoopText = 'MedieBot Building------!!Do Not Disturb!!------';
    const ChatLoopSpeed = 100;
    let ChatLoopIndex = 0;
    const CDBarWidth = 20;
    const CDBarYPos = 675;
    const CDBarLength = 900;
    const CDBarColor = 'rgba(255, 165, 0, 0.5)';
    const CDBarBackgroundColor = 'rgba(128, 128, 128, 0.5)';
    const CDBarDuration = 7500;
    let CDBarStartTime = 0;
    let IsCDBarActive = false;
    let CDBarAnimationID = null;
    const CDBarCanvas = document.createElement('canvas');
    const CDBarContext = CDBarCanvas.getContext('2d');
    CDBarCanvas.style.position = 'fixed';
    CDBarCanvas.style.top = '0';
    CDBarCanvas.style.left = '0';
    CDBarCanvas.style.width = '100%';
    CDBarCanvas.style.height = '100%';
    CDBarCanvas.style.pointerEvents = 'none';
    CDBarCanvas.style.zIndex = '9998';
    CDBarCanvas.style.display = IsGUIActive ? 'block' : 'none';
    document.body.appendChild(CDBarCanvas);
    const MainCanvas = document.createElement('canvas');
    const MainContext = MainCanvas.getContext('2d');
    MainCanvas.style.position = 'fixed';
    MainCanvas.style.top = '0';
    MainCanvas.style.left = '0';
    MainCanvas.style.width = '100%';
    MainCanvas.style.height = '100%';
    MainCanvas.style.pointerEvents = 'none';
    MainCanvas.style.zIndex = '9999';
    MainCanvas.style.display = 'none';
    document.body.appendChild(MainCanvas);
    const GUIDiv = document.createElement('div');
    document.body.appendChild(GUIDiv);
    const ExGUIDiv = document.createElement('div');
    document.body.appendChild(ExGUIDiv);
    let MedbayFileContent;
    let MedbayFile;
    let MedbayFileName = 'None';

    // Functions

    function ResizeCanvas() {
        MainCanvas.width = window.innerWidth;
        MainCanvas.height = window.innerHeight;
        CDBarCanvas.width = window.innerWidth;
        CDBarCanvas.height = window.innerHeight;
        ScreenXPos = MainCanvas.width / 2;
        ScreenYPos = MainCanvas.height / 2;
    }

    // Copied from Gats.io- Chat scroller by nitrogem35 hehe

    function ChatLoop() {
        if (!IsChatLoopActive) return;
        let displayText = ChatLoopText.substring(ChatLoopIndex, ChatLoopIndex + ChatLoopTextLength);
        if (displayText.length < ChatLoopTextLength) {
            displayText += ChatLoopText.substring(0, ChatLoopTextLength - displayText.length);
        }

        let ChatLoopOutput = displayText.split('');

        ChatLoopOutput = ChatLoopOutput.join("");
        //encode commas (,) as tilde (~) because gats client does that
        ChatLoopOutput = ChatLoopOutput.replaceAll(",", "~");
        Connection.list[0].socket.send(`c,${ChatLoopOutput}`);

        ChatLoopIndex = (ChatLoopIndex + 1) % ChatLoopText.length;

        setTimeout(ChatLoop, ChatLoopSpeed);
    }

    function StartChatLoop() {
        IsChatLoopActive = true;
        ChatLoop();
    }

    function StopChatLoop() {
        IsChatLoopActive = false;
    }

    function ToggleChatLoop() {
        if (IsChatLoopActive) {
            StopChatLoop();
        } else {
            StartChatLoop();
        }
        ExGUI();
    }

    function GUI() {
        GUIDiv.innerHTML = `
        <style>
            .main {
                pointer-events: none;
                position: fixed;
                z-index: 9999;
                top: 210px;
                left: 10px;
                font-family: 'arial';
                color: pink;
                font-size: 15px;
                background-color: rgba(1,1,1,0.5);
                padding: 10px;
                border-radius: 5px;
                display: ${IsGUIActive ? 'block' : 'none'};
            }
            .key {
                color: #9400d3;
            }
            .status {
                color: #9400d3;
            }
            .Keys-Button {
                pointer-events: auto;
                cursor: pointer;
                background-color: rgba(148, 0, 211, 0.7);
                color: white;
                border: none;
                padding: 5px 10px;
                border-radius: 3px;
                margin: 5px 0;
                font-family: 'arial';
                font-size: 14px;
                width: 100%;
                text-align: center;
            }
            .Keys-Button:hover {
                background-color: rgba(148, 0, 211, 0.9);
            }
        </style>
        <div class="main">
            <p>Medbay3medic's BuildOutliner Mod ^ ̳⩌ ̫ ⩌ ̳^</p>
            <button class="Keys-Button">${IsKeysActive ? 'Disable Keys' : 'Enable Keys'}</button>
            <br>"<span class="key">${KeyToggleOutline}</span>" to toggle outline</br>
            <br>"<span class="key">${KeyToggleGUI}</span>" to toggle GUI</br>
            <br>"<span class="key">${KeyToggleBinocular}</span>" to toggle Binocular mode</br>
            <br>"<span class="key">${KeyToggleExGUI}</span>" to toggle Extension</br>
            <br>Current mode: "<span class="status">${IsOutline ? 'On' : 'Off'}</span>"</br>
            <br>Binocular mode: "<span class="status">${IsBinocular ? 'On' : 'Off'}</span>"</br>
            <br>Extension: "<span class="status">${IsExGUIActive ? 'On' : 'Off'}</span>"</br>
            <br>Keys: "<span class="status">${IsKeysActive ? 'Enabled' : 'Disabled'}</span>"</br>
            <br><3 Thank You So Much For Using My Script! <3</br>
        </div>`;

        const KeysButton = GUIDiv.querySelector('.Keys-Button');
        KeysButton.addEventListener('click', () => {
            IsKeysActive = !IsKeysActive;
            GUI();
            ExGUI();
        });
    }

    function ExGUI() {
        let ModeText, ModeKeys;
        switch(Mode) {
            case ModeGrid:
                ModeText = `Grid (${GridSize}x${GridSize})`;
                ModeKeys = `
                <br>"<span class="key">${KeyIncreaseSize}</span>"/"<span class="key">${KeyDecreaseSize}</span>" to adjust grid size</br>
                <br>"<span class="key">${KeyIncreasePos}</span>"/"<span class="key">${KeyDecreasePos}</span>" to adjust X position (${GridXPos})</br>
                <br>"<span class="key">${KeyIncreaseAngle}</span>"/"<span class="key">${KeyDecreaseAngle}</span>" to adjust Y position (${GridYPos})</br>
            `;
                break;
            case ModeSlanted:
                ModeText = `Slanted Line (${SlantedDirection}°, ${SlantedSize} squares)`;
                ModeKeys = `
                <br>"<span class="key">${KeyIncreaseSize}</span>"/"<span class="key">${KeyDecreaseSize}</span>" to adjust line length (${SlantedSize})</br>
                <br>"<span class="key">${KeyIncreasePos}</span>"/"<span class="key">${KeyDecreasePos}</span>" to adjust position (${SlantedPosition})</br>
                <br>"<span class="key">${KeyIncreaseAngle}</span>"/"<span class="key">${KeyDecreaseAngle}</span>" to adjust angle</br>
            `;
                break;
            case ModeCircle:
                ModeText = `Circle (R:${CircleSize})`;
                ModeKeys = `
                <br>"<span class="key">${KeyIncreaseSize}</span>"/"<span class="key">${KeyDecreaseSize}</span>" to adjust radius</br>
                <br>"<span class="key">${KeyIncreasePos}</span>"/"<span class="key">${KeyDecreasePos}</span>" to adjust position</br>
                <br>"<span class="key">${KeyToggleOverlap}</span>" to toggle overlapping (${IsCircleOverlapping ? 'ON' : 'OFF'})</br>
                <br>Position: ${CirclePos} of ${CircleSquares.length}</br>
            `;
                break;
            case ModeCustom:
                ModeText = `Custom File: ${MedbayFileName}`;
                ModeKeys = `
                <br>"<span class="key">${KeyIncreasePos}</span>"/"<span class="key">${KeyDecreasePos}</span>" to adjust position</br>
                <br>"<span class="key">${KeyToggleGradient}</span>" to toggle gradient (${IsGradientActive ? 'ON' : 'OFF'})</br>
                <br>Position: ${CustomPos} of ${CustomSquares.length}</br>
            `;
                break;
            default:
                ModeText = 'None';
                ModeKeys = '';
        }

        ExGUIDiv.innerHTML = `
    <style>
        .extension {
            pointer-events: none;
            position: fixed;
            z-index: 9999;
            top: 250px;
            right: 10px;
            font-family: 'arial';
            color: pink;
            font-size: 15px;
            background-color: rgba(1,1,1,0.5);
            padding: 10px;
            border-radius: 5px;
            display: ${IsExGUIActive ? 'block' : 'none'};
        }
        .extension-header {
            color: pink;
            font-size: 15px;
            margin-bottom: 5px;
        }
        .key {
            color: #9400d3;
        }
        .status {
            color: #9400d3;
        }
        .action-button {
            pointer-events: auto;
            cursor: pointer;
            background-color: rgba(148, 0, 211, 0.7);
            color: white;
            border: none;
            padding: 5px 10px;
            border-radius: 3px;
            margin: 5px 0;
            font-family: 'arial';
            font-size: 14px;
            width: 100%;
            text-align: center;
        }
        .action-button:hover {
            background-color: rgba(148, 0, 211, 0.9);
        }
        .chatloop-button {
            background-color: ${IsChatLoopActive ? 'rgba(255, 0, 0, 0.7)' : 'rgba(148, 0, 211, 0.7)'} !important;
        }
        .chatloop-button:hover {
            background-color: ${IsChatLoopActive ? 'rgba(255, 0, 0, 0.9)' : 'rgba(148, 0, 211, 0.9)'} !important;
        }
        .medbay-result {
            color: #9400d3;
            font-weight: bold;
            margin: 0 0 5px 0;
            text-align: center;
            font-size: 14px;
        }
    </style>
    <div class="extension">
        <div class="extension-header">Medbay3medic's BuildOutliner Mod ^ ̳⩌ ̫ ⩌ ̳^</div>

        <button class="action-button" id="medbayButton">Input .medbay File</button>
        <input type="file" id="MedbayFileContent" accept=".medbay" style="display:none">

        <button class="action-button chatloop-button" id="chatLoopButton">
            ${IsChatLoopActive ? 'Stop Chat Scroll' : 'Start Chat Scroll'}
        </button>

        <br>Extension GUI</br>
        <br>"<span class="key">${KeyToggleExMode}</span>" to toggle mode</br>
        <br>Mode: "<span class="status">${ModeText}</span>"</br>
        ${ModeKeys}
    </div>`;

        document.getElementById('chatLoopButton').addEventListener('click', ToggleChatLoop);

        document.getElementById('medbayButton').addEventListener('click', () => {
            document.getElementById('MedbayFileContent').click();
        });

        document.getElementById('MedbayFileContent').addEventListener('change', (e) => {
            MedbayFile = e.target.files[0];
            if (!MedbayFile || !MedbayFile.name.endsWith('.medbay')) {
                showFileError();
                return;
            }

            const reader = new FileReader();
            reader.onload = (event) => {
                try {
                    MedbayFileContent = event.target.result.trim();
                    MedbayFileName = MedbayFile.name;
                    if (!CalculateCustomSquares()) {
                        return;
                    }
                    ExGUI();
                } catch (error) {
                    showFileError();
                }
            };
            reader.onerror = () => {
                showFileError();
            };
            reader.readAsText(MedbayFile);
        });
    }


    function CalculateCircleSquares() {
        CircleSquares = [];
        const CircleSquarePoints = [];
        if (IsCircleOverlapping) {
            let sides = (CircleSize + 1) * 8;
            for (let i = 0; i < sides; i++) {
                const angle = (i / sides) * Math.PI * 2;
                const x = Math.cos(angle) * CircleSize;
                const y = Math.sin(angle) * CircleSize;
                CircleSquarePoints.push({x: x, y: y});
            }
        } else {
            let sides = (CircleSize + 1) * 4;
            for (let i = 0; i < sides; i++) {
                const angle = (i / sides) * Math.PI * 2;
                const x = Math.cos(angle) * CircleSize;
                const y = Math.sin(angle) * CircleSize;
                CircleSquarePoints.push({x: x, y: y});
            }
        }
        CircleSquarePoints.sort((a, b) => {
            if (Math.abs(a.y - b.y) > 0.0001) {
                return a.y - b.y;
            }
            return a.x - b.x;
        });

        CircleSquares = CircleSquarePoints;
        CirclePos = Math.max(1, Math.min(CirclePos, CircleSquares.length));
    }

    function CalculateCustomSquares() {
        try {
            const MedbayFormat = /^Medbay3medicBuiltOutlinerCode:({\(\d{3}\.\d{3}\)\(\d{3}\.\d{3}\)\(\d\)})+$/;
            if (!MedbayFormat.test(MedbayFileContent)) {
                throw new Error("Invalid file format");
            }

            CustomSquares = [];
            const CustomSquarePoints = [];
            const pointRegex = /\((\d{3}\.\d{3})\)\((\d{3}\.\d{3})\)\((\d)\)/g;
            let match;
            while ((match = pointRegex.exec(MedbayFileContent)) !== null) {
                CustomSquarePoints.push({
                    x: match[1],
                    y: match[2],
                    gradient: match[3]
                });
            }
            CustomSquares = CustomSquarePoints;
            CustomPos = 1;
            if (Mode === ModeCustom && IsOutline) {
                Draw();
            }
            return true;
        } catch (error) {
            showFileError();
            return false;
        }
    }

    function showFileError() {
        const button = document.getElementById('medbayButton');
        if (!button) return;

        const originalText = button.textContent;
        const originalBackground = button.style.backgroundColor;
        const fileInput = document.getElementById('MedbayFileContent');
        fileInput.value = '';
        button.textContent = 'Error! Invalid file!';
        button.style.backgroundColor = '#ff3333';
        setTimeout(() => {
            button.textContent = originalText;
            button.style.backgroundColor = originalBackground;
        }, 2000);
    }

    function DrawCDBar() {
        CDBarContext.clearRect(0, 0, CDBarCanvas.width, CDBarCanvas.height);
        if (!IsGUIActive) return;
        const CDBarRemaining = (CDBarCanvas.width - CDBarLength) / 2;
        CDBarContext.fillStyle = CDBarColor;
        CDBarContext.fillRect(CDBarRemaining, CDBarYPos, CDBarLength, CDBarWidth);
        let CDBarRemainingTime = 0;
        let IsCDBarReady = false;

        if (IsCDBarActive) {
            const CurrentTime = Date.now();
            const CDBarTimeElapsed = CurrentTime - CDBarStartTime;
            const CDBarProgress = Math.min(CDBarTimeElapsed / CDBarDuration, 1);
            CDBarRemainingTime = (CDBarDuration - CDBarTimeElapsed) / 1000;

            CDBarContext.fillStyle = CDBarBackgroundColor;
            CDBarContext.fillRect(
                CDBarRemaining + (CDBarLength * CDBarProgress),
                CDBarYPos,
                CDBarLength * (1 - CDBarProgress),
                CDBarWidth
            );

            if (CDBarProgress >= 1) {
                IsCDBarActive = false;
                IsCDBarReady = true;
            }
        }

        CDBarContext.fillStyle = 'white';
        CDBarContext.font = '16px Arial';
        CDBarContext.textAlign = 'left';
        CDBarContext.textBaseline = 'middle';

        const statusText = IsCDBarReady ? 'Ready' : `Build CD: ${CDBarRemainingTime > 0 ? CDBarRemainingTime.toFixed(2) + 's' : 'Ready'}`;
        CDBarContext.fillText(
            statusText,
            CDBarRemaining + 10,
            CDBarYPos + CDBarWidth / 2
        );

        CDBarAnimationID = requestAnimationFrame(DrawCDBar);
    }

    function DrawGrid(SquareX, SquareY, Size) {
        const GridShiftX = (GridXPos - GridCentrePos) * Size;
        const GridShiftY = (GridYPos - GridCentrePos) * Size;

        MainContext.strokeStyle = BlockColorTertiary;
        MainContext.lineWidth = IsBinocular ? BlockOutlineWidth * BinocularScaleRatio : BlockOutlineWidth;

        for (let x = 0; x < GridSize; x++) {
            for (let y = 0; y < GridSize; y++) {
                if (x === GridXPos && y === GridYPos) continue;

                const GridX = SquareX + (x - GridCentrePos) * Size - GridShiftX;
                const GridY = SquareY + (y - GridCentrePos) * Size - GridShiftY;

                if (GridX >= 0 && GridX <= MainCanvas.width && GridY >= 0 && GridY <= MainCanvas.height) {
                    MainContext.strokeRect(
                        GridX - Size / 2,
                        GridY - Size / 2,
                        Size,
                        Size
                    );
                    DrawCrosshair(GridX, GridY, Size, BlockColorTertiary);
                }
            }
        }

        MainContext.strokeStyle = BlockColorPrimary;
        MainContext.strokeRect(
            SquareX - Size / 2,
            SquareY - Size / 2,
            Size,
            Size
        );
        DrawCrosshair(SquareX, SquareY, Size, BlockColorPrimary);
    }

    function DrawSlantedLine(SquareX, SquareY, Size) {
        const SlantedCentrePosition = (SlantedSize - 1)/2;
        const SlantedAngleRadian = SlantedDirection * Math.PI / 180;
        const CosAngle = Math.cos(SlantedAngleRadian);
        const SinAngle = Math.sin(SlantedAngleRadian);

        const SlantedPosOffset = (SlantedPosition - SlantedCentrePosition) * Size;
        const OffsetX = SlantedPosOffset * CosAngle;
        const OffsetY = SlantedPosOffset * SinAngle;

        MainContext.strokeStyle = BlockColorTertiary;
        MainContext.lineWidth = IsBinocular ? BlockOutlineWidth * BinocularScaleRatio : BlockOutlineWidth;

        for (let i = 0; i < SlantedSize; i++) {
            if (i === SlantedPosition) continue;

            const Distance = (i - SlantedCentrePosition) * Size;
            const GridX = SquareX + Distance * CosAngle - OffsetX;
            const GridY = SquareY + Distance * SinAngle - OffsetY;

            MainContext.strokeRect(
                GridX - Size / 2,
                GridY - Size / 2,
                Size,
                Size
            );

            DrawCrosshair(GridX, GridY, Size, BlockColorTertiary);
        }

        MainContext.strokeStyle = BlockColorPrimary;
        MainContext.strokeRect(
            SquareX - Size / 2,
            SquareY - Size / 2,
            Size,
            Size
        );

        DrawCrosshair(SquareX, SquareY, Size, BlockColorPrimary);
    }

    function DrawCircle(SquareX, SquareY, Size) {
        if (CircleSquares.length === 0) return;

        const TargetSquare = CirclePos >= 1 && CirclePos <= CircleSquares.length
        ? CircleSquares[CirclePos - 1]
        : {x: 0, y: 0};

        const OffsetX = SquareX - TargetSquare.x * Size;
        const OffsetY = SquareY - TargetSquare.y * Size;

        MainContext.strokeStyle = BlockColorTertiary;
        MainContext.lineWidth = IsBinocular ? BlockOutlineWidth * BinocularScaleRatio : BlockOutlineWidth;

        for (let i = 0; i < CircleSquares.length; i++) {
            if (i === CirclePos - 1) continue;

            const Square = CircleSquares[i];
            const GridX = OffsetX + Square.x * Size;
            const GridY = OffsetY + Square.y * Size;

            if (IsCircleOverlapping) {
                MainContext.globalAlpha = 0.7;
            }

            MainContext.strokeRect(
                GridX - Size / 2,
                GridY - Size / 2,
                Size,
                Size
            );

            if (IsCircleOverlapping) {
                MainContext.globalAlpha = 1.0;
            }

            DrawCrosshair(GridX, GridY, Size, BlockColorTertiary);
        }

        MainContext.strokeStyle = BlockColorPrimary;
        MainContext.strokeRect(
            SquareX - Size / 2,
            SquareY - Size / 2,
            Size,
            Size
        );

        DrawCrosshair(OffsetX, OffsetY, Size, CrosshairColorCircumcentre);
        DrawCrosshair(SquareX, SquareY, Size, BlockColorPrimary);
    }

    function DrawCustom(SquareX, SquareY, Size) {
        if (CustomSquares.length === 0) return;

        const TargetSquare = CustomPos >= 1 && CustomPos <= CustomSquares.length
        ? CustomSquares[CustomPos - 1]
        : {x: 0, y: 0, gradient: 0};

        const OffsetX = SquareX - parseFloat(TargetSquare.x) * Size;
        const OffsetY = SquareY - parseFloat(TargetSquare.y) * Size;

        MainContext.strokeStyle = BlockColorTertiary;
        MainContext.lineWidth = IsBinocular ? BlockOutlineWidth * BinocularScaleRatio : BlockOutlineWidth;
        const gradientColors = [
            '#53c68c',
            '#79d2a6',
            '#9fdfbf',
            '#c6ecd9',
            '#ecf9f2'
        ];

        for (let i = 0; i < CustomSquares.length; i++) {
            if (i === CustomPos - 1) continue;

            const Square = CustomSquares[i];
            const GridX = OffsetX + parseFloat(Square.x) * Size;
            const GridY = OffsetY + parseFloat(Square.y) * Size;

            MainContext.strokeRect(
                GridX - Size / 2,
                GridY - Size / 2,
                Size,
                Size
            );
            if (IsGradientActive) {
                const bgSize = Size * 0.5;
                const gradient = Math.min(parseInt(Square.gradient), 4);
                MainContext.fillStyle = gradientColors[gradient];
                MainContext.fillRect(
                    GridX - bgSize / 2,
                    GridY - bgSize / 2,
                    bgSize,
                    bgSize
                );

                MainContext.fillStyle = 'black';
                MainContext.font = `${Size * 0.4}px Arial`;
                MainContext.textAlign = 'center';
                MainContext.textBaseline = 'middle';
                MainContext.fillText(Square.gradient, GridX, GridY);
            } else {
                DrawCrosshair(GridX, GridY, Size, BlockColorTertiary);
            }
        }

        MainContext.strokeStyle = BlockColorPrimary;
        MainContext.strokeRect(
            SquareX - Size / 2,
            SquareY - Size / 2,
            Size,
            Size
        );

        if (IsGradientActive) {
            const bgSize = Size * 0.5;
            const gradient = Math.min(parseInt(TargetSquare.gradient), 4);
            MainContext.fillStyle = gradientColors[gradient];
            MainContext.fillRect(
                SquareX - bgSize / 2,
                SquareY - bgSize / 2,
                bgSize,
                bgSize
            );

            MainContext.fillStyle = 'black';
            MainContext.font = `${Size * 0.4}px Arial`;
            MainContext.textAlign = 'center';
            MainContext.textBaseline = 'middle';
            MainContext.fillText(TargetSquare.gradient, SquareX, SquareY);
        } else {
            DrawCrosshair(SquareX, SquareY, Size, BlockColorPrimary);
        }

        DrawCrosshair(OffsetX, OffsetY, Size, CrosshairColorCircumcentre);
    }


    function DrawCrosshair(x, y, Size, Color) {
        const CurrentCrosshairSize = IsBinocular ? CrosshairSize * BinocularScaleRatio : CrosshairSize;
        const CrosshairHalfSize = CurrentCrosshairSize / 2;
        const CrosshairLineWidth = IsBinocular ? BlockOutlineWidth * BinocularScaleRatio : BlockOutlineWidth;

        MainContext.strokeStyle = Color;
        MainContext.lineWidth = CrosshairLineWidth;

        MainContext.beginPath();
        MainContext.moveTo(x - CrosshairHalfSize, y);
        MainContext.lineTo(x + CrosshairHalfSize, y);
        MainContext.stroke();

        MainContext.beginPath();
        MainContext.moveTo(x, y - CrosshairHalfSize);
        MainContext.lineTo(x, y + CrosshairHalfSize);
        MainContext.stroke();
    }

    function Draw() {
        MainContext.clearRect(0, 0, MainCanvas.width, MainCanvas.height);

        if (!IsOutline) return;

        MainContext.save();

        if (IsBinocular) {
            MainContext.translate(MainCanvas.width/2, MainCanvas.height/2);
            MainContext.scale(BinocularScaleRatio, BinocularScaleRatio);
            MainContext.translate(-MainCanvas.width/2, -MainCanvas.height/2);
        }

        const ScaleMultiplier = IsBinocular ? 1/BinocularScaleRatio : 1;
        const MouseXDistance = (MouseXPos - ScreenXPos) * ScaleMultiplier;
        const MouseYDistance = (MouseYPos - ScreenYPos) * ScaleMultiplier;
        const OffsetX = -MouseXDistance * ScreenMouseOffsetRatio;
        const OffsetY = -MouseYDistance * ScreenMouseOffsetRatio;
        const CharacterX = ScreenXPos + OffsetX;
        const CharacterY = ScreenYPos + OffsetY;

        MainContext.strokeStyle = BlockColorSecondary;
        MainContext.lineWidth = IsBinocular ? BlockOutlineWidth * BinocularScaleRatio : BlockOutlineWidth;
        MainContext.beginPath();
        MainContext.arc(CharacterX, CharacterY,
                        IsBinocular ? CharacterSize * BinocularScaleRatio : CharacterSize,
                        0, Math.PI * 2);
        MainContext.stroke();

        DrawCrosshair(CharacterX, CharacterY, CharacterSize, CrosshairColorCircumcentre);

        const MouseAngle = Math.atan2(MouseYPos - CharacterY, MouseXPos - CharacterX);
        const TangentAngle = MouseAngle - Math.PI/2;
        const CurrentCharacterRadius = IsBinocular ? CharacterSize * BinocularScaleRatio : CharacterSize;
        const TangentX = CharacterX + Math.cos(TangentAngle) * (CurrentCharacterRadius - CharacterLeftHandInset);
        const TangentY = CharacterY + Math.sin(TangentAngle) * (CurrentCharacterRadius - CharacterLeftHandInset);

        const OffsetAngle = Math.atan2(MouseYPos - TangentY, MouseXPos - TangentX);
        const CurrentDistanceFromCharacter = IsBinocular ? BlockDistanceFromCharacter * BinocularScaleRatio : BlockDistanceFromCharacter;
        const CurrentSquareX = CharacterX + Math.cos(OffsetAngle) * CurrentDistanceFromCharacter;
        const CurrentSquareY = CharacterY + Math.sin(OffsetAngle) * CurrentDistanceFromCharacter;
        const CurrentSquareSize = IsBinocular ? BlockSize * BinocularScaleRatio : BlockSize;

        if (Mode === ModeGrid) {
            DrawGrid(CurrentSquareX, CurrentSquareY, CurrentSquareSize);
        }
        else if (Mode === ModeSlanted) {
            DrawSlantedLine(CurrentSquareX, CurrentSquareY, CurrentSquareSize);
        }
        else if (Mode === ModeCircle) {
            DrawCircle(CurrentSquareX, CurrentSquareY, CurrentSquareSize);
        }
        else if (Mode === ModeCustom) {
            DrawCustom(CurrentSquareX, CurrentSquareY, CurrentSquareSize);
        }
        else {
            MainContext.strokeStyle = BlockColorPrimary;
            MainContext.lineWidth = IsBinocular ? BlockOutlineWidth * BinocularScaleRatio : BlockOutlineWidth;
            MainContext.strokeRect(
                CurrentSquareX - CurrentSquareSize / 2,
                CurrentSquareY - CurrentSquareSize / 2,
                CurrentSquareSize,
                CurrentSquareSize
            );
            DrawCrosshair(CurrentSquareX, CurrentSquareY, CurrentSquareSize, CrosshairColor);
        }

        MainContext.restore();
        AnimationID = requestAnimationFrame(Draw);
    }

    document.addEventListener('mousemove', (e) => {
        MouseXPos = e.clientX;
        MouseYPos = e.clientY;
    });

    function IsChatActive() {
        const ChatActiveElement = document.activeElement;
        return ChatActiveElement && (ChatActiveElement.tagName === 'INPUT' || ChatActiveElement.tagName === 'TEXTAREA');
    }

    document.addEventListener('keydown', (e) => {
        if (!IsKeysActive) return;
        if (IsChatActive()) return;

        const Key = e.key.toUpperCase();

        if (Key === KeyBuild) {
            e.preventDefault();
            if (!IsCDBarActive) {
                IsCDBarActive = true;
                CDBarStartTime = Date.now();
                if (!CDBarAnimationID) {
                    DrawCDBar();
                }
            }
            return;
        }

        if (Key === KeyToggleOutline) {
            IsOutline = !IsOutline;
            MainCanvas.style.display = IsOutline ? 'block' : 'none';
            if (IsOutline && !AnimationID) {
                Draw();
            } else if (!IsOutline && AnimationID) {
                cancelAnimationFrame(AnimationID);
                AnimationID = null;
            }
            GUI();
            ExGUI();
        }
        else if (Key === KeyToggleGUI) {
            IsGUIActive = !IsGUIActive;
            CDBarCanvas.style.display = IsGUIActive ? 'block' : 'none';
            GUIDiv.style.display = IsGUIActive ? 'block' : 'none';
            if (IsGUIActive && !CDBarAnimationID) {
                DrawCDBar();
            } else if (!IsGUIActive && CDBarAnimationID) {
                cancelAnimationFrame(CDBarAnimationID);
                CDBarAnimationID = null;
            }
        }
        else if (Key === KeyToggleBinocular) {
            IsBinocular = !IsBinocular;
            GUI();
            ExGUI();
        }
        else if (Key === KeyToggleExGUI) {
            IsExGUIActive = !IsExGUIActive;
            GUI();
            ExGUI();
        }
        else if (Key === KeyToggleExMode && IsExGUIActive) {
            Mode = (Mode + 1) % 5;
            ExGUI();
        }
        else if (IsExGUIActive) {
            if (Mode === ModeSlanted) {
                if (Key === KeyIncreaseAngle) {
                    SlantedDirection = (SlantedDirection + SlantedDirectionStep) % 360;
                    ExGUI();
                }
                else if (Key === KeyDecreaseAngle) {
                    SlantedDirection = (SlantedDirection - SlantedDirectionStep + 360) % 360;
                    ExGUI();
                }
                else if (Key === KeyIncreasePos) {
                    SlantedPosition = Math.min(SlantedPosition + 1, SlantedSize - 1);
                    ExGUI();
                }
                else if (Key === KeyDecreasePos) {
                    SlantedPosition = Math.max(SlantedPosition - 1, 0);
                    ExGUI();
                }
                else if (Key === KeyIncreaseSize) {
                    SlantedSize = Math.min(SlantedSize + 1, SlantedSizeMax);
                    SlantedPosition = Math.min(SlantedPosition, SlantedSize - 1);
                    ExGUI();
                }
                else if (Key === KeyDecreaseSize) {
                    SlantedSize = Math.max(SlantedSize - 1, SlantedSizeMin);
                    SlantedPosition = Math.min(SlantedPosition, SlantedSize - 1);
                    ExGUI();
                }
            }
            else if (Mode === ModeGrid) {
                if (Key === KeyIncreaseSize) {
                    GridSizeIndex = (GridSizeIndex % GridSizeMax) + 1
                    GridSize = GridSizeIndex * 2 - 1
                    GridXPos = Math.floor(GridSize / 2);
                    GridYPos = Math.floor(GridSize / 2);
                    ExGUI();
                }
                else if (Key === KeyDecreaseSize) {
                    GridSizeIndex = ((GridSizeIndex - 2 + GridSizeMax) % GridSizeMax) + 1
                    GridSize = GridSizeIndex * 2 - 1
                    GridXPos = Math.floor(GridSize / 2)
                    GridYPos = Math.floor(GridSize / 2)
                    ExGUI();
                }
                else if (Key === KeyIncreasePos) {
                    GridXPos = (GridXPos + 1) % GridSize;
                    ExGUI();
                }
                else if (Key === KeyDecreasePos) {
                    GridXPos = (GridXPos - 1 + GridSize) % GridSize;
                    ExGUI();
                }
                else if (Key === KeyIncreaseAngle) {
                    GridYPos = (GridYPos + 1) % GridSize;
                    ExGUI();
                }
                else if (Key === KeyDecreaseAngle) {
                    GridYPos = (GridYPos - 1 + GridSize) % GridSize;
                    ExGUI();
                }
            }
            else if (Mode === ModeCircle) {
                if (Key === KeyIncreaseSize) {
                    CircleSize = Math.min(CircleSize + 1, CircleSizeMax);
                    CirclePos = 1;
                    CalculateCircleSquares();
                    ExGUI();
                }
                else if (Key === KeyDecreaseSize) {
                    CircleSize = Math.max(CircleSize - 1, CircleSizeMin);
                    CirclePos = 1;
                    CalculateCircleSquares();
                    ExGUI();
                }
                else if (Key === KeyIncreasePos) {
                    CirclePos = (CirclePos % CircleSquares.length) + 1;
                    ExGUI();
                }
                else if (Key === KeyDecreasePos) {
                    CirclePos = (CirclePos - 2 + CircleSquares.length) % CircleSquares.length + 1;
                    ExGUI();
                }
                else if (Key === KeyToggleOverlap) {
                    IsCircleOverlapping = !IsCircleOverlapping;
                    CalculateCircleSquares();
                    ExGUI();
                }
            }
            else if (Mode === ModeCustom) {
                if (Key === KeyIncreasePos) {
                    CustomPos = (CustomPos % CustomSquares.length) + 1;
                    ExGUI();
                }
                else if (Key === KeyDecreasePos) {
                    CustomPos = (CustomPos - 2 + CustomSquares.length) % CustomSquares.length + 1;
                    ExGUI();
                }
                else if (Key === KeyToggleGradient) {
                    IsGradientActive = !IsGradientActive;
                    CalculateCustomSquares();
                    ExGUI();
                }
            }
        }
    });

    // Main

    window.addEventListener('resize', ResizeCanvas);
    ResizeCanvas();
    CalculateCircleSquares();
    GUI();
    ExGUI();
    if (IsGUIActive) {
        DrawCDBar();
    }
})();