🐭️ MouseHunt - Minluck & Catch Rate Estimate

View the minluck and catch rate estimate, right on the camp page.

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

// ==UserScript==
// @name         🐭️ MouseHunt - Minluck & Catch Rate Estimate
// @version      1.42.0
// @description  View the minluck and catch rate estimate, right on the camp page.
// @license      MIT
// @author       bradp
// @namespace    bradp
// @match        https://www.mousehuntgame.com/*
// @icon         https://brrad.com/mouse.png
// @require      https://cdn.jsdelivr.net/npm/[email protected]/minlucks.js
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function () {
	'use strict';

	const allMiceInfo = window.minlucks;

	/**
	 * Add styles to the page.
	 *
	 * @param {string} styles The styles to add.
	 */
	const addStyles = (styles) => {
		const existingStyles = document.getElementById('mh-mouseplace-custom-styles');

		if (existingStyles) {
			existingStyles.innerHTML += styles;
		} else {
			const style = document.createElement('style');
			style.id = 'mh-mouseplace-custom-styles';

			style.innerHTML = styles;
			document.head.appendChild(style);
		}
	};

	/**
	 * Get the current page slug.
	 *
	 * @return {string} The page slug.
	 */
	const getCurrentPage = () => {
		const container = document.getElementById('mousehuntContainer');
		if (! container || container.classList.length <= 0) {
			return null;
		}

		return container.classList[ 0 ].replace('Page', '').toLowerCase();
	};

	/**
	 * Do something when ajax requests are completed.
	 *
	 * @param {Function} callback    The callback to call when an ajax request is completed.
	 * @param {string}   url         The url to match. If not provided, all ajax requests will be matched.
	 * @param {boolean}  skipSuccess Skip the success check.
	 */
	const onAjaxRequest = (callback, url = false, skipSuccess = false) => {
		const req = XMLHttpRequest.prototype.open;
		XMLHttpRequest.prototype.open = function () {
			this.addEventListener('load', function () {
				if (this.responseText) {
					let response = {};
					try {
						response = JSON.parse(this.responseText);
					} catch (e) {
						return;
					}

					if (response.success || skipSuccess) {
						if (! url) {
							callback(response);
							return;
						}

						if (this.responseURL.indexOf(url) !== -1) {
							callback(response);
						}
					}
				}
			});
			req.apply(this, arguments);
		};
	};

	/**
	 * Do something when the page or tab changes.
	 *
	 * @param {Object}   callbacks
	 * @param {Function} callbacks.show   The callback to call when the overlay is shown.
	 * @param {Function} callbacks.hide   The callback to call when the overlay is hidden.
	 * @param {Function} callbacks.change The callback to call when the overlay is changed.
	 */
	const onPageChange = (callbacks) => {
		const observer = new MutationObserver(() => {
			if (callbacks.change) {
				callbacks.change();
			}
		});

		const observeTarget = document.getElementById('mousehuntContainer');
		if (observeTarget) {
			observer.observe(observeTarget, {
				attributes: true,
				attributeFilter: ['class']
			});
		}
	};

	const allType = [
		'Arcane',
		'Draconic',
		'Forgotten',
		'Hydro',
		'Parental',
		'Physical',
		'Shadow',
		'Tactical',
		'Law',
		'Rift'
	];

	const updateMinLucks = async () => {
		if ('camp' !== getCurrentPage()) {
			return;
		}

		const effectiveness = await getMiceEffectivness();

		const miceNames = Object.values(effectiveness)
			.map(({ mice }) => mice).flat()
			.map(({ name }) => name).flat();

		renderList(miceNames);
	};

	function renderList(list) {
		const minWrap = document.getElementById('minluck-list');
		if (minWrap) {
			minWrap.remove();
		}
		const mintable = document.getElementById('minluck-list-table');
		if (mintable) {
			mintable.remove();
		}

		const powerEl = document.querySelector('.campPage-trap-trapStat.power');
		const luckEl = document.querySelector('.campPage-trap-trapStat.luck');
		const powerTypeEl = document.querySelector('.campPage-trap-trapStat.power');

		if (! powerEl || ! luckEl || ! powerTypeEl) {
			return;
		}

		const luck = luckEl.innerText.match(/\d/g).join('');
		const power = powerEl.innerText.match(/\d/g).join('');
		const powerType = powerTypeEl.innerText.match(/[A-Za-z]/g).join('');

		const minluckList = document.createElement('div');
		minluckList.id = 'minluck-list';
		minluckList.className = 'campPage-trap-trapEffectiveness';

		const miceheader = document.createElement('th');
		miceheader.innerText = 'Mouse';
		miceheader.classList = 'mousename-header';

		const table = document.createElement('table');
		table.id = 'minluck-list-table';
		table.appendChild(miceheader);

		const minluckheader = document.createElement('th');
		minluckheader.innerText = 'Minluck';
		table.appendChild(minluckheader);

		const crheader = document.createElement('th');
		crheader.innerText = 'CRE';
		table.appendChild(crheader);

		const totalStats = { good: 0, bad: 0, okay: 0, total: 0 };

		for (let i = 0; i < list.length; i++) {
			const mouseNameConverted = list[ i ];
			const powerIndex = allType.indexOf(powerType);
			const micePower = allMiceInfo[ mouseNameConverted ].power;
			const miceEff = allMiceInfo[ mouseNameConverted ].effs[ powerIndex ];
			const minluckString = replaceInfinity(micePower, miceEff);
			const catchRateString = convertToCR(power, luck, micePower, miceEff);

			const row = document.createElement('tr');
			row.className = 'minlucklist-minluck-row';

			const mouseName = document.createElement('td');
			mouseName.innerText = mouseNameConverted;
			mouseName.className = 'minlucklist-minluck-name';
			row.appendChild(mouseName);

			const minLuck = document.createElement('td');
			minLuck.className = 'minlucklist-minluck-data' + (luck >= minluckString ? ' minlucklist-minluck-data-good' : '');
			minLuck.innerText = minluckString;
			row.appendChild(minLuck);

			const catchRate = document.createElement('td');
			catchRate.innerText = catchRateString;
			if (catchRateString === '100.00%') {
				totalStats.good += 1;
				catchRate.className = 'minlucklist-minluck-data minlucklist-minluck-data-good';
			} else if ((parseInt(catchRateString)) <= 60) {
				totalStats.bad += 1;
				catchRate.className = 'minlucklist-minluck-data minlucklist-minluck-data-bad';
			} else {
				catchRate.className = 'minlucklist-minluck-data';
			}
			totalStats.total += 1;
			row.appendChild(catchRate);

			table.appendChild(row);
		}

		minluckList.appendChild(table);

		const statsContainer = document.querySelector('.campPage-trap-statsContainer');
		if (statsContainer) {
			statsContainer.appendChild(minluckList);
		}

		const trap = document.getElementsByClassName('trapImageView-layerWrapper')[ 0 ];

		trap.classList.remove('minluck-indicator-all-good');
		trap.classList.remove('minluck-indicator-bad');
		trap.classList.remove('minluck-indicator-good');
		trap.classList.remove('minluck-indicator-none');

		if (totalStats.good === totalStats.total) {
			trap.classList.add('minluck-indicator-all-good');
		} else if (totalStats.bad === totalStats.total) {
			trap.classList.add('minluck-indicator-bad');
		} else if (totalStats.good > totalStats.total / 2) {
			trap.classList.add('minluck-indicator-good');
		} else {
			trap.classList.add('minluck-indicator-none');
		}

		const rowData = table.getElementsByTagName('tr');

		for (let i = 0; i < rowData.length - 1; i++) {
			for (let j = 0; j < rowData.length - (i + 1); j++) {
				if (Number(rowData.item(j).getElementsByTagName('td').item(1).innerHTML.replace(/[^0-9.]+/g, '')) < Number(rowData.item(j + 1).getElementsByTagName('td').item(1).innerHTML.replace(/[^0-9.]+/g, ''))) {
					table.insertBefore(rowData.item(j + 1), rowData.item(j));
				}
			}
		}
	}

	const getUserHash = () => {
		// eslint-disable-next-line no-undef
		if (typeof unsafeWindow !== 'undefined' && unsafeWindow.user.unique_hash) {
			// eslint-disable-next-line no-undef
			return unsafeWindow.user.unique_hash;
		}

		// if window.user exists, return the hash
		if (window.user && window.user.unique_hash) {
			return window.user.unique_hash;
		}
	};

	const getMiceEffectivness = async () => {
		const url = 'https://www.mousehuntgame.com/managers/ajax/users/getmiceeffectiveness.php';
		const formData = 'sn=Hitgrab&hg_is_ajax=1&uh=' + getUserHash();

		// post the data to the url
		const response = await fetch(url, {
			method: 'POST',
			headers: {
				'Content-Type': 'application/x-www-form-urlencoded',
			},
			body: formData,
		});

		const responseText = await response.text();
		const data = JSON.parse(responseText);

		if (! data.effectiveness) {
			return;
		}

		return data.effectiveness;
	};

	function replaceInfinity(mousePower, eff) {
		// Can't evalute infinity symbol, so was replaced with 9999 as minluck instead
		const infinitySym = String.fromCharCode(0x221E);
		eff = eff / 100;

		if (eff === 0) {
			return infinitySym;
		}

		const minluck = Math.ceil(Math.ceil(Math.sqrt(mousePower / 2)) / Math.min(eff, 1.4));
		if (minluck >= 9999) {
			return infinitySym;
		}

		if (2 * Math.pow(Math.floor(Math.min(1.4, eff) * minluck), 2) >= mousePower) {
			return minluck;
		}

		return minluck + 1;
	}

	function convertToCR(power, luck, mPower, mEff) {
		mEff = mEff / 100;
		// eslint-disable-next-line no-mixed-operators
		let result = Math.min(1, (power * mEff + 2 * Math.pow(Math.floor(luck * Math.min(mEff, 1.4)), 2)) / (mPower + power * mEff));
		result = (result * 100).toFixed(2) + '%';
		return result;
	}

	const makeMinLuckButton = () => {
		const button = document.getElementById('minluck-button');
		if (! button) {
			const minluckButton = document.createElement('a');
			minluckButton.id = 'minluck-button';
			minluckButton.classList.add('campPage-trap-trapEffectiveness');
			minluckButton.textContent = '🐭️ Minluck & Catch Rate Estimate';

			const statsContainer = document.querySelector('.campPage-trap-statsContainer');
			if (statsContainer) {
				statsContainer.appendChild(minluckButton);
			}
		}
	};

	addStyles(`
	#minluck-button {
		margin-top: 10px;
	}

	#minluck-list {
		padding: 10px;
		margin-bottom: 10px;
		margin-top: -2px;
		border-top-left-radius: 0;
		border-top-right-radius: 0;
	}

	#minluck-list table {
		margin: 0 10px 10px 10px;
		width: 100%;
	}

	#minluck-list table th {
		text-align: center;
		font-weight: bold;
	}

	#minluck-list table th.mousename-header {
		text-align: left;
	}

	#minluck-list table:first-child {
		text-align: left;
	}

	.minlucklist-minluck-name {
		min-width: 85px;
		white-space: nowrap;
		overflow: hidden;
		max-width: 150px;
		text-overflow: ellipsis;
	}

	.minlucklist-minluck-data {
		text-align: center;
		min-width: 70px;
	}

	.minlucklist-minluck-data-good {
		color: #138f13;
	}

	.minlucklist-minluck-data-bad {
		color: #bb4646;
	}

	.minluck-indicator-all-good {
		border-top: 5px solid blue;
	}

	.minluck-indicator-good {
		border-top: 5px solid #2f82ec;
	}

	.minluck-indicator-bad {
		border-top: 5px solid #990000;
	}

	.minluck-indicator-all-good,
	.minluck-indicator-good,
	.minluck-indicator-bad {
		border-top: none;
	}`);

	onAjaxRequest(updateMinLucks, '/managers/ajax/users/changetrap.php');
	onPageChange({ change: updateMinLucks });

	makeMinLuckButton();
	updateMinLucks();

	setTimeout(() => {
		makeMinLuckButton();
		updateMinLucks();
	}, 750);
}());