dailyhwm / RepairAssistant

// ==UserScript==
// @name RepairAssistant
// @author Небылица, achepta, Kolt Piton
// @namespace repair
// @version 1.1.3
// @homepage https://greasyfork.org/ru/scripts/419228
// https://greasyfork.org/scripts/382911
// @description Помощь в ремонте: время окончания в протоколе, передачи в ремонт, список кузнецов

// @include https://*.heroeswm.ru/pl_transfers.php*
// @include https://*.heroeswm.ru/art_transfer.php*

// @include https://*.lordswm.com/pl_transfers.php*
// @include https://*.lordswm.com/art_transfer.php*

// @exclude */rightcol.php*
// @exclude */ch_box.php*
// @exclude */chat*
// @exclude */ticker.html*
// @exclude */frames*
// @exclude */brd.php*

// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// ==/UserScript==
(function () {
	"use strict";

	// Вспомогательные функции
	function validateNick(nick) { // Возвращает массив ["результат валидации (true/false)", "строка с тем, что не так (либо ник, если верен)"]
		if (nick.length < 3 || nick.length > 18) {
			return [false, "Ник должен быть не длиннее 18 символов и не короче 3"];
		}
		if (nick.match(/^\d+$/)) {
			return [false, "Ник не может содержать только цифры"];
		}
		if (nick.match(/[^\W_\d]/) && nick.match(/[а-яА-ЯёЁ]/)) { // _ и цифры могут сочетаться с кириллицей
			return [false, "Ник может быть либо целиком из русских букв, либо целиком из английских (смешивать нельзя)"];
		}
		if (!nick.match(/^[a-zA-zа-яА-ЯёЁ\d\s_-]+$/)) {
			return [false, "Ник не должен содержать символов помимо русских или английских букв, цифр, знака подчёркивания, дефиса и пробела"];
		}
		return [true, nick];
	}

	function validatePrice(price) { // Возвращает цену в формате "2/3 цифры и знак %", либо false при невозможности обработки
		if (price.match(/^\d{2,3}%$/)) {
			return price;
		}
		if (price.match(/^\d{2,3}$/)) {
			return price + "%";
		}
		return false;
	}

	function drawSmithsTable(smiths, smithsDiv) { // Принимает массив кузнецов и рабочий элемент, отрисовывает в него таблицу и вяжет события удаления
		if (smiths.length > 0) {
			var smithsDivInnerHTML =
				"<style>" +
				".RA_smithsTr:hover{background: #DCDCDC;}" +
				".RA_smithsTdNick{font-weight: bold;}" +
				".RA_smithsTdDelete{font-size: 11px;}" +
				"</style>";

			smithsDivInnerHTML += "<table style='text-align: center;'><tbody>";
			maxI = smiths.length;
			for (i = 0; i < maxI; i++) {
				smithsDivInnerHTML += "<tr class='RA_smithsTr' title='" + smiths[i][3] + "'>";
				smithsDivInnerHTML += "<td class='RA_smithsTdNick' width='119px'><a href='pl_info.php?nick=" + encodeCP1251(smiths[i][0]) + "' target='_blank'>" + smiths[i][0] + "</a></td>";
				smithsDivInnerHTML += "<td class='RA_smithsTdEfficiency' width='55px'>" + smiths[i][1] + "</td>";
				smithsDivInnerHTML += "<td class='RA_smithsTdPrice' width='55px'>" + smiths[i][2] + "</td>";
				smithsDivInnerHTML += "<td class='RA_smithsTdDelete' id='RA_smithsTdDeleteSmith" + i + "' width='16px' title='Удалить кузнеца'>x</td>";
				smithsDivInnerHTML += "</tr>";
			}
			smithsDivInnerHTML += "</tbody></table>";

			smithsDiv.innerHTML = smithsDivInnerHTML;
			setStylesByMode();
			setDeleteSmithEvents();
			if (repairRadio) {
				setFillSmithEvents();
			}
		} else {
			smithsDiv.innerHTML = "<center>Кузнецов пока нет</center>";
		}
	}

	function setDeleteSmithEvents() { // Вяжет события к кнопкам удаления
		var deleteButtons = document.querySelectorAll("td.RA_smithsTdDelete");
		if (deleteButtons.length > 0) {
			var maxI = deleteButtons.length;
			for (i = 0; i < maxI; i++) {
				deleteButtons[i].onclick = function (event) {
					smiths.splice(parseInt(event.target.getAttribute("id").replace("RA_smithsTdDeleteSmith", "")), 1);
					setSmiths(smiths);
					drawSmithsTable(smiths, document.getElementById("RA_smithsDiv"));
				}
			}
		} else {
			window.setTimeout(function () {
				setDeleteSmithEvents()
			}, 50);
		}
	}

	function setFillSmithEvents() { // Вяжет события подстановки кузнецов
		var smithsTds = document.querySelectorAll("td.RA_smithsTdNick, td.RA_smithsTdEfficiency, td.RA_smithsTdPrice");
		if (smithsTds.length > 0) {
			var maxI = smithsTds.length;
			for (i = 0; i < maxI; i++) {
				smithsTds[i].onclick = function (event) {
					if (repairRadio.checked) {
						var thisSmithTds = event.target.parentElement.childNodes,
							nickInput = document.querySelector("input[name='nick']"),
							finalPrice = Math.ceil(parseInt(baseGoldPrice) * priceToFloat(thisSmithTds[2].innerText));
						nickInput.value = thisSmithTds[0].innerText;

						var a = document.createElement("a");
						a.href = "#";
						a.setAttribute("onclick", "set_price(" + finalPrice + ");");
						document.body.appendChild(a);
						a.click();
						setTimeout(function () {
							document.body.removeChild(a);
						}, 0);
					}
				}
			}
		} else {
			window.setTimeout(function () {
				setFillSmithEvents()
			}, 50);
		}
	}

	function clearNewSmith() { // Очистка полей в newSmithDiv
		newSmithNick.value = "";
		newSmithEfficiency.value = "90%";
		newSmithPrice.value = "100%";
		newSmithNote.value = "";
	}

	function setSmiths(smiths) { // Принимает массив кузнецов, формирует и записывает в хранилище строку
		var newSmithsString = "";
		maxI = smiths.length;
		for (i = 0; i < maxI; i++) {
			newSmithsString += smiths[i].join(":");
			if (i !== (smiths.length - 1)) {
				newSmithsString += "|";
			}
		}
		GM_setValue("smiths" + plIdSubKey, newSmithsString);
	}

	function priceToFloat(price) { // Переводит цену из строки в число (100% --> 1)
		return parseFloat(price.replace("%", "") / 100);
	}

	function calculateRepairTime(baseGoldPrice, repairBeginningMomentOnServer) { // Принимает базовую цену и момент начала ремонта, возвращает массив ["время ремонта (A ч. B мин.)", "время окончания (xx:yy)", "объект времени окончания"]
		var repairHoursFull = baseGoldPrice / 4000,
			repairHours = Math.trunc(repairHoursFull),
			repairMinutes = Math.ceil(60 * (repairHoursFull - Math.trunc(repairHoursFull))),
			repairEndMomentOnServer = new Date(repairBeginningMomentOnServer.getTime() + Math.ceil(repairHoursFull * 60 * 60 * 1000));
		return [repairHours + " ч. " + repairMinutes + " мин.",
			addLeadingZero(repairEndMomentOnServer.getHours()) + ":" +
			addLeadingZero(repairEndMomentOnServer.getMinutes()),
			repairEndMomentOnServer];
	}

	function setStylesByMode() { // Показ/скрытие строки с ценой арта и выставление стиля курсора на кузнецах в зависимости от режима
		var repairMode = (repairRadio) ? repairRadio.checked : false,
			artPriceTrDisplay = (repairMode) ? "none" : "table-row",
			smithsTdsCursor = (repairMode) ? "pointer" : "auto";
		artPriceTr.style.display = artPriceTrDisplay;

		var smithsTds = document.querySelectorAll("td.RA_smithsTdNick, td.RA_smithsTdEfficiency, td.RA_smithsTdPrice");
		if (smithsTds.length > 0) {
			var maxI = smithsTds.length;
			for (i = 0; i < maxI; i++) {
				smithsTds[i].style.cursor = smithsTdsCursor;
			}
		}
	}

	function getBaseGoldPrice() { // Возвращает базовую цену ремонта
		var baseGoldPriceMatch = document.documentElement.innerHTML.match(/repair_price\s=\s(\d+);/),
			baseGoldPrice =
				(baseGoldPriceMatch) ? baseGoldPriceMatch[1] :
					document.getElementById("rep").childNodes[4].childNodes[0].childNodes[6].childNodes[3].innerText.replace(",", "");
		return baseGoldPrice;
	}

	function encodeCP1251(text) { // Перекодирует русский текст так, чтобы при отправке запроса не выходили кракозябры
		var result = "",
			CP1251toUTF8 = {
				"А": "%C0",
				"Б": "%C1",
				"В": "%C2",
				"Г": "%C3",
				"Д": "%C4",
				"Е": "%C5",
				"Ж": "%C6",
				"З": "%C7",
				"И": "%C8",
				"Й": "%C9",
				"К": "%CA",
				"Л": "%CB",
				"М": "%CC",
				"Н": "%CD",
				"О": "%CE",
				"П": "%CF",

				"Р": "%D0",
				"С": "%D1",
				"Т": "%D2",
				"У": "%D3",
				"Ф": "%D4",
				"Х": "%D5",
				"Ц": "%D6",
				"Ч": "%D7",
				"Ш": "%D8",
				"Щ": "%D9",
				"Ъ": "%DA",
				"Ы": "%DB",
				"Ь": "%DC",
				"Э": "%DD",
				"Ю": "%DE",
				"Я": "%DF",

				"а": "%E0",
				"б": "%E1",
				"в": "%E2",
				"г": "%E3",
				"д": "%E4",
				"е": "%E5",
				"ж": "%E6",
				"з": "%E7",
				"и": "%E8",
				"й": "%E9",
				"к": "%EA",
				"л": "%EB",
				"м": "%EC",
				"н": "%ED",
				"о": "%EE",
				"п": "%EF",

				"р": "%F0",
				"с": "%F1",
				"т": "%F2",
				"у": "%F3",
				"ф": "%F4",
				"х": "%F5",
				"ц": "%F6",
				"ч": "%F7",
				"ш": "%F8",
				"щ": "%F9",
				"ъ": "%FA",
				"ы": "%FB",
				"ь": "%FC",
				"э": "%FD",
				"ю": "%FE",
				"я": "%FF",

				"Ё": "%A8",
				"ё": "%B8",

				" ": "%20",
				"!": "%21",
				"(": "%28",
				")": "%29",
				"*": "%2A",
				"+": "%2B",
				",": "%2C",
				"-": "%2D",
				".": "%2E",
				"/": "%2F"
			};

		var i,
			maxI = text.length;
		for (i = 0; i < maxI; i++) {
			if (CP1251toUTF8[text[i]] !== undefined) {
				result += CP1251toUTF8[text[i]];
			} else {
				result += text[i];
			}
		}

		return result;
	}

	function insertAfter(newNode, referenceNode) { // Вставка newNode после referenceNode
		referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
	}

	function setupObserver(target, config, callback) { // Привязка к target observer'а с параметрами config и вызовом callback при срабатывании c передачей observer внутрь
		var observer = new MutationObserver(function (mutations) {
			mutations.forEach(function (mutation) {
				callback.apply(observer);
			});
		});
		observer.observe(target, config);
	}

	function addLeadingZero(string) { // Вставляет ведущий ноль в строку с элементом даты/времени, если в ней только 1 цифра
		string = string.toString();
		if (string.length === 1) {
			string = "0" + string;
		}
		return string;
	}

	//

	var i,
		maxI,
		currentMoment,
		currentMomentOnServer,
		repairTimeArr,
		baseGoldPrice;
	if (location.pathname.indexOf("art_transfer") !== -1) { // передача арта
		// создаём дублированную кнопку
		var artPriceTr = document.querySelector("body > center > table > tbody > tr > td > table:nth-child(2) > tbody > tr:nth-child(5)");
		var artSendButtonTr2 = document.querySelector("body > center > table > tbody > tr > td > table:nth-child(2) > tbody > tr:nth-child(7)");
		var artSendButtonTr1 = artSendButtonTr2.cloneNode(true);
		var repairRadio = document.querySelector("input[type='radio'][name='sendtype'][value='5']");
		var noTransferRadio = document.querySelector("input[type='radio'][name='sendtype'][value='0']");
		var ownershipRadio = document.querySelector("input[type='radio'][name='sendtype'][value='1']");
		var rentRadio = document.querySelector("input[type='radio'][name='sendtype'][value='2']");
		//console.log(artSendButtonTr1)

		artSendButtonTr1.children[0].innerHTML = " <input type='button' value='Передать' id='tr_button_clone'>";
		artSendButtonTr1.style.display = "table-row";
		artSendButtonTr2.style.display = "none";
		insertAfter(artSendButtonTr1, artPriceTr);

		var tr_button = document.getElementById("tr_button"),
			tr_button_clone = document.getElementById("tr_button_clone");
		tr_button_clone.onclick = function () {
			tr_button.click();
		};
		tr_button_clone.disabled = tr_button.disabled;
		setupObserver(tr_button, {attributes: true, attributeFilter: ['disabled']}, function () {
			tr_button_clone.disabled = tr_button.disabled;
		});

		// получаем id текущего персонажа и кусок ключа по нему
		var plId = getCookie("pl_id");
		var plIdSubKey = "|#" + plId;

		// задаём дефолтный/получаем список кузнецов
		if (GM_getValue("smiths" + plIdSubKey) === undefined) {
			GM_setValue("smiths" + plIdSubKey, "");
		}
		var smithsString = GM_getValue("smiths" + plIdSubKey),
			smiths = (smithsString) ? smithsString.split("|") : [];
		maxI = smiths.length;
		for (i = 0; i < maxI; i++) {
			smiths[i] = smiths[i].split(":");
		}

		// создаём элемент под таблицу кузнецов
		var containerDiv = document.createElement("div"),
			smithsDiv = document.createElement("div"),
			containerTd = document.querySelector("td.wbwhite[valign='top'][align='right']");

		containerDiv.setAttribute("id", "RA_containerDiv");
		smithsDiv.setAttribute("id", "RA_smithsDiv");

		drawSmithsTable(smiths, smithsDiv);

		containerTd.innerHTML = "";
		containerTd.appendChild(containerDiv);
		containerDiv.appendChild(smithsDiv);
		containerDiv.innerHTML += "<hr width='100%'>";
		containerTd.nextSibling.nextSibling.setAttribute("valign", "top");

		// создаём элемент под панель добавления нового кузнеца
		var newSmithDiv = document.createElement("div");
		newSmithDiv.setAttribute("id", "RA_newSmithDiv");
		newSmithDiv.innerHTML =
			"<center><b>Добавить нового кузнеца</b><br>" +
			"<table><tbody><tr>" +
			"<td width='122px'>" +
			"<input type='text' id='RA_newSmithNick' style='width: 120px; padding-left: 3px;' placeholder='Ник'>" +
			"</td>" +
			"<td width='55px'>" +
			"<select id='RA_newSmithEfficiency' style='height: 21px;'>" +
			"<option value='90%'>90%</option>" +
			"<option value='80%'>80%</option>" +
			"<option value='70%'>70%</option>" +
			"<option value='60%'>60%</option>" +
			"<option value='50%'>50%</option>" +
			"<option value='40%'>40%</option>" +
			"<option value='30%'>30%</option>" +
			"<option value='20%'>20%</option>" +
			"<option value='10%'>10%</option>" +
			"</select></td>" +
			"<td width='55px'>" +
			"<input type='text' id='RA_newSmithPrice' style='width: 53px; padding-left: 3px;' placeholder='Цена' value='100%'>" +
			"</td></tr>" +
			"<tr><td colspan='3'><input type='text' id='RA_newSmithNote' style='width: 232px; padding-left: 3px;' placeholder='Примечание (опционально)'>" +
			"</td></tr></tbody><table>" +
			"<input type='button' id='RA_newSmithButton' value='Добавить'></center>";
		containerDiv.appendChild(newSmithDiv);

		var newSmithNick = document.getElementById("RA_newSmithNick"),
			newSmithEfficiency = document.getElementById("RA_newSmithEfficiency"),
			newSmithPrice = document.getElementById("RA_newSmithPrice"),
			newSmithNote = document.getElementById("RA_newSmithNote"),
			newSmithButton = document.getElementById("RA_newSmithButton"),
			defaultPrices = {
				"90%": "100%",
				"80%": "80%",
				"70%": "70%",
				"60%": "60%",
				"50%": "50%",
				"40%": "40%",
				"30%": "30%",
				"20%": "20%",
				"10%": "10%"
			};

		// автоподстановка цены по эффективности
		newSmithEfficiency.onchange = function () {
			newSmithPrice.value = defaultPrices[newSmithEfficiency.value];
		};

		// запись нового кузнеца
		newSmithButton.onclick = function () {
			var newSmithNickValue = validateNick(newSmithNick.value),
				newSmithPriceValue = validatePrice(newSmithPrice.value);
			if (newSmithNickValue[0]) { // проверка ника
				if (newSmithPriceValue) { // проверка цены
					smiths.push([newSmithNickValue[1], newSmithEfficiency.value, newSmithPriceValue, newSmithNote.value]);

					// сортировка массива кузнецов: по эффективности по убыванию, затем по цене по возрастанию
					smiths.sort(function (smith1, smith2) {
						if (smith1[1] < smith2[1]) {
							return 1;
						}
						if (smith1[1] > smith2[1]) {
							return -1;
						}
						if (smith1[1] === smith2[1]) {
							if (priceToFloat(smith1[2]) < priceToFloat(smith2[2])) {
								return -1;
							}
							if (priceToFloat(smith1[2]) > priceToFloat(smith2[2])) {
								return 1;
							}
							return 0;
						}
					});

					// сохраняем кузнецов и перерисовывем таблицу
					setSmiths(smiths);
					drawSmithsTable(smiths, document.getElementById("RA_smithsDiv"));
					clearNewSmith();
				} else {
					window.alert("Цена должна представлять из себя 2 или 3 цифры с/без знака %");
				}
			} else { // если ник неверен, показываем, что не так
				alert(newSmithNickValue[1]);
			}
		};

		// если арт сломанный
		if (repairRadio) {
			// вешаем скрытие/показ строки с ценой арта и выставление стиля курсора на кузнецах в зависимости от режима
			repairRadio.addEventListener("click", function () {
				setStylesByMode();
			});
			if (noTransferRadio) {
				noTransferRadio.addEventListener("click", function () {
					setStylesByMode();
				});
			}
			if (ownershipRadio) {
				ownershipRadio.addEventListener("click", function () {
					setStylesByMode();
				});
			}
			if (rentRadio) {
				rentRadio.addEventListener("click", function () {
					setStylesByMode();
				});
			}

			repairRadio.click(); // автовыбираем режим ремонта

			// показываем время ремонта
			var priceInput = document.getElementById("rep_price"),
				repairTimeSpan = document.createElement("span");
			currentMoment = new Date();
			currentMomentOnServer = new Date(Date.now() + currentMoment.getTimezoneOffset() * 60000 + 10800000);
			baseGoldPrice = getBaseGoldPrice();
			repairTimeArr = calculateRepairTime(baseGoldPrice, currentMomentOnServer);

			repairTimeSpan.setAttribute("id", "RA_repairTimeSpan");
			repairTimeSpan.style.marginLeft = "5px";
			repairTimeSpan.innerHTML = "(" + repairTimeArr[0] + ", >=" + repairTimeArr[1] + ")";
			insertAfter(repairTimeSpan, priceInput);

			// заменяем кнопку "+10%" на -1%
			var addPrice10Button = document.querySelector("input[type='submit'][onclick*='add_price(10)']");
			addPrice10Button.setAttribute("onclick", "add_price(-1); return false;");
			addPrice10Button.value = "-1%";
		}
	} else { // протокол передач
		// var protocolTd = document.querySelector("table[cellpadding='0'][border='0'][cellspacing='0'][width='95%'] > tbody > tr > td"),
		var protocolTd = document.querySelector("#set_mobile_max_width"), // fix 05.04.2023
			repairsMatch = protocolTd.innerHTML.match(/\d{2}-\d{2}-\d{2}\s\d{2}:\d{2}:.+?\sза\sремонт:\s\d+\s\(\d+%\)(?:,\sдоп\.\sкомиссия:\s\d+)?/g),
			repairBeginningTime,
			repairBeginningTimeParts,
			repairRealGoldPrice,
			repairPercent,
			oldSubstring,
			newSubstring,
			repairBeginningMomentOnServer;
		currentMoment = new Date();
		currentMomentOnServer = new Date(Date.now() + currentMoment.getTimezoneOffset() * 60000 + 10800000);

		maxI = repairsMatch.length;
		for (i = 0; i < maxI; i++) {
			oldSubstring = repairsMatch[i];
			repairBeginningTime = oldSubstring.match(/(\d{2}-\d{2}-\d{2}\s\d{2}:\d{2}):/)[1];
			repairBeginningTimeParts = repairBeginningTime.split(/[\s-:]/);
			repairRealGoldPrice = oldSubstring.match(/\sза\sремонт:\s(\d+)\s/)[1];
			repairPercent = oldSubstring.match(/\sза\sремонт:\s\d+\s\((\d+)%\)/)[1];

			repairBeginningMomentOnServer = new Date("20" + repairBeginningTimeParts[2],
				(parseInt(repairBeginningTimeParts[1]) - 1),
				repairBeginningTimeParts[0],
				repairBeginningTimeParts[3],
				repairBeginningTimeParts[4]);
			baseGoldPrice = Math.floor(repairRealGoldPrice / (repairPercent / 100));
			repairTimeArr = calculateRepairTime(baseGoldPrice, repairBeginningMomentOnServer);

			if (repairTimeArr[2] > currentMomentOnServer) {
				newSubstring = oldSubstring + ". <i>В ремонте до " + repairTimeArr[1] + ".</i>";
				protocolTd.innerHTML = protocolTd.innerHTML.replace(oldSubstring, newSubstring);
			}
		}
	}

	function getCookie(name) {
		const value = `; ${document.cookie}`;
		const parts = value.split(`; ${name}=`);
		if (parts.length === 2) return parts.pop().split(';').shift();
	}
})();