DFProfiler Path Finder

Find the fastest path in DFProfiler

  1. // ==UserScript==
  2. // @name DFProfiler Path Finder
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.4
  5. // @description Find the fastest path in DFProfiler
  6. // @author Runonstof
  7. // @match https://*.dfprofiler.com/bossmap
  8. // @match https://*.dfprofiler.com/profile/view/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=dfprofiler.com
  10. // @grant unsafeWindow
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // === Utility functions ===
  18.  
  19. function GM_addStyle(css) {
  20. var style = document.getElementById("GM_addStyleBy8626") || (function() {
  21. var style = document.createElement('style');
  22. style.type = 'text/css';
  23. style.id = "GM_addStyleBy8626";
  24. document.head.appendChild(style);
  25. return style;
  26. })();
  27. var sheet = style.sheet;
  28.  
  29. sheet.insertRule(css, (sheet.rules || sheet.cssRules || []).length);
  30. }
  31.  
  32. function GM_addStyle_object(selector, styles) {
  33. var css = selector + "{";
  34. for (var key in styles) {
  35. css += key + ":" + styles[key] + ";";
  36. }
  37. css += "}";
  38. GM_addStyle(css);
  39. }
  40.  
  41.  
  42. function ready(fn) {
  43. if (document.readyState != 'loading'){
  44. fn();
  45. } else {
  46. document.addEventListener('DOMContentLoaded', fn);
  47. }
  48. }
  49.  
  50. function AStar(emptyCells) {
  51. this.emptyCells == emptyCells || [];
  52.  
  53. this.Node = function Node(x, y) {
  54. this.x = parseInt(x);
  55. this.y = parseInt(y);
  56. this.g = 0; // cost from start node
  57. this.h = 0; // heuristic (estimated cost to target)
  58. this.f = 0; // total cost (g + h)
  59. this.parent = null;
  60.  
  61. this.withoutParent = function () {
  62. var node = new Node(this.x, this.y);
  63. node.g = this.g;
  64. node.h = this.h;
  65. node.f = this.f;
  66. return node;
  67. }
  68. }
  69.  
  70. this.isInsideMap = function(x, y) {
  71. return x >= 1000 && x <= 1058 && y >= 981 && y <= 1035;
  72. };
  73.  
  74. this.isCellEmpty = function(x, y) {
  75. return emptyCells.some(function(cell) {
  76. return cell.x == x && cell.y == y;
  77. });
  78. };
  79.  
  80. this.heuristic = function(node, target) {
  81. var dx = Math.abs(node.x - target.x);
  82. var dy = Math.abs(node.y - target.y);
  83. var diagonalSteps = Math.min(dx, dy);
  84. var straightSteps = Math.abs(dx - dy);
  85.  
  86. return (1.2 * diagonalSteps) + straightSteps;
  87. }
  88.  
  89. this.find = function(startPos, endPos) {
  90. var openList = [];
  91. var closedList = [];
  92.  
  93. var startNode = new this.Node(startPos.x, startPos.y);
  94. var endNode = new this.Node(endPos.x, endPos.y);
  95.  
  96. var diagonalCost = 1.2;
  97. var neighbors = [
  98. { x: 1, y: 0 }, // Right
  99. { x: -1, y: 0 }, // Left
  100. { x: 0, y: 1 }, // Down
  101. { x: 0, y: -1 }, // Up
  102. { x: 1, y: 1 }, // Diagonal down-right
  103. { x: -1, y: -1 },// Diagonal up-left
  104. { x: 1, y: -1 }, // Diagonal up-right
  105. { x: -1, y: 1 }, // Diagonal down-left
  106. ];
  107.  
  108. openList.push(startNode);
  109.  
  110. while (openList.length > 0) {
  111. var currentNode = openList[0];
  112. var currentIndex = 0;
  113.  
  114. for (var i = 1; i < openList.length; i++) {
  115. if (openList[i].f < currentNode.f) {
  116. currentNode = openList[i];
  117. currentIndex = i;
  118. }
  119. }
  120.  
  121. openList.splice(currentIndex, 1);
  122. closedList.push(currentNode);
  123.  
  124. if (currentNode.x === endNode.x && currentNode.y === endNode.y) {
  125. var path = [];
  126. var current = currentNode;
  127. while (current !== null) {
  128. path.push(current.withoutParent());
  129. current = current.parent;
  130. }
  131. return path.reverse();
  132. }
  133.  
  134.  
  135. for (var neighbourIndex in neighbors) {
  136. var neighborDelta = neighbors[neighbourIndex];
  137. var neighborX = currentNode.x + neighborDelta.x;
  138. var neighborY = currentNode.y + neighborDelta.y;
  139.  
  140. if (!this.isInsideMap(neighborX, neighborY) || this.isCellEmpty(neighborX, neighborY)) {
  141. // console.log('cell is empty or outside map:', neighborX, neighborY);
  142. continue;
  143. }
  144.  
  145. var neighborNode = new this.Node(neighborX, neighborY);
  146.  
  147. var checkNeighbor = function(node) {
  148. return node.x === neighborX && node.y === neighborY;
  149. };
  150.  
  151. if (closedList.some(checkNeighbor)) {
  152. continue;
  153. }
  154.  
  155. var movementCost = 1;
  156.  
  157. if (neighborDelta.x != 0 && neighborDelta.y != 0) {
  158. movementCost = diagonalCost;
  159. }
  160. // movementCost = diagonalCost;
  161. // // // console.log('diagonal');
  162. // // // movementCost = diagonalCost;=
  163. // // // This is a diagonal movement
  164. // // if (this.isCellEmpty(currentNode.x + neighborDelta.x, currentNode.y) ||
  165. // // this.isCellEmpty(currentNode.x, currentNode.y + neighborDelta.y)) {
  166. // // // Increase diagonal cost if there is an adjacent empty cell
  167. // // movementCost = 1.50; // You can adjust this value based on your preference
  168. // // } else {
  169. // // movementCost = 1;
  170. // // }
  171.  
  172. // }
  173.  
  174. var tentativeG = currentNode.g + movementCost; // Assuming each step costs 1
  175.  
  176. if (!openList.some(checkNeighbor) || tentativeG < neighborNode.g) {
  177. neighborNode.g = tentativeG;
  178. neighborNode.h = this.heuristic(neighborNode, endNode);
  179. neighborNode.f = neighborNode.g + neighborNode.h;
  180. neighborNode.parent = currentNode;
  181.  
  182. if (!openList.some(checkNeighbor)) {
  183. openList.push(neighborNode);
  184. }
  185. }
  186. }
  187. }
  188.  
  189. // console.log(closedList);
  190.  
  191. return null; // No path found
  192. }
  193. }
  194.  
  195. // formats ms to hh:mm:ss
  196. function formatTime(ms) {
  197. var seconds = Math.floor(ms / 1000);
  198. var minutes = Math.floor(seconds / 60);
  199. var hours = Math.floor(minutes / 60);
  200.  
  201. seconds -= minutes * 60;
  202. minutes -= hours * 60;
  203.  
  204. var time = '';
  205. if (hours > 0) {
  206. time += hours + 'h ';
  207. }
  208. if (minutes > 0) {
  209. time += minutes + 'm ';
  210. }
  211. if (seconds > 0) {
  212. time += seconds + 's';
  213. }
  214.  
  215. return time;
  216. }
  217.  
  218. // === CSS styles ===
  219.  
  220. GM_addStyle_object('#boss-data-section #mission-info, #bossmap-page #mission-info', {
  221. 'border-radius': '25px 25px 0 0',
  222. });
  223. GM_addStyle_object('#boss-data-section #mission-info-distance-viewer, #bossmap-page #mission-info-distance-viewer', {
  224. 'position': 'absolute !important',
  225. 'background-color': 'hsla(0,0%,5%,.8)',
  226. 'border-radius': '0 0 25px 25px',
  227. 'padding': '5px',
  228. 'top': '770px',
  229. 'left': 'calc(50% - 16pt * 20)',
  230. 'right': 'calc(50% - 16pt * 20)',
  231. });
  232.  
  233. GM_addStyle_object('#boss-data-section #mission-info-buttons-title, #bossmap-page #mission-info-buttons-title', {
  234. 'color': 'white',
  235. 'font-size': '20px',
  236. });
  237. // GM_addStyle_object('#boss-data-section #mission-info-buttons-subtitle, #bossmap-page #mission-info-buttons-subtitle', {
  238. // 'color': 'white',
  239. // 'font-size': '14px',
  240. // });
  241. GM_addStyle_object('#boss-data-section button.mission-info-button, #bossmap-page button.mission-info-button', {
  242. 'background-color': 'gray',
  243. 'color': 'black',
  244. 'padding': '0.25em 0.5em',
  245. });
  246. GM_addStyle_object('#boss-data-section button.mission-info-button:hover, #bossmap-page button.mission-info-button:hover', {
  247. 'color': 'white',
  248. });
  249. GM_addStyle_object('#boss-data-section .dist-buttons, #bossmap-page .dist-buttons', {
  250. 'display': 'flex',
  251. 'gap': '10px',
  252. 'justify-content': 'center',
  253. 'margin-bottom': '10px',
  254. 'align-items': 'center',
  255. });
  256. GM_addStyle_object('#boss-data-section td.coord.path, #bossmap-page td.coord.path', {
  257. 'background-color': 'yellow !important',
  258. 'color': 'black !important',
  259. });
  260.  
  261. // GM_addStyle_object('#coord-hover-tooltip', {
  262. // 'position': 'absolute',
  263. // 'background-color': 'hsla(0,0%,5%,.8)',
  264. // 'border-radius': '5px',
  265. // 'padding': '5px',
  266. // });
  267.  
  268.  
  269. ready(function () {
  270.  
  271. // === Create Elements ===
  272. var missionHolder = document.getElementById('mission-holder');
  273.  
  274. var container = document.createElement('div');
  275. container.id = 'mission-info-distance-viewer';
  276.  
  277. container.innerHTML = '<div id="mission-info-buttons-title">Path finder</div>';
  278. container.innerHTML += '<div class="dist-buttons"><button id="dist-clear" style="display: none;" class="mission-info-button">Clear path</button></div>';
  279. 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>';
  280. 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>';
  281.  
  282. missionHolder.appendChild(container);
  283.  
  284. var mapTopInfo = document.createElement('div');
  285. mapTopInfo.id = 'map-top-info';
  286. mapTopInfo.innerHTML = '<div id="mission-info-buttons-subtitle">No path selected, click on a cell to set a start and end point</div>';
  287.  
  288. var bossTable = document.querySelector('#boss-table');
  289. if (bossTable.previousElementSibling) {
  290. bossTable.previousElementSibling.insertAdjacentElement('afterend', mapTopInfo);
  291. } else {
  292. bossTable.insertAdjacentElement('beforebegin', mapTopInfo);
  293. }
  294.  
  295. // var tooltip = document.createElement('div');
  296. // tooltip.id = 'coord-hover-tooltip';
  297. // tooltip.style.display = 'none';
  298.  
  299. // unsafeWindow.document.body.appendChild(tooltip);
  300.  
  301. // function showTooltip(event, info) {
  302. // tooltip.style.display = 'block';
  303. // tooltip.style.position = 'absolute';
  304. // // muose position
  305. // tooltip.style.top = event.pageY + 10 + 'px';
  306. // tooltip.style.left = event.pageX + 10 + 'px';
  307. // tooltip.innerHTML = info;
  308. // }
  309.  
  310. // function hideTooltip() {
  311. // tooltip.style.display = 'none';
  312. // }
  313.  
  314.  
  315. unsafeWindow.closeMissionHolder = function (event) {
  316. if (event.target.closest('#mission-info-distance-viewer')) return;
  317.  
  318. missionHolder.style.display = 'none';
  319. };
  320.  
  321. missionHolder.setAttribute('onclick', 'closeMissionHolder(event)');
  322.  
  323. var startCellButton = document.getElementById('dist-set-start');
  324. var endCellButton = document.getElementById('dist-set-end');
  325. var clearPathButton = document.getElementById('dist-clear');
  326. var setGoalButton = document.getElementById('dist-set-goal');
  327.  
  328. var subtitle = document.getElementById('mission-info-buttons-subtitle');
  329.  
  330. // === Scan empty cells
  331.  
  332. var emptyCells = Array.from(document.querySelectorAll('td.coord'))
  333. .filter(function (el) {
  334. return el.computedStyleMap().get('opacity').toString() == '0';
  335. })
  336. .map(function (el) {
  337. return {
  338. x: el.classList[1].replace('x', ''),
  339. y: el.classList[2].replace('y', ''),
  340. };
  341. });
  342.  
  343. var startCell = null;
  344. var endCell = null;
  345.  
  346. var trackingGps = false;
  347. var lastTrackTime = null;
  348. var lastRemainingCellCount = 0;
  349.  
  350.  
  351. var pathFinder = new AStar(emptyCells);
  352. function maybeUpdatePath() {
  353. if (!startCell || !endCell) return false;
  354.  
  355. var path = pathFinder.find(startCell, endCell);
  356.  
  357. // Clear existing path cells
  358. var pathCells = unsafeWindow.document.querySelectorAll('td.coord.path');
  359. for (var i = 0; i < pathCells.length; i++) {
  360. pathCells[i].classList.remove('path');
  361. delete pathCells[i].dataset.distanceDebug;
  362. delete pathCells[i].dataset.distanceIndex;
  363. // pathCells[i].onmouseover = null;
  364. }
  365.  
  366. // console.log(path);
  367. if (!path) return false;
  368.  
  369. for(var i = 0; i < path.length; i++) {
  370. var cellCoord = path[i];
  371. // console.log(cellCoord);
  372. var cell = unsafeWindow.document.querySelector('td.coord.x' + cellCoord.x + '.y' + cellCoord.y);
  373. cell.classList.add('path');
  374. cell.dataset.distanceDebug = JSON.stringify(cellCoord);
  375. cell.dataset.distanceIndex = i;
  376. // cell.onmouseover = function(event) {
  377. // if (!event.target.dataset.distanceDebug) return;
  378.  
  379. // var info = JSON.parse(event.target.dataset.distanceDebug);
  380. // var index = parseInt(event.target.dataset.distanceIndex) + 1;
  381. // showTooltip(event, 'Index: ' + index + '<br>Cell: ' + info.x + 'x' + info.y + '<br>Total cost: ' + info.g + '<br>Heuristic: ' + info.h + '<br>Total: ' + info.f);
  382. // };
  383. }
  384. clearPathButton.style.display = 'initial';
  385.  
  386. subtitle.innerHTML = 'Path length: ' + path.length + ' cells';
  387. if (trackingGps) {
  388. subtitle.innerHTML += ' (tracking player)';
  389.  
  390. // subtitle.innerHTML += '<br>' + (lastTrackTime === null ? 'Start walking to see estimated time' : 'Estimated time: ');
  391. subtitle.innerHTML += '<br>';
  392.  
  393. if (lastTrackTime === null) {
  394. subtitle.innerHTML += 'Start walking to see estimated time';
  395. } else {
  396. var now = new Date().getTime();
  397. var timeDiff = now - lastTrackTime;
  398. var cellsTraveled = Math.abs(lastRemainingCellCount - path.length);
  399.  
  400. var timeRemaining;
  401. if (!cellsTraveled) {
  402. timeRemaining = 'Unknown';
  403. } else {
  404. var timePerCell = timeDiff / cellsTraveled;
  405. timeRemaining = formatTime(timePerCell * path.length);
  406. }
  407.  
  408. subtitle.innerHTML += 'Estimated time remaining: ' + timeRemaining;
  409. subtitle.innerHTML += '<br>Cells traveled: ' + cellsTraveled;
  410. subtitle.innerHTML += '<br>Time passed: ' + formatTime(timeDiff);
  411. }
  412. }
  413. return true;
  414. }
  415.  
  416. var playerCoords = null;
  417. var playerCell = unsafeWindow.document.querySelector('td.playerlocation');
  418.  
  419. if (playerCell) {
  420. playerCoords = [
  421. playerCell.classList[1].replace('x', ''),
  422. playerCell.classList[2].replace('y', ''),
  423. ];
  424. }
  425.  
  426.  
  427. startCellButton.onclick = function () {
  428. // current pos
  429. var img = unsafeWindow.document.querySelector('#mission-info img');
  430. var matches = img.src.match(/Fairview_(\d+)x(\d+)/);
  431. var x = matches[1];
  432. var y = matches[2];
  433. trackingGps = false;
  434. lastTrackTime = null;
  435.  
  436. startCell = { x: x, y: y };
  437.  
  438. unsafeWindow.document.querySelector('td.coord.x' + x + '.y' + y).classList.add('path');
  439.  
  440. missionHolder.style.display = 'none';
  441. maybeUpdatePath();
  442. };
  443.  
  444. endCellButton.onclick = function () {
  445. // current pos
  446. var img = document.querySelector('#mission-info img');
  447. var matches = img.src.match(/Fairview_(\d+)x(\d+)/);
  448. var x = matches[1];
  449. var y = matches[2];
  450.  
  451. endCell = { x: x, y: y };
  452.  
  453. unsafeWindow.document.querySelector('td.coord.x' + x + '.y' + y).classList.add('path');
  454.  
  455. missionHolder.style.display = 'none';
  456. maybeUpdatePath();
  457. };
  458.  
  459. clearPathButton.onclick = function () {
  460. startCell = null;
  461. endCell = null;
  462. trackingGps = false;
  463. lastTrackTime = null;
  464.  
  465. clearPathButton.style.display = 'none';
  466.  
  467. // Clear existing path cells
  468. var pathCells = unsafeWindow.document.querySelectorAll('td.coord.path');
  469. for (var i = 0; i < pathCells.length; i++) {
  470. pathCells[i].classList.remove('path');
  471. }
  472.  
  473. subtitle.innerHTML = 'No path selected, click on a cell to set a start and end point';
  474. missionHolder.style.display = 'none';
  475. };
  476.  
  477. var updatePlayerTrackPath = function () {
  478. if (!trackingGps) return;
  479.  
  480. var playerCell = unsafeWindow.document.querySelector('td.playerlocation');
  481. if (!playerCell) {
  482. setTimeout(updatePlayerTrackPath, 7500);
  483. return;
  484. };
  485.  
  486.  
  487. playerCoords = [
  488. playerCell.classList[1].replace('x', ''),
  489. playerCell.classList[2].replace('y', ''),
  490. ];
  491.  
  492. startCell = { x: playerCoords[0], y: playerCoords[1] };
  493.  
  494. maybeUpdatePath();
  495.  
  496. setTimeout(updatePlayerTrackPath, 7500);
  497. };
  498.  
  499.  
  500. setGoalButton.onclick = function () {
  501. trackingGps = true;
  502. missionHolder.style.display = 'none';
  503.  
  504. // current pos
  505. var img = document.querySelector('#mission-info img');
  506. var matches = img.src.match(/Fairview_(\d+)x(\d+)/);
  507. var x = matches[1];
  508. var y = matches[2];
  509.  
  510. endCell = { x: x, y: y };
  511.  
  512. if (playerCoords) {
  513. startCell = { x: playerCoords[0], y: playerCoords[1] };
  514. } else {
  515. startCell = { x: x, y: y };
  516. }
  517.  
  518. unsafeWindow.document.querySelector('td.coord.x' + x + '.y' + y).classList.add('path');
  519. var result = maybeUpdatePath();
  520. if (!result) {
  521. trackingGps = false;
  522. return;
  523. }
  524.  
  525. lastTrackTime = new Date().getTime();
  526. lastRemainingCellCount = pathFinder.find(startCell, endCell).length;
  527. updatePlayerTrackPath();
  528. };
  529.  
  530.  
  531. $(unsafeWindow.document).ajaxComplete(function(event, jqXHR, ajaxOptions) {
  532. console.log('ajaxComplete', ajaxOptions.url)
  533. if (ajaxOptions.url.indexOf('/profile/json/') == -1) return;
  534. setGoalButton.parentElement.style.display = 'initial';
  535. playerCoords = jqXHR.responseJSON.gpscoords;
  536.  
  537. // if (!trackingGps) return;
  538. // startCell = { x: coords[0], y: coords[1] };
  539. // maybeUpdatePath();
  540. });
  541. });
  542. })();