IQRPG Stats

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

当前为 2022-09-06 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name IQRPG Stats
  3. // @namespace https://www.iqrpg.com/
  4. // @version 0.48
  5. // @description Tracks all possible in game drops per hour/day, as well as various battle stats over multiple battles.
  6. // @author Coastis
  7. // @match http://iqrpg.com/game.html
  8. // @match https://iqrpg.com/game.html
  9. // @match http://www.iqrpg.com/game.html
  10. // @match https://www.iqrpg.com/game.html
  11. // @match http://test.iqrpg.com/game.html
  12. // @match https://test.iqrpg.com/game.html
  13. // @require http://code.jquery.com/jquery-latest.js
  14. // @grant GM_addStyle
  15. // ==/UserScript==
  16.  
  17. //TODO add persistant storage between browser sessions
  18.  
  19. ///////////////////////////////////
  20. /////////// config ////////////////
  21. ///////////////////////////////////
  22.  
  23. const track_boss_battles = false; // true or false
  24. const track_clan_battles = false; // true or false
  25. const track_abyss_battles = false; // true or false
  26. const track_min_health_perc = true; // true or false
  27. const track_stats_n_drops = true;
  28.  
  29. //////////////////////////////////////////////////////////////////
  30. /////////// Don't change anything below this line ////////////////
  31. //////////////////////////////////////////////////////////////////
  32.  
  33. /* globals jQuery, $ */
  34.  
  35. // init
  36. var player_stats = { dmg:0, hits:0, misses:0, hp_perc:100 }; //TODO update from local storage
  37. var enemy_stats = { dmg:0, hits:0, misses:0, dodges:0 }; //TODO update from local storage
  38. var player_cache = '';
  39. var enemy_cache = '';
  40. var action_timer_cache = '';
  41. var dropalyse_cache = '';
  42. var dropalyse_store = {};
  43. var dropalyse_rendered = false;
  44. var dropalyse_start_datum = Date.now();
  45. var dropalyse_format = 'hour'; // hour/day/total
  46.  
  47. // run every xxx ms updating stuffs
  48. // NOTE we set a delay of 500ms before starting to help slow rendering cpu's, otherwise there is the possibility of the very 1st drop not being counted
  49. setTimeout(function() {
  50. setInterval(iqrpgbs_loop, 100);
  51. setInterval(iqrpgbs_timer_loop, 1000);
  52. }, 500);
  53.  
  54. // timer loop in it's own interval to optimise cpu usage.
  55. function iqrpgbs_timer_loop() {
  56. $('div.game-grid > div > div#iqrpgbs_dropalyse_container > div.main-section__body > div > div#iqrpgbs_dropalyse_timer').html( dropalyse_render_nice_timer() );
  57. }
  58.  
  59. // main loop
  60. function iqrpgbs_loop() {
  61.  
  62. // first run?
  63. if(dropalyse_cache === '') {
  64. if( $( 'div#log-div > div' ).length > 0 ) {
  65. dropalyse_cache = dropalyse_clean_entry( $( 'div#log-div > div:first' ) );
  66. } else {
  67. dropalyse_cache = '123 Temporary';
  68. }
  69. }
  70. // insert p/h panel
  71. if(dropalyse_rendered === false) {
  72. dropalyse_insert_log();
  73. dropalyse_rendered = true;
  74. }
  75. // NEW drop log parsing
  76. if( $("div.fixed-top > div.section-2 > div.action-timer > div.action-timer__text").length > 0 ) {
  77. let action_data = $("div.fixed-top > div.section-2 > div.action-timer > div.action-timer__text").prop('innerHTML').trim();
  78. if(track_stats_n_drops === true && action_data !== action_timer_cache) {
  79. action_timer_cache = action_data;
  80. parse_drop_log(); // parse it
  81. dropalyse_render_content(); // update p/h
  82. }
  83. } else {
  84. return false; // skip as autos hasnt rendered yet
  85. }
  86.  
  87.  
  88. // battle stats
  89. var we_battling = false;
  90. var display_needs_update = false;
  91.  
  92. // check we're on the battle page
  93. if(document.getElementsByClassName("battle-container").length > 0) {
  94. we_battling = true;
  95. } else return false;
  96.  
  97. // and not in a boss battle - Boss Tokens
  98. if(track_boss_battles!==true && $("div.game-grid > div.main-game-section > div.main-section__body:contains('Boss Tokens')").length > 0) {
  99. return false;
  100. }
  101.  
  102. // and not in a dungeon - Dungeoneering Exp
  103. if($("div.game-grid > div.main-game-section > div.main-section__body:contains('Dungeoneering Exp')").length > 0) {
  104. return false;
  105. }
  106.  
  107. // and not in a standard clan battle - Clan Exp
  108. if(track_clan_battles!==true && $("div.game-grid > div.main-game-section > div.main-section__body:contains('Clan Exp')").length > 0) {
  109. return false;
  110. }
  111.  
  112. // all is well, so let's get the mob name
  113. var n_obj = $("div.battle-container > div:nth-child(3) > div.participant-name > span");
  114. if(n_obj.length > 0) {
  115. var mob_name = n_obj.prop('innerHTML').trim();
  116. } else {
  117. return false; // couldn't find the mob name, lets skip just in case
  118. }
  119.  
  120. // and not in a clan dragon battle
  121. // exact matches
  122. const clan_dragons = ['Baby Dragon','Young Dragon','Adolescent Dragon','Adult Dragon','Dragon'];
  123. if(track_clan_battles!==true && clan_dragons.indexOf(mob_name) > -1 ) {
  124. //console.log('Skipping Clan Dragons');
  125. return false;
  126. }
  127.  
  128. // and not in an abyss battle or clan battle - Abyssal
  129. if(track_abyss_battles!==true && ['Abyssal'].some(term => mob_name.includes(term))) {
  130. return false;
  131. }
  132.  
  133. // all good
  134. if(we_battling === true) {
  135.  
  136. // Add the battle stat panel to dom, if not already there
  137. if(!document.getElementById("iqrpgbs_bs_panel")) {
  138. var iqrpg_body = $( "div.game-grid > div.main-game-section > div.main-section__body" );
  139. iqrpg_body[0].children[0].children[0].insertAdjacentHTML('beforeend', render_battle_stats_panel() );
  140. document.getElementById('igrpg_bs_reset').addEventListener('click', iqrpg_bs_reset_stats, false);
  141. }
  142.  
  143. // get the players stat line & compare it to previous stored stats
  144. var player_sl = $("div.game-grid > div.main-game-section > div.main-section__body > div > div > div > div:nth-child(2)");
  145. if(player_sl.prop('innerHTML') !== player_cache) {
  146. player_cache = player_sl.prop('innerHTML');
  147. parse_player_stat_line(player_sl);
  148. display_needs_update = true;
  149. }
  150.  
  151. // get the mobs stat line & compare it to previous stored stats
  152. var mobs_sl = $("div.game-grid > div.main-game-section > div.main-section__body > div > div > div > div:nth-child(3)");
  153. if(mobs_sl.prop('innerHTML') !== enemy_cache) {
  154. enemy_cache = mobs_sl.prop('innerHTML');
  155. parse_enemy_stat_line(mobs_sl);
  156. display_needs_update = true;
  157. }
  158.  
  159. // we already have display_needs_update, so let's use it as a trigger for our new health tracking
  160. if(display_needs_update === true && track_min_health_perc === true) {
  161. let 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");
  162. let hp_totals = hp_sl.prop('innerHTML').split(" / ");
  163. let this_perc = (parseInt(hp_totals[0].replaceAll(",", "")) / parseInt(hp_totals[1].replaceAll(",", ""))) * 100;
  164. if(this_perc < player_stats.hp_perc) player_stats.hp_perc = this_perc;
  165. }
  166.  
  167. // update displayed values
  168. if(display_needs_update === true) {
  169. update_display();
  170. }
  171.  
  172. }
  173.  
  174.  
  175. }
  176.  
  177. // parses the drop log and buils array of quantified contents
  178. function parse_drop_log() {
  179.  
  180. const skiplist = ['[Gold]','Gold Rush:','Action Bonus:','Resource Rush:','Skill:','Mastery:','[Wood]','[Stone]','[Metal]'];
  181. let first_log_entry = '';
  182. first_log_entry = dropalyse_clean_entry( $( 'div#log-div > div:first' ) ); // capture cached entry for possible later use
  183. let count = 0;
  184.  
  185. $( 'div#log-div > div' ).each(function( index ) {
  186.  
  187. // check if already analysed
  188. let str = dropalyse_clean_entry($(this));
  189. //console.log("str - " + str);
  190. //console.log("cache - " + dropalyse_cache );
  191. if(str === dropalyse_cache) return false; // break loop
  192.  
  193. // skip unwanted
  194. if (skiplist.some(v => str.includes(v))) return true; // continue loop
  195.  
  196. // parse into time, qty, item
  197. let entry = parse_drop_log_entry($( this ));
  198. count++;
  199.  
  200. // add to data store
  201. if (typeof dropalyse_store[entry.item] !== 'undefined') {
  202. dropalyse_store[entry.item] += entry.qty;
  203. } else {
  204. dropalyse_store[entry.item] = entry.qty;
  205. }
  206.  
  207. });
  208.  
  209. // do we have new entries?
  210. if(count>0) {
  211. dropalyse_cache = first_log_entry;
  212. console.log("------------------------------");
  213. console.log(dropalyse_store);
  214. }
  215.  
  216. }
  217.  
  218. function dropalyse_clean_entry(txt) {
  219. //console.log("Cleaning - " + $(txt).text() );
  220. var r = txt.clone();
  221. r.find('.popup').remove();
  222. //console.log("Clean - " + r.text() );
  223. return r.text();
  224. }
  225.  
  226. function parse_drop_log_entry(entry) {
  227.  
  228. let r = {};
  229.  
  230. // timestamp - not needed??
  231. r.timestamp = $('span:first', entry).text();
  232.  
  233. // item
  234. let data_str = $('span', entry).eq(1).text();
  235. let matches = data_str.match(/^[+-]?\d+(\.\d+)?[%]?/g);
  236. if(matches && matches.length>0) {
  237. let n = matches[0];
  238. r.qty = Number(n.replace('+', '').replace('%', ''));
  239. r.item = data_str.replace(n,'').trim();
  240. } else {
  241. r.qty = 1; // it's something unusual
  242. r.item = data_str.trim();
  243. }
  244.  
  245. // strip extra data
  246. r.item = r.item.split("]")[0].replace("[","").replace("]","");
  247.  
  248. return r;
  249. }
  250.  
  251. function dropalyse_render_content() {
  252. let html = '';
  253. if(Object.entries(dropalyse_store).length == 0 ) {
  254. html = '<div>Waiting for drops...</div>';
  255. } else {
  256. for (let [key, qty] of Object.entries(dropalyse_store)) {
  257. let formatted_qty = qty;
  258. if(dropalyse_format==='hour') {
  259. formatted_qty = ( ( qty / dropalyse_get_time_elapsed() ) * 60 * 60 ).toFixed(2);
  260. } else if(dropalyse_format==='day') {
  261. formatted_qty = ( ( qty / dropalyse_get_time_elapsed() ) * 60 * 60 * 24).toFixed(2);
  262. }
  263. html += '<div style="position: relative;" class="iqrpgbs_hover_highlight"><div style="display: flex; justify-content: space-between;">'
  264. + '<span>' + key + '</span>'
  265. + '<span style="color:#3c3">' + formatted_qty + '</span>'
  266. + '</div></div>';
  267. }
  268. }
  269. $('div#iqrpgbs_drop_log_content').html(html);
  270. }
  271.  
  272. function dropalyse_insert_log() {
  273. let html = `<div id="iqrpgbs_dropalyse_container" class="main-section" style="background-color: #0a0a0a;margin-bottom: .2rem;border: 1px solid #333;">
  274. <div id="iqrpgbs_drop_log_header">
  275. <p>Drops per hour</p><!---->
  276. </div>
  277. <div class="main-section__body" style="border-top: 1px solid #333;padding: .5rem;">
  278. <div>
  279.  
  280. <div id="iqrpgbs_drop_log_content">Waiting for drops...</div>
  281.  
  282. <div id="iqrpgbs_dropalyse_timer">0:0:0:0</div>
  283. <div id="iqrpgbs_dropalyse_options">[<a id="iqrpgbs_dropalyse_opt_hour" href="#">Hour</a>
  284. - <a id="iqrpgbs_dropalyse_opt_day" href="#">Day</a>
  285. - <a id="iqrpgbs_dropalyse_opt_total" href="#">Total</a>]
  286. [<a id="iqrpgbs_dropalyse_reset" href="#">Reset</a>]</div>
  287.  
  288. </div></div></div>`;
  289. $(html).insertAfter($('div.game-grid > div:first > div.main-section').last());
  290.  
  291. // setup format options and events
  292. dropalyse_set_format(dropalyse_format); // set the initial format
  293. document.getElementById('iqrpgbs_dropalyse_opt_hour').addEventListener("click", function(e) { e.preventDefault(); dropalyse_set_format('hour'); });
  294. document.getElementById('iqrpgbs_dropalyse_opt_day').addEventListener("click", function(e) { e.preventDefault(); dropalyse_set_format('day'); });
  295. document.getElementById('iqrpgbs_dropalyse_opt_total').addEventListener("click", function(e) { e.preventDefault(); dropalyse_set_format('total'); });
  296. document.getElementById('iqrpgbs_dropalyse_reset').addEventListener("click", function(e) { e.preventDefault(); dropalyse_reset(); });
  297.  
  298. }
  299.  
  300. function dropalyse_reset() {
  301. dropalyse_store = {};
  302. dropalyse_start_datum = Date.now();
  303. dropalyse_render_content();
  304. }
  305.  
  306. function dropalyse_set_format(format) {
  307. $('a#iqrpgbs_dropalyse_opt_hour').removeClass("iqrpgbs_highlight");
  308. $('a#iqrpgbs_dropalyse_opt_day').removeClass("iqrpgbs_highlight");
  309. $('a#iqrpgbs_dropalyse_opt_total').removeClass("iqrpgbs_highlight");
  310. if(format==='hour') {
  311. dropalyse_format = 'hour';
  312. $('a#iqrpgbs_dropalyse_opt_hour').addClass("iqrpgbs_highlight");
  313. $('div#iqrpgbs_drop_log_header > p').html('Drops Per Hour');
  314. } else if(format==='day') {
  315. dropalyse_format = 'day';
  316. $('a#iqrpgbs_dropalyse_opt_day').addClass("iqrpgbs_highlight");
  317. $('div#iqrpgbs_drop_log_header > p').html('Drops Per Day');
  318. } else if(format==='total') {
  319. dropalyse_format = 'total';
  320. $('a#iqrpgbs_dropalyse_opt_total').addClass("iqrpgbs_highlight");
  321. $('div#iqrpgbs_drop_log_header > p').html('Drops - Total');
  322. }
  323. dropalyse_render_content(); // update view
  324. }
  325.  
  326. function dropalyse_get_time_elapsed() {
  327. return ( Date.now() - dropalyse_start_datum )/1000;
  328. }
  329.  
  330. function dropalyse_render_nice_timer() {
  331. var delta = dropalyse_get_time_elapsed();
  332. var days = Math.floor(delta / 86400);
  333. delta -= days * 86400;
  334. var hours = Math.floor(delta / 3600) % 24;
  335. delta -= hours * 3600;
  336. var minutes = Math.floor(delta / 60) % 60;
  337. delta -= minutes * 60;
  338. var seconds = delta % 60;
  339. let html = '';
  340. if(days>0) html += days + 'd ';
  341. if(hours>0||days>0) html += hours + 'h ';
  342. if(hours>0||days>0||minutes>0) html += minutes + 'm ';
  343. html += Math.floor(seconds) + 's';
  344. return html;
  345. }
  346.  
  347. function parse_player_stat_line(player_sl) {
  348. var hits = player_sl.find("p:nth-child(1) > span:nth-child(2)");
  349. if(hits.length > 0) {
  350. var actual_hits = hits.prop('innerHTML').replaceAll(" time(s)", "");
  351. player_stats.hits += parseInt(actual_hits);
  352. }
  353. var dmg = player_sl.find("p:nth-child(1) > span:nth-child(3)");
  354. if(dmg.length > 0 && hits.length > 0) {
  355. var actual_dmg = dmg.prop('innerHTML').replaceAll(" damage", "").replaceAll(",", "");
  356. player_stats.dmg += parseInt(actual_dmg) * parseInt(actual_hits);
  357. }
  358. var misses = player_sl.find("p:nth-child(2) > span:nth-child(1)");
  359. if(misses.length > 0) {
  360. var actual_misses = misses.prop('innerHTML').replaceAll(" time(s)", "");
  361. player_stats.misses += parseInt(actual_misses);
  362. }
  363. }
  364.  
  365. function parse_enemy_stat_line(stat_line) {
  366. var hits = stat_line.find("p:nth-child(1) > span:nth-child(2)");
  367. if(hits.length > 0) {
  368. var actual_hits = hits.prop('innerHTML').replaceAll(" time(s)", "");
  369. enemy_stats.hits += parseInt(actual_hits);
  370. }
  371. var dmg = stat_line.find("p:nth-child(1) > span:nth-child(3)");
  372. if(dmg.length > 0 && hits.length > 0) {
  373. var actual_dmg = dmg.prop('innerHTML').replaceAll(" damage", "").replaceAll(",", "");
  374. enemy_stats.dmg += parseInt(actual_dmg) * parseInt(actual_hits);
  375. }
  376. var misses = stat_line.find("p:nth-child(2) > span:nth-child(2)");
  377. if(misses.length > 0) {
  378. var actual_misses = misses.prop('innerHTML').replaceAll(" time(s)", "");
  379. enemy_stats.misses += parseInt(actual_misses);
  380. }
  381. var dodges = stat_line.find("p:nth-child(2) > span:nth-child(3)");
  382. if(dodges.length > 0) {
  383. var actual_dodges = dodges.prop('innerHTML').replaceAll(" attack(s)", "");
  384. enemy_stats.dodges += parseInt(actual_dodges);
  385. }
  386. }
  387.  
  388. function iqrpg_bs_reset_stats(e) {
  389. e.preventDefault();
  390. player_stats.dmg = 0;
  391. player_stats.hits = 0;
  392. player_stats.misses = 0;
  393. player_stats.hp_perc = 100;
  394. enemy_stats.dmg = 0;
  395. enemy_stats.hits = 0;
  396. enemy_stats.misses = 0;
  397. enemy_stats.dodges = 0;
  398. update_display();
  399. }
  400.  
  401. function update_display() {
  402. // players
  403. $("#iqrpgbs_pl_dmg").html(new Intl.NumberFormat().format(player_stats.dmg));
  404. $("#iqrpgbs_pl_hits").html(new Intl.NumberFormat().format(player_stats.hits));
  405. var avg = Math.round(player_stats.dmg / player_stats.hits) || 0;
  406. $("#iqrpgbs_pl_avg").html(new Intl.NumberFormat().format(avg));
  407. var acc = (player_stats.hits / (player_stats.hits + player_stats.misses))*100 || 0;
  408. $("#iqrpgbs_pl_acc").html(new Intl.NumberFormat().format(acc.toFixed(2)));
  409. let min_hp = player_stats.hp_perc || 0;
  410. $("#iqrpgbs_min_hp").html(new Intl.NumberFormat().format(min_hp));
  411. // enemy
  412. var enemy_avg = Math.round(enemy_stats.dmg / enemy_stats.hits) || 0;
  413. $("#iqrpgbs_enmy_avg").html(new Intl.NumberFormat().format(enemy_avg));
  414. var enemy_acc = ( (enemy_stats.hits + enemy_stats.dodges) / (enemy_stats.hits + enemy_stats.misses + enemy_stats.dodges))*100 || 0;
  415. $("#iqrpgbs_enmy_acc").html(new Intl.NumberFormat().format(enemy_acc.toFixed(2)));
  416. var enemy_dodges = (enemy_stats.dodges / (enemy_stats.hits /*+ enemy_stats.misses*/ + enemy_stats.dodges))*100 || 0;
  417. $("#iqrpgbs_enmy_dodges").html(new Intl.NumberFormat().format(enemy_dodges.toFixed(2)));
  418. }
  419.  
  420. function render_battle_stats_panel() {
  421. var content = `
  422. <div id="iqrpgbs_bs_panel" class="margin-top-large">
  423. <div>Battle Stats <span>by Coastis</span></div>
  424. <div>You dealt a total of <span id="iqrpgbs_pl_dmg">0</span> damage in <span id="iqrpgbs_pl_hits">0</span> hits,
  425. with an average of <span id="iqrpgbs_pl_avg">0</span> per hit and <span id="iqrpgbs_pl_acc">0</span>% accuracy</div>
  426. <div>Enemy dealt an average of <span id="iqrpgbs_enmy_avg">0</span> per hit
  427. with an accuracy of <span id="iqrpgbs_enmy_acc">0</span>%, and you dodged <span id="iqrpgbs_enmy_dodges">0</span>% of attacks</div>
  428. `;
  429. if(track_min_health_perc === true) content += '<div>Your health reached a low of <span id="iqrpgbs_min_hp">100</span>%</div>';
  430. content += '<div>[<a href="#" id="igrpg_bs_reset">Reset Battle Stats</a>]</div>';
  431. content += '</div>';
  432. return content;
  433. }
  434.  
  435. GM_addStyle ( `
  436. div#iqrpgbs_bs_panel div { text-align:center;padding:1px;}
  437. div#iqrpgbs_bs_panel div:nth-child(1) { font-weight:bold;}
  438. div#iqrpgbs_bs_panel div:nth-child(1) span { font-size:8px;font-weight:normal;}
  439. div#iqrpgbs_drop_log_header {display: flex; justify-content: center; align-items: center; padding: .5rem; background: linear-gradient(#000,#151515);}
  440. div#iqrpgbs_dropalyse_timer { padding:2px; margin-top:4px; text-align:center; }
  441. div#iqrpgbs_dropalyse_options { padding:2px; text-align:center; }
  442. .iqrpgbs_highlight { color:#3c3 !important; }
  443. div.iqrpgbs_hover_highlight:hover { background-color:#222222; }
  444. ` );
  445.