Elethor General Purpose

Provides some general additions to Elethor

当前为 2022-04-28 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Elethor General Purpose
  3. // @description Provides some general additions to Elethor
  4. // @namespace https://www.elethor.com/
  5. // @version 1.7.9
  6. // @author Xortrox
  7. // @contributor Kidel
  8. // @contributor Saya
  9. // @contributor Archeron
  10. // @contributor Hito
  11. // @match https://elethor.com/*
  12. // @match https://www.elethor.com/*
  13. // @run-at document-start
  14. // @grant none
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. const currentUserData = {};
  19.  
  20. const moduleName = 'Elethor General Purpose';
  21. const version = '1.7.9';
  22.  
  23. const profileURL = '/profile/';
  24.  
  25. function initializeXHRHook() {
  26. let rawSend = XMLHttpRequest.prototype.send;
  27.  
  28. XMLHttpRequest.prototype.send = function() {
  29. if (!this._hooked) {
  30. this._hooked = true;
  31.  
  32. this.addEventListener('readystatechange', function() {
  33. if (this.readyState === XMLHttpRequest.DONE) {
  34. setupHook(this);
  35. }
  36. }, false);
  37. }
  38. rawSend.apply(this, arguments);
  39. }
  40.  
  41. function setupHook(xhr) {
  42. if (window.elethorGeneralPurposeOnXHR) {
  43. const e = new Event('EGPXHR');
  44. e.xhr = xhr;
  45.  
  46. window.elethorGeneralPurposeOnXHR.dispatchEvent(e);
  47. }
  48. }
  49. window.elethorGeneralPurposeOnXHR = new EventTarget();
  50.  
  51. console.log(`[${moduleName} v${version}] XHR Hook initialized.`);
  52. }
  53.  
  54. function initializeUserLoadListener() {
  55. elethorGeneralPurposeOnXHR.addEventListener('EGPXHR', function (e) {
  56. console.log('user load?:', e?.xhr?.responseURL);
  57. if (e && e.xhr
  58. && e.xhr.responseURL
  59. && e.xhr.responseURL.endsWith
  60. && e.xhr.responseURL.endsWith('/game/user')
  61. ) {
  62. try {
  63. const userData = JSON.parse(e.xhr.responseText);
  64.  
  65. if (userData) {
  66. for (const key of Object.keys(userData)) {
  67. currentUserData[key] = userData[key];
  68. }
  69. }
  70. } catch (e) {
  71. console.log(`[${moduleName} v${version}] Error parsing userData:`, e);
  72. }
  73.  
  74. }
  75. });
  76.  
  77. console.log(`[${moduleName} v${version}] User Load Listener initialized.`);
  78. }
  79.  
  80. function initializeToastKiller() {
  81. document.addEventListener('click', function(e) {
  82. if (e.target
  83. && e.target.className
  84. && e.target.className.includes
  85. && e.target.className.includes('toasted toasted-primary')
  86. ) {
  87. e.target.remove();
  88. }
  89. });
  90.  
  91. console.log(`[${moduleName} v${version}] Toast Killer initialized.`);
  92. }
  93.  
  94. async function forceLoadUser() {
  95. const userLoad = await getUserSelf();
  96. }
  97.  
  98. function initializeInventoryStatsLoadListener() {
  99. elethorGeneralPurposeOnXHR.addEventListener('EGPXHR', function (e) {
  100. if (e && e.xhr
  101. && e.xhr.responseURL
  102. && e.xhr.responseURL.endsWith
  103. && e.xhr.responseURL.endsWith('/game/inventory/stats')
  104. ) {
  105. setTimeout(async () => {
  106. if (Object.keys(currentUserData).length === 0) {
  107. await forceLoadUser();
  108. }
  109.  
  110. updateEquipmentPercentageSummary();
  111.  
  112. setTimeout(updateInventoryStatsPercentages, 1000);
  113. });
  114. }
  115. });
  116.  
  117. console.log(`[${moduleName} v${version}] Inventory Stats Load Listener initialized.`);
  118. }
  119.  
  120. function updateEquipmentPercentageSummary() {
  121. document.querySelector('.contains-equipment>div>div:nth-child(1)').setAttribute('style', 'width: 50%');
  122. document.querySelector('.contains-equipment>div>div:nth-child(2)').setAttribute('style', 'width: 25%');
  123. let percentagesTable = document.querySelector('#egpPercentagesSummary')
  124. if (!percentagesTable){
  125. percentagesTable = document.querySelector('.contains-equipment>div>div:nth-child(2)').cloneNode(true);
  126. percentagesTable.setAttribute('style', 'width: 25%');
  127. percentagesTable.id='egpPercentagesSummary';
  128. document.querySelector('.contains-equipment>div').appendChild(percentagesTable);
  129.  
  130. for (const child of percentagesTable.children[0].children) {
  131. if (child && child.children && child.children[0]) {
  132. child.children[0].remove();
  133. }
  134. }
  135.  
  136. document.querySelector('#egpPercentagesSummary>table>tr:nth-child(8)').setAttribute('style', 'height:43px');
  137. }
  138. }
  139.  
  140. function getStatSummary(equipment) {
  141. const summary = {
  142. base: {},
  143. energizements: {}
  144. };
  145.  
  146. if (equipment) {
  147. for (const key of Object.keys(equipment)) {
  148. const item = equipment[key];
  149.  
  150. /**
  151. * Sums base attributes by name
  152. * */
  153. if (item && item.attributes) {
  154. for (const attributeName of Object.keys(item.attributes)) {
  155. const attributeValue = item.attributes[attributeName];
  156.  
  157. if (!summary.base[attributeName]) {
  158. summary.base[attributeName] = 0;
  159. }
  160.  
  161. summary.base[attributeName] += attributeValue;
  162. }
  163. }
  164.  
  165. /**
  166. * Sums energizements by stat name
  167. * */
  168. if (item && item.upgrade && item.upgrade.energizements) {
  169. for (const energizement of item.upgrade.energizements) {
  170. if (!summary.energizements[energizement.stat]) {
  171. summary.energizements[energizement.stat] = 0;
  172. }
  173.  
  174. summary.energizements[energizement.stat] += Number(energizement.boost);
  175. }
  176. }
  177. }
  178. }
  179.  
  180. return summary;
  181. }
  182.  
  183. function updateInventoryStatsPercentages() {
  184. let percentagesTable = document.querySelector('#egpPercentagesSummary')
  185. if (percentagesTable && currentUserData && currentUserData.equipment){
  186. const statSummary = getStatSummary(currentUserData.equipment);
  187.  
  188. const baseKeys = Object.keys(statSummary.base);
  189. const energizementKeys = Object.keys(statSummary.energizements);
  190.  
  191. let allKeys = baseKeys.concat(energizementKeys);
  192. const filterUniques = {};
  193. for (const key of allKeys){
  194. filterUniques[key] = true;
  195. }
  196. allKeys = Object.keys(filterUniques);
  197. allKeys.sort();
  198.  
  199. allKeys.push('actions');
  200.  
  201. const tableRows = percentagesTable.children[0].children;
  202.  
  203. for(const row of tableRows) {
  204. if (row
  205. && row.children
  206. && row.children[0]
  207. && row.children[0].children[0]
  208. ) {
  209. const rowText = row.children[0].children[0];
  210. rowText.innerText = '';
  211. }
  212. }
  213.  
  214. let rowIndex = 0;
  215. for (const key of allKeys) {
  216. if (key === 'puncture') {
  217. continue;
  218. }
  219.  
  220. const row = tableRows[rowIndex];
  221. if (row
  222. && row.children
  223. && row.children[0]
  224. && row.children[0].children[0]
  225. ) {
  226. const rowText = row.children[0].children[0];
  227.  
  228. const rowBase = statSummary.base[key] || 0;
  229. const rowEnergizement = (statSummary.energizements[key] || 0);
  230. const rowEnergizementPercentage = (statSummary.energizements[key] || 0) * 100;
  231.  
  232. if (key.startsWith('+')) {
  233. rowText.innerText = `${key} per 10 levels: ${rowEnergizement}`;
  234. } else if (key === 'actions') {
  235. const actions = currentUserData.user.bonus_actions || 0;
  236. rowText.innerText = `Bonus Actions: ${actions}`;
  237. } else {
  238. rowText.innerText = `${key}: ${rowBase} (${rowEnergizementPercentage.toFixed(0)}%)`;
  239. }
  240.  
  241. rowIndex++;
  242. }
  243. }
  244. }
  245. }
  246.  
  247. function initializeLocationChangeListener() {
  248. let previousLocation = window.location.href;
  249.  
  250. window.elethorGeneralPurposeOnLocationChange = new EventTarget();
  251.  
  252. window.elethorLocationInterval = setInterval(() => {
  253. if (previousLocation !== window.location.href) {
  254. previousLocation = window.location.href;
  255.  
  256. const e = new Event('EGPLocation');
  257. e.newLocation = window.location.href;
  258. window.elethorGeneralPurposeOnLocationChange.dispatchEvent(e);
  259. }
  260.  
  261. }, 500);
  262.  
  263. console.log(`[${moduleName} v${version}] Location Change Listener initialized.`);
  264. }
  265.  
  266. function getProfileCombatElement() {
  267. const skillElements = document.querySelectorAll('.profile-skills .is-round-skill .progressbar-text>div>p:first-child');
  268. const skillElements2 = document.querySelectorAll('.profile-skills .is-round-skill .progressbar-text>div>p:nth-child(2)');
  269.  
  270. let index = 0;
  271. for (const skillElement of skillElements2) {
  272. if (skillElement.innerText?.toLowerCase().includes('combat')) {
  273. return skillElements[index].parentElement?.parentElement?.parentElement?.parentElement?.parentElement;
  274. }
  275. index++;
  276. }
  277. }
  278.  
  279. function getProfileMiningElement() {
  280. const skillElements = document.querySelectorAll('.profile-skills .is-round-skill .progressbar-text>div>p:first-child');
  281. const skillElements2 = document.querySelectorAll('.profile-skills .is-round-skill .progressbar-text>div>p:nth-child(2)');
  282.  
  283. let index = 0;
  284. for (const skillElement of skillElements2) {
  285. if (skillElement.innerText?.toLowerCase().includes('mining')) {
  286. return skillElements[index].parentElement?.parentElement?.parentElement?.parentElement?.parentElement;
  287. }
  288. index++;
  289. }
  290. }
  291.  
  292. function updateXPTracker(difference) {
  293. const combatElement = getProfileCombatElement();
  294. if (combatElement) {
  295. if (difference.combat > 0) {
  296. combatElement.setAttribute('data-combat-experience-ahead', `(+${formatNormalNumber(difference.combat)})`);
  297. combatElement.setAttribute('style', `color:lime`);
  298. } else {
  299. combatElement.setAttribute('data-combat-experience-ahead', `(${formatNormalNumber(difference.combat)})`);
  300. combatElement.setAttribute('style', `color:red`);
  301. }
  302. }
  303.  
  304. const miningElement = getProfileMiningElement();
  305. if (difference.mining > 0) {
  306. miningElement.setAttribute('data-mining-experience-ahead', `(+${formatNormalNumber(difference.mining)})`);
  307. miningElement.setAttribute('style', `color:lime`);
  308. } else {
  309. miningElement.setAttribute('data-mining-experience-ahead', `(${formatNormalNumber(difference.mining)})`);
  310. miningElement.setAttribute('style', `color:red`);
  311. }
  312. }
  313.  
  314. function initializeProfileLoadListener() {
  315. let css = '[data-combat-experience-ahead]::after { content: attr(data-combat-experience-ahead); padding: 12px;}';
  316. css += '[data-mining-experience-ahead]::after { content: attr(data-mining-experience-ahead); padding: 12px;}';
  317.  
  318. appendCSS(css);
  319.  
  320. window.elethorGeneralPurposeOnLocationChange.addEventListener('EGPLocation', async function (e) {
  321. if (e && e.newLocation) {
  322. if(e.newLocation.includes('/profile/')) {
  323. console.log('Profile view detected:', e);
  324. const url = e.newLocation;
  325. const path = url.substr(url.indexOf(profileURL));
  326.  
  327. // We know we have a profile lookup, and not user-data load if the length differs.
  328. if (path.length > profileURL.length) {
  329. const userId = Number(path.substr(path.lastIndexOf('/') + 1));
  330.  
  331. const difference = await getExperienceDifference(userId, currentUserData.user.id);
  332.  
  333. updateXPTracker(difference);
  334. }
  335. }
  336. }
  337. });
  338.  
  339. console.log(`[${moduleName} v${version}] Profile Load Listener initialized.`);
  340. }
  341.  
  342. async function getUser(id) {
  343. const result = await window.axios.get(`/game/user/${id}?egpIgnoreMe=true`);
  344. return result.data;
  345. }
  346.  
  347. window.getUser = getUser;
  348.  
  349. async function getUserSelf() {
  350. const result = await window.axios.get(`/game/user`);
  351. return result.data;
  352. }
  353.  
  354. window.getUserSelf = getUserSelf;
  355.  
  356. async function getUserStats() {
  357. const result = await window.axios.get(`/game/inventory/stats`);
  358. return result.data;
  359. }
  360.  
  361. window.getUserStats = getUserStats;
  362.  
  363. async function getUserStatsJSON(pretty) {
  364. const stats = await getUserStats();
  365.  
  366. if (pretty) {
  367. return JSON.stringify(stats, null, 2);
  368. }
  369.  
  370. return JSON.stringify(stats);
  371. }
  372.  
  373. window.getUserStatsJSON = getUserStatsJSON;
  374.  
  375. function getUserCombatStats(user) {
  376. for (const skill of user.skills) {
  377. if (skill.name === 'combat') {
  378. return skill.pivot;
  379. }
  380. }
  381. }
  382.  
  383. function getMiningStats(user) {
  384. for (const skill of user.skills) {
  385. if (skill.id === 1) {
  386. return skill.pivot;
  387. }
  388. }
  389.  
  390. return 0;
  391. }
  392.  
  393. async function getExperienceDifference(userId1, userId2) {
  394. const [user1, user2] = await Promise.all([
  395. getUser(userId1),
  396. getUser(userId2)
  397. ]);
  398.  
  399. const combatStats1 = getUserCombatStats(user1);
  400. const miningStats1 = getMiningStats(user1);
  401.  
  402. const combatStats2 = getUserCombatStats(user2);
  403. const miningStats2 = getMiningStats(user2);
  404.  
  405. return {
  406. combat: combatStats2.experience - combatStats1.experience,
  407. mining: miningStats2.experience - miningStats1.experience,
  408. };
  409. }
  410.  
  411. (async function run() {
  412. await waitForField(window, 'axios');
  413. forceLoadUser();
  414. initializeToastKiller();
  415. initializeXHRHook();
  416. initializeUserLoadListener();
  417. initializeInventoryStatsLoadListener();
  418. initializeLocationChangeListener();
  419. initializeProfileLoadListener();
  420. initializePackStats();
  421. loadMarketRecyclobotVisualizer();
  422.  
  423. console.log(`[${moduleName} v${version}] Loaded.`);
  424. })();
  425.  
  426. (async function loadRerollDisableButtonModule() {
  427. async function waitForEcho() {
  428. return new Promise((resolve, reject) => {
  429. const interval = setInterval(() => {
  430. if (window.Echo) {
  431. clearInterval(interval);
  432. resolve();
  433. }
  434. }, 100);
  435. });
  436. }
  437.  
  438. await waitForEcho();
  439. await waitForUser();
  440.  
  441. elethorGeneralPurposeOnXHR.addEventListener('EGPXHR', async function (e) {
  442. if (e && e.xhr && e.xhr.responseURL) {
  443. if(e.xhr.responseURL.includes('/game/energize')) {
  444. const itemID = e.xhr.responseURL.substr(e.xhr.responseURL.lastIndexOf('/')+1);
  445. window.lastEnergizeID = Number(itemID);
  446. }
  447. }
  448. });
  449. })();
  450.  
  451. (async function loadResourceNodeUpdater() {
  452. await waitForField(currentUserData, 'user');
  453. const user = await getUser(currentUserData.user.id);
  454.  
  455. function updateExperienceRates() {
  456. document.querySelectorAll('.is-resource-node').forEach(async (node) => {
  457. visualizeResourceNodeExperienceRates(node, user)
  458. });
  459.  
  460. function visualizeResourceNodeExperienceRates(node, user) {
  461. const purity = getNodePurityPercentage(node, user);
  462. const density = getNodeDensityPercentage(node, user);
  463. const experience = getNodeExperience(node, density, user);
  464. const ore = 16;
  465. const experienceRate = experience.toFixed(2);
  466. const oreRate = getOreRate(density, purity, ore);
  467.  
  468. node.children[0].setAttribute('data-after', `${experienceRate} xp/h ${oreRate} ore/h`);
  469. }
  470.  
  471. function getNodePurityPercentage(node, user) {
  472. const column = node.children[0].children[2];
  473. const label = column.getElementsByClassName('has-text-weight-bold')[0].parentElement;
  474. let percentage = Number(label.innerText.replace('%','').split(':')[1]);
  475.  
  476. let miningLevel = getMiningLevel(user);
  477. percentage = percentage + (miningLevel * 0.1);
  478.  
  479. return percentage;
  480. }
  481.  
  482. function getNodeDensityPercentage(node, user) {
  483. const column = node.children[0].children[1];
  484. const label = column.getElementsByClassName('has-text-weight-bold')[0].parentElement;
  485. let percentage = Number(label.innerText.replace('%','').split(':')[1]);
  486.  
  487. let miningLevel = getMiningLevel(user);
  488. percentage = percentage + (miningLevel * 0.1);
  489.  
  490. return percentage;
  491. }
  492.  
  493. function getNodeExperience(node, density, user) {
  494. /** Gets the Exp: label */
  495. const column = node.children[0].children[3];
  496. const label = column.getElementsByClassName('has-text-weight-bold')[0].parentElement;
  497. let value = Number(label.innerText.replace('%','').split(':')[1]);
  498.  
  499. const skilledExtraction = getSkilledExtractionLevel(user);
  500. const knowledgeExtraction = getKnowledgeExtractionLevel(user) / 100;
  501.  
  502. const ore = getActiveMining();
  503. const expertiseLevel = getExpertiseLevel(user);
  504. const expertiseGain = getExpertiseXPGainByOre(ore);
  505. const additionalMiningGain = expertiseLevel / 5 * expertiseGain;
  506. console.log('additionalMiningGain:', additionalMiningGain);
  507.  
  508. value += additionalMiningGain;
  509.  
  510. const actionsPerHour = getActionsPerHour(density);
  511. const experienceBase = value * actionsPerHour;
  512. const experienceSkilled = actionsPerHour * skilledExtraction;
  513. const experienceKnowledge = value * knowledgeExtraction;
  514.  
  515. value = experienceBase + experienceSkilled + experienceKnowledge;
  516.  
  517. const vip = isUserVIP(user);
  518.  
  519. value *= vip ? 1.1 : 1;
  520.  
  521. return value;
  522. }
  523.  
  524. function getActionsPerHour(density) {
  525. return 3600 / (60 / (density / 100))
  526. }
  527.  
  528. function getExperienceRate(density, experience) {
  529. return Number((3600 / (60 / (density / 100)) * experience).toFixed(2));
  530. }
  531.  
  532. function getOreRate(density, purity, ore) {
  533. return Number((3600 / (60 / (density / 100)) * (purity / 100) * ore).toFixed(2));
  534. }
  535. }
  536.  
  537. function isUserVIP(user) {
  538. return new Date(user.vip_expires) > new Date();
  539. }
  540.  
  541. updateExperienceRates();
  542. window.elethorResourceInterval = setInterval(updateExperienceRates, 500);
  543. initializeResourceNodeView();
  544.  
  545. async function initializeResourceNodeView() {
  546. await waitForField(document, 'head');
  547.  
  548. let css = '[data-after]::after { content: attr(data-after); padding: 12px;}';
  549.  
  550. appendCSS(css);
  551. }
  552. })();
  553.  
  554. async function loadMarketRecyclobotVisualizer() {
  555. if (!window.egpItemPoints) {
  556. const companions = await getCompanions();
  557. const recyclobot = companions.recyclobot;
  558. window.egpItemPoints = recyclobot.itemPoints;
  559.  
  560. console.log(`[${moduleName} v${version}] Companion Data initialized.`);
  561. }
  562.  
  563. window.elethorGeneralPurposeOnLocationChange.addEventListener('EGPLocation', async function (e) {
  564. if (e && e.newLocation) {
  565. window.egpRecyclobotOnMarket = e.newLocation.includes('/market/');
  566.  
  567. if (!window.egpRecyclobotOnMarket) {
  568. return;
  569. }
  570.  
  571. if (!window.egpItemPoints) {
  572. console.warn(`[${moduleName} v${version}] Entered market page but had no recyclobot item points list.`);
  573. return;
  574. }
  575.  
  576. const marketObjects = getMarketListingObjects();
  577.  
  578. for (const marketListing of marketObjects) {
  579. for (const recyclobotEntry of window.egpItemPoints) {
  580. if (recyclobotEntry.item_name === marketListing.name) {
  581. //marketListing.element.setAttribute('data-points-recyclobot',`R: ${recyclobotEntry.quantity}:${recyclobotEntry.points}`);
  582. break;
  583. }
  584. }
  585. }
  586. }
  587. });
  588.  
  589. function getMarketListingObjects() {
  590. return Array.prototype.slice.call(document.querySelectorAll('.is-market-side-menu ul li')).map((e) => {return {element: e, name: e.innerText}});
  591. }
  592.  
  593. async function getCompanions() {
  594. return (await window.axios.get('/game/companions')).data;
  595. }
  596. };
  597.  
  598. async function initializePackStats() {
  599. await waitForField(document, 'head');
  600. await waitForUser();
  601.  
  602. function loadPackStatsCSS() {
  603. const css = '' +
  604. '[data-pack-stat-ATTACK-SPEED]::after {content: attr(data-pack-stat-ATTACK-SPEED);float: right;}' +
  605. '[data-pack-stat-HEALTH]::after {content: attr(data-pack-stat-HEALTH);float: right;}' +
  606. '[data-pack-stat-FORTITUDE]::after {content: attr(data-pack-stat-FORTITUDE);float: right;}' +
  607. '[data-pack-stat-SPEED]::after {content: attr(data-pack-stat-SPEED);float: right;}' +
  608. '[data-pack-stat-SAVAGERY]::after {content: attr(data-pack-stat-SAVAGERY);float: right;}' +
  609. '[data-pack-stat-PIERCE]::after {content: attr(data-pack-stat-PIERCE);float: right;}' +
  610. '[data-pack-stat-ARMOR]::after {content: attr(data-pack-stat-ARMOR);float: right;}' +
  611. '[data-pack-stat-DAMAGE-REDUCTION]::after {content: attr(data-pack-stat-DAMAGE-REDUCTION);float: right;}' +
  612. '[data-points-recyclobot]::after {content: attr(data-points-recyclobot);float: right; top: -24px; position: relative;}'
  613. ;
  614.  
  615. appendCSS(css);
  616. }
  617. loadPackStatsCSS();
  618.  
  619. function getMonsterStatSpans() {
  620. return document.querySelectorAll('.is-desktop-monster-stats table tr td:last-child>span');
  621. }
  622.  
  623. function applyPackStats(packAmount) {
  624. const statSpans = getMonsterStatSpans();
  625.  
  626. let counter = 0;
  627.  
  628. let attackSpeedSpan, healthSpan;
  629.  
  630. for (const span of statSpans) {
  631. // Save attack speed span for later when we get speed stat
  632. if (counter === 0) {
  633. counter++;
  634. attackSpeedSpan = span;
  635. continue;
  636. }
  637.  
  638. // Save health span for later when we get fort stat
  639. if (counter === 1) {
  640. counter++;
  641. healthSpan = span;
  642. continue;
  643. }
  644.  
  645. const stat = processSpan(span);
  646.  
  647. // Health
  648. if (counter === 2) {
  649. processSpan(healthSpan, stat, 'health');
  650. }
  651.  
  652. // Speed
  653. if (counter === 3) {
  654. processSpan(attackSpeedSpan, stat, 'speed');
  655. }
  656.  
  657. counter++;
  658. }
  659.  
  660. function processSpan(span, statOverride, overrideType) {
  661. let stat = Number(span.innerText.replace('%', '').replace(',', '').replace(',', '').replace(',', ''));
  662.  
  663. const original10Percent = stat * 0.1;
  664. for (let i = 0; i < packAmount; i++) {
  665. stat += original10Percent;
  666. }
  667. stat = Math.floor(stat);
  668.  
  669. if (statOverride) {
  670. stat = statOverride;
  671.  
  672. if (overrideType === 'speed') {
  673. stat = getAttackSpeed(stat);
  674. }
  675.  
  676. if (overrideType === 'health') {
  677. stat = getHealth(stat);
  678. }
  679. }
  680.  
  681. let statPackFormatted;
  682.  
  683. if (span.innerText.includes('%')) {
  684. statPackFormatted = `${stat.toFixed(3)}%`;
  685. } else {
  686. statPackFormatted = Math.floor(stat).toFixed(0);
  687. }
  688.  
  689. statPackFormatted = statPackFormatted.toLocaleString();
  690.  
  691. span.setAttribute('data-pack-stat-' + span.parentElement.parentElement.children[0].innerText.replace(' ', '-'), `${statPackFormatted} (${packAmount+1}x)`);
  692.  
  693. return stat;
  694. }
  695. }
  696.  
  697. loadPackStatsCSS();
  698.  
  699. const multiplierByPackLevel = {
  700. 0: 3,
  701. 1: 4,
  702. 2: 5,
  703. 3: 6,
  704. 4: 8,
  705. 5: 10
  706. }
  707.  
  708. const multiplierByCarnageLevel = {
  709. 1: 11,
  710. 2: 12,
  711. 3: 13,
  712. 4: 14,
  713. 5: 15
  714. }
  715.  
  716. const packStatsUser = await getUser(currentUserData.user.id);
  717. const packLevel = getPackLevel(packStatsUser);
  718. const carnageLevel = getCarnageLevel(packStatsUser);
  719.  
  720. let packAmount = multiplierByPackLevel[packLevel];
  721.  
  722. if (carnageLevel > 0) {
  723. packAmount = multiplierByCarnageLevel[carnageLevel];
  724. }
  725.  
  726. setPackAmount(packAmount-1);
  727.  
  728. window.elethorPackStatsInterval = setInterval(() => {
  729. applyPackStats(window.packAmount);
  730. }, 500);
  731.  
  732. document.addEventListener('click', function(e) {
  733. if (e.target
  734. && e.target.id
  735. && (e.target.id === 'egpPackButtonUp' || e.target.id === 'egpPackButtonDown')
  736. ) {
  737. if (e.target.id === 'egpPackButtonUp') {
  738. setPackAmount(window.packAmount + 1);
  739. } else {
  740. setPackAmount(window.packAmount - 1);
  741. }
  742.  
  743. applyPackStats(window.packAmount);
  744. }
  745. });
  746.  
  747. function addPackChangeButtons() {
  748. if (document.querySelector('#egpPackButtonUp')) { return; }
  749.  
  750. const table = document.querySelector('.is-desktop-monster-stats table');
  751.  
  752. if (!table) { return; }
  753.  
  754. const buttonUp = document.createElement('button');
  755. buttonUp.setAttribute('style', 'float: right; height: 25px; width: 25px;');
  756. buttonUp.innerText = '→';
  757. buttonUp.id ='egpPackButtonUp';
  758. buttonUp.className ='button egpButton';
  759.  
  760. const buttonDown = document.createElement('button');
  761. buttonDown.setAttribute('style', 'float: right; height: 25px; width: 25px;');
  762. buttonDown.innerText = '←';
  763. buttonDown.id ='egpPackButtonDown';
  764. buttonDown.className ='button egpButton';
  765.  
  766. table.parentElement.insertBefore(buttonUp, table);
  767. table.parentElement.insertBefore(buttonDown, table);
  768. }
  769. addPackChangeButtons();
  770.  
  771. window.elethorPackStatsButtonInterval = setInterval(addPackChangeButtons, 500);
  772. }
  773.  
  774. function setPackAmount(amount) {
  775. window.packAmount = amount;
  776.  
  777. if (window.packAmount < 0) {
  778. window.packAmount = 0;
  779. }
  780. }
  781.  
  782. window.setPackAmount = setPackAmount;
  783.  
  784. function formatNormalNumber(num){
  785. return num.toLocaleString();
  786. }
  787.  
  788. function appendCSS(css) {
  789. let head = document.head || document.getElementsByTagName('head')[0];
  790. let style = document.createElement('style');
  791.  
  792. head.appendChild(style);
  793.  
  794. style.type = 'text/css';
  795. if (style.styleSheet){
  796. // This is required for IE8 and below.
  797. style.styleSheet.cssText = css;
  798. } else {
  799. style.appendChild(document.createTextNode(css));
  800. }
  801. }
  802.  
  803. function getMiningLevel(user) {
  804. for (const skill of user.skills) {
  805. if (skill.id === 1) {
  806. return skill.pivot.level;
  807. }
  808. }
  809.  
  810. return 0;
  811. }
  812.  
  813. function getSkilledExtractionLevel(user) {
  814. for (const skill of user.skills) {
  815. if (skill.id === 17) {
  816. return skill.pivot.level;
  817. }
  818. }
  819.  
  820. return 0;
  821. }
  822.  
  823. function getKnowledgeExtractionLevel(user) {
  824. for (const skill of user.skills) {
  825. if (skill.id === 18) {
  826. return skill.pivot.level;
  827. }
  828. }
  829.  
  830. return 0;
  831. }
  832.  
  833. function getExpertiseLevel(user) {
  834. for (const skill of user.skills) {
  835. if (skill.id === 38) {
  836. return skill.pivot.level;
  837. }
  838. }
  839.  
  840. return 0;
  841. }
  842.  
  843. expertiseXPGainByOre = {
  844. 'Orthoclase': 780,
  845. 'Anorthite': 825,
  846. 'Ferrisum': 600,
  847. 'Rhenium': 465,
  848. 'Jaspil': 480,
  849. 'Skasix': 0,
  850. };
  851.  
  852. function getExpertiseXPGainByOre(ore) {
  853. return expertiseXPGainByOre[ore];
  854. }
  855.  
  856. function getActiveMining() {
  857. const activeMiningButtons = document.querySelectorAll('.buttons .button.is-success');
  858.  
  859. if (!activeMiningButtons[0] ||
  860. !activeMiningButtons[0].children[0] ||
  861. !activeMiningButtons[0].children[0].innerText ||
  862. !activeMiningButtons[0].children[0].innerText.trim
  863. ) {
  864. return '';
  865. }
  866.  
  867. return activeMiningButtons[0].children[0].innerText.trim();
  868. }
  869.  
  870. function getPackLevel(user) {
  871. for (const skill of user.skills) {
  872. if (skill.id === 22) {
  873. return skill.pivot.level;
  874. }
  875. }
  876.  
  877. return 0;
  878. }
  879.  
  880. function getCarnageLevel(user) {
  881. for (const skill of user.skills) {
  882. if (skill.id === 23) {
  883. return skill.pivot.level;
  884. }
  885. }
  886.  
  887. return 0;
  888. }
  889.  
  890. function getAttackSpeed(speed) {
  891. return 50 - (50 * speed / (speed + 400));
  892. }
  893.  
  894. function getHealth(fortitude) {
  895. return 100000 * fortitude / (fortitude + 4000);
  896. }
  897.  
  898. async function waitForField(target, field) {
  899. return new Promise((resolve, reject) => {
  900. const interval = setInterval(() => {
  901. if (target[field] !== undefined) {
  902. clearInterval(interval);
  903. resolve();
  904. }
  905. }, 100);
  906. });
  907. }
  908.  
  909. async function waitForUser() {
  910. return new Promise((resolve, reject) => {
  911. const interval = setInterval(() => {
  912. if (currentUserData.user && currentUserData.user.id !== undefined) {
  913. clearInterval(interval);
  914. resolve();
  915. }
  916. }, 100);
  917. });
  918. }
  919.  
  920. (async function loadAlarms() {
  921. await waitForField(window, 'Echo');
  922. await waitForField(document, 'head');
  923. await waitForUser();
  924.  
  925.  
  926. const meta1 = document.createElement('meta');
  927. meta1.setAttribute('content', "media-src https://furious.no/elethor/sound/");
  928. meta1.setAttribute('http-equiv', 'Content-Security-Policy');
  929. document.getElementsByTagName('head')[0].appendChild(meta1);
  930.  
  931. /**
  932. * You can add an alternative audio by setting the local storage keys:
  933. * localStorage.setItem('egpSoundOutOfActions', 'your-url')
  934. * localStorage.setItem('egpSoundQuestCompleted', 'your-url')
  935. * */
  936.  
  937. // Alternative: https://furious.no/elethor/sound/elethor-actions-have-ran-out.mp3
  938. window.egpSoundOutOfActions =
  939. localStorage.getItem('egpSoundOutOfActions') || 'https://furious.no/elethor/sound/out-of-actions.wav';
  940.  
  941. // Alternative: https://furious.no/elethor/sound/elethor-quest-completed.mp3
  942. window.egpSoundQuestCompleted =
  943. localStorage.getItem('egpSoundQuestCompleted') || 'https://furious.no/elethor/sound/quest-complete.wav';
  944.  
  945. const masterAudioLevel = 0.3;
  946.  
  947. window.Echo.private(`App.User.${currentUserData.user.id}`).listen(".App\\Domain\\Monster\\Events\\FightingAgain", handleActionData);
  948. window.Echo.private(`App.User.${currentUserData.user.id}`).listen(".App\\Domain\\ResourceNode\\Events\\GatheringAgain", handleActionData);
  949. window.Echo.private(`App.User.${currentUserData.user.id}`).listen(".App\\Domain\\Quest\\Events\\UpdateQuestProgress", handleQuestData);
  950.  
  951. let hasWarnedAboutActions = false;
  952. let hasWarnedAboutQuest = false;
  953.  
  954. /**
  955. * Warns once when action runs out.
  956. * */
  957. function handleActionData(data) {
  958. if (data && data.action) {
  959. if (data.action.remaining <= 0 && !hasWarnedAboutActions) {
  960. playOutOfActions();
  961. hasWarnedAboutActions = true;
  962. } else if (data.action.remaining > 0 && hasWarnedAboutActions) {
  963. hasWarnedAboutActions = false;
  964. }
  965. }
  966. }
  967. window.handleActionData = handleActionData;
  968.  
  969. /**
  970. * Warns once when quest completes.
  971. * */
  972. function handleQuestData(data) {
  973. if (data
  974. && data.step
  975. && data.step.tasks) {
  976. if (data.step.progress >= data.step.tasks[0].quantity && !hasWarnedAboutQuest) {
  977. playQuestCompleted();
  978. hasWarnedAboutQuest = true;
  979. } else if (data.step.progress < data.step.tasks[0].quantity && hasWarnedAboutQuest) {
  980. hasWarnedAboutQuest = false;
  981. }
  982. }
  983. }
  984. window.handleQuestData = handleQuestData;
  985.  
  986. function playOutOfActions() {
  987. playSound(window.egpSoundOutOfActions);
  988. }
  989. window.playOutOfActions = playOutOfActions;
  990.  
  991. function playQuestCompleted() {
  992. playSound(window.egpSoundQuestCompleted);
  993. }
  994. window.playQuestCompleted = playQuestCompleted;
  995.  
  996. function playSound(sound, volume = masterAudioLevel){
  997. const audio = new Audio(sound);
  998.  
  999. audio.volume = volume;
  1000. audio.play();
  1001. }
  1002. })();
  1003. })();