Elethor General Purpose

Provides some general additions to Elethor

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

  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.0
  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.7.0';
  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 updateEquipmentPercentageSummary() {
  111. document.querySelector('.is-equipment>div>div').setAttribute('style', 'width: 50%')
  112. let percentagesTable = document.querySelector('#egpPercentagesSummary')
  113. if (!percentagesTable){
  114. percentagesTable = document.querySelector('.is-equipment>div>div:nth-child(2)').cloneNode(true);
  115. percentagesTable.setAttribute('style', 'width: 25%');
  116. percentagesTable.id='egpPercentagesSummary';
  117. document.querySelector('.is-equipment>div').appendChild(percentagesTable);
  118.  
  119. for (const child of percentagesTable.children[0].children) {
  120. if (child && child.children && child.children[0]) {
  121. child.children[0].remove();
  122. }
  123. }
  124.  
  125. document.querySelector('#egpPercentagesSummary>table>tr:nth-child(8)').setAttribute('style', 'height:43px');
  126. }
  127. }
  128.  
  129. function getStatSummary(equipment) {
  130. const summary = {
  131. base: {},
  132. energizements: {}
  133. };
  134.  
  135. if (equipment) {
  136. for (const key of Object.keys(equipment)) {
  137. const item = equipment[key];
  138.  
  139. /**
  140. * Sums base attributes by name
  141. * */
  142. if (item && item.attributes) {
  143. for (const attributeName of Object.keys(item.attributes)) {
  144. const attributeValue = item.attributes[attributeName];
  145.  
  146. if (!summary.base[attributeName]) {
  147. summary.base[attributeName] = 0;
  148. }
  149.  
  150. summary.base[attributeName] += attributeValue;
  151. }
  152. }
  153.  
  154. /**
  155. * Sums energizements by stat name
  156. * */
  157. if (item && item.upgrade && item.upgrade.energizements) {
  158. for (const energizement of item.upgrade.energizements) {
  159. if (!summary.energizements[energizement.stat]) {
  160. summary.energizements[energizement.stat] = 0;
  161. }
  162.  
  163. summary.energizements[energizement.stat] += Number(energizement.boost);
  164. }
  165. }
  166. }
  167. }
  168.  
  169. return summary;
  170. }
  171.  
  172. function updateInventoryStatsPercentages() {
  173. let percentagesTable = document.querySelector('#egpPercentagesSummary')
  174. if (percentagesTable && currentUserData && currentUserData.equipment){
  175. const statSummary = getStatSummary(currentUserData.equipment);
  176.  
  177. const baseKeys = Object.keys(statSummary.base);
  178. const energizementKeys = Object.keys(statSummary.energizements);
  179.  
  180. let allKeys = baseKeys.concat(energizementKeys);
  181. const filterUniques = {};
  182. for (const key of allKeys){
  183. filterUniques[key] = true;
  184. }
  185. allKeys = Object.keys(filterUniques);
  186. allKeys.sort();
  187.  
  188. allKeys.push('actions');
  189.  
  190. const tableRows = percentagesTable.children[0].children;
  191.  
  192. for(const row of tableRows) {
  193. if (row
  194. && row.children
  195. && row.children[0]
  196. && row.children[0].children[0]
  197. ) {
  198. const rowText = row.children[0].children[0];
  199. rowText.innerText = '';
  200. }
  201. }
  202.  
  203. let rowIndex = 0;
  204. for (const key of allKeys) {
  205. if (key === 'puncture') {
  206. continue;
  207. }
  208.  
  209. const row = tableRows[rowIndex];
  210. if (row
  211. && row.children
  212. && row.children[0]
  213. && row.children[0].children[0]
  214. ) {
  215. const rowText = row.children[0].children[0];
  216.  
  217. const rowBase = statSummary.base[key] || 0;
  218. const rowEnergizement = (statSummary.energizements[key] || 0);
  219. const rowEnergizementPercentage = (statSummary.energizements[key] || 0) * 100;
  220.  
  221. if (key.startsWith('+')) {
  222. rowText.innerText = `${key} per 10 levels: ${rowEnergizement}`;
  223. } else if (key === 'actions') {
  224. const actions = currentUserData.user.bonus_actions || 0;
  225. rowText.innerText = `Bonus Actions: ${actions}`;
  226. } else {
  227. rowText.innerText = `${key}: ${rowBase} (${rowEnergizementPercentage.toFixed(0)}%)`;
  228. }
  229.  
  230. rowIndex++;
  231. }
  232. }
  233. }
  234. }
  235.  
  236. function initializeLocationChangeListener() {
  237. let previousLocation = window.location.href;
  238.  
  239. window.elethorGeneralPurposeOnLocationChange = new EventTarget();
  240.  
  241. window.elethorLocationInterval = setInterval(() => {
  242. if (previousLocation !== window.location.href) {
  243. previousLocation = window.location.href;
  244.  
  245. const e = new Event('EGPLocation');
  246. e.newLocation = window.location.href;
  247. window.elethorGeneralPurposeOnLocationChange.dispatchEvent(e);
  248. }
  249.  
  250. }, 500);
  251.  
  252. console.log(`[${moduleName} v${version}] Location Change Listener initialized.`);
  253. }
  254.  
  255. function getProfileCombatElement() {
  256. const skillElements = document.querySelectorAll('.is-skill div>span.has-text-weight-bold');
  257. for (const skillElement of skillElements) {
  258. if (skillElement.innerText.includes('Combat')) {
  259. return skillElement;
  260. }
  261. }
  262. }
  263.  
  264. function updateXPTracker(difference) {
  265. const combatElement = getProfileCombatElement();
  266. if (!combatElement) {
  267. return;
  268. }
  269.  
  270. if (difference > 0) {
  271. combatElement.setAttribute('data-combat-experience-ahead', `(+${formatNormalNumber(difference)})`);
  272. combatElement.setAttribute('style', `color:lime`);
  273. } else {
  274. combatElement.setAttribute('data-combat-experience-ahead', `(${formatNormalNumber(difference)})`);
  275. combatElement.setAttribute('style', `color:red`);
  276. }
  277. }
  278.  
  279. function initializeProfileLoadListener() {
  280. let css = '[data-combat-experience-ahead]::after { content: attr(data-combat-experience-ahead); padding: 12px;}';
  281.  
  282. appendCSS(css);
  283.  
  284. window.elethorGeneralPurposeOnLocationChange.addEventListener('EGPLocation', async function (e) {
  285. if (e && e.newLocation) {
  286. if(e.newLocation.includes('/profile/')) {
  287. console.log('Profile view detected:', e);
  288. const url = e.newLocation;
  289. const path = url.substr(url.indexOf(profileURL));
  290.  
  291. // We know we have a profile lookup, and not user-data load if the length differs.
  292. if (path.length > profileURL.length) {
  293. const userId = Number(path.substr(path.lastIndexOf('/') + 1));
  294.  
  295. const difference = await getExperienceDifference(userId, currentUserData.user.id);
  296.  
  297. updateXPTracker(difference);
  298. }
  299. }
  300. }
  301. });
  302.  
  303. console.log(`[${moduleName} v${version}] Profile Load Listener initialized.`);
  304. }
  305.  
  306. async function getUser(id) {
  307. const result = await window.axios.get(`/game/user/${id}?egpIgnoreMe=true`);
  308. return result.data;
  309. }
  310.  
  311. window.getUser = getUser;
  312.  
  313. async function getUserStats() {
  314. const result = await window.axios.get(`/game/inventory/stats`);
  315. return result.data;
  316. }
  317.  
  318. window.getUserStats = getUserStats;
  319.  
  320. async function getUserStatsJSON(pretty) {
  321. const stats = await getUserStats();
  322.  
  323. if (pretty) {
  324. return JSON.stringify(stats, null, 2);
  325. }
  326.  
  327. return JSON.stringify(stats);
  328. }
  329.  
  330. window.getUserStatsJSON = getUserStatsJSON;
  331.  
  332. function getUserCombatStats(user) {
  333. for (const skill of user.skills) {
  334. if (skill.name === 'combat') {
  335. return skill.pivot;
  336. }
  337. }
  338. }
  339.  
  340. async function getExperienceDifference(userId1, userId2) {
  341. const [user1, user2] = await Promise.all([
  342. getUser(userId1),
  343. getUser(userId2)
  344. ]);
  345.  
  346. const combatStats1 = getUserCombatStats(user1);
  347. const combatStats2 = getUserCombatStats(user2);
  348.  
  349. return combatStats2.experience - combatStats1.experience;
  350. }
  351.  
  352. (function run() {
  353. initializeToastKiller();
  354. initializeXHRHook();
  355. initializeUserLoadListener();
  356. initializeInventoryStatsLoadListener();
  357. initializeLocationChangeListener();
  358. initializeProfileLoadListener();
  359. initializePackStats();
  360.  
  361. console.log(`[${moduleName} v${version}] Loaded.`);
  362. })();
  363.  
  364. (async function loadRerollDisableButtonModule() {
  365. async function waitForEcho() {
  366. return new Promise((resolve, reject) => {
  367. const interval = setInterval(() => {
  368. if (window.Echo) {
  369. clearInterval(interval);
  370. resolve();
  371. }
  372. }, 100);
  373. });
  374. }
  375.  
  376. await waitForEcho();
  377. await waitForUser();
  378.  
  379. elethorGeneralPurposeOnXHR.addEventListener('EGPXHR', async function (e) {
  380. if (e && e.xhr && e.xhr.responseURL) {
  381. if(e.xhr.responseURL.includes('/game/energize')) {
  382. const itemID = e.xhr.responseURL.substr(e.xhr.responseURL.lastIndexOf('/')+1);
  383. window.lastEnergizeID = Number(itemID);
  384. }
  385. }
  386. });
  387. })();
  388.  
  389. (async function loadResourceNodeUpdater() {
  390. await waitForField(currentUserData, 'user');
  391. const user = await getUser(currentUserData.user.id);
  392.  
  393. function updateExperienceRates() {
  394. document.querySelectorAll('.is-resource-node').forEach(async (node) => {
  395. visualizeResourceNodeExperienceRates(node, user)
  396. });
  397.  
  398. function visualizeResourceNodeExperienceRates(node, user) {
  399. const purity = getNodePurityPercentage(node, user);
  400. const density = getNodeDensityPercentage(node, user);
  401. const experience = getNodeExperience(node, density, user);
  402. const ore = 16;
  403. const experienceRate = experience.toFixed(2);
  404. const oreRate = getOreRate(density, purity, ore);
  405.  
  406. node.children[0].setAttribute('data-after', `${experienceRate} xp/h ${oreRate} ore/h`);
  407. }
  408.  
  409. function getNodePurityPercentage(node, user) {
  410. const column = node.children[0].children[2];
  411. const label = column.getElementsByClassName('has-text-weight-bold')[0].parentElement;
  412. let percentage = Number(label.innerText.replace('%','').split(':')[1]);
  413.  
  414. let miningLevel = getMiningLevel(user);
  415. percentage = percentage + (miningLevel * 0.1);
  416.  
  417. return percentage;
  418. }
  419.  
  420. function getNodeDensityPercentage(node, user) {
  421. const column = node.children[0].children[1];
  422. const label = column.getElementsByClassName('has-text-weight-bold')[0].parentElement;
  423. let percentage = Number(label.innerText.replace('%','').split(':')[1]);
  424.  
  425. let miningLevel = getMiningLevel(user);
  426. percentage = percentage + (miningLevel * 0.1);
  427.  
  428. return percentage;
  429. }
  430.  
  431. function getNodeExperience(node, density, user) {
  432. const column = node.children[0].children[3];
  433. const label = column.getElementsByClassName('has-text-weight-bold')[0].parentElement;
  434. let value = Number(label.innerText.replace('%','').split(':')[1]);
  435.  
  436. const skilledExtraction = getSkilledExtractionLevel(user);
  437. const knowledgeExtraction = getKnowledgeExtractionLevel(user) / 100;
  438.  
  439. const actionsPerHour = getActionsPerHour(density);
  440. const experienceBase = value * actionsPerHour;
  441. const experienceSkilled = actionsPerHour * skilledExtraction;
  442. const experienceKnowledge = value * knowledgeExtraction;
  443.  
  444. value = experienceBase + experienceSkilled + experienceKnowledge;
  445.  
  446. const vip = isUserVIP(user);
  447.  
  448. value *= vip ? 1.1 : 1;
  449.  
  450. return value;
  451. }
  452.  
  453. function getActionsPerHour(density) {
  454. return 3600 / (60 / (density / 100))
  455. }
  456.  
  457. function getExperienceRate(density, experience) {
  458. return Number((3600 / (60 / (density / 100)) * experience).toFixed(2));
  459. }
  460.  
  461. function getOreRate(density, purity, ore) {
  462. return Number((3600 / (60 / (density / 100)) * (purity / 100) * ore).toFixed(2));
  463. }
  464. }
  465.  
  466. function isUserVIP(user) {
  467. return new Date(user.vip_expires) > new Date();
  468. }
  469.  
  470. updateExperienceRates();
  471. window.elethorResourceInterval = setInterval(updateExperienceRates, 500);
  472. initializeResourceNodeView();
  473.  
  474. async function initializeResourceNodeView() {
  475. await waitForField(document, 'head');
  476.  
  477. let css = '[data-after]::after { content: attr(data-after); padding: 12px;}';
  478.  
  479. appendCSS(css);
  480. }
  481. })();
  482.  
  483. async function initializePackStats() {
  484. await waitForField(document, 'head');
  485. await waitForUser();
  486.  
  487. function loadPackStatsCSS() {
  488. const css = '' +
  489. '[data-pack-stat-ATTACK-SPEED]::after {content: attr(data-pack-stat-ATTACK-SPEED);float: right;}' +
  490. '[data-pack-stat-HEALTH]::after {content: attr(data-pack-stat-HEALTH);float: right;}' +
  491. '[data-pack-stat-FORTITUDE]::after {content: attr(data-pack-stat-FORTITUDE);float: right;}' +
  492. '[data-pack-stat-SPEED]::after {content: attr(data-pack-stat-SPEED);float: right;}' +
  493. '[data-pack-stat-SAVAGERY]::after {content: attr(data-pack-stat-SAVAGERY);float: right;}' +
  494. '[data-pack-stat-PIERCE]::after {content: attr(data-pack-stat-PIERCE);float: right;}' +
  495. '[data-pack-stat-ARMOR]::after {content: attr(data-pack-stat-ARMOR);float: right;}' +
  496. '[data-pack-stat-DAMAGE-REDUCTION]::after {content: attr(data-pack-stat-DAMAGE-REDUCTION);float: right;}'
  497. ;
  498.  
  499. appendCSS(css);
  500. }
  501. loadPackStatsCSS();
  502.  
  503. function getMonsterStatSpans() {
  504. return document.querySelectorAll('.is-desktop-monster-stats table tr td:last-child>span');
  505. }
  506.  
  507. function applyPackStats(packAmount) {
  508. const statSpans = getMonsterStatSpans();
  509.  
  510. let counter = 0;
  511.  
  512. let attackSpeedSpan, healthSpan;
  513.  
  514. for (const span of statSpans) {
  515. // Save attack speed span for later when we get speed stat
  516. if (counter === 0) {
  517. counter++;
  518. attackSpeedSpan = span;
  519. continue;
  520. }
  521.  
  522. // Save health span for later when we get fort stat
  523. if (counter === 1) {
  524. counter++;
  525. healthSpan = span;
  526. continue;
  527. }
  528.  
  529. const stat = processSpan(span);
  530.  
  531. // Health
  532. if (counter === 2) {
  533. processSpan(healthSpan, stat, 'health');
  534. }
  535.  
  536. // Speed
  537. if (counter === 3) {
  538. processSpan(attackSpeedSpan, stat, 'speed');
  539. }
  540.  
  541. counter++;
  542. }
  543.  
  544. function processSpan(span, statOverride, overrideType) {
  545. let stat = Number(span.innerText.replace('%', '').replace(',', '').replace(',', '').replace(',', ''));
  546. stat *= Math.pow(1.1, packAmount);
  547.  
  548. if (statOverride) {
  549. stat = statOverride;
  550.  
  551. if (overrideType === 'speed') {
  552. stat = getAttackSpeed(stat);
  553. }
  554.  
  555. if (overrideType === 'health') {
  556. stat = getHealth(stat);
  557. }
  558. }
  559.  
  560. let statPackFormatted;
  561.  
  562. if (span.innerText.includes('%')) {
  563. statPackFormatted = `${stat.toFixed(3)}%`;
  564. } else {
  565. statPackFormatted = Math.floor(stat).toFixed(0);
  566. }
  567.  
  568. statPackFormatted = statPackFormatted.toLocaleString();
  569.  
  570. span.setAttribute('data-pack-stat-' + span.parentElement.parentElement.children[0].innerText.replace(' ', '-'), `${statPackFormatted} (${packAmount}x)`);
  571.  
  572. return stat;
  573. }
  574. }
  575.  
  576. loadPackStatsCSS();
  577.  
  578. const multiplierByPackLevel = {
  579. 0: 3,
  580. 1: 4,
  581. 2: 5,
  582. 3: 6,
  583. 4: 8,
  584. 5: 10
  585. }
  586.  
  587. const multiplierByCarnageLevel = {
  588. 1: 11,
  589. 2: 12,
  590. 3: 13,
  591. 4: 14,
  592. 5: 15
  593. }
  594.  
  595. const packStatsUser = await getUser(currentUserData.user.id);
  596. const packLevel = getPackLevel(packStatsUser);
  597. const carnageLevel = getCarnageLevel(packStatsUser);
  598.  
  599. let packAmount = multiplierByPackLevel[packLevel];
  600.  
  601. if (carnageLevel > 0) {
  602. packAmount = multiplierByCarnageLevel[carnageLevel];
  603. }
  604.  
  605. setPackAmount(packAmount);
  606.  
  607. window.elethorPackStatsInterval = setInterval(() => {
  608. applyPackStats(window.packAmount);
  609. }, 500);
  610.  
  611. document.addEventListener('click', function(e) {
  612. if (e.target
  613. && e.target.id
  614. && (e.target.id === 'egpPackButtonUp' || e.target.id === 'egpPackButtonDown')
  615. ) {
  616. if (e.target.id === 'egpPackButtonUp') {
  617. setPackAmount(window.packAmount + 1);
  618. } else {
  619. setPackAmount(window.packAmount - 1);
  620. }
  621.  
  622. applyPackStats(window.packAmount);
  623. }
  624. });
  625.  
  626. function addPackChangeButtons() {
  627. if (document.querySelector('#egpPackButtonUp')) { return; }
  628.  
  629. const table = document.querySelector('.is-desktop-monster-stats table');
  630.  
  631. if (!table) { return; }
  632.  
  633. const buttonUp = document.createElement('button');
  634. buttonUp.setAttribute('style', 'float: right; height: 25px; width: 25px;');
  635. buttonUp.innerText = '→';
  636. buttonUp.id ='egpPackButtonUp';
  637. buttonUp.className ='button egpButton';
  638.  
  639. const buttonDown = document.createElement('button');
  640. buttonDown.setAttribute('style', 'float: right; height: 25px; width: 25px;');
  641. buttonDown.innerText = '←';
  642. buttonDown.id ='egpPackButtonDown';
  643. buttonDown.className ='button egpButton';
  644.  
  645. table.parentElement.insertBefore(buttonUp, table);
  646. table.parentElement.insertBefore(buttonDown, table);
  647. }
  648. addPackChangeButtons();
  649.  
  650. window.elethorPackStatsButtonInterval = setInterval(addPackChangeButtons, 500);
  651. }
  652.  
  653. function setPackAmount(amount) {
  654. window.packAmount = amount;
  655. }
  656.  
  657. window.setPackAmount = setPackAmount;
  658.  
  659. function formatNormalNumber(num){
  660. return num.toLocaleString();
  661. }
  662.  
  663. function appendCSS(css) {
  664. let head = document.head || document.getElementsByTagName('head')[0];
  665. let style = document.createElement('style');
  666.  
  667. head.appendChild(style);
  668.  
  669. style.type = 'text/css';
  670. if (style.styleSheet){
  671. // This is required for IE8 and below.
  672. style.styleSheet.cssText = css;
  673. } else {
  674. style.appendChild(document.createTextNode(css));
  675. }
  676. }
  677.  
  678. function getMiningLevel(user) {
  679. for (const skill of user.skills) {
  680. if (skill.id === 1) {
  681. return skill.pivot.level;
  682. }
  683. }
  684.  
  685. return 0;
  686. }
  687.  
  688. function getSkilledExtractionLevel(user) {
  689. for (const skill of user.skills) {
  690. if (skill.id === 17) {
  691. return skill.pivot.level;
  692. }
  693. }
  694.  
  695. return 0;
  696. }
  697.  
  698. function getKnowledgeExtractionLevel(user) {
  699. for (const skill of user.skills) {
  700. if (skill.id === 18) {
  701. return skill.pivot.level;
  702. }
  703. }
  704.  
  705. return 0;
  706. }
  707.  
  708. function getPackLevel(user) {
  709. for (const skill of user.skills) {
  710. if (skill.id === 22) {
  711. return skill.pivot.level;
  712. }
  713. }
  714.  
  715. return 0;
  716. }
  717.  
  718. function getCarnageLevel(user) {
  719. for (const skill of user.skills) {
  720. if (skill.id === 23) {
  721. return skill.pivot.level;
  722. }
  723. }
  724.  
  725. return 0;
  726. }
  727.  
  728. function getAttackSpeed(speed) {
  729. return 50 - (50 * speed / (speed + 25));
  730. }
  731.  
  732. function getHealth(fortitude) {
  733. return 100000 * fortitude / (fortitude + 4000);
  734. }
  735.  
  736. async function waitForField(target, field) {
  737. return new Promise((resolve, reject) => {
  738. const interval = setInterval(() => {
  739. if (target[field] !== undefined) {
  740. clearInterval(interval);
  741. resolve();
  742. }
  743. }, 100);
  744. });
  745. }
  746.  
  747. async function waitForUser() {
  748. return new Promise((resolve, reject) => {
  749. const interval = setInterval(() => {
  750. if (currentUserData.user && currentUserData.user.id !== undefined) {
  751. clearInterval(interval);
  752. resolve();
  753. }
  754. }, 100);
  755. });
  756. }
  757.  
  758. (async function loadAlarms() {
  759. await waitForField(window, 'Echo');
  760. await waitForUser();
  761.  
  762. /**
  763. * You can add an alternative audio by setting the local storage keys:
  764. * localStorage.setItem('egpSoundOutOfActions', 'your-url')
  765. * localStorage.setItem('egpSoundQuestCompleted', 'your-url')
  766. * */
  767.  
  768. // Alternative: https://furious.no/elethor/sound/elethor-actions-have-ran-out.mp3
  769. window.egpSoundOutOfActions =
  770. localStorage.getItem('egpSoundOutOfActions') || 'https://furious.no/elethor/sound/out-of-actions.wav';
  771.  
  772. // Alternative: https://furious.no/elethor/sound/elethor-quest-completed.mp3
  773. window.egpSoundQuestCompleted =
  774. localStorage.getItem('egpSoundQuestCompleted') || 'https://furious.no/elethor/sound/quest-complete.wav';
  775.  
  776. const masterAudioLevel = 0.3;
  777.  
  778. window.Echo.private(`App.User.${currentUserData.user.id}`).listen(".App\\Domain\\Monster\\Events\\FightingAgain", handleActionData);
  779. window.Echo.private(`App.User.${currentUserData.user.id}`).listen(".App\\Domain\\ResourceNode\\Events\\GatheringAgain", handleActionData);
  780. window.Echo.private(`App.User.${currentUserData.user.id}`).listen(".App\\Domain\\Quest\\Events\\UpdateQuestProgress", handleQuestData);
  781.  
  782. let hasWarnedAboutActions = false;
  783.  
  784. /**
  785. * Warns once when action runs out.
  786. * */
  787. function handleActionData(data) {
  788. if (data && data.action) {
  789. if (data.action.remaining <= 0 && !hasWarnedAboutActions) {
  790. playOutOfActions();
  791. hasWarnedAboutActions = true;
  792. } else if (data.action.remaining > 0 && hasWarnedAboutActions) {
  793. hasWarnedAboutActions = false;
  794. }
  795. }
  796. }
  797. window.handleActionData = handleActionData;
  798.  
  799. /**
  800. * Warns once when quest completes.
  801. * */
  802. function handleQuestData(data) {
  803. if (data
  804. && data.step
  805. && data.step.status
  806. && data.step.status === 'Complete') {
  807. playQuestCompleted();
  808. }
  809. }
  810. window.handleQuestData = handleQuestData;
  811.  
  812. function playOutOfActions() {
  813. playSound(window.egpSoundOutOfActions);
  814. }
  815. window.playOutOfActions = playOutOfActions;
  816.  
  817. function playQuestCompleted() {
  818. playSound(window.egpSoundQuestCompleted);
  819. }
  820. window.playQuestCompleted = playQuestCompleted;
  821.  
  822. function playSound(sound, volume = masterAudioLevel){
  823. const audio = new Audio(sound);
  824.  
  825. audio.volume = volume;
  826. audio.play();
  827. }
  828. })();
  829. })();