IQRPG Stats

Includes a drop tracker for all your IQ drops, as well as tracking hits to kill/die breakpoints and showing how much you need to reach the next breakpoint! Plus various other battle stats.

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