IQRPG Stats

Tracks all possible in game drops per hour/day/total, as well as various battle stats over multiple battles.

目前为 2023-11-11 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name IQRPG Stats
  3. // @namespace https://www.iqrpg.com/
  4. // @version 0.55
  5. // @description Tracks all possible in game drops per hour/day/total, as well as various battle stats over multiple battles.
  6. // @author Coastis
  7. // @match *://*.iqrpg.com/*
  8. // @require http://code.jquery.com/jquery-latest.js
  9. // @run-at document-idle
  10. // @grant GM_addStyle
  11. // ==/UserScript==
  12.  
  13. ///////////////////////////////////
  14. /////////// config ////////////////
  15. ///////////////////////////////////
  16.  
  17. const track_battle_stats = true; // true or false - enable or disable battle stats
  18. const track_stats_n_drops = true; // true or false - enable or disable drop tracker
  19. const dropalyse_decimal_precision = 3; // the number of decimal places to show for drops per hour/day averages
  20. const track_boss_battles = false; // true or false
  21. const track_clan_battles = false; // true or false
  22. const track_abyss_battles = false; // true or false
  23. const track_min_health_perc = true; // true or false
  24.  
  25. //////////////////////////////////////////////////////////////////
  26. /////////// Don't change anything below this line ////////////////
  27. //////////////////////////////////////////////////////////////////
  28.  
  29. /* globals jQuery, $ */
  30.  
  31. // init
  32. var player_stats = { dmg:0, hits:0, misses:0, hp_perc:100, max_hp:0 }; //TODO update from local storage
  33. var enemy_stats = { dmg:0, hits:0, misses:0, dodges:0,max_hp:0 }; //TODO update from local storage
  34. var player_cache = '';
  35. var enemy_cache = '';
  36. //var action_timer_cache = ''; // delete
  37. var actions = 0;
  38. var dropalyse_rendered = false;
  39. var dropalyse_format = 'hour'; // hour/day/total
  40. var track_extra_battle_stats = true;
  41.  
  42. // setup persistant vars
  43. var dropalyse_cache = '';
  44. var dropalyse_store = {};
  45. //var dropalyse_start_datum = Date.now(); // delete
  46. var dropalyse_filters = [];
  47. dropalyse_load_ls();
  48.  
  49. // run every xxx ms updating stuffs
  50. // NOTE we set a delay of 1000ms before starting to help slow rendering cpu's, otherwise there is the possibility of the very 1st drop not being counted
  51. setTimeout(function() {
  52. // setup observer
  53. if(track_stats_n_drops === true) {
  54. if(dropalyse_rendered === false) { // insert p/h panel
  55. dropalyse_insert_log();
  56. dropalyse_rendered = true;
  57. }
  58. const actionsElement = document.querySelector(".action-timer__text");
  59. const actionObserver = new MutationObserver(actionTimerChanged);
  60. actionObserver.observe(actionsElement, { childList: true, subtree: true,characterData: true, characterDataOldValue: true});
  61. }
  62. // others
  63. setInterval(iqrpgbs_loop, 100);
  64. //setInterval(iqrpgbs_timer_loop, 1000);
  65. }, 1000);
  66.  
  67. // observes action timer changes and updates actions count as needed
  68. function actionTimerChanged(mutationList, observer) {
  69. mutationList.forEach((mutation) => {
  70. if(mutation.type === 'characterData') {
  71. if(actionTimerClean(mutation.target.textContent) < actionTimerClean(mutation.oldValue)) {
  72. actions++;
  73. parse_drop_log(); // parse it
  74. dropalyse_render_content(); // update p/h
  75. }
  76. }
  77. });
  78. }
  79. function actionTimerClean(entry) {
  80. return +(entry.replace("Autos Remaining: ",""));
  81. }
  82.  
  83. // main loop
  84. function iqrpgbs_loop() {
  85.  
  86. /*
  87. FIXED in v0.55
  88. - Completed grouping/sorting of all ingame items
  89. - Added action counter as well as time elapsed
  90. - Added drop filters to backup/restore data
  91. - Minor optimisations
  92. - Fixed bug where reseting drop stats would sometimes leave a legacy copy of the stats until browser was refreshed
  93. - Fixed bug with mis-reading high qtys of drops, such as +1,234 gathering shards - thanks Duck!
  94. - Fixed timer still counting when offline or player has ran out of actions
  95. */
  96.  
  97. //TODO dungeon key total tokens
  98.  
  99. //TODO add persistance for battle stats
  100. //TODO add "number of rushes" for gold/resources - would need to remove the popup associated with [Gold][Wood] etc
  101. //TODO seperate out battle stats so boss/dungeon etc stats track seperately
  102. //rarity tracking could detect if a new class is found and exit if carl changes css, thus preventing our nightmare
  103. //TODO battle stats - winrate% improvment calculations e,g, attacj winrate% delta
  104.  
  105. // dropalyse
  106. if(track_stats_n_drops === true) {
  107. // first run?
  108. if(dropalyse_cache === '' && $( 'div#log-div > div' ).length > 0 ) {
  109. dropalyse_cache = dropalyse_clean_entry( $( 'div#log-div > div:first' ) );
  110. }
  111. /* // insert p/h panel
  112. if(dropalyse_rendered === false) {
  113. dropalyse_insert_log();
  114. dropalyse_rendered = true;
  115. } */
  116. // NEW drop log parsing
  117. /* if( $("div.fixed-top > div.section-2 > div.action-timer > div.action-timer__text").length > 0 ) {
  118. let action_data = $("div.fixed-top > div.section-2 > div.action-timer > div.action-timer__text").prop('innerHTML').trim();
  119. if(action_data !== action_timer_cache) {
  120. action_timer_cache = action_data;
  121. parse_drop_log(); // parse it
  122. dropalyse_render_content(); // update p/h
  123. }
  124. } else {
  125. return false; // skip as autos hasnt rendered yet
  126. } */
  127. }
  128.  
  129. // battle stats
  130. if(track_battle_stats === false) return true; // enable or disable battle stats
  131. var we_battling = false;
  132. var display_needs_update = false;
  133.  
  134. // check we're on the battle page
  135. if(document.getElementsByClassName("battle-container").length > 0) {
  136. we_battling = true;
  137. } else return false;
  138.  
  139. // and not in a boss battle - Boss Tokens
  140. if(track_boss_battles!==true && $("div.game-grid > div.main-game-section > div.main-section__body:contains('Boss Tokens')").length > 0) {
  141. return false;
  142. }
  143.  
  144. // and not in a dungeon - Dungeoneering Exp
  145. if($("div.game-grid > div.main-game-section > div.main-section__body:contains('Dungeoneering Exp')").length > 0) {
  146. return false;
  147. }
  148.  
  149. // and not in a standard clan battle - Clan Exp
  150. if(track_clan_battles!==true && $("div.game-grid > div.main-game-section > div.main-section__body:contains('Clan Exp')").length > 0) {
  151. return false;
  152. }
  153.  
  154. // all is well, so let's get the mob name
  155. var n_obj = $("div.battle-container > div:nth-child(3) > div.participant-name > span");
  156. if(n_obj.length > 0) {
  157. var mob_name = n_obj.prop('innerHTML').trim();
  158. } else {
  159. return false; // couldn't find the mob name, lets skip just in case
  160. }
  161.  
  162. // and not in a clan dragon battle
  163. // exact matches
  164. const clan_dragons = ['Baby Dragon','Young Dragon','Adolescent Dragon','Adult Dragon','Dragon','Mythical Dragon'];
  165. if(track_clan_battles!==true && clan_dragons.indexOf(mob_name) > -1 ) {
  166. //console.log('Skipping Clan Dragons');
  167. return false;
  168. }
  169.  
  170. // and not in an abyss battle or clan battle - Abyssal
  171. if(track_abyss_battles!==true && ['Abyssal'].some(term => mob_name.includes(term))) {
  172. return false;
  173. }
  174.  
  175. // all good
  176. if(we_battling === true) {
  177.  
  178. // Add the battle stat panel to dom, if not already there
  179. if(!document.getElementById("iqrpgbs_bs_panel")) {
  180. var iqrpg_body = $( "div.game-grid > div.main-game-section > div.main-section__body" );
  181. iqrpg_body[0].children[0].children[0].insertAdjacentHTML('beforeend', render_battle_stats_panel() );
  182. document.getElementById('igrpg_bs_reset').addEventListener('click', iqrpg_bs_reset_stats, false);
  183. }
  184.  
  185. // get the players stat line & compare it to previous stored stats
  186. var player_sl = $("div.game-grid > div.main-game-section > div.main-section__body > div > div > div > div:nth-child(2)");
  187. if(player_sl.prop('innerHTML') !== player_cache) {
  188. player_cache = player_sl.prop('innerHTML');
  189. parse_player_stat_line(player_sl);
  190. display_needs_update = true;
  191. }
  192.  
  193. // get the mobs stat line & compare it to previous stored stats
  194. var mobs_sl = $("div.game-grid > div.main-game-section > div.main-section__body > div > div > div > div:nth-child(3)");
  195. if(mobs_sl.prop('innerHTML') !== enemy_cache) {
  196. enemy_cache = mobs_sl.prop('innerHTML');
  197. parse_enemy_stat_line(mobs_sl);
  198. display_needs_update = true;
  199. }
  200.  
  201. // we already have display_needs_update, so let's use it as a trigger for our new health tracking
  202. if(display_needs_update === true ) {
  203.  
  204. // get player health line
  205. let players_hp_sl = $("div.game-grid > div.main-game-section > div.main-section__body > div > div > div > div.battle-container > div.battle-container__section > div:nth-child(2) > div.progress__text");
  206. player_stats.max_hp = players_hp_sl.prop('innerHTML').split(" / ")[1].replaceAll(",", "");
  207. // get enemy health line
  208. let enemy_hp_sl = $("div.game-grid > div.main-game-section > div.main-section__body > div > div > div > div.battle-container > div.battle-container__section:nth-child(3) > div:nth-child(2) > div.progress__text ");
  209. enemy_stats.max_hp = enemy_hp_sl.prop('innerHTML').split(" / ")[1].replaceAll(",", "");
  210.  
  211. if(track_min_health_perc === true) {
  212. let hp_totals = players_hp_sl.prop('innerHTML').split(" / ");
  213. let this_perc = (parseInt(hp_totals[0].replaceAll(",", "")) / parseInt(hp_totals[1].replaceAll(",", ""))) * 100;
  214. if(this_perc < player_stats.hp_perc) player_stats.hp_perc = this_perc;
  215. }
  216. }
  217.  
  218. // update displayed values
  219. if(display_needs_update === true) {
  220. update_display();
  221. }
  222.  
  223. }
  224.  
  225.  
  226. }
  227.  
  228. // parses the drop log and builds array of quantified contents
  229. function parse_drop_log() {
  230.  
  231. if( $( 'div#log-div > div' ).length == 0 ) return false; // either log isn't rendered yet, or it's just been cleared
  232.  
  233. const skiplist = ['[Gold]','Gold Rush:','Action Bonus:','Resource Rush:','Skill:','Mastery:','[Wood]','[Stone]','[Metal]'];
  234. let first_log_entry = dropalyse_clean_entry( $( 'div#log-div > div:first' ) ); // capture cached entry for possible later use
  235. let count = 0;
  236.  
  237. $( 'div#log-div > div' ).each(function( index ) {
  238.  
  239. // check if already analysed
  240. let str = dropalyse_clean_entry($(this));
  241. //console.log("str - " + str);
  242. //console.log("cache - " + dropalyse_cache );
  243. if(str === dropalyse_cache) return false; // break loop
  244.  
  245. // skip unwanted
  246. if (skiplist.some(v => str.includes(v))) return true; // continue loop
  247.  
  248. // parse into time, qty, item
  249. let entry = parse_drop_log_entry($( this ));
  250. count++;
  251.  
  252. // add to data store
  253. if (typeof dropalyse_store[entry.item] !== 'undefined') {
  254. dropalyse_store[entry.item] += entry.qty;
  255. } else {
  256. dropalyse_store[entry.item] = entry.qty;
  257. }
  258.  
  259. });
  260.  
  261. // do we have new entries?
  262. if(count>0) {
  263. dropalyse_cache = first_log_entry;
  264. }
  265.  
  266. // save data to localstorage
  267. dropalyse_save_ls();
  268.  
  269. }
  270.  
  271. function dropalyse_clean_entry(txt) {
  272. //console.log("Cleaning - " + $(txt).text() );
  273. var r = txt.clone();
  274. r.find('.popup').remove();
  275. //console.log("Clean - " + r.text() );
  276. let ret = r.text();
  277. // rarity
  278. //let rarity = $("div.item > p[class^='text-rarity-']", r).attr('class');
  279. //if(typeof rarity !== 'undefined') ret = ret + "#iqrpgstats#" + rarity;
  280. return ret;
  281. }
  282.  
  283. function parse_drop_log_entry(entry) {
  284.  
  285. let r = {};
  286.  
  287. // timestamp - not needed??
  288. r.timestamp = $('span:first', entry).text();
  289.  
  290. // item
  291. let data_str = $('span', entry).eq(1).text().replaceAll(",","");
  292. let matches = data_str.match(/^[+-]?\d+(\.\d+)?[%]?/g);
  293. if(matches && matches.length>0) {
  294. let n = matches[0];
  295. r.qty = Number(n.replace('+', '').replace('%', ''));
  296. r.item = data_str.replace(n,'').trim();
  297. } else {
  298. r.qty = 1; // it's something unusual
  299. r.item = data_str.trim();
  300. }
  301.  
  302. // strip extra data
  303. r.item = r.item.split("]")[0].replace("[","").replace("]","");
  304.  
  305. // append new rarity data
  306. let rarity = $("div.item > p[class^='text-rarity-']", entry).attr('class');
  307. if(typeof rarity !== 'undefined') r.item = r.item + "#iqrpgstats#" + rarity;
  308. //console.log(rarity + " - " + r.item);
  309.  
  310. return r;
  311. }
  312.  
  313. function dropalyse_load_ls() {
  314. if (localStorage.getItem('dropalyse_cache')) { dropalyse_cache = localStorage.getItem('dropalyse_cache'); }
  315. if (localStorage.getItem('dropalyse_actions')) { actions = localStorage.getItem('dropalyse_actions'); }
  316. if (localStorage.getItem('dropalyse_store')) { dropalyse_store = JSON.parse(localStorage.getItem('dropalyse_store')); }
  317. if (localStorage.getItem('dropalyse_filters')) { dropalyse_filters = JSON.parse(localStorage.getItem('dropalyse_filters')); }
  318. // if we have a start datum, convert to new actions method, and delete start datum from LS
  319. if (localStorage.getItem('dropalyse_start_datum')) {
  320. let dropalyse_start_datum = localStorage.getItem('dropalyse_start_datum');
  321. actions = Math.floor((( Date.now() - dropalyse_start_datum ) / 1000) /6 ); // approximate actions from old timing data
  322. localStorage.removeItem('dropalyse_start_datum'); // remove old data
  323. dropalyse_save_ls(); // save new data
  324. }
  325. }
  326.  
  327. function dropalyse_save_ls() {
  328. localStorage.setItem('dropalyse_cache', dropalyse_cache);
  329. localStorage.setItem('dropalyse_store', JSON.stringify(dropalyse_store));
  330. localStorage.setItem('dropalyse_actions', actions);
  331. localStorage.setItem('dropalyse_filters', JSON.stringify(dropalyse_filters));
  332. localStorage.setItem('dropalyse_version', GM_info.script.version);
  333. // DELETE
  334. //localStorage.setItem('dropalyse_start_datum', dropalyse_start_datum);
  335. //localStorage.setItem('dropalyse_save_datum', Date.now());
  336. }
  337.  
  338. function dropalyse_reset() {
  339. dropalyse_store = {};
  340. actions = 0;
  341. //dropalyse_start_datum = Date.now(); // DELETE
  342. dropalyse_save_ls(); // update persistant storage
  343. dropalyse_render_content();
  344. }
  345.  
  346. function dropalyse_render_content() {
  347. let html = '';
  348. if(Object.entries(dropalyse_store).length == 0 ) {
  349. html = '<div>Waiting for drops...</div>';
  350. } else {
  351.  
  352. // get a sorted copy of the store
  353. let copy_of_dropalyse_store = Object.assign(dropalyse_objectOrder(), dropalyse_store);
  354.  
  355. for (let [key, qty] of Object.entries(copy_of_dropalyse_store)) {
  356. // skip null
  357. if(Number(qty) == 0 ) continue;
  358. // format qty
  359. let formatted_qty = qty;
  360. if(dropalyse_format==='hour') {
  361. //formatted_qty = ( ( qty / dropalyse_get_time_elapsed() ) * 60 * 60 ).toFixed(dropalyse_decimal_precision);
  362. formatted_qty = ( ( qty / actions ) * 10 * 60 ).toFixed(dropalyse_decimal_precision);
  363. } else if(dropalyse_format==='day') {
  364. //formatted_qty = ( ( qty / dropalyse_get_time_elapsed() ) * 60 * 60 * 24).toFixed(dropalyse_decimal_precision);
  365. formatted_qty = ( ( qty / actions ) * 10 * 60 * 24).toFixed(dropalyse_decimal_precision);
  366. } else {
  367. formatted_qty = Number(formatted_qty.toFixed(dropalyse_decimal_precision)).toString();
  368. }
  369. // format rarity
  370. let rarity_class = '';
  371. let parts = key.split("#iqrpgstats#");
  372. if(parts.length === 2) rarity_class = parts[1];
  373. // is it filtered?
  374. if(dropalyse_filters.includes(parts[0])) continue;
  375. // render
  376. html += '<div style="position: relative;" class="iqrpgbs_hover_highlight"><div style="display: flex; justify-content: space-between;">'
  377. + '<span class="'+ escapeHTML(rarity_class) + '">' + escapeHTML(parts[0]) + '</span>'
  378. + '<span style="color:#3c3">' + escapeHTML(formatted_qty) + '</span>'
  379. + '</div></div>';
  380. }
  381. }
  382. $('div#iqrpgbs_drop_log_content').html(html);
  383. document.querySelector('#iqrpgbs_dropalyse_actioncount').textContent = Number(actions).toLocaleString();
  384. document.querySelector('#iqrpgbs_dropalyse_actiondatum').textContent = dropalyse_render_nice_timer( actions * 6);
  385. }
  386.  
  387. function escapeHTML(unsafe) {
  388. return unsafe
  389. .replace(/&/g, "&amp;")
  390. .replace(/</g, "&lt;")
  391. .replace(/>/g, "&gt;")
  392. .replace(/"/g, "&quot;")
  393. .replace(/'/g, "&#039;");
  394. }
  395.  
  396. function dropalyse_insert_log() {
  397. let html = `<div id="iqrpgbs_dropalyse_container" class="main-section" style="background-color: #0a0a0a;margin-bottom: .2rem;border: 1px solid #333;">
  398. <div id="iqrpgbs_drop_log_header" style="cursor:pointer;">
  399. <p><span id="iqrpgbs_drop_log_header_title">Drops per hour</span><span class="grey-text" style="margin-left: 0.5rem; font-size: 0.9rem;display:none;">(Collapsed)</span></p><!---->
  400. </div>
  401. <div id="iqrpgbs_drop_log_body" class="main-section__body" style="border-top: 1px solid #333;padding: .5rem;">
  402. <div>
  403.  
  404. <div id="iqrpgbs_drop_log_content">Waiting for drops...</div>
  405.  
  406. <div class="iqrpgbs_dropalyse_spacing"><span id="iqrpgbs_dropalyse_actiondatum">0s</span> <a title="Based on 6 seconds per action" style="cursor:help">&#x1f6c8;</a></div>
  407. <div class="iqrpgbs_dropalyse_spacing"><span id="iqrpgbs_dropalyse_actioncount">0</span> Actions</div>
  408.  
  409.  
  410. <div id="iqrpgbs_dropalyse_options">[<a id="iqrpgbs_dropalyse_opt_hour" href="#">Hour</a>
  411. - <a id="iqrpgbs_dropalyse_opt_day" href="#">Day</a>
  412. - <a id="iqrpgbs_dropalyse_opt_total" href="#">Total</a>]
  413. [<a id="iqrpgbs_dropalyse_backup_toggle" href="#">Options</a>]
  414. [<a id="iqrpgbs_dropalyse_reset" href="#">Reset</a>]</div>
  415.  
  416. <div id="iqrpgbs_dropalyse_backup_panel" style="margin-top:12px;display:none;">
  417.  
  418. <p class="heading">Drop Filters</p>
  419. <p>You can filter out drops that you're not interested in tracking by entering them, one per line, in the field below. e.g. to filter out goblin keys, you would enter Goblin Cave Key on a new line, then click the update button</p>
  420. <textarea rows="5" placeholder="Enter one filter per line..." style="width:100%;margin-top:6px;height: auto;" id="iqrpgbs_dropalyse_filter_textarea"></textarea>
  421. <p class="text-center" style="margin:6px;"><button id="iqrpgbs_dropalyse_but_update_filters">Update Filters</button></p>
  422. <p>&nbsp;</p>
  423.  
  424. <p class="heading">Backup &amp; Restore Drops</p>
  425. <p>You can download a copy of your drop data by clicking the button below...</p>
  426. <p class="text-center" style="margin:6px;"><button id="iqrpgbs_dropalyse_but_export">Backup Data</button></p>
  427. <p>To restore your data, paste the contents of your backup file into the field below and click the button</p>
  428. <textarea placeholder="" style="width:100%;margin-top:6px;" id="iqrpgbs_dropalyse_import_textarea"></textarea>
  429. <p class="text-center" style="margin:6px;"><button id="iqrpgbs_dropalyse_but_import">Restore Data</button></p>
  430.  
  431. </div>
  432.  
  433. </div></div></div>`;
  434. $(html).insertAfter($('div.game-grid > div:first > div.main-section').last());
  435. dropalyse_render_filters();
  436.  
  437. // setup format options and events
  438. dropalyse_set_format(dropalyse_format); // set the initial format
  439. document.getElementById('iqrpgbs_dropalyse_opt_hour').addEventListener("click", function(e) { e.preventDefault(); dropalyse_set_format('hour'); });
  440. document.getElementById('iqrpgbs_dropalyse_opt_day').addEventListener("click", function(e) { e.preventDefault(); dropalyse_set_format('day'); });
  441. document.getElementById('iqrpgbs_dropalyse_opt_total').addEventListener("click", function(e) { e.preventDefault(); dropalyse_set_format('total'); });
  442. document.getElementById('iqrpgbs_dropalyse_reset').addEventListener("click", function(e) { e.preventDefault(); dropalyse_reset(); });
  443. document.getElementById('iqrpgbs_dropalyse_but_export').addEventListener("click", function(e) { e.preventDefault(); dropalyse_export_data(); });
  444. document.getElementById('iqrpgbs_dropalyse_but_import').addEventListener("click", function(e) { e.preventDefault(); dropalyse_import_data(); });
  445. $('a#iqrpgbs_dropalyse_backup_toggle').click(function(e){
  446. e.preventDefault();
  447. $("div#iqrpgbs_dropalyse_backup_panel").toggle();
  448. $('a#iqrpgbs_dropalyse_backup_toggle').toggleClass( "iqrpgbs_highlight" );
  449. });
  450. document.getElementById('iqrpgbs_drop_log_header').addEventListener("click", function(e) {
  451. e.preventDefault();
  452. $("div#iqrpgbs_drop_log_body").toggle();
  453. $("div#iqrpgbs_drop_log_header > p > span.grey-text").toggle();
  454. });
  455. document.getElementById('iqrpgbs_dropalyse_but_update_filters').addEventListener("click", function(e) { e.preventDefault(); dropalyse_update_filters(); });
  456. }
  457.  
  458. function dropalyse_set_format(format) {
  459. $('a#iqrpgbs_dropalyse_opt_hour').removeClass("iqrpgbs_highlight");
  460. $('a#iqrpgbs_dropalyse_opt_day').removeClass("iqrpgbs_highlight");
  461. $('a#iqrpgbs_dropalyse_opt_total').removeClass("iqrpgbs_highlight");
  462. if(format==='hour') {
  463. dropalyse_format = 'hour';
  464. $('a#iqrpgbs_dropalyse_opt_hour').addClass("iqrpgbs_highlight");
  465. $('div#iqrpgbs_drop_log_header > p > span#iqrpgbs_drop_log_header_title').html('Drops Per Hour');
  466. } else if(format==='day') {
  467. dropalyse_format = 'day';
  468. $('a#iqrpgbs_dropalyse_opt_day').addClass("iqrpgbs_highlight");
  469. $('div#iqrpgbs_drop_log_header > p > span#iqrpgbs_drop_log_header_title').html('Drops Per Day');
  470. } else if(format==='total') {
  471. dropalyse_format = 'total';
  472. $('a#iqrpgbs_dropalyse_opt_total').addClass("iqrpgbs_highlight");
  473. $('div#iqrpgbs_drop_log_header > p > span#iqrpgbs_drop_log_header_title').html('Drops - Total');
  474. }
  475. dropalyse_render_content(); // update view
  476. }
  477.  
  478. // DELETE ME
  479. /* function dropalyse_get_time_elapsed() {
  480. return ( Date.now() - dropalyse_start_datum )/1000;
  481. } */
  482.  
  483. function dropalyse_render_nice_timer(delta) {
  484. //var delta = dropalyse_get_time_elapsed();
  485. var days = Math.floor(delta / 86400);
  486. delta -= days * 86400;
  487. var hours = Math.floor(delta / 3600) % 24;
  488. delta -= hours * 3600;
  489. var minutes = Math.floor(delta / 60) % 60;
  490. delta -= minutes * 60;
  491. var seconds = delta % 60;
  492. let html = '';
  493. if(days>0) html += days + 'd ';
  494. if(hours>0||days>0) html += hours + 'h ';
  495. if(hours>0||days>0||minutes>0) html += minutes + 'm ';
  496. html += Math.floor(seconds) + 's';
  497. return html;
  498. }
  499.  
  500. function dropalyse_render_filters() {
  501. //document.getElementById("iqrpgbs_dropalyse_filter_textarea").value = escapeHTML(dropalyse_filters.join("\r\n"));
  502. // don't need to escape it as long as we set it using .value = 'stuffs'
  503. document.getElementById("iqrpgbs_dropalyse_filter_textarea").value = dropalyse_filters.join("\r\n");
  504. }
  505.  
  506. function dropalyse_update_filters() {
  507. let our_filters = document.getElementById("iqrpgbs_dropalyse_filter_textarea").value;
  508. dropalyse_filters = our_filters.trim().split(/\r?\n/).map(s => s.trim());
  509. dropalyse_save_ls(); // save to ls
  510. dropalyse_render_content(); // render drops again
  511. // hide panel - don't hide it?
  512. $("div#iqrpgbs_dropalyse_backup_panel").hide();
  513. $('a#iqrpgbs_dropalyse_backup_toggle').removeClass( "iqrpgbs_highlight" );
  514. }
  515.  
  516. function dropalyse_import_data() {
  517.  
  518. // get and test for empty data
  519. let our_data = document.getElementById("iqrpgbs_dropalyse_import_textarea").value;
  520. if(our_data=='') return false; // blank data
  521. // catch errors on parsing
  522. try {
  523. let backup = JSON.parse(our_data);
  524. // let's set the new vals
  525. dropalyse_cache = ''; // reset cache to '' so we can pull fresh data in, in our main loop
  526. dropalyse_store = backup.dropalyse_store;
  527. if(backup.dropalyse_filters) dropalyse_filters = backup.dropalyse_filters;
  528. if(backup.dropalyse_actions) { // if actions, use them
  529. actions = backup.dropalyse_actions;
  530. } else if(backup.dropalyse_start_datum) { // approximate actions from old timing data
  531. let start_datum = ( Date.now() - Number(backup.dropalyse_backup_datum)) + Number(backup.dropalyse_start_datum);
  532. actions = Math.floor((( Date.now() - start_datum ) / 1000) /6 );
  533. } else {
  534. actions = 0;
  535. console.log('Drop Tracker could not determine action count from backup, reseting it to 0 as a fallback!');
  536. }
  537. //dropalyse_start_datum = ( Date.now() - Number(backup.dropalyse_backup_datum)) + Number(backup.dropalyse_start_datum);
  538. // all done, let's re-render the tracker
  539. dropalyse_render_content();
  540. $("div#iqrpgbs_dropalyse_backup_panel").hide();
  541. $('a#iqrpgbs_dropalyse_backup_toggle').removeClass( "iqrpgbs_highlight" );
  542. document.getElementById("iqrpgbs_dropalyse_import_textarea").value = '';
  543. } catch(e) {
  544. alert("Backup data does not appear to be valid JSON!"); // error in the above string (in this case, yes)!
  545. return false;
  546. }
  547.  
  548. }
  549.  
  550. function dropalyse_export_data() {
  551.  
  552. const backup = {
  553. dropalyse_cache: dropalyse_cache,
  554. dropalyse_store: dropalyse_store,
  555. dropalyse_actions: actions,
  556. dropalyse_filters: dropalyse_filters,
  557. dropalyse_version: GM_info.script.version,
  558. dropalyse_backup_datum: Date.now()
  559. };
  560.  
  561. // Convert object to Blob
  562. const blobConfig = new Blob(
  563. [ JSON.stringify(backup) ],
  564. { type: 'text/json;charset=utf-8' }
  565. )
  566.  
  567. // Convert Blob to URL
  568. const blobUrl = URL.createObjectURL(blobConfig);
  569.  
  570. // Create an a element with blobl URL
  571. const anchor = document.createElement('a');
  572. anchor.href = blobUrl;
  573. anchor.target = "_blank";
  574. anchor.download = "IQRPG-Stats-" + Date.now() + ".json";
  575.  
  576. // Auto click on a element, trigger the file download
  577. anchor.click();
  578.  
  579. // Don't forget ;)
  580. URL.revokeObjectURL(blobUrl);
  581. }
  582.  
  583. // Dropalyse item order
  584. function dropalyse_objectOrder() {
  585. return {
  586. // attributes
  587. 'Health': 0, 'Attack': 0, 'Defence':0, 'Accuracy':0, 'Dodge':0,
  588. // credits
  589. 'Credits':0, 'Bound Credits':0, 'Dungeoneering Tokens#iqrpgstats#text-rarity-1':0,
  590. // components + gather shards
  591. 'Weapon Component#iqrpgstats#text-rarity-2': 0, 'Armor Component#iqrpgstats#text-rarity-2': 0, 'Tool Component#iqrpgstats#text-rarity-2':0, 'Gathering Skill Shard#iqrpgstats#text-rarity-2':0,
  592. 'Resource Cache#iqrpgstats#text-rarity-2': 0,
  593. 'Gem Fragments#iqrpgstats#text-rarity-2': 0,
  594. 'Trinket Fragments#iqrpgstats#text-rarity-3': 0,
  595. 'Runic Leather#iqrpgstats#text-rarity-4':0,
  596. // keys
  597. 'Goblin Cave Key#iqrpgstats#text-rarity-2': 0,
  598. 'Mountain Pass Key#iqrpgstats#text-rarity-2': 0,
  599. 'Desolate Tombs Key#iqrpgstats#text-rarity-2':0,
  600. 'Dragonkin Lair Key#iqrpgstats#text-rarity-2':0,
  601. 'Abandoned Tower Key#iqrpgstats#text-rarity-3':0,
  602. 'Haunted Cells Key#iqrpgstats#text-rarity-3':0,
  603. 'Hall Of Dragons Key#iqrpgstats#text-rarity-3':0,
  604. 'The Vault Key#iqrpgstats#text-rarity-4':0,
  605. 'The Treasury Key#iqrpgstats#text-rarity-4':0,
  606. // upgrade stones
  607. 'Health Upgrade Stone#iqrpgstats#text-rarity-3': 0,
  608. 'Damage Upgrade Stone#iqrpgstats#text-rarity-4': 0,
  609. // wood drops - in alchemy?
  610. // stone drops
  611. 'Sandstone#iqrpgstats#text-rarity-2': 0,
  612. 'Marble#iqrpgstats#text-rarity-3': 0,
  613. 'Malachite#iqrpgstats#text-rarity-4':0,
  614. // metal drops
  615. 'Sapphire#iqrpgstats#text-rarity-2':0,
  616. 'Ruby#iqrpgstats#text-rarity-3':0,
  617. 'Emerald#iqrpgstats#text-rarity-3':0,
  618. 'Diamond#iqrpgstats#text-rarity-4':0,
  619. // alchemy ingrediants
  620. 'Tree Sap#iqrpgstats#text-rarity-2': 0,
  621. 'Spider Egg#iqrpgstats#text-rarity-2': 0,
  622. 'Bone Meal#iqrpgstats#text-rarity-2': 0,
  623. 'Vial Of Orc Blood#iqrpgstats#text-rarity-3': 0,
  624. 'Undead Heart#iqrpgstats#text-rarity-3': 0,
  625. "Bird's Nest#iqrpgstats#text-rarity-3": 0,
  626. 'Alchemic Essence#iqrpgstats#text-rarity-3': 0,
  627. 'Golden Egg#iqrpgstats#text-rarity-4': 0,
  628. 'Demonic Dust#iqrpgstats#text-rarity-4': 0,
  629. // potions
  630. 'Training Gathering Potion#iqrpgstats#text-rarity-2':0,
  631. 'Training Exp Potion#iqrpgstats#text-rarity-2':0,
  632. 'Minor Gathering Potion#iqrpgstats#text-rarity-2':0,
  633. 'Minor Exp Potion#iqrpgstats#text-rarity-2':0,
  634. 'Haste Potion#iqrpgstats#text-rarity-3':0,
  635. 'Minor Autos Potion#iqrpgstats#text-rarity-3':0,
  636. 'Aptitude Potion#iqrpgstats#text-rarity-3':0,
  637. 'Greater Exp Potion#iqrpgstats#text-rarity-3':0,
  638. 'Heroic Potion#iqrpgstats#text-rarity-4':0,
  639. 'Ultra Exp Potion#iqrpgstats#text-rarity-4':0,
  640. // runes
  641. 'Training Rune 1#iqrpgstats#text-rarity-2':0,
  642. 'Training Rune 2#iqrpgstats#text-rarity-2':0,
  643. 'Training Rune 3#iqrpgstats#text-rarity-2':0,
  644. 'Training Rune 4#iqrpgstats#text-rarity-2':0,
  645. 'Rune Of The Warrior#iqrpgstats#text-rarity-3':0,
  646. 'Rune Of The Gladiator#iqrpgstats#text-rarity-3':0,
  647. 'Rune Of The Warlord#iqrpgstats#text-rarity-4':0,
  648. 'Rune Of The Overlord#iqrpgstats#text-rarity-4':0,
  649. 'Greater Rune Of The Warlord#iqrpgstats#text-rarity-5':0,
  650. // jewels
  651. 'Sapphire Jewel#iqrpgstats#text-rarity-1':0,
  652. 'Sapphire Jewel#iqrpgstats#text-rarity-2':0,
  653. 'Sapphire Jewel#iqrpgstats#text-rarity-3':0,
  654. 'Sapphire Jewel#iqrpgstats#text-rarity-4':0,
  655. 'Sapphire Jewel#iqrpgstats#text-rarity-5':0,
  656. 'Sapphire Jewel#iqrpgstats#text-rarity-6':0,
  657. 'Ruby Jewel#iqrpgstats#text-rarity-1':0,
  658. 'Ruby Jewel#iqrpgstats#text-rarity-2':0,
  659. 'Ruby Jewel#iqrpgstats#text-rarity-3':0,
  660. 'Ruby Jewel#iqrpgstats#text-rarity-4':0,
  661. 'Ruby Jewel#iqrpgstats#text-rarity-5':0,
  662. 'Ruby Jewel#iqrpgstats#text-rarity-6':0,
  663. 'Emerald Jewel#iqrpgstats#text-rarity-1':0,
  664. 'Emerald Jewel#iqrpgstats#text-rarity-2':0,
  665. 'Emerald Jewel#iqrpgstats#text-rarity-3':0,
  666. 'Emerald Jewel#iqrpgstats#text-rarity-4':0,
  667. 'Emerald Jewel#iqrpgstats#text-rarity-5':0,
  668. 'Diamond Jewel#iqrpgstats#text-rarity-1':0,
  669. 'Diamond Jewel#iqrpgstats#text-rarity-2':0,
  670. 'Diamond Jewel#iqrpgstats#text-rarity-3':0,
  671. 'Diamond Jewel#iqrpgstats#text-rarity-4':0,
  672. 'Diamond Jewel#iqrpgstats#text-rarity-5':0,
  673. // res cache??
  674. }
  675. }
  676.  
  677. ///////////////////////////////////
  678. // Battle Stats
  679. ///////////////////////////////////
  680.  
  681. function parse_player_stat_line(player_sl) {
  682. var hits = player_sl.find("p:nth-child(1) > span:nth-child(2)");
  683. if(hits.length > 0) {
  684. var actual_hits = hits.prop('innerHTML').replaceAll(" time(s)", "");
  685. player_stats.hits += parseInt(actual_hits);
  686. }
  687. var dmg = player_sl.find("p:nth-child(1) > span:nth-child(3)");
  688. if(dmg.length > 0 && hits.length > 0) {
  689. var actual_dmg = dmg.prop('innerHTML').replaceAll(" damage", "").replaceAll(",", "");
  690. player_stats.dmg += parseInt(actual_dmg) * parseInt(actual_hits);
  691. }
  692. var misses = player_sl.find("p:nth-child(2) > span:nth-child(1)");
  693. if(misses.length > 0) {
  694. var actual_misses = misses.prop('innerHTML').replaceAll(" time(s)", "");
  695. player_stats.misses += parseInt(actual_misses);
  696. }
  697. }
  698.  
  699. function parse_enemy_stat_line(stat_line) {
  700. var hits = stat_line.find("p:nth-child(1) > span:nth-child(2)");
  701. if(hits.length > 0) {
  702. var actual_hits = hits.prop('innerHTML').replaceAll(" time(s)", "");
  703. enemy_stats.hits += parseInt(actual_hits);
  704. }
  705. var dmg = stat_line.find("p:nth-child(1) > span:nth-child(3)");
  706. if(dmg.length > 0 && hits.length > 0) {
  707. var actual_dmg = dmg.prop('innerHTML').replaceAll(" damage", "").replaceAll(",", "");
  708. enemy_stats.dmg += parseInt(actual_dmg) * parseInt(actual_hits);
  709. }
  710. var misses = stat_line.find("p:nth-child(2) > span:nth-child(2)");
  711. if(misses.length > 0) {
  712. var actual_misses = misses.prop('innerHTML').replaceAll(" time(s)", "");
  713. enemy_stats.misses += parseInt(actual_misses);
  714. }
  715. var dodges = stat_line.find("p:nth-child(2) > span:nth-child(3)");
  716. if(dodges.length > 0) {
  717. var actual_dodges = dodges.prop('innerHTML').replaceAll(" attack(s)", "");
  718. enemy_stats.dodges += parseInt(actual_dodges);
  719. }
  720. }
  721.  
  722. function iqrpg_bs_reset_stats(e) {
  723. e.preventDefault();
  724. player_stats.dmg = 0;
  725. player_stats.hits = 0;
  726. player_stats.misses = 0;
  727. player_stats.hp_perc = 100;
  728. enemy_stats.dmg = 0;
  729. enemy_stats.hits = 0;
  730. enemy_stats.misses = 0;
  731. enemy_stats.dodges = 0;
  732. update_display();
  733. }
  734.  
  735. function update_display() {
  736. // players
  737. $("#iqrpgbs_pl_dmg").html(new Intl.NumberFormat().format(player_stats.dmg));
  738. $("#iqrpgbs_pl_hits").html(new Intl.NumberFormat().format(player_stats.hits));
  739. var avg = Math.round(player_stats.dmg / player_stats.hits) || 0;
  740. $("#iqrpgbs_pl_avg").html(new Intl.NumberFormat().format(avg));
  741. var acc = (player_stats.hits / (player_stats.hits + player_stats.misses))*100 || 0;
  742. $("#iqrpgbs_pl_acc").html(new Intl.NumberFormat().format(acc.toFixed(2)));
  743. let min_hp = player_stats.hp_perc || 0;
  744. $("#iqrpgbs_min_hp").html(new Intl.NumberFormat().format(min_hp));
  745.  
  746. // enemy
  747. var enemy_avg = Math.round(enemy_stats.dmg / enemy_stats.hits) || 0;
  748. $("#iqrpgbs_enmy_avg").html(new Intl.NumberFormat().format(enemy_avg));
  749. var enemy_acc = ( (enemy_stats.hits + enemy_stats.dodges) / (enemy_stats.hits + enemy_stats.misses + enemy_stats.dodges))*100 || 0;
  750. $("#iqrpgbs_enmy_acc").html(new Intl.NumberFormat().format(enemy_acc.toFixed(2)));
  751. var enemy_dodges = (enemy_stats.dodges / (enemy_stats.hits /*+ enemy_stats.misses*/ + enemy_stats.dodges))*100 || 0;
  752. $("#iqrpgbs_enmy_dodges").html(new Intl.NumberFormat().format(enemy_dodges.toFixed(2)));
  753.  
  754. // extras
  755. }
  756.  
  757. function render_battle_stats_panel() {
  758. var content = `
  759. <div id="iqrpgbs_bs_panel" class="margin-top-large">
  760. <div>Battle Stats <span>by Coastis</span></div>
  761. <div>You dealt a total of <span id="iqrpgbs_pl_dmg">0</span> damage in <span id="iqrpgbs_pl_hits">0</span> hits,
  762. with an average of <span id="iqrpgbs_pl_avg">0</span> per hit and <span id="iqrpgbs_pl_acc">0</span>% accuracy</div>
  763. <div>Enemy dealt an average of <span id="iqrpgbs_enmy_avg">0</span> per hit
  764. with an accuracy of <span id="iqrpgbs_enmy_acc">0</span>%, and you dodged <span id="iqrpgbs_enmy_dodges">0</span>% of attacks</div>
  765. `;
  766. if(track_min_health_perc === true) content += '<div>Your health reached a low of <span id="iqrpgbs_min_hp">100</span>%</div>';
  767.  
  768. if(track_extra_battle_stats === true) {
  769. content += '<div><span id="iqrpgbs_attk_delta"></span></div>'
  770. + '<div><span id="iqrpgbs_def_delta"></span></div>';
  771. }
  772.  
  773. content += '<div>[<a href="#" id="igrpg_bs_reset">Reset Battle Stats</a>]</div>';
  774. content += '</div>';
  775. return content;
  776. }
  777.  
  778. GM_addStyle ( `
  779. div#iqrpgbs_bs_panel div { text-align:center;padding:1px;}
  780. div#iqrpgbs_bs_panel div:nth-child(1) { font-weight:bold;}
  781. div#iqrpgbs_bs_panel div:nth-child(1) span { font-size:8px;font-weight:normal;}
  782. div#iqrpgbs_drop_log_header {display: flex; justify-content: center; align-items: center; padding: .5rem; background: linear-gradient(#000,#151515);}
  783. div#iqrpgbs_dropalyse_timer { padding:2px; margin-top:4px; text-align:center; }
  784. .iqrpgbs_dropalyse_spacing { padding:2px; margin-top:4px; text-align:center; }
  785. div#iqrpgbs_dropalyse_options { padding:2px; text-align:center; }
  786. .iqrpgbs_highlight { color:#3c3 !important; }
  787. div.iqrpgbs_hover_highlight:hover { background-color:#222222; }
  788. ` );
  789.