Greasy Fork is available in English.

Better Rrolf

record game crafts

// ==UserScript==
// @name         Better Rrolf
// @name:zh-CN   Better Rrolf
// @namespace    http://tampermonkey.net/
// @version      1.07
// @description  record game crafts
// @description:zh-CN   record game crafts
// @author       NetheriteJam
// @match        https://rrolf.io/
// @run-at		 document-start
// @license		 MIT
// @grant        none
// ==/UserScript==

const color = {
	default: '#6137BD',
	red: '#FF002F',
	green: '#00FF6F',
}

const types = [
	'basic',
	'pellet',
	'rock',
	'spikes',
	'light',
	'missile',
	'peas',
	'leaf',
	'egg',
	'magnet',
	'uranium',
	'feather',
	'azalea',
	'bone',
	'web',
	'seed',
	'gravel',
	'club',
	'crest',
	'droplet',
	'beak',
	'lightning',
];

const rarities = [
	'common',
	'uncommon',
	'rare',
	'epic',
	'legendary',
	'mythic',
	'exotic',
];

const craftP = [
	0.5,
	0.3,
	0.15,
	0.05,
	0.03,
	0.01,
]

const craftC = [
	0.3021030253,
	0.1189491927,
	0.0322209144,
	0.0038016583,
	0.0013861777,
	0.0001560417,
]

const consoleLogPrefix = '[BetterRrolf] ';

const consoleHeading = 'Better Rrolf by NetheriteJam'

let enabled = true; // if the console is enabled

let input; // the console

let state = 0; // state of console
let selectedOption;

// window.localStorage.setItem('betterRrolf_settings', 'null');
// window.localStorage.setItem('betterRrolf_craft_record', 'null');

let settings = window.localStorage.getItem('betterRrolf_settings'); // get stored craft records

let craftRecord = window.localStorage.getItem('betterRrolf_craft_record'); // get stored craft records

let selectedRarity, selectedType;

let qRarity, qType;

let listPage, pageLength = 20;

function getLocalStorage() {
	if ( settings == 'null' ) {
		settings = [
			false, // abbrCheckWhenLogging
			false, // focusEventLogging
		];
	} else {
		settings = JSON.parse(settings);
	}

	if ( craftRecord == 'null' ) {
		craftRecord = [];
	} else {
		craftRecord = JSON.parse(craftRecord);
	}
}

log(consoleHeading);
log('Loading...');
log('Refresh the page if it doesn\'t load.');
getLocalStorage();
betterRrolf();
// window.addEventListener('load', betterRrolf);

async function betterRrolf() {
	await new Promise(resolve => setTimeout(resolve, 1000));
	initInputBox();
	initDependencies();
	log('Finished loading.');
	log('Execute command \'help\' for more information.');
	log('Press [/] to toggle BetterRrolf console');
	log('Press [.] to focus on BetterRrolf console.');
	window.addEventListener('keydown', e => {
		if ( e.code == 'Slash' ) {
			clr();
			if ( enabled ) {
				log('Disabled console.');
				log('Press [/] to toggle BetterRrolf console');
			} else {
				log('Enabled console.');
				log('Press [/] to toggle BetterRrolf console');
				log('Press [.] to focus on BetterRrolf console.');
				log('Execute command \'help\' for more information.');
			}
			toggleInputBox();
		} else if ( e.code == 'Period' ) {
			if ( settings[1] ) {
				log('Focused on console.');
			}
			input.focus();
			e.preventDefault();
		}
	});
	input.addEventListener('keydown', e => {
		if ( e.code == 'Enter' ) {
			let cmd = input.value;
			input.value = '';
			if ( cmd !== '' ) {
				if ( state == 0 ) { // waiting
					if ( cmd == 'help' ) {
						clr();
						log(`Executed command '${cmd}'`);
						log("");
						help();
					} else if ( cmd == 'set' ) {
						clr();
						log(`Executed command '${cmd}'`);
						log("");
						set();
					} else if ( cmd == 'clr' ) {
						clr();
						log(`Executed command '${cmd}'`);
						log('Execute command \'help\' for more information.');
						log("");
					} else if ( cmd == 'log' ) {
						clr();
						log(`Executed command '${cmd}'`);
						log("");
						log('Input rarity or type to select that of the craft.');
						log('Then input the number of petals you lose.');
						log('Input 5 for a successful craft.');
						log('You DON\'T need to select the rarity and type every time you input the number above.');
						log('You can only select them when you start to craft petals of a new type or rarity.');
						log('Use \'exit\' to exit.');
						state = 3;
						selectedRarity = undefined;
						selectedType = undefined;
					} else if ( cmd == 'q' ) {
						clr();
						log(`Executed command '${cmd}'`);
						log("");
						qRarity = -1;
						qType = -1;
						state = 4;
						q();
					} else if ( cmd == 'list' ) {
						clr();
						log(`Executed command '${cmd}'`);
						log("");
						listPage = 1;
						state = 7;
						list();
					}
				} else if ( state == 1 ) { // select id of an option in settings
					if ( cmd == '0' ) {
						state = 0;
						clr();
						log('Better Rrolf by Netheritejam');
						log('Execute command \'help\' for more information.');
						log('Press [/] to toggle BetterRrolf console');
					} else {
						selectedOption = parseInt(cmd) - 1;
						if ( settings[selectedOption] === undefined ) {
							clr();
							log(`Invalid option ID '${cmd}'`, 'red');
							set();
						} else {
							log('Input the value you want to set this option to:');
							state = 2;
						}
					}
				} else if ( state == 2 ) { // change value of selected option in settings
					if ( selectedOption == 0 || selectedOption == 1 ) {
						if ( cmd == "true" || cmd == "false" ) {
							settings[selectedOption] = (cmd == "true" ? true : false);
							state = 0;
							clr();
							console.log(settings);
							log('Settings has been updated.');
							set();
						} else {
							log('Invalid value', 'red');
						}
					}
				} else if ( state == 3 ) { // log crafts
					if ( cmd == 'exit' ) {
						log('Exited.');
						log('Execute command \'help\' for more information.');
						state = 0;
					} else if ( parseInt(cmd) !== NaN && parseInt(cmd) > 0 && parseInt(cmd) < 6 ) {
						if ( selectedRarity === undefined || selectedType === undefined ) {
							if ( selectedRarity === undefined )
								log('Rarity not selected.', 'red');
							if ( selectedType === undefined )
								log('Type not selected.', 'red');
						} else {
							log(`Recorded craft ${rarities[selectedRarity]} ${types[selectedType]} at ${moment().format('YYYY-MM-DD hh:mm:ss')}.`);
							if ( parseInt(cmd) == 5 )
								log('Successful', 'green');
							else
								log('Unsuccessful', 'red');
							craftRecord.push({
								rarity: selectedRarity,
								type: selectedType,
								cost: parseInt(cmd),
								state: ( parseInt(cmd) == 5 ? true : false ),
								date: moment().format('YYYY-MM-DD hh:mm:ss'),
							});
						}
					} else {
						let matched = [];
						let len = cmd.length;
						if ( settings[0] ) {
							rarities.forEach(rarity => {
								if ( rarity.slice(0, len) == cmd )
									matched.push(rarity);
							});
							types.forEach(type => {
								if ( type.slice(0, len) == cmd )
									matched.push(type);
							});
						} else {
							rarities.forEach(rarity => {
								if ( rarity == cmd )
									matched.push(rarity);
							});
							types.forEach(type => {
								if ( type == cmd )
									matched.push(type);
							});
						}
						if ( matched.length > 1 ) {
							let s = '';
							matched.forEach(str => {
								s += str + ', ';
							});
							s = s.slice(0, s.length - 2);
							log(`Abbreviation is not distinguishable(${s}).`, 'red');
						} else if ( matched.length == 0 ) {
							log(`No match for '${cmd}' in dictionary.`, 'red');
						} else {
							matched = matched[0];
							let idx = rarities.findIndex(rarity => rarity == matched);
							if ( idx != -1 ) {
								selectedRarity = idx;
								log(`Selected rarity ${matched}.`);
							} else {
								selectedType = types.findIndex(type => type == matched);
								log(`Selected type ${matched}.`);
							}
						}
					}
				} else if ( state == 4 ) {
					if ( cmd == '1' ) {
						clr();
						log("Input the rarity you want to apply to the filter.");
						log("Input '1' for any.");
						state = 5;
					} else if ( cmd == '2' ) {
						clr();
						log("Input the type you want to apply to the filter.");
						log("Input '1' for any.");
						state = 6;
					} else if ( cmd == 'q' ) {
						clr();
						let cnt0 = 0, cnt1 = 0, cost = 0;
						let latest;
						craftRecord.forEach(record => {
							if ( record.rarity == qRarity || qRarity == -1 ) {
								if ( record.type == qType || qType == -1 ) {
									latest = record;
									if ( record.state == false ) {
										cnt0 ++;
									} else {
										cnt1 ++;
									}
									cost += record.cost;
								}
							}
						});
						if ( latest === undefined ) {
							log(`There are no crafts that satisfy all the filters.`);
						} else {
							let be = 'are';
							if ( cnt1 == 1 )
								be = 'is';

							if ( qRarity != -1 && qType != -1 )
								log(`${cnt1} crafts using ${rarities[qRarity]} ${types[qType]} ${be} successful`, 'green');
							else if ( qRarity == -1 && qType == -1 ) 
								log(`${cnt1} crafts ${be} successful`, 'green');
							else if ( qRarity == -1 ) 
								log(`${cnt1} crafts using any ${types[qType]} ${be} successful`, 'green');
							else
								log(`${cnt1} crafts using ${rarities[qRarity]} petals ${be} successful`, 'green');

							be = 'are';
							if ( cnt0 == 1 )
								be = 'is';

							if ( qRarity != -1 && qType != -1 )
								log(`${cnt0} crafts using ${rarities[qRarity]} ${types[qType]} ${be} unsuccessful`, 'red');
							else if ( qRarity == -1 && qType == -1 ) 
								log(`${cnt0} crafts ${be} unsuccessful`, 'red');
							else if ( qRarity == -1 ) 
								log(`${cnt0} crafts using any ${types[qType]} ${be} unsuccessful`, 'red');
							else
								log(`${cnt0} crafts using ${rarities[qRarity]} petals ${be} unsuccessful`, 'red');

							log(`You have ${cnt1} out of ${cnt0 + cnt1} crafts successful.`);
							log(`Your success rate is ${(cnt1 / (cnt0 + cnt1) * 100).toFixed(2)}%`);
							if ( qRarity != -1 ) {
								log(`The nominal success rate is ${craftP[qRarity] * 100}%`);
								log(`You have ${(craftP[qRarity] * (cnt0 + cnt1)).toFixed(2)} crafts successful on expectations.`);
							}
							log("");
							log(`You crafted ${cnt1} petals out of ${cost} petals.`);
							log(`You spend ${(cost / cnt1).toFixed(2)} petals for each success on average.`);
							if ( qRarity != -1 ) {
								log(`You spend ${((craftP[qRarity] * 5 + (1 - craftP[qRarity]) * 2.5) / craftP[qRarity]).toFixed(2)} petals for each success on expectations.`);
							}
							log("");
							log(`Your last craft was on ${latest.date}, which was ${latest.state ? 'successful' : 'unsuccessful'}.`, latest.state ? 'green' : 'red');
							if ( qRarity != -1 && qType != -1 ) {
								let cnt = 0;
								for (let i = craftRecord.length - 1; i > 0; i -- ) {
									let record = craftRecord[i];
									if ( record.type == qType && record.rarity == qRarity) {
										if ( record.state ) {
											break;
										} else {
											cnt ++;
										}
									}
								}
								log(`You have failed crafting this petal for ${cnt} times since last success.`);
								log(`The next craft will have a ${(cnt * craftC[qRarity] * 100).toFixed(2)}% chance to success.`);
							}
						}
						log('Execute command \'help\' for more information.');
						state = 0;
					} else if ( cmd == 'exit' ) {
						log('Exited');
						log('Execute command \'help\' for more information.');
						state = 0;
					} else {
						log("Invalid input");
					}
				} else if ( state == 5 ) {
					let matched = [];
					let len = cmd.length;
					if ( settings[0] ) {
						rarities.forEach(rarity => {
							if ( rarity.slice(0, len) == cmd )
								matched.push(rarity);
						});
					} else {
						rarities.forEach(rarity => {
							if ( rarity == cmd )
								matched.push(rarity);
						});
					}
					if ( matched.length > 1 ) {
						let s = '';
						matched.forEach(str => {
							s += str + ', ';
						});
						s = s.slice(0, s.length - 2);
						log(`Abbreviation is not distinguishable(${s}).`, 'red');
					} else if ( matched.length == 0 ) {
						qRarity = -1;
						clr();
						state = 4;
						log(`Rarity filter has been changed to any.`);
						q();
					} else {
						matched = matched[0];
						let idx = rarities.findIndex(rarity => rarity == matched);
						if ( idx != -1 ) {
							qRarity = idx;
							clr();
							state = 4;
							log(`Rarity filter has been changed to ${matched}.`);
							q();
						} else { // should not happen
							log('ERROR', 'red');
						}
					}
				} else if ( state == 6 ) {
					let matched = [];
					let len = cmd.length;
					if ( settings[0] ) {
						types.forEach(type => {
							if ( type.slice(0, len) == cmd )
								matched.push(type);
						});
					} else {
						types.forEach(type => {
							if ( type == cmd )
								matched.push(type);
						});
					}
					if ( matched.length > 1 ) {
						let s = '';
						matched.forEach(str => {
							s += str + ', ';
						});
						s = s.slice(0, s.length - 2);
						log(`Abbreviation is not distinguishable(${s}).`, 'red');
					} else if ( matched.length == 0 ) {
						qType = -1;
						clr();
						state = 4;
						log(`Type filter has been changed to any.`);
						q();
					} else {
						matched = matched[0];
						let idx = types.findIndex(type => type == matched);
						if ( idx != -1 ) {
							qType = idx;
							clr();
							state = 4;
							log(`Type filter has been changed to ${matched}.`);
							q();
						} else { // should not happen
							log('ERROR', 'red');
						}
					}
					
				} else if ( state == 7 ) {
					if ( cmd == 'exit' ) {
						log('Exited');
						log('Execute command \'help\' for more information.');
						state = 0;
					} else if ( cmd == 'del' ) {
						log("THIS OPERATION CAN'T BE REVOKED", 'red');
						log('Input the ID of the log you want to delete.');
						state = 8;
					} else {
						let page = parseInt(cmd);
						if ( page === NaN || page <= 0 ) {
							log('Invalid input', 'red');
						} else {
							listPage = page;
							clr();
							list();
						}
					}
				} else if ( state == 8 ) {
					let id = parseInt(cmd);
					if ( id === NaN ) {
						log('Invalid Input', 'red');
					} else {
						craftRecord.splice(id - 1, 1);
						state = 7;
						clr();
						list();
					}
				}
			}
		}
	});
	window.addEventListener('beforeunload', () => {
		window.localStorage.setItem('betterRrolf_craft_record', JSON.stringify(craftRecord));
		window.localStorage.setItem('betterRrolf_settings', JSON.stringify(settings));
	});
}

function help() {
	log('Press [/] to toggle BetterRrolf console.');
	log('Press [.] to focus on BetterRrolf console.');
	log("Commands(Use without dot):");
	log(".help: Open this page.");
	log(".set: Open settings page.");
	log(".clr: Clear console.");
	log(".log: Start logging your crafts.");
	log(".q: Query your crafts.");
	log(".list: List your crafts(you can also operate logs here | currently unavailable).");
	// log(".acc: ");
	log("Commands will be explained more specificly when you execute them.");
}

function set() {
	state = 1;

	log("Settings:");

	log("ID: 1");
	log("abbrCheckWhenLogging(true/false):");
	if ( settings[0] )
		log("true", 'green');
	else
		log("false", 'red');
	log("When enabled, checks abbreviation for rarities and types (of petals) when logging crafts.");
	log("");

	log("ID: 2");
	log("focusEventLogging(true/false):");
	if ( settings[1] )
		log("true", 'green');
	else
		log("false", 'red');
	log("When enabled, focus events will be logged in console.");
	log("");

	log("Select the ID of the option you want to change or 0 to exit");
}

function log(str, clr) {
	if ( clr === undefined ) {
		clr = 'default';
	}
	console.log('%c' + consoleLogPrefix + str, `background: #E3F8FF; color: ${color[clr]}`);
}

function q() {
	let len = craftRecord.length;
	if ( len == 0 ) {
		log('There are no crafting records yet.');
		log('Execute command \'help\' for more information.');
		state = 0;
	} else {
		log("Current filters:");
		log("rarity:");
		if ( qRarity == -1 ) {
			log("any");
		} else {
			log(rarities[qRarity]);
		}
		log("type:");
		if ( qType == -1 ) {
			log("any");
		} else {
			log(types[qType]);
		}
		log("");
		log("Input '1' to change rarity filter.");
		log("Input '2' to change type filter.");
		log("Input 'q' again to query.");
		log("Input 'exit' to exit. ");
	}
}

function list() {
	let len = craftRecord.length;
	if ( len == 0 ) {
		log('There are no crafting records yet.');
		log('Execute command \'help\' for more information.');
		state = 0;
	} else {
		log("Input a page number to turn to that page.");
		log("Input 'del' to delete a record.");
		log("Input 'exit' to exit.");
		let pageCount = Math.floor((len - 1) / pageLength) + 1;
		log(`Page ${listPage} / ${pageCount}`);
		for (let i = (listPage - 1) * pageLength + 1; i <= listPage * pageLength && i <= len; i ++ ) {
			let record = craftRecord[i - 1];
			log(`${i} | Attempt to craft ${record.rarity} ${record.type} on ${record.date} ${record.state ? "succeeded" : "failed and lost " + record.cost }`, record.state ? 'green' : 'red');
		}
	}
}

function initInputBox() {
	let newInputBox = document.createElement('input');
	newInputBox.id = `betterRrolfConsole`;
	newInputBox.type = 'text';
	newInputBox.placeholder = `Better Rrolf Console`;
	document.body.append(newInputBox);
	let style = document.getElementById('betterRrolfConsole').style;
	style['height'] = '3vh';
	style['width'] = '27vw';
	style['position'] = 'absolute';
	style['z-index'] = '4';
	style['left'] = '85vw';
	style['top'] = '3vh';
	style['transform'] = 'translateY(-50%) translateX(-50%)';
	style['padding'] = '0vh';
	style['background-color'] = 'rgba(255, 255, 255, 0.7)';
	style['outline-color'] = 'rgba(0, 0, 0, 0)';
	style['outline-style'] = 'solid';
	style['outline-width'] = '0vh';
	style['border-radius'] = '0.5vh';
	style['font-size'] = '2.5vh';
	style['font-family'] = 'Ubuntu';
	input = document.getElementById('betterRrolfConsole');
}

function initDependencies() {
	let script = document.createElement('script');
	script.src = 'https://momentjs.com/downloads/moment.min.js';
	script.async = true;
	document.head.append(script);
}

function toggleInputBox() {
	if ( enabled ) {
		enabled = false;
		document.getElementById('betterRrolfConsole').style.visibility = 'hidden';
	} else {
		enabled = true;
		document.getElementById('be·tterRrolfConsole').style.visibility = 'visible';
	}
}

function clr() {
	console.clear();
}