// ==UserScript==
// @name IQRPG Stats
// @namespace https://www.iqrpg.com/
// @version 0.51
// @description Tracks all possible in game drops per hour/day, as well as various battle stats over multiple battles.
// @author Coastis
// @match http://iqrpg.com/game.html
// @match https://iqrpg.com/game.html
// @match http://www.iqrpg.com/game.html
// @match https://www.iqrpg.com/game.html
// @match http://test.iqrpg.com/game.html
// @match https://test.iqrpg.com/game.html
// @require http://code.jquery.com/jquery-latest.js
// @grant GM_addStyle
// ==/UserScript==
///////////////////////////////////
/////////// config ////////////////
///////////////////////////////////
const track_battle_stats = true; // true or false - enable or disable battle stats
const track_stats_n_drops = true; // true or false - enable or disable drop tracker
const dropalyse_decimal_precision = 3; // the number of decimal places to show for drops per hour/day averages
const track_boss_battles = false; // true or false
const track_clan_battles = false; // true or false
const track_abyss_battles = false; // true or false
const track_min_health_perc = true; // true or false
//////////////////////////////////////////////////////////////////
/////////// Don't change anything below this line ////////////////
//////////////////////////////////////////////////////////////////
/* globals jQuery, $ */
// init
var player_stats = { dmg:0, hits:0, misses:0, hp_perc:100 }; //TODO update from local storage
var enemy_stats = { dmg:0, hits:0, misses:0, dodges:0 }; //TODO update from local storage
var player_cache = '';
var enemy_cache = '';
var action_timer_cache = '';
var dropalyse_rendered = false;
var dropalyse_format = 'hour'; // hour/day/total
// setup persistant vars
var dropalyse_cache = '';
var dropalyse_store = {};
var dropalyse_start_datum = Date.now();
dropalyse_load_ls();
// run every xxx ms updating stuffs
// 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
setTimeout(function() {
setInterval(iqrpgbs_loop, 100);
setInterval(iqrpgbs_timer_loop, 1000);
}, 500);
// timer loop in it's own interval to optimise cpu usage.
function iqrpgbs_timer_loop() {
$('div.game-grid > div > div#iqrpgbs_dropalyse_container > div.main-section__body > div > div#iqrpgbs_dropalyse_timer').html( dropalyse_render_nice_timer() );
}
// main loop
function iqrpgbs_loop() {
//FIXED in v0.51
/*
- Added Backup/Restore options for your drop data
- Fixed Accuracy/Dodge formating
- Drop tracker can now be disabled in the config
- Battle Stats can also be disabled in the config
- Decimal precision can be set in the config
*/
//TODO add persistance for battle stats
//TODO add "number of rushes" for gold/resources - would need to remove the popup associated with [Gold][Wood] etc
//TODO seperate out battle stats so boss/dungeon etc stats track seperately
// dropalyse
if(track_stats_n_drops === true) {
// first run?
if(dropalyse_cache === '' && $( 'div#log-div > div' ).length > 0 ) {
dropalyse_cache = dropalyse_clean_entry( $( 'div#log-div > div:first' ) );
}
// insert p/h panel
if(dropalyse_rendered === false) {
dropalyse_insert_log();
dropalyse_rendered = true;
}
// NEW drop log parsing
if( $("div.fixed-top > div.section-2 > div.action-timer > div.action-timer__text").length > 0 ) {
let action_data = $("div.fixed-top > div.section-2 > div.action-timer > div.action-timer__text").prop('innerHTML').trim();
if(action_data !== action_timer_cache) {
action_timer_cache = action_data;
parse_drop_log(); // parse it
dropalyse_render_content(); // update p/h
}
} else {
return false; // skip as autos hasnt rendered yet
}
}
// battle stats
if(track_battle_stats === false) return true; // enable or disable battle stats
var we_battling = false;
var display_needs_update = false;
// check we're on the battle page
if(document.getElementsByClassName("battle-container").length > 0) {
we_battling = true;
} else return false;
// and not in a boss battle - Boss Tokens
if(track_boss_battles!==true && $("div.game-grid > div.main-game-section > div.main-section__body:contains('Boss Tokens')").length > 0) {
return false;
}
// and not in a dungeon - Dungeoneering Exp
if($("div.game-grid > div.main-game-section > div.main-section__body:contains('Dungeoneering Exp')").length > 0) {
return false;
}
// and not in a standard clan battle - Clan Exp
if(track_clan_battles!==true && $("div.game-grid > div.main-game-section > div.main-section__body:contains('Clan Exp')").length > 0) {
return false;
}
// all is well, so let's get the mob name
var n_obj = $("div.battle-container > div:nth-child(3) > div.participant-name > span");
if(n_obj.length > 0) {
var mob_name = n_obj.prop('innerHTML').trim();
} else {
return false; // couldn't find the mob name, lets skip just in case
}
// and not in a clan dragon battle
// exact matches
const clan_dragons = ['Baby Dragon','Young Dragon','Adolescent Dragon','Adult Dragon','Dragon'];
if(track_clan_battles!==true && clan_dragons.indexOf(mob_name) > -1 ) {
//console.log('Skipping Clan Dragons');
return false;
}
// and not in an abyss battle or clan battle - Abyssal
if(track_abyss_battles!==true && ['Abyssal'].some(term => mob_name.includes(term))) {
return false;
}
// all good
if(we_battling === true) {
// Add the battle stat panel to dom, if not already there
if(!document.getElementById("iqrpgbs_bs_panel")) {
var iqrpg_body = $( "div.game-grid > div.main-game-section > div.main-section__body" );
iqrpg_body[0].children[0].children[0].insertAdjacentHTML('beforeend', render_battle_stats_panel() );
document.getElementById('igrpg_bs_reset').addEventListener('click', iqrpg_bs_reset_stats, false);
}
// get the players stat line & compare it to previous stored stats
var player_sl = $("div.game-grid > div.main-game-section > div.main-section__body > div > div > div > div:nth-child(2)");
if(player_sl.prop('innerHTML') !== player_cache) {
player_cache = player_sl.prop('innerHTML');
parse_player_stat_line(player_sl);
display_needs_update = true;
}
// get the mobs stat line & compare it to previous stored stats
var mobs_sl = $("div.game-grid > div.main-game-section > div.main-section__body > div > div > div > div:nth-child(3)");
if(mobs_sl.prop('innerHTML') !== enemy_cache) {
enemy_cache = mobs_sl.prop('innerHTML');
parse_enemy_stat_line(mobs_sl);
display_needs_update = true;
}
// we already have display_needs_update, so let's use it as a trigger for our new health tracking
if(display_needs_update === true && track_min_health_perc === true) {
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");
let hp_totals = hp_sl.prop('innerHTML').split(" / ");
let this_perc = (parseInt(hp_totals[0].replaceAll(",", "")) / parseInt(hp_totals[1].replaceAll(",", ""))) * 100;
if(this_perc < player_stats.hp_perc) player_stats.hp_perc = this_perc;
}
// update displayed values
if(display_needs_update === true) {
update_display();
}
}
}
// parses the drop log and builds array of quantified contents
function parse_drop_log() {
if( $( 'div#log-div > div' ).length == 0 ) return false; // either log isn't rendered yet, or it's just been cleared
const skiplist = ['[Gold]','Gold Rush:','Action Bonus:','Resource Rush:','Skill:','Mastery:','[Wood]','[Stone]','[Metal]'];
let first_log_entry = dropalyse_clean_entry( $( 'div#log-div > div:first' ) ); // capture cached entry for possible later use
let count = 0;
$( 'div#log-div > div' ).each(function( index ) {
// check if already analysed
let str = dropalyse_clean_entry($(this));
//console.log("str - " + str);
//console.log("cache - " + dropalyse_cache );
if(str === dropalyse_cache) return false; // break loop
// skip unwanted
if (skiplist.some(v => str.includes(v))) return true; // continue loop
// parse into time, qty, item
let entry = parse_drop_log_entry($( this ));
count++;
// add to data store
if (typeof dropalyse_store[entry.item] !== 'undefined') {
dropalyse_store[entry.item] += entry.qty;
} else {
dropalyse_store[entry.item] = entry.qty;
}
});
// do we have new entries?
if(count>0) {
dropalyse_cache = first_log_entry;
//console.log("------------------------------");
//console.log(dropalyse_store);
}
// save data to localstorage
dropalyse_save_ls();
}
function dropalyse_clean_entry(txt) {
//console.log("Cleaning - " + $(txt).text() );
var r = txt.clone();
r.find('.popup').remove();
//console.log("Clean - " + r.text() );
let ret = r.text();
// rarity
//let rarity = $("div.item > p[class^='text-rarity-']", r).attr('class');
//if(typeof rarity !== 'undefined') ret = ret + "#iqrpgstats#" + rarity;
return ret;
}
function parse_drop_log_entry(entry) {
let r = {};
// timestamp - not needed??
r.timestamp = $('span:first', entry).text();
// item
let data_str = $('span', entry).eq(1).text();
let matches = data_str.match(/^[+-]?\d+(\.\d+)?[%]?/g);
if(matches && matches.length>0) {
let n = matches[0];
r.qty = Number(n.replace('+', '').replace('%', ''));
r.item = data_str.replace(n,'').trim();
} else {
r.qty = 1; // it's something unusual
r.item = data_str.trim();
}
// strip extra data
r.item = r.item.split("]")[0].replace("[","").replace("]","");
// append new rarity data
let rarity = $("div.item > p[class^='text-rarity-']", entry).attr('class');
if(typeof rarity !== 'undefined') r.item = r.item + "#iqrpgstats#" + rarity;
//console.log(rarity + " - " + r.item);
return r;
}
function dropalyse_load_ls() {
if (localStorage.getItem('dropalyse_cache')) { dropalyse_cache = localStorage.getItem('dropalyse_cache'); }
if (localStorage.getItem('dropalyse_store')) { dropalyse_store = JSON.parse(localStorage.getItem('dropalyse_store')); }
if (localStorage.getItem('dropalyse_start_datum')) { dropalyse_start_datum = localStorage.getItem('dropalyse_start_datum'); }
}
function dropalyse_save_ls() {
localStorage.setItem('dropalyse_cache', dropalyse_cache);
localStorage.setItem('dropalyse_store', JSON.stringify(dropalyse_store));
localStorage.setItem('dropalyse_start_datum', dropalyse_start_datum);
localStorage.setItem('dropalyse_version', GM_info.script.version);
}
function dropalyse_render_content() {
let html = '';
if(Object.entries(dropalyse_store).length == 0 ) {
html = '<div>Waiting for drops...</div>';
} else {
for (let [key, qty] of Object.entries(dropalyse_store)) {
// format qty
let formatted_qty = qty;
if(dropalyse_format==='hour') {
formatted_qty = ( ( qty / dropalyse_get_time_elapsed() ) * 60 * 60 ).toFixed(dropalyse_decimal_precision);
} else if(dropalyse_format==='day') {
formatted_qty = ( ( qty / dropalyse_get_time_elapsed() ) * 60 * 60 * 24).toFixed(dropalyse_decimal_precision);
} else {
formatted_qty = Number(formatted_qty.toFixed(dropalyse_decimal_precision));
}
// format rarity
let rarity_class = '';
let parts = key.split("#iqrpgstats#");
if(parts.length === 2) rarity_class = parts[1];
// render
html += '<div style="position: relative;" class="iqrpgbs_hover_highlight"><div style="display: flex; justify-content: space-between;">'
+ '<span class="'+ rarity_class + '">' + parts[0] + '</span>'
+ '<span style="color:#3c3">' + formatted_qty + '</span>'
+ '</div></div>';
}
}
$('div#iqrpgbs_drop_log_content').html(html);
}
function dropalyse_insert_log() {
let html = `<div id="iqrpgbs_dropalyse_container" class="main-section" style="background-color: #0a0a0a;margin-bottom: .2rem;border: 1px solid #333;">
<div id="iqrpgbs_drop_log_header">
<p>Drops per hour</p><!---->
</div>
<div class="main-section__body" style="border-top: 1px solid #333;padding: .5rem;">
<div>
<div id="iqrpgbs_drop_log_content">Waiting for drops...</div>
<div id="iqrpgbs_dropalyse_timer">0:0:0:0</div>
<div id="iqrpgbs_dropalyse_options">[<a id="iqrpgbs_dropalyse_opt_hour" href="#">Hour</a>
- <a id="iqrpgbs_dropalyse_opt_day" href="#">Day</a>
- <a id="iqrpgbs_dropalyse_opt_total" href="#">Total</a>]
[<a id="iqrpgbs_dropalyse_backup_toggle" href="#">Backup</a>]
[<a id="iqrpgbs_dropalyse_reset" href="#">Reset</a>]</div>
<div id="iqrpgbs_dropalyse_backup_panel" style="margin-top:12px;display:none;">
<p class="heading">Backup & Restore Drops</p>
<p>You can download a copy of your drop data by clicking the button below...</p>
<p class="text-center" style="margin:6px;"><button id="iqrpgbs_dropalyse_but_export">Backup Data</button></p>
<p>To restore your data, paste the contents of your backup file into the field below and click the button</p>
<textarea placeholder="" style="width:100%;margin-top:6px;" id="iqrpgbs_dropalyse_import_textarea"></textarea>
<p class="text-center" style="margin:6px;"><button id="iqrpgbs_dropalyse_but_import">Restore Data</button></p>
</div>
</div></div></div>`;
$(html).insertAfter($('div.game-grid > div:first > div.main-section').last());
// setup format options and events
dropalyse_set_format(dropalyse_format); // set the initial format
document.getElementById('iqrpgbs_dropalyse_opt_hour').addEventListener("click", function(e) { e.preventDefault(); dropalyse_set_format('hour'); });
document.getElementById('iqrpgbs_dropalyse_opt_day').addEventListener("click", function(e) { e.preventDefault(); dropalyse_set_format('day'); });
document.getElementById('iqrpgbs_dropalyse_opt_total').addEventListener("click", function(e) { e.preventDefault(); dropalyse_set_format('total'); });
document.getElementById('iqrpgbs_dropalyse_reset').addEventListener("click", function(e) { e.preventDefault(); dropalyse_reset(); });
document.getElementById('iqrpgbs_dropalyse_but_export').addEventListener("click", function(e) { e.preventDefault(); dropalyse_export_data(); });
document.getElementById('iqrpgbs_dropalyse_but_import').addEventListener("click", function(e) { e.preventDefault(); dropalyse_import_data(); });
$('a#iqrpgbs_dropalyse_backup_toggle').click(function(e){
e.preventDefault();
$("div#iqrpgbs_dropalyse_backup_panel").toggle();
$('a#iqrpgbs_dropalyse_backup_toggle').toggleClass( "iqrpgbs_highlight" );
});
}
function dropalyse_reset() {
dropalyse_store = {};
dropalyse_start_datum = Date.now();
dropalyse_save_ls(); // update persistant storage
dropalyse_render_content();
}
function dropalyse_set_format(format) {
$('a#iqrpgbs_dropalyse_opt_hour').removeClass("iqrpgbs_highlight");
$('a#iqrpgbs_dropalyse_opt_day').removeClass("iqrpgbs_highlight");
$('a#iqrpgbs_dropalyse_opt_total').removeClass("iqrpgbs_highlight");
if(format==='hour') {
dropalyse_format = 'hour';
$('a#iqrpgbs_dropalyse_opt_hour').addClass("iqrpgbs_highlight");
$('div#iqrpgbs_drop_log_header > p').html('Drops Per Hour');
} else if(format==='day') {
dropalyse_format = 'day';
$('a#iqrpgbs_dropalyse_opt_day').addClass("iqrpgbs_highlight");
$('div#iqrpgbs_drop_log_header > p').html('Drops Per Day');
} else if(format==='total') {
dropalyse_format = 'total';
$('a#iqrpgbs_dropalyse_opt_total').addClass("iqrpgbs_highlight");
$('div#iqrpgbs_drop_log_header > p').html('Drops - Total');
}
dropalyse_render_content(); // update view
}
function dropalyse_get_time_elapsed() {
return ( Date.now() - dropalyse_start_datum )/1000;
}
function dropalyse_render_nice_timer() {
var delta = dropalyse_get_time_elapsed();
var days = Math.floor(delta / 86400);
delta -= days * 86400;
var hours = Math.floor(delta / 3600) % 24;
delta -= hours * 3600;
var minutes = Math.floor(delta / 60) % 60;
delta -= minutes * 60;
var seconds = delta % 60;
let html = '';
if(days>0) html += days + 'd ';
if(hours>0||days>0) html += hours + 'h ';
if(hours>0||days>0||minutes>0) html += minutes + 'm ';
html += Math.floor(seconds) + 's';
return html;
}
function dropalyse_import_data() {
// get and test for empty data
let our_data = document.getElementById("iqrpgbs_dropalyse_import_textarea").value;
if(our_data=='') return false; // blank data
// catch errors on parsing
try {
let backup = JSON.parse(our_data);
// let's set the new vals
dropalyse_cache = ''; // reset cache to '' so we can pull fresh data in, in our main loop
dropalyse_store = backup.dropalyse_store;
dropalyse_start_datum = ( Date.now() - Number(backup.dropalyse_backup_datum)) + Number(backup.dropalyse_start_datum);
// all done, let's re-render the tracker
dropalyse_render_content();
$("div#iqrpgbs_dropalyse_backup_panel").hide();
$('a#iqrpgbs_dropalyse_backup_toggle').removeClass( "iqrpgbs_highlight" );
document.getElementById("iqrpgbs_dropalyse_import_textarea").value = '';
} catch(e) {
alert("Backup data does not appear to be valid JSON!"); // error in the above string (in this case, yes)!
return false;
}
}
function dropalyse_export_data() {
const backup = {
dropalyse_cache: dropalyse_cache,
dropalyse_store: dropalyse_store,
dropalyse_start_datum: dropalyse_start_datum,
dropalyse_version: GM_info.script.version,
dropalyse_backup_datum: Date.now()
};
// Convert object to Blob
const blobConfig = new Blob(
[ JSON.stringify(backup) ],
{ type: 'text/json;charset=utf-8' }
)
// Convert Blob to URL
const blobUrl = URL.createObjectURL(blobConfig);
// Create an a element with blobl URL
const anchor = document.createElement('a');
anchor.href = blobUrl;
anchor.target = "_blank";
anchor.download = "IQRPG-Stats-" + Date.now() + ".json";
// Auto click on a element, trigger the file download
anchor.click();
// Don't forget ;)
URL.revokeObjectURL(blobUrl);
}
///////////////////////////////////
// Battle Stats
///////////////////////////////////
function parse_player_stat_line(player_sl) {
var hits = player_sl.find("p:nth-child(1) > span:nth-child(2)");
if(hits.length > 0) {
var actual_hits = hits.prop('innerHTML').replaceAll(" time(s)", "");
player_stats.hits += parseInt(actual_hits);
}
var dmg = player_sl.find("p:nth-child(1) > span:nth-child(3)");
if(dmg.length > 0 && hits.length > 0) {
var actual_dmg = dmg.prop('innerHTML').replaceAll(" damage", "").replaceAll(",", "");
player_stats.dmg += parseInt(actual_dmg) * parseInt(actual_hits);
}
var misses = player_sl.find("p:nth-child(2) > span:nth-child(1)");
if(misses.length > 0) {
var actual_misses = misses.prop('innerHTML').replaceAll(" time(s)", "");
player_stats.misses += parseInt(actual_misses);
}
}
function parse_enemy_stat_line(stat_line) {
var hits = stat_line.find("p:nth-child(1) > span:nth-child(2)");
if(hits.length > 0) {
var actual_hits = hits.prop('innerHTML').replaceAll(" time(s)", "");
enemy_stats.hits += parseInt(actual_hits);
}
var dmg = stat_line.find("p:nth-child(1) > span:nth-child(3)");
if(dmg.length > 0 && hits.length > 0) {
var actual_dmg = dmg.prop('innerHTML').replaceAll(" damage", "").replaceAll(",", "");
enemy_stats.dmg += parseInt(actual_dmg) * parseInt(actual_hits);
}
var misses = stat_line.find("p:nth-child(2) > span:nth-child(2)");
if(misses.length > 0) {
var actual_misses = misses.prop('innerHTML').replaceAll(" time(s)", "");
enemy_stats.misses += parseInt(actual_misses);
}
var dodges = stat_line.find("p:nth-child(2) > span:nth-child(3)");
if(dodges.length > 0) {
var actual_dodges = dodges.prop('innerHTML').replaceAll(" attack(s)", "");
enemy_stats.dodges += parseInt(actual_dodges);
}
}
function iqrpg_bs_reset_stats(e) {
e.preventDefault();
player_stats.dmg = 0;
player_stats.hits = 0;
player_stats.misses = 0;
player_stats.hp_perc = 100;
enemy_stats.dmg = 0;
enemy_stats.hits = 0;
enemy_stats.misses = 0;
enemy_stats.dodges = 0;
update_display();
}
function update_display() {
// players
$("#iqrpgbs_pl_dmg").html(new Intl.NumberFormat().format(player_stats.dmg));
$("#iqrpgbs_pl_hits").html(new Intl.NumberFormat().format(player_stats.hits));
var avg = Math.round(player_stats.dmg / player_stats.hits) || 0;
$("#iqrpgbs_pl_avg").html(new Intl.NumberFormat().format(avg));
var acc = (player_stats.hits / (player_stats.hits + player_stats.misses))*100 || 0;
$("#iqrpgbs_pl_acc").html(new Intl.NumberFormat().format(acc.toFixed(2)));
let min_hp = player_stats.hp_perc || 0;
$("#iqrpgbs_min_hp").html(new Intl.NumberFormat().format(min_hp));
// enemy
var enemy_avg = Math.round(enemy_stats.dmg / enemy_stats.hits) || 0;
$("#iqrpgbs_enmy_avg").html(new Intl.NumberFormat().format(enemy_avg));
var enemy_acc = ( (enemy_stats.hits + enemy_stats.dodges) / (enemy_stats.hits + enemy_stats.misses + enemy_stats.dodges))*100 || 0;
$("#iqrpgbs_enmy_acc").html(new Intl.NumberFormat().format(enemy_acc.toFixed(2)));
var enemy_dodges = (enemy_stats.dodges / (enemy_stats.hits /*+ enemy_stats.misses*/ + enemy_stats.dodges))*100 || 0;
$("#iqrpgbs_enmy_dodges").html(new Intl.NumberFormat().format(enemy_dodges.toFixed(2)));
}
function render_battle_stats_panel() {
var content = `
<div id="iqrpgbs_bs_panel" class="margin-top-large">
<div>Battle Stats <span>by Coastis</span></div>
<div>You dealt a total of <span id="iqrpgbs_pl_dmg">0</span> damage in <span id="iqrpgbs_pl_hits">0</span> hits,
with an average of <span id="iqrpgbs_pl_avg">0</span> per hit and <span id="iqrpgbs_pl_acc">0</span>% accuracy</div>
<div>Enemy dealt an average of <span id="iqrpgbs_enmy_avg">0</span> per hit
with an accuracy of <span id="iqrpgbs_enmy_acc">0</span>%, and you dodged <span id="iqrpgbs_enmy_dodges">0</span>% of attacks</div>
`;
if(track_min_health_perc === true) content += '<div>Your health reached a low of <span id="iqrpgbs_min_hp">100</span>%</div>';
content += '<div>[<a href="#" id="igrpg_bs_reset">Reset Battle Stats</a>]</div>';
content += '</div>';
return content;
}
GM_addStyle ( `
div#iqrpgbs_bs_panel div { text-align:center;padding:1px;}
div#iqrpgbs_bs_panel div:nth-child(1) { font-weight:bold;}
div#iqrpgbs_bs_panel div:nth-child(1) span { font-size:8px;font-weight:normal;}
div#iqrpgbs_drop_log_header {display: flex; justify-content: center; align-items: center; padding: .5rem; background: linear-gradient(#000,#151515);}
div#iqrpgbs_dropalyse_timer { padding:2px; margin-top:4px; text-align:center; }
div#iqrpgbs_dropalyse_options { padding:2px; text-align:center; }
.iqrpgbs_highlight { color:#3c3 !important; }
div.iqrpgbs_hover_highlight:hover { background-color:#222222; }
` );