DFProfiler Path Finder

Find the fastest path in DFProfiler

// ==UserScript==
// @name         DFProfiler Path Finder
// @namespace    http://tampermonkey.net/
// @version      0.4
// @description  Find the fastest path in DFProfiler
// @author       Runonstof
// @match        https://*.dfprofiler.com/bossmap
// @match        https://*.dfprofiler.com/profile/view/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=dfprofiler.com
// @grant        unsafeWindow
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // === Utility functions ===

    function GM_addStyle(css) {
        var style = document.getElementById("GM_addStyleBy8626") || (function() {
            var style = document.createElement('style');
            style.type = 'text/css';
            style.id = "GM_addStyleBy8626";
            document.head.appendChild(style);
            return style;
        })();
        var sheet = style.sheet;

        sheet.insertRule(css, (sheet.rules || sheet.cssRules || []).length);
    }

    function GM_addStyle_object(selector, styles) {
        var css = selector + "{";
        for (var key in styles) {
            css += key + ":" + styles[key] + ";";
        }
        css += "}";
        GM_addStyle(css);
    }


    function ready(fn) {
        if (document.readyState != 'loading'){
            fn();
        } else {
            document.addEventListener('DOMContentLoaded', fn);
        }
    }

    function AStar(emptyCells) {
        this.emptyCells == emptyCells || [];

        this.Node = function Node(x, y) {
            this.x = parseInt(x);
            this.y = parseInt(y);
            this.g = 0; // cost from start node
            this.h = 0; // heuristic (estimated cost to target)
            this.f = 0; // total cost (g + h)
            this.parent = null;

            this.withoutParent = function () {
                var node = new Node(this.x, this.y);
                node.g = this.g;
                node.h = this.h;
                node.f = this.f;
                return node;
            }
        }

        this.isInsideMap = function(x, y) {
            return x >= 1000 && x <= 1058 && y >= 981 && y <= 1035;
        };

        this.isCellEmpty = function(x, y) {
            return emptyCells.some(function(cell) {
                return cell.x == x && cell.y == y;
            });
        };

        this.heuristic = function(node, target) {
            var dx = Math.abs(node.x - target.x);
            var dy = Math.abs(node.y - target.y);
            var diagonalSteps = Math.min(dx, dy);
            var straightSteps = Math.abs(dx - dy);

            return (1.2 * diagonalSteps) + straightSteps;
        }

        this.find = function(startPos, endPos) {
            var openList = [];
            var closedList = [];

            var startNode = new this.Node(startPos.x, startPos.y);
            var endNode = new this.Node(endPos.x, endPos.y);

            var diagonalCost = 1.2;
            var neighbors = [
                { x: 1, y: 0 },  // Right
                { x: -1, y: 0 }, // Left
                { x: 0, y: 1 },  // Down
                { x: 0, y: -1 }, // Up
                { x: 1, y: 1 },  // Diagonal down-right
                { x: -1, y: -1 },// Diagonal up-left
                { x: 1, y: -1 }, // Diagonal up-right
                { x: -1, y: 1 },   // Diagonal down-left
            ];

            openList.push(startNode);

            while (openList.length > 0) {
                var currentNode = openList[0];
                var currentIndex = 0;

                for (var i = 1; i < openList.length; i++) {
                    if (openList[i].f < currentNode.f) {
                        currentNode = openList[i];
                        currentIndex = i;
                    }
                }

                openList.splice(currentIndex, 1);
                closedList.push(currentNode);

                if (currentNode.x === endNode.x && currentNode.y === endNode.y) {
                    var path = [];
                    var current = currentNode;
                    while (current !== null) {
                        path.push(current.withoutParent());
                        current = current.parent;
                    }
                    return path.reverse();
                }


                for (var neighbourIndex in neighbors) {
                    var neighborDelta = neighbors[neighbourIndex];
                    var neighborX = currentNode.x + neighborDelta.x;
                    var neighborY = currentNode.y + neighborDelta.y;

                    if (!this.isInsideMap(neighborX, neighborY) || this.isCellEmpty(neighborX, neighborY)) {
                        // console.log('cell is empty or outside map:', neighborX, neighborY);
                        continue;
                    }

                    var neighborNode = new this.Node(neighborX, neighborY);

                    var checkNeighbor = function(node) {
                        return node.x === neighborX && node.y === neighborY;
                    };

                    if (closedList.some(checkNeighbor)) {
                        continue;
                    }

                    var movementCost = 1;

                    if (neighborDelta.x != 0 && neighborDelta.y != 0) {
                        movementCost = diagonalCost;
                    }
                    //     movementCost = diagonalCost;
                    //     // // console.log('diagonal');
                    //     // // movementCost = diagonalCost;=
                    //     // // This is a diagonal movement
                    //     // if (this.isCellEmpty(currentNode.x + neighborDelta.x, currentNode.y) ||
                    //     //     this.isCellEmpty(currentNode.x, currentNode.y + neighborDelta.y)) {
                    //     //     // Increase diagonal cost if there is an adjacent empty cell
                    //     //     movementCost = 1.50; // You can adjust this value based on your preference
                    //     // } else {
                    //     //     movementCost = 1;
                    //     // }

                    // }

                    var tentativeG = currentNode.g + movementCost; // Assuming each step costs 1

                    if (!openList.some(checkNeighbor) || tentativeG < neighborNode.g) {
                        neighborNode.g = tentativeG;
                        neighborNode.h = this.heuristic(neighborNode, endNode);
                        neighborNode.f = neighborNode.g + neighborNode.h;
                        neighborNode.parent = currentNode;

                        if (!openList.some(checkNeighbor)) {
                            openList.push(neighborNode);
                        }
                    }
                }
            }

            // console.log(closedList);

            return null; // No path found
        }
    }

    // formats ms to hh:mm:ss
    function formatTime(ms) {
        var seconds = Math.floor(ms / 1000);
        var minutes = Math.floor(seconds / 60);
        var hours = Math.floor(minutes / 60);

        seconds -= minutes * 60;
        minutes -= hours * 60;

        var time = '';
        if (hours > 0) {
            time += hours + 'h ';
        }
        if (minutes > 0) {
            time += minutes + 'm ';
        }
        if (seconds > 0) {
            time += seconds + 's';
        }

        return time;
    }

    // === CSS styles ===

    GM_addStyle_object('#boss-data-section #mission-info, #bossmap-page #mission-info', {
        'border-radius': '25px 25px 0 0',
    });
    GM_addStyle_object('#boss-data-section #mission-info-distance-viewer, #bossmap-page #mission-info-distance-viewer', {
        'position': 'absolute !important',
        'background-color': 'hsla(0,0%,5%,.8)',
        'border-radius': '0 0 25px 25px',
        'padding': '5px',
        'top': '770px',
        'left': 'calc(50% - 16pt * 20)',
        'right': 'calc(50% - 16pt * 20)',
    });

    GM_addStyle_object('#boss-data-section #mission-info-buttons-title, #bossmap-page #mission-info-buttons-title', {
        'color': 'white',
        'font-size': '20px',
    });
    // GM_addStyle_object('#boss-data-section #mission-info-buttons-subtitle, #bossmap-page #mission-info-buttons-subtitle', {
    //     'color': 'white',
    //     'font-size': '14px',
    // });
    GM_addStyle_object('#boss-data-section button.mission-info-button, #bossmap-page button.mission-info-button', {
        'background-color': 'gray',
        'color': 'black',
        'padding': '0.25em 0.5em',
    });
    GM_addStyle_object('#boss-data-section button.mission-info-button:hover, #bossmap-page button.mission-info-button:hover', {
        'color': 'white',
    });
    GM_addStyle_object('#boss-data-section .dist-buttons, #bossmap-page .dist-buttons', {
        'display': 'flex',
        'gap': '10px',
        'justify-content': 'center',
        'margin-bottom': '10px',
        'align-items': 'center',
    });
    GM_addStyle_object('#boss-data-section td.coord.path, #bossmap-page td.coord.path', {
        'background-color': 'yellow !important',
        'color': 'black !important',
    });

    // GM_addStyle_object('#coord-hover-tooltip', {
    //     'position': 'absolute',
    //     'background-color': 'hsla(0,0%,5%,.8)',
    //     'border-radius': '5px',
    //     'padding': '5px',
    // });


    ready(function () {

        // === Create Elements ===
        var missionHolder = document.getElementById('mission-holder');

        var container = document.createElement('div');
        container.id = 'mission-info-distance-viewer';

        container.innerHTML = '<div id="mission-info-buttons-title">Path finder</div>';
        container.innerHTML += '<div class="dist-buttons"><button id="dist-clear" style="display: none;" class="mission-info-button">Clear path</button></div>';
        container.innerHTML += '<div class="dist-buttons"><button id="dist-set-start" class="mission-info-button">Set path start</button><button id="dist-set-end" class="mission-info-button">Set path end</button></div>';
        container.innerHTML += '<div class="dist-buttons" style="display:none;">Player navigation: <button id="dist-set-goal" class="mission-info-button">Set path goal</button></div>';

        missionHolder.appendChild(container);

        var mapTopInfo = document.createElement('div');
        mapTopInfo.id = 'map-top-info';
        mapTopInfo.innerHTML = '<div id="mission-info-buttons-subtitle">No path selected, click on a cell to set a start and end point</div>';

        var bossTable = document.querySelector('#boss-table');
        if (bossTable.previousElementSibling) {
            bossTable.previousElementSibling.insertAdjacentElement('afterend', mapTopInfo);
        } else {
            bossTable.insertAdjacentElement('beforebegin', mapTopInfo);
        }

        // var tooltip = document.createElement('div');
        // tooltip.id = 'coord-hover-tooltip';
        // tooltip.style.display = 'none';

        // unsafeWindow.document.body.appendChild(tooltip);

        // function showTooltip(event, info) {
        //     tooltip.style.display = 'block';
        //     tooltip.style.position = 'absolute';
        //     // muose position
        //     tooltip.style.top = event.pageY + 10 + 'px';
        //     tooltip.style.left = event.pageX + 10 + 'px';
        //     tooltip.innerHTML = info;
        // }

        // function hideTooltip() {
        //     tooltip.style.display = 'none';
        // }


        unsafeWindow.closeMissionHolder = function (event) {
            if (event.target.closest('#mission-info-distance-viewer')) return;

            missionHolder.style.display = 'none';
        };

        missionHolder.setAttribute('onclick', 'closeMissionHolder(event)');

        var startCellButton = document.getElementById('dist-set-start');
        var endCellButton = document.getElementById('dist-set-end');
        var clearPathButton = document.getElementById('dist-clear');
        var setGoalButton = document.getElementById('dist-set-goal');

        var subtitle = document.getElementById('mission-info-buttons-subtitle');

        // === Scan empty cells

        var emptyCells = Array.from(document.querySelectorAll('td.coord'))
            .filter(function (el) {
                return el.computedStyleMap().get('opacity').toString() == '0';
            })
            .map(function (el) {
                return {
                    x: el.classList[1].replace('x', ''),
                    y: el.classList[2].replace('y', ''),
                };
            });

        var startCell = null;
        var endCell = null;

        var trackingGps = false;
        var lastTrackTime = null;
        var lastRemainingCellCount = 0;


        var pathFinder = new AStar(emptyCells);
        function maybeUpdatePath() {
            if (!startCell || !endCell) return false;

            var path = pathFinder.find(startCell, endCell);

            // Clear existing path cells
            var pathCells = unsafeWindow.document.querySelectorAll('td.coord.path');
            for (var i = 0; i < pathCells.length; i++) {
                pathCells[i].classList.remove('path');
                delete pathCells[i].dataset.distanceDebug;
                delete pathCells[i].dataset.distanceIndex;
                // pathCells[i].onmouseover = null;
            }

            // console.log(path);
            if (!path) return false;

            for(var i = 0; i < path.length; i++) {
                var cellCoord = path[i];
                // console.log(cellCoord);
                var cell = unsafeWindow.document.querySelector('td.coord.x' + cellCoord.x + '.y' + cellCoord.y);
                cell.classList.add('path');
                cell.dataset.distanceDebug = JSON.stringify(cellCoord);
                cell.dataset.distanceIndex = i;
                // cell.onmouseover = function(event) {
                //     if (!event.target.dataset.distanceDebug) return;

                //     var info = JSON.parse(event.target.dataset.distanceDebug);
                //     var index = parseInt(event.target.dataset.distanceIndex) + 1;
                //     showTooltip(event, 'Index: ' + index + '<br>Cell: ' + info.x + 'x' + info.y + '<br>Total cost: ' + info.g + '<br>Heuristic: ' + info.h + '<br>Total: ' + info.f);
                // };
            }
            clearPathButton.style.display = 'initial';

            subtitle.innerHTML = 'Path length: ' + path.length + ' cells';
            if (trackingGps) {
                subtitle.innerHTML += ' (tracking player)';

                // subtitle.innerHTML += '<br>' + (lastTrackTime === null ? 'Start walking to see estimated time' : 'Estimated time: ');
                subtitle.innerHTML += '<br>';

                if (lastTrackTime === null) {
                    subtitle.innerHTML += 'Start walking to see estimated time';
                } else {
                    var now = new Date().getTime();
                    var timeDiff = now - lastTrackTime;
                    var cellsTraveled = Math.abs(lastRemainingCellCount - path.length);

                    var timeRemaining;
                    if (!cellsTraveled) {
                        timeRemaining = 'Unknown';
                    } else {
                        var timePerCell = timeDiff / cellsTraveled;
                        timeRemaining = formatTime(timePerCell * path.length);
                    }

                    subtitle.innerHTML += 'Estimated time remaining: ' + timeRemaining;
                    subtitle.innerHTML += '<br>Cells traveled: ' + cellsTraveled;
                    subtitle.innerHTML += '<br>Time passed: ' + formatTime(timeDiff);
                }
            }
            return true;
        }

        var playerCoords = null;
        var playerCell = unsafeWindow.document.querySelector('td.playerlocation');

        if (playerCell) {
            playerCoords = [
                playerCell.classList[1].replace('x', ''),
                playerCell.classList[2].replace('y', ''),
            ];
        }


        startCellButton.onclick = function () {
            // current pos
            var img = unsafeWindow.document.querySelector('#mission-info img');
            var matches = img.src.match(/Fairview_(\d+)x(\d+)/);
            var x = matches[1];
            var y = matches[2];
            trackingGps = false;
            lastTrackTime = null;

            startCell = { x: x, y: y };

            unsafeWindow.document.querySelector('td.coord.x' + x + '.y' + y).classList.add('path');

            missionHolder.style.display = 'none';
            maybeUpdatePath();
        };

        endCellButton.onclick = function () {
            // current pos
            var img = document.querySelector('#mission-info img');
            var matches = img.src.match(/Fairview_(\d+)x(\d+)/);
            var x = matches[1];
            var y = matches[2];

            endCell = { x: x, y: y };

            unsafeWindow.document.querySelector('td.coord.x' + x + '.y' + y).classList.add('path');

            missionHolder.style.display = 'none';
            maybeUpdatePath();
        };

        clearPathButton.onclick = function () {
            startCell = null;
            endCell = null;
            trackingGps = false;
            lastTrackTime = null;

            clearPathButton.style.display = 'none';

            // Clear existing path cells
            var pathCells = unsafeWindow.document.querySelectorAll('td.coord.path');
            for (var i = 0; i < pathCells.length; i++) {
                pathCells[i].classList.remove('path');
            }

            subtitle.innerHTML = 'No path selected, click on a cell to set a start and end point';
            missionHolder.style.display = 'none';
        };

        var updatePlayerTrackPath = function () {
            if (!trackingGps) return;

            var playerCell = unsafeWindow.document.querySelector('td.playerlocation');
            if (!playerCell) {
                setTimeout(updatePlayerTrackPath, 7500);
                return;
            };


            playerCoords = [
                playerCell.classList[1].replace('x', ''),
                playerCell.classList[2].replace('y', ''),
            ];

            startCell = { x: playerCoords[0], y: playerCoords[1] };

            maybeUpdatePath();

            setTimeout(updatePlayerTrackPath, 7500);
        };


        setGoalButton.onclick = function () {
            trackingGps = true;
            missionHolder.style.display = 'none';

            // current pos
            var img = document.querySelector('#mission-info img');
            var matches = img.src.match(/Fairview_(\d+)x(\d+)/);
            var x = matches[1];
            var y = matches[2];

            endCell = { x: x, y: y };

            if (playerCoords) {
                startCell = { x: playerCoords[0], y: playerCoords[1] };
            } else {
                startCell = { x: x, y: y };
            }

            unsafeWindow.document.querySelector('td.coord.x' + x + '.y' + y).classList.add('path');
            var result = maybeUpdatePath();
            if (!result) {
                trackingGps = false;
                return;
            }

            lastTrackTime = new Date().getTime();
            lastRemainingCellCount = pathFinder.find(startCell, endCell).length;
            updatePlayerTrackPath();
        };


        $(unsafeWindow.document).ajaxComplete(function(event, jqXHR, ajaxOptions) {
            console.log('ajaxComplete', ajaxOptions.url)
            if (ajaxOptions.url.indexOf('/profile/json/') == -1) return;
            setGoalButton.parentElement.style.display = 'initial';
            playerCoords = jqXHR.responseJSON.gpscoords;

            // if (!trackingGps) return;
            // startCell = { x: coords[0], y: coords[1] };
            // maybeUpdatePath();
        });
    });
})();