Elethor General Purpose

Provides some general additions to Elethor

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

  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.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.7.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 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.  
  547. const original10Percent = stat * 0.1;
  548. for (let i = 0; i < packAmount; i++) {
  549. stat += original10Percent;
  550. }
  551. stat = Math.floor(stat);
  552.  
  553. if (statOverride) {
  554. stat = statOverride;
  555.  
  556. if (overrideType === 'speed') {
  557. stat = getAttackSpeed(stat);
  558. }
  559.  
  560. if (overrideType === 'health') {
  561. stat = getHealth(stat);
  562. }
  563. }
  564.  
  565. let statPackFormatted;
  566.  
  567. if (span.innerText.includes('%')) {
  568. statPackFormatted = `${stat.toFixed(3)}%`;
  569. } else {
  570. statPackFormatted = Math.floor(stat).toFixed(0);
  571. }
  572.  
  573. statPackFormatted = statPackFormatted.toLocaleString();
  574.  
  575. span.setAttribute('data-pack-stat-' + span.parentElement.parentElement.children[0].innerText.replace(' ', '-'), `${statPackFormatted} (${packAmount+1}x)`);
  576.  
  577. return stat;
  578. }
  579. }
  580.  
  581. loadPackStatsCSS();
  582.  
  583. const multiplierByPackLevel = {
  584. 0: 3,
  585. 1: 4,
  586. 2: 5,
  587. 3: 6,
  588. 4: 8,
  589. 5: 10
  590. }
  591.  
  592. const multiplierByCarnageLevel = {
  593. 1: 11,
  594. 2: 12,
  595. 3: 13,
  596. 4: 14,
  597. 5: 15
  598. }
  599.  
  600. const packStatsUser = await getUser(currentUserData.user.id);
  601. const packLevel = getPackLevel(packStatsUser);
  602. const carnageLevel = getCarnageLevel(packStatsUser);
  603.  
  604. let packAmount = multiplierByPackLevel[packLevel];
  605.  
  606. if (carnageLevel > 0) {
  607. packAmount = multiplierByCarnageLevel[carnageLevel];
  608. }
  609.  
  610. setPackAmount(packAmount-1);
  611.  
  612. window.elethorPackStatsInterval = setInterval(() => {
  613. applyPackStats(window.packAmount);
  614. }, 500);
  615.  
  616. document.addEventListener('click', function(e) {
  617. if (e.target
  618. && e.target.id
  619. && (e.target.id === 'egpPackButtonUp' || e.target.id === 'egpPackButtonDown')
  620. ) {
  621. if (e.target.id === 'egpPackButtonUp') {
  622. setPackAmount(window.packAmount + 1);
  623. } else {
  624. setPackAmount(window.packAmount - 1);
  625. }
  626.  
  627. applyPackStats(window.packAmount);
  628. }
  629. });
  630.  
  631. function addPackChangeButtons() {
  632. if (document.querySelector('#egpPackButtonUp')) { return; }
  633.  
  634. const table = document.querySelector('.is-desktop-monster-stats table');
  635.  
  636. if (!table) { return; }
  637.  
  638. const buttonUp = document.createElement('button');
  639. buttonUp.setAttribute('style', 'float: right; height: 25px; width: 25px;');
  640. buttonUp.innerText = '→';
  641. buttonUp.id ='egpPackButtonUp';
  642. buttonUp.className ='button egpButton';
  643.  
  644. const buttonDown = document.createElement('button');
  645. buttonDown.setAttribute('style', 'float: right; height: 25px; width: 25px;');
  646. buttonDown.innerText = '←';
  647. buttonDown.id ='egpPackButtonDown';
  648. buttonDown.className ='button egpButton';
  649.  
  650. table.parentElement.insertBefore(buttonUp, table);
  651. table.parentElement.insertBefore(buttonDown, table);
  652. }
  653. addPackChangeButtons();
  654.  
  655. window.elethorPackStatsButtonInterval = setInterval(addPackChangeButtons, 500);
  656. }
  657.  
  658. function setPackAmount(amount) {
  659. window.packAmount = amount;
  660.  
  661. if (window.packAmount < 0) {
  662. window.packAmount = 0;
  663. }
  664. }
  665.  
  666. window.setPackAmount = setPackAmount;
  667.  
  668. function formatNormalNumber(num){
  669. return num.toLocaleString();
  670. }
  671.  
  672. function appendCSS(css) {
  673. let head = document.head || document.getElementsByTagName('head')[0];
  674. let style = document.createElement('style');
  675.  
  676. head.appendChild(style);
  677.  
  678. style.type = 'text/css';
  679. if (style.styleSheet){
  680. // This is required for IE8 and below.
  681. style.styleSheet.cssText = css;
  682. } else {
  683. style.appendChild(document.createTextNode(css));
  684. }
  685. }
  686.  
  687. function getMiningLevel(user) {
  688. for (const skill of user.skills) {
  689. if (skill.id === 1) {
  690. return skill.pivot.level;
  691. }
  692. }
  693.  
  694. return 0;
  695. }
  696.  
  697. function getSkilledExtractionLevel(user) {
  698. for (const skill of user.skills) {
  699. if (skill.id === 17) {
  700. return skill.pivot.level;
  701. }
  702. }
  703.  
  704. return 0;
  705. }
  706.  
  707. function getKnowledgeExtractionLevel(user) {
  708. for (const skill of user.skills) {
  709. if (skill.id === 18) {
  710. return skill.pivot.level;
  711. }
  712. }
  713.  
  714. return 0;
  715. }
  716.  
  717. function getPackLevel(user) {
  718. for (const skill of user.skills) {
  719. if (skill.id === 22) {
  720. return skill.pivot.level;
  721. }
  722. }
  723.  
  724. return 0;
  725. }
  726.  
  727. function getCarnageLevel(user) {
  728. for (const skill of user.skills) {
  729. if (skill.id === 23) {
  730. return skill.pivot.level;
  731. }
  732. }
  733.  
  734. return 0;
  735. }
  736.  
  737. function getAttackSpeed(speed) {
  738. return 50 - (50 * speed / (speed + 25));
  739. }
  740.  
  741. function getHealth(fortitude) {
  742. return 100000 * fortitude / (fortitude + 4000);
  743. }
  744.  
  745. async function waitForField(target, field) {
  746. return new Promise((resolve, reject) => {
  747. const interval = setInterval(() => {
  748. if (target[field] !== undefined) {
  749. clearInterval(interval);
  750. resolve();
  751. }
  752. }, 100);
  753. });
  754. }
  755.  
  756. async function waitForUser() {
  757. return new Promise((resolve, reject) => {
  758. const interval = setInterval(() => {
  759. if (currentUserData.user && currentUserData.user.id !== undefined) {
  760. clearInterval(interval);
  761. resolve();
  762. }
  763. }, 100);
  764. });
  765. }
  766.  
  767. (async function loadAlarms() {
  768. await waitForField(window, 'Echo');
  769. await waitForUser();
  770.  
  771. /**
  772. * You can add an alternative audio by setting the local storage keys:
  773. * localStorage.setItem('egpSoundOutOfActions', 'your-url')
  774. * localStorage.setItem('egpSoundQuestCompleted', 'your-url')
  775. * */
  776.  
  777. // Alternative: https://furious.no/elethor/sound/elethor-actions-have-ran-out.mp3
  778. window.egpSoundOutOfActions =
  779. localStorage.getItem('egpSoundOutOfActions') || 'https://furious.no/elethor/sound/out-of-actions.wav';
  780.  
  781. // Alternative: https://furious.no/elethor/sound/elethor-quest-completed.mp3
  782. window.egpSoundQuestCompleted =
  783. localStorage.getItem('egpSoundQuestCompleted') || 'https://furious.no/elethor/sound/quest-complete.wav';
  784.  
  785. const masterAudioLevel = 0.3;
  786.  
  787. window.Echo.private(`App.User.${currentUserData.user.id}`).listen(".App\\Domain\\Monster\\Events\\FightingAgain", handleActionData);
  788. window.Echo.private(`App.User.${currentUserData.user.id}`).listen(".App\\Domain\\ResourceNode\\Events\\GatheringAgain", handleActionData);
  789. window.Echo.private(`App.User.${currentUserData.user.id}`).listen(".App\\Domain\\Quest\\Events\\UpdateQuestProgress", handleQuestData);
  790.  
  791. let hasWarnedAboutActions = false;
  792. let hasWarnedAboutQuest = false;
  793.  
  794. /**
  795. * Warns once when action runs out.
  796. * */
  797. function handleActionData(data) {
  798. if (data && data.action) {
  799. if (data.action.remaining <= 0 && !hasWarnedAboutActions) {
  800. playOutOfActions();
  801. hasWarnedAboutActions = true;
  802. } else if (data.action.remaining > 0 && hasWarnedAboutActions) {
  803. hasWarnedAboutActions = false;
  804. }
  805. }
  806. }
  807. window.handleActionData = handleActionData;
  808.  
  809. /**
  810. * Warns once when quest completes.
  811. * */
  812. function handleQuestData(data) {
  813. if (data
  814. && data.step
  815. && data.step.tasks) {
  816. if (data.step.progress >= data.step.tasks[0].quantity && !hasWarnedAboutQuest) {
  817. playQuestCompleted();
  818. hasWarnedAboutQuest = true;
  819. } else if (data.step.progress < data.step.tasks[0].quantity && hasWarnedAboutQuest) {
  820. hasWarnedAboutQuest = false;
  821. }
  822. }
  823. }
  824. window.handleQuestData = handleQuestData;
  825.  
  826. function playOutOfActions() {
  827. playSound(window.egpSoundOutOfActions);
  828. }
  829. window.playOutOfActions = playOutOfActions;
  830.  
  831. function playQuestCompleted() {
  832. playSound(window.egpSoundQuestCompleted);
  833. }
  834. window.playQuestCompleted = playQuestCompleted;
  835.  
  836. function playSound(sound, volume = masterAudioLevel){
  837. const audio = new Audio(sound);
  838.  
  839. audio.volume = volume;
  840. audio.play();
  841. }
  842. })();
  843. })();