Elethor General Purpose

Provides some general additions to Elethor

当前为 2021-01-16 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Elethor General Purpose
  3. // @description Provides some general additions to Elethor
  4. // @namespace https://www.elethor.com/
  5. // @version 1.6.1
  6. // @author Anders Morgan Larsen (Xortrox)
  7. // @contributor Kidel
  8. // @contributor Saya
  9. // @contributor Archeron
  10. // @match https://elethor.com/*
  11. // @match https://www.elethor.com/*
  12. // @run-at document-start
  13. // @grant none
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. const currentUserData = {};
  18.  
  19. const moduleName = 'Elethor General Purpose';
  20. const version = '1.6.1';
  21.  
  22. const profileURL = '/profile/';
  23.  
  24. function initializeXHRHook() {
  25. let rawSend = XMLHttpRequest.prototype.send;
  26.  
  27. XMLHttpRequest.prototype.send = function() {
  28. if (!this._hooked) {
  29. this._hooked = true;
  30.  
  31. this.addEventListener('readystatechange', function() {
  32. if (this.readyState === XMLHttpRequest.DONE) {
  33. setupHook(this);
  34. }
  35. }, false);
  36. }
  37. rawSend.apply(this, arguments);
  38. }
  39.  
  40. function setupHook(xhr) {
  41. if (window.elethorGeneralPurposeOnXHR) {
  42. const e = new Event('EGPXHR');
  43. e.xhr = xhr;
  44.  
  45. window.elethorGeneralPurposeOnXHR.dispatchEvent(e);
  46. }
  47. }
  48. window.elethorGeneralPurposeOnXHR = new EventTarget();
  49.  
  50. console.log(`[${moduleName} v${version}] XHR Hook initialized.`);
  51. }
  52.  
  53. function initializeUserLoadListener() {
  54. elethorGeneralPurposeOnXHR.addEventListener('EGPXHR', function (e) {
  55. if (e && e.xhr
  56. && e.xhr.responseURL
  57. && e.xhr.responseURL.endsWith
  58. && e.xhr.responseURL.endsWith('/game/user')
  59. ) {
  60. try {
  61. const userData = JSON.parse(e.xhr.responseText);
  62.  
  63. if (userData) {
  64. for (const key of Object.keys(userData)) {
  65. currentUserData[key] = userData[key];
  66. }
  67. }
  68. } catch (e) {
  69. console.log(`[${moduleName} v${version}] Error parsing userData:`, e);
  70. }
  71.  
  72. }
  73. });
  74.  
  75. console.log(`[${moduleName} v${version}] User Load Listener initialized.`);
  76. }
  77.  
  78. function initializeToastKiller() {
  79. document.addEventListener('click', function(e) {
  80. if (e.target
  81. && e.target.className
  82. && e.target.className.includes
  83. && e.target.className.includes('toasted')
  84. ) {
  85. e.target.remove();
  86. }
  87. });
  88.  
  89. console.log(`[${moduleName} v${version}] Toast Killer initialized.`);
  90. }
  91.  
  92. function initializeInventoryStatsLoadListener() {
  93. elethorGeneralPurposeOnXHR.addEventListener('EGPXHR', function (e) {
  94. if (e && e.xhr
  95. && e.xhr.responseURL
  96. && e.xhr.responseURL.endsWith
  97. && e.xhr.responseURL.endsWith('/game/inventory/stats')
  98. ) {
  99. setTimeout(() => {
  100. updateEquipmentPercentageSummary();
  101.  
  102. setTimeout(updateInventoryStatsPercentages, 1000);
  103. });
  104. }
  105. });
  106.  
  107. console.log(`[${moduleName} v${version}] Inventory Stats Load Listener initialized.`);
  108. }
  109.  
  110. function initializeProfileLoadListener2() {
  111. elethorGeneralPurposeOnXHR.addEventListener('EGPXHR', async function (e) {
  112. if (e && e.xhr
  113. && e.xhr.responseURL
  114. && e.xhr.responseURL.includes
  115. && e.xhr.responseURL.includes(profileURL)
  116. ) {
  117. const url = e.xhr.responseURL;
  118. const path = url.substr(url.indexOf(profileURL));
  119.  
  120. // We know we have a profile lookup, and not user-data load if the length differs.
  121. if (path.length > profileURL.length) {
  122. const userId = Number(path.substr(path.lastIndexOf('/') + 1));
  123. console.log('Inspected userId:', userId);
  124. console.log('Current user:', currentUserData.user.id);
  125.  
  126. const difference = await getExperienceDifference(currentUserData.user.id, userId);
  127.  
  128. console.log('Difference:', difference);
  129. console.log(e);
  130. }
  131. }
  132. });
  133.  
  134. console.log(`[${moduleName} v${version}] Profile Load Listener initialized.`);
  135. }
  136.  
  137. function updateEquipmentPercentageSummary() {
  138. document.querySelector('.is-equipment>div>div').setAttribute('style', 'width: 50%')
  139. let percentagesTable = document.querySelector('#egpPercentagesSummary')
  140. if (!percentagesTable){
  141. percentagesTable = document.querySelector('.is-equipment>div>div:nth-child(2)').cloneNode(true);
  142. percentagesTable.setAttribute('style', 'width: 25%');
  143. percentagesTable.id='egpPercentagesSummary';
  144. document.querySelector('.is-equipment>div').appendChild(percentagesTable);
  145.  
  146. for (const child of percentagesTable.children[0].children) {
  147. if (child && child.children && child.children[0]) {
  148. child.children[0].remove();
  149. }
  150. }
  151.  
  152. document.querySelector('#egpPercentagesSummary>table>tr:nth-child(8)').setAttribute('style', 'height:43px');
  153. }
  154. }
  155.  
  156. function getStatSummary(equipment) {
  157. const summary = {
  158. base: {},
  159. energizements: {}
  160. };
  161.  
  162. if (equipment) {
  163. for (const key of Object.keys(equipment)) {
  164. const item = equipment[key];
  165.  
  166. /**
  167. * Sums base attributes by name
  168. * */
  169. if (item && item.attributes) {
  170. for (const attributeName of Object.keys(item.attributes)) {
  171. const attributeValue = item.attributes[attributeName];
  172.  
  173. if (!summary.base[attributeName]) {
  174. summary.base[attributeName] = 0;
  175. }
  176.  
  177. summary.base[attributeName] += attributeValue;
  178. }
  179. }
  180.  
  181. /**
  182. * Sums energizements by stat name
  183. * */
  184. if (item && item.upgrade && item.upgrade.energizements) {
  185. for (const energizement of item.upgrade.energizements) {
  186. if (!summary.energizements[energizement.stat]) {
  187. summary.energizements[energizement.stat] = 0;
  188. }
  189.  
  190. summary.energizements[energizement.stat] += Number(energizement.boost);
  191. }
  192. }
  193. }
  194. }
  195.  
  196. return summary;
  197. }
  198.  
  199. function updateInventoryStatsPercentages() {
  200. let percentagesTable = document.querySelector('#egpPercentagesSummary')
  201. if (percentagesTable && currentUserData && currentUserData.equipment){
  202. const statSummary = getStatSummary(currentUserData.equipment);
  203.  
  204. const baseKeys = Object.keys(statSummary.base);
  205. const energizementKeys = Object.keys(statSummary.energizements);
  206.  
  207. let allKeys = baseKeys.concat(energizementKeys);
  208. const filterUniques = {};
  209. for (const key of allKeys){
  210. filterUniques[key] = true;
  211. }
  212. allKeys = Object.keys(filterUniques);
  213. allKeys.sort();
  214.  
  215. allKeys.push('actions');
  216.  
  217. const tableRows = percentagesTable.children[0].children;
  218.  
  219. for(const row of tableRows) {
  220. if (row
  221. && row.children
  222. && row.children[0]
  223. && row.children[0].children[0]
  224. ) {
  225. const rowText = row.children[0].children[0];
  226. rowText.innerText = '';
  227. }
  228. }
  229.  
  230. let rowIndex = 0;
  231. for (const key of allKeys) {
  232. if (key === 'puncture') {
  233. continue;
  234. }
  235.  
  236. const row = tableRows[rowIndex];
  237. if (row
  238. && row.children
  239. && row.children[0]
  240. && row.children[0].children[0]
  241. ) {
  242. const rowText = row.children[0].children[0];
  243.  
  244. const rowBase = statSummary.base[key] || 0;
  245. const rowEnergizement = (statSummary.energizements[key] || 0);
  246. const rowEnergizementPercentage = (statSummary.energizements[key] || 0) * 100;
  247.  
  248. if (key.startsWith('+')) {
  249. rowText.innerText = `${key} per 10 levels: ${rowEnergizement}`;
  250. } else if (key === 'actions') {
  251. const actions = currentUserData.user.bonus_actions || 0;
  252. rowText.innerText = `Bonus Actions: ${actions}`;
  253. } else {
  254. rowText.innerText = `${key}: ${rowBase} (${rowEnergizementPercentage.toFixed(0)}%)`;
  255. }
  256.  
  257. rowIndex++;
  258. }
  259. }
  260. }
  261. }
  262.  
  263. function initializeLocationChangeListener() {
  264. let previousLocation = window.location.href;
  265.  
  266. window.elethorGeneralPurposeOnLocationChange = new EventTarget();
  267.  
  268. window.elethorLocationInterval = setInterval(() => {
  269. if (previousLocation !== window.location.href) {
  270. previousLocation = window.location.href;
  271.  
  272. const e = new Event('EGPLocation');
  273. e.newLocation = window.location.href;
  274. window.elethorGeneralPurposeOnLocationChange.dispatchEvent(e);
  275. }
  276.  
  277. }, 500);
  278.  
  279. console.log(`[${moduleName} v${version}] Location Change Listener initialized.`);
  280. }
  281.  
  282. function getProfileCombatElement() {
  283. const skillElements = document.querySelectorAll('.is-skill div>span.has-text-weight-bold');
  284. for (const skillElement of skillElements) {
  285. if (skillElement.innerText.includes('Combat')) {
  286. return skillElement;
  287. }
  288. }
  289. }
  290.  
  291. function updateXPTracker(difference) {
  292. const combatElement = getProfileCombatElement();
  293. if (!combatElement) {
  294. return;
  295. }
  296.  
  297. if (difference > 0) {
  298. combatElement.setAttribute('data-combat-experience-ahead', `(+${formatNormalNumber(difference)})`);
  299. combatElement.setAttribute('style', `color:lime`);
  300. } else {
  301. combatElement.setAttribute('data-combat-experience-ahead', `(${formatNormalNumber(difference)})`);
  302. combatElement.setAttribute('style', `color:red`);
  303. }
  304. }
  305.  
  306. function initializeProfileLoadListener() {
  307. let css = '[data-combat-experience-ahead]::after { content: attr(data-combat-experience-ahead); padding: 12px;}';
  308.  
  309. appendCSS(css);
  310.  
  311. window.elethorGeneralPurposeOnLocationChange.addEventListener('EGPLocation', async function (e) {
  312. if (e && e.newLocation) {
  313. if(e.newLocation.includes('/profile/')) {
  314. console.log('Profile view detected:', e);
  315. const url = e.newLocation;
  316. const path = url.substr(url.indexOf(profileURL));
  317.  
  318. // We know we have a profile lookup, and not user-data load if the length differs.
  319. if (path.length > profileURL.length) {
  320. const userId = Number(path.substr(path.lastIndexOf('/') + 1));
  321.  
  322. const difference = await getExperienceDifference(userId, currentUserData.user.id);
  323.  
  324. updateXPTracker(difference);
  325. }
  326. }
  327. }
  328. });
  329.  
  330. console.log(`[${moduleName} v${version}] Profile Load Listener initialized.`);
  331. }
  332.  
  333. async function getUser(id) {
  334. const result = await window.axios.get(`/game/user/${id}?egpIgnoreMe=true`);
  335. return result.data;
  336. }
  337.  
  338. function getUserCombatStats(user) {
  339. for (const skill of user.skills) {
  340. if (skill.name === 'combat') {
  341. return skill.pivot;
  342. }
  343. }
  344. }
  345.  
  346. async function getExperienceDifference(userId1, userId2) {
  347. const [user1, user2] = await Promise.all([
  348. getUser(userId1),
  349. getUser(userId2)
  350. ]);
  351.  
  352. const combatStats1 = getUserCombatStats(user1);
  353. const combatStats2 = getUserCombatStats(user2);
  354.  
  355. return combatStats2.experience - combatStats1.experience;
  356. }
  357.  
  358. (function run() {
  359. initializeToastKiller();
  360. initializeXHRHook();
  361. initializeUserLoadListener();
  362. initializeInventoryStatsLoadListener();
  363. initializeLocationChangeListener();
  364. initializeProfileLoadListener();
  365. initializePackStats();
  366.  
  367. console.log(`[${moduleName} v${version}] Loaded.`);
  368. })();
  369.  
  370. (async function loadRerollDisableButtonModule() {
  371. async function waitForEcho() {
  372. return new Promise((resolve, reject) => {
  373. const interval = setInterval(() => {
  374. if (window.Echo) {
  375. clearInterval(interval);
  376. resolve();
  377. }
  378. }, 100);
  379. });
  380. }
  381.  
  382. await waitForEcho();
  383. await waitForUser();
  384.  
  385. elethorGeneralPurposeOnXHR.addEventListener('EGPXHR', async function (e) {
  386. if (e && e.xhr && e.xhr.responseURL) {
  387. if(e.xhr.responseURL.includes('/game/energize')) {
  388. const itemID = e.xhr.responseURL.substr(e.xhr.responseURL.lastIndexOf('/')+1);
  389. window.lastEnergizeID = Number(itemID);
  390. }
  391. }
  392. });
  393. })();
  394.  
  395. (async function loadResourceNodeUpdater() {
  396. await waitForField(currentUserData, 'user');
  397. const user = await getUser(currentUserData.user.id);
  398.  
  399. function updateExperienceRates() {
  400. document.querySelectorAll('.is-resource-node').forEach(async (node) => {
  401. visualizeResourceNodeExperienceRates(node, user)
  402. });
  403.  
  404. function visualizeResourceNodeExperienceRates(node, user) {
  405. const purity = getNodePurityPercentage(node, user);
  406. const density = getNodeDensityPercentage(node, user);
  407. const experience = getNodeExperience(node, density, user);
  408. const ore = 16;
  409. const experienceRate = experience.toFixed(2);
  410. const oreRate = getOreRate(density, purity, ore);
  411.  
  412. node.children[0].setAttribute('data-after', `${experienceRate} xp/h ${oreRate} ore/h`);
  413. }
  414.  
  415. function getNodePurityPercentage(node, user) {
  416. const column = node.children[0].children[2];
  417. const label = column.getElementsByClassName('has-text-weight-bold')[0].parentElement;
  418. let percentage = Number(label.innerText.replace('%','').split(':')[1]);
  419.  
  420. let miningLevel = getMiningLevel(user);
  421. percentage = percentage + (miningLevel * 0.1);
  422.  
  423. return percentage;
  424. }
  425.  
  426. function getNodeDensityPercentage(node, user) {
  427. const column = node.children[0].children[1];
  428. const label = column.getElementsByClassName('has-text-weight-bold')[0].parentElement;
  429. let percentage = Number(label.innerText.replace('%','').split(':')[1]);
  430.  
  431. let miningLevel = getMiningLevel(user);
  432. percentage = percentage + (miningLevel * 0.1);
  433.  
  434. return percentage;
  435. }
  436.  
  437. function getNodeExperience(node, density, user) {
  438. const column = node.children[0].children[3];
  439. const label = column.getElementsByClassName('has-text-weight-bold')[0].parentElement;
  440. let value = Number(label.innerText.replace('%','').split(':')[1]);
  441.  
  442. const skilledExtraction = getSkilledExtractionLevel(user);
  443. const knowledgeExtraction = getKnowledgeExtractionLevel(user) / 100;
  444.  
  445. const actionsPerHour = getActionsPerHour(density);
  446. const experienceBase = value * actionsPerHour;
  447. const experienceSkilled = actionsPerHour * skilledExtraction;
  448. const experienceKnowledge = value * knowledgeExtraction;
  449.  
  450. value = experienceBase + experienceSkilled + experienceKnowledge;
  451.  
  452. const vip = isUserVIP(user);
  453.  
  454. value *= vip ? 1.1 : 1;
  455.  
  456. return value;
  457. }
  458.  
  459. function getActionsPerHour(density) {
  460. return 3600 / (60 / (density / 100))
  461. }
  462.  
  463. function getExperienceRate(density, experience) {
  464. return Number((3600 / (60 / (density / 100)) * experience).toFixed(2));
  465. }
  466.  
  467. function getOreRate(density, purity, ore) {
  468. return Number((3600 / (60 / (density / 100)) * (purity / 100) * ore).toFixed(2));
  469. }
  470. }
  471.  
  472. function isUserVIP(user) {
  473. return new Date(user.vip_expires) > new Date();
  474. }
  475.  
  476. updateExperienceRates();
  477. window.elethorResourceInterval = setInterval(updateExperienceRates, 500);
  478. initializeResourceNodeView();
  479.  
  480. async function initializeResourceNodeView() {
  481. await waitForField(document, 'head');
  482.  
  483. let css = '[data-after]::after { content: attr(data-after); padding: 12px;}';
  484.  
  485. appendCSS(css);
  486. }
  487. })();
  488.  
  489. async function initializePackStats() {
  490. await waitForField(document, 'head');
  491. await waitForUser();
  492.  
  493. function loadPackStatsCSS() {
  494. const css = '' +
  495. '[data-pack-stat-ATTACK-SPEED]::after {content: attr(data-pack-stat-ATTACK-SPEED);float: right;}' +
  496. '[data-pack-stat-HEALTH]::after {content: attr(data-pack-stat-HEALTH);float: right;}' +
  497. '[data-pack-stat-FORTITUDE]::after {content: attr(data-pack-stat-FORTITUDE);float: right;}' +
  498. '[data-pack-stat-SPEED]::after {content: attr(data-pack-stat-SPEED);float: right;}' +
  499. '[data-pack-stat-SAVAGERY]::after {content: attr(data-pack-stat-SAVAGERY);float: right;}' +
  500. '[data-pack-stat-PIERCE]::after {content: attr(data-pack-stat-PIERCE);float: right;}' +
  501. '[data-pack-stat-ARMOR]::after {content: attr(data-pack-stat-ARMOR);float: right;}' +
  502. '[data-pack-stat-DAMAGE-REDUCTION]::after {content: attr(data-pack-stat-DAMAGE-REDUCTION);float: right;}'
  503. ;
  504.  
  505. appendCSS(css);
  506. }
  507. loadPackStatsCSS();
  508.  
  509. function getMonsterStatSpans() {
  510. return document.querySelectorAll('.is-desktop-monster-stats table tr td:last-child>span');
  511. }
  512.  
  513. function applyPackStats(packAmount) {
  514. const statSpans = getMonsterStatSpans();
  515.  
  516. let counter = 0;
  517.  
  518. let attackSpeedSpan, healthSpan;
  519.  
  520. for (const span of statSpans) {
  521. // Save attack speed span for later when we get speed stat
  522. if (counter === 0) {
  523. counter++;
  524. attackSpeedSpan = span;
  525. continue;
  526. }
  527.  
  528. // Save health span for later when we get fort stat
  529. if (counter === 1) {
  530. counter++;
  531. healthSpan = span;
  532. continue;
  533. }
  534.  
  535. const stat = processSpan(span);
  536.  
  537. // Health
  538. if (counter === 2) {
  539. processSpan(healthSpan, stat, 'health');
  540. }
  541.  
  542. // Speed
  543. if (counter === 3) {
  544. processSpan(attackSpeedSpan, stat, 'speed');
  545. }
  546.  
  547. counter++;
  548. }
  549.  
  550. function processSpan(span, statOverride, overrideType) {
  551. let stat = Number(span.innerText.replace('%', '').replace(',', '').replace(',', '').replace(',', ''));
  552. stat *= Math.pow(1.1, packAmount);
  553.  
  554. if (statOverride) {
  555. stat = statOverride;
  556.  
  557. if (overrideType === 'speed') {
  558. stat = getAttackSpeed(stat);
  559. }
  560.  
  561. if (overrideType === 'health') {
  562. stat = getHealth(stat);
  563. }
  564. }
  565.  
  566. let statPackFormatted;
  567.  
  568. if (span.innerText.includes('%')) {
  569. statPackFormatted = `${stat.toFixed(3)}%`;
  570. } else {
  571. statPackFormatted = stat.toFixed(0);
  572. }
  573.  
  574. statPackFormatted = statPackFormatted.toLocaleString();
  575.  
  576. span.setAttribute('data-pack-stat-' + span.parentElement.parentElement.children[0].innerText.replace(' ', '-'), `${statPackFormatted} (${packAmount}x)`);
  577.  
  578. return stat;
  579. }
  580. }
  581.  
  582. loadPackStatsCSS();
  583.  
  584. const multiplierByPackLevel = {
  585. 0: 3,
  586. 1: 4,
  587. 2: 5,
  588. 3: 6,
  589. 4: 8,
  590. 5: 10
  591. }
  592.  
  593. const multiplierByCarnageLevel = {
  594. 1: 11,
  595. 2: 12,
  596. 3: 13,
  597. 4: 14,
  598. 5: 15
  599. }
  600.  
  601. const packStatsUser = await getUser(currentUserData.user.id);
  602. const packLevel = getPackLevel(packStatsUser);
  603. const carnageLevel = getCarnageLevel(packStatsUser);
  604.  
  605. let packAmount = multiplierByPackLevel[packLevel];
  606.  
  607. if (carnageLevel > 0) {
  608. packAmount = multiplierByCarnageLevel[carnageLevel];
  609. }
  610.  
  611. window.elethorPackStatsInterval = setInterval(() => {
  612. applyPackStats(packAmount);
  613. }, 500);
  614. }
  615.  
  616. function formatNormalNumber(num){
  617. return num.toLocaleString();
  618. }
  619.  
  620. function appendCSS(css) {
  621. let head = document.head || document.getElementsByTagName('head')[0];
  622. let style = document.createElement('style');
  623.  
  624. head.appendChild(style);
  625.  
  626. style.type = 'text/css';
  627. if (style.styleSheet){
  628. // This is required for IE8 and below.
  629. style.styleSheet.cssText = css;
  630. } else {
  631. style.appendChild(document.createTextNode(css));
  632. }
  633. }
  634.  
  635. function getMiningLevel(user) {
  636. for (const skill of user.skills) {
  637. if (skill.id === 1) {
  638. return skill.pivot.level;
  639. }
  640. }
  641.  
  642. return 0;
  643. }
  644.  
  645. function getSkilledExtractionLevel(user) {
  646. for (const skill of user.skills) {
  647. if (skill.id === 17) {
  648. return skill.pivot.level;
  649. }
  650. }
  651.  
  652. return 0;
  653. }
  654.  
  655. function getKnowledgeExtractionLevel(user) {
  656. for (const skill of user.skills) {
  657. if (skill.id === 18) {
  658. return skill.pivot.level;
  659. }
  660. }
  661.  
  662. return 0;
  663. }
  664.  
  665. function getPackLevel(user) {
  666. for (const skill of user.skills) {
  667. if (skill.id === 22) {
  668. return skill.pivot.level;
  669. }
  670. }
  671.  
  672. return 0;
  673. }
  674.  
  675. function getCarnageLevel(user) {
  676. for (const skill of user.skills) {
  677. if (skill.id === 23) {
  678. return skill.pivot.level;
  679. }
  680. }
  681.  
  682. return 0;
  683. }
  684.  
  685. function getAttackSpeed(speed) {
  686. return 50 - (50 * speed / (speed + 25));
  687. }
  688.  
  689. function getHealth(fortitude) {
  690. return 100000 * fortitude / (fortitude + 4000);
  691. }
  692.  
  693. async function waitForField(target, field) {
  694. return new Promise((resolve, reject) => {
  695. const interval = setInterval(() => {
  696. if (target[field] !== undefined) {
  697. clearInterval(interval);
  698. resolve();
  699. }
  700. }, 100);
  701. });
  702. }
  703.  
  704. async function waitForUser() {
  705. return new Promise((resolve, reject) => {
  706. const interval = setInterval(() => {
  707. if (currentUserData.user && currentUserData.user.id !== undefined) {
  708. clearInterval(interval);
  709. resolve();
  710. }
  711. }, 100);
  712. });
  713. }
  714. })();