HeroWarsHelper

Automation of actions for the game Hero Wars

目前为 2023-08-16 提交的版本。查看 最新版本

// ==UserScript==
// @name			HeroWarsHelper
// @name:en			HeroWarsHelper
// @name:ru			HeroWarsHelper
// @namespace		HeroWarsHelper
// @version			2.099
// @description		Automation of actions for the game Hero Wars
// @description:en	Automation of actions for the game Hero Wars
// @description:ru	Автоматизация действий для игры Хроники Хаоса
// @author			ZingerY
// @license 		Copyright ZingerY
// @homepage		http://ilovemycomp.narod.ru/HeroWarsHelper.user.js
// @icon			http://ilovemycomp.narod.ru/VaultBoyIco16.ico
// @icon64			http://ilovemycomp.narod.ru/VaultBoyIco64.png
// @encoding		utf-8
// @include			https://*.nextersglobal.com/*
// @include			https://*.hero-wars*.com/*
// @match			https://www.solfors.com/
// @match			https://t.me/s/hw_ru
// @run-at			document-start
// ==/UserScript==

(function() {
/**
 * Start script
 *
 * Стартуем скрипт
 */
console.log('Start ' + GM_info.script.name + ', v' + GM_info.script.version);
/**
 * Script info
 *
 * Информация о скрипте
 */
const scriptInfo = (({name, version, author, homepage, lastModified}, updateUrl, source) =>
	({name, version, author, homepage, lastModified, updateUrl, source}))
	(GM_info.script, GM_info.scriptUpdateURL, arguments.callee.toString());
/**
 * If we are on the gifts page, then we collect and send them to the server
 *
 * Если находимся на странице подарков, то собираем и отправляем их на сервер
 */
if (['www.solfors.com', 't.me'].includes(location.host)) {
	setTimeout(sendCodes, 2000);
	return;
}
/**
 * Information for completing daily quests
 *
 * Информация для выполнения ежендевных квестов
 */
const questsInfo = {};
/**
 * Is the game data loaded
 *
 * Загружены ли данные игры
 */
let isLoadGame = false;
/**
 * Headers of the last request
 *
 * Заголовки последнего запроса
 */
let lastHeaders = {};
/**
 * Information about sent gifts
 *
 * Информация об отправленных подарках
 */
let freebieCheckInfo = null;
/**
 * User data
 *
 * Данные пользователя
 */
let userInfo;
/**
 * Original methods for working with AJAX
 *
 * Оригинальные методы для работы с AJAX
 */
const original = {
	open: XMLHttpRequest.prototype.open,
	send: XMLHttpRequest.prototype.send,
	setRequestHeader: XMLHttpRequest.prototype.setRequestHeader,
};
/**
 * Decoder for converting byte data to JSON string
 *
 * Декодер для перобразования байтовых данных в JSON строку
 */
const decoder = new TextDecoder("utf-8");
/**
 * Stores a history of requests
 *
 * Хранит историю запросов
 */
let requestHistory = {};
/**
 * URL for API requests
 *
 * URL для запросов к API
 */
let apiUrl = '';

/**
 * Connecting to the game code
 *
 * Подключение к коду игры
 */
this.cheats = new hackGame();
/**
 * The function of calculating the results of the battle
 *
 * Функция расчета результатов боя
 */
this.BattleCalc = cheats.BattleCalc;
/**
 * Sending a request available through the console
 *
 * Отправка запроса доступная через консоль
 */
this.SendRequest = send;
/**
 * Simple combat calculation available through the console
 *
 * Простой расчет боя доступный через консоль
 */
this.Calc = function (data) {
	const type = getBattleType(data?.type);
	return new Promise((resolve, reject) => {
		try {
			BattleCalc(data, type, resolve);
		} catch (e) {
			reject(e);
		}
	})
}
/**
 * Short asynchronous request
 * Usage example (returns information about a character):
 * const userInfo = await Send('{"calls":[{"name":"userGetInfo","args":{},"ident":"body"}]}')
 *
 * Короткий асинхронный запрос
 * Пример использования (возвращает информацию о персонаже):
 * const userInfo = await Send('{"calls":[{"name":"userGetInfo","args":{},"ident":"body"}]}')
*/
this.Send = function (json, pr) {
	return new Promise((resolve, reject) => {
		try {
			send(json, resolve, pr);
		} catch (e) {
			reject(e);
		}
	})
}

const i18nLangData = {
	/* English translation by BaBa */
	en: {
		/* Checkboxes */
		SKIP_FIGHTS: 'Skip battle',
		SKIP_FIGHTS_TITLE: 'Skip battle in Outland and the arena of the titans, auto-pass in the tower and campaign',
		ENDLESS_CARDS: 'Endless cards',
		ENDLESS_CARDS_TITLE: 'Endless Prediction Cards',
		AUTO_EXPEDITION: 'Auto Expedition',
		AUTO_EXPEDITION_TITLE: 'Auto-sending expeditions',
		CANCEL_FIGHT: 'Cancel battle',
		CANCEL_FIGHT_TITLE: 'The possibility of canceling the battle on VG',
		GIFTS: 'Gifts',
		GIFTS_TITLE: 'Collect gifts automatically',
		BATTLE_RECALCULATION: 'Battle recalculation',
		BATTLE_RECALCULATION_TITLE: 'Preliminary calculation of the battle',
		QUANTITY_CONTROL: 'Quantity control',
		QUANTITY_CONTROL_TITLE: 'Ability to specify the number of opened "lootboxes"',
		REPEAT_CAMPAIGN: 'Repeat missions',
		REPEAT_CAMPAIGN_TITLE: 'Auto-repeat battles in the campaign',
		DISABLE_DONAT: 'Disable donation',
		DISABLE_DONAT_TITLE: 'Removes all donation offers',
		DAILY_QUESTS: 'Daily quests',
		DAILY_QUESTS_TITLE: 'Complete daily quests',
		AUTO_QUIZ: 'AutoQuiz',
		AUTO_QUIZ_TITLE: 'Automatically receive correct answers to quiz questions',
		SECRET_WEALTH_CHECKBOX: 'Automatic purchase in the store "Secret Wealth" when entering the game',
		/* Input fields */
		HOW_MUCH_TITANITE: 'How much titanite to farm',
		COMBAT_SPEED: 'Combat Speed Multiplier',
		NUMBER_OF_TEST: 'Number of test fights',
		NUMBER_OF_AUTO_BATTLE: 'Number of auto-battle attempts',
		/* Buttons */
		RUN_SCRIPT: 'Run the',
		TO_DO_EVERYTHING: 'Do All',
		TO_DO_EVERYTHING_TITLE: 'Perform multiple actions of your choice',
		OUTLAND: 'Outland',
		OUTLAND_TITLE: 'Collect Outland',
		TITAN_ARENA: 'ToE',
		TITAN_ARENA_TITLE: 'Complete the titan arena',
		DUNGEON: 'Dungeon',
		DUNGEON_TITLE: 'Go through the dungeon',
		TOWER: 'Tower',
		TOWER_TITLE: 'Pass the tower',
		EXPEDITIONS: 'Expeditions',
		EXPEDITIONS_TITLE: 'Sending and collecting expeditions',
		SYNC: 'Sync',
		SYNC_TITLE: 'Partial synchronization of game data without reloading the page',
		ARCHDEMON: 'Archdemon',
		ARCHDEMON_TITLE: 'Hitting kills and collecting rewards',
		ESTER_EGGS: 'Easter eggs',
		ESTER_EGGS_TITLE: 'Collect all Easter eggs or rewards',
		REWARDS: 'Rewards',
		REWARDS_TITLE: 'Collect all quest rewards',
		MAIL: 'Mail',
		MAIL_TITLE: 'Collect all mail, except letters with energy and charges of the portal',
		MINIONS: 'Minions',
		MINIONS_TITLE: 'Attack minions with saved packs',
		ADVENTURE: 'Adventure',
		ADVENTURE_TITLE: 'Passes the adventure along the specified route',
		STORM: 'Storm',
		STORM_TITLE: 'Passes the Storm along the specified route',
		SANCTUARY: 'Sanctuary',
		SANCTUARY_TITLE: 'Fast travel to Sanctuary',
		GUILD_WAR: 'Guild War',
		GUILD_WAR_TITLE: 'Fast travel to Guild War',
		SECRET_WEALTH: 'Secret Wealth',
		SECRET_WEALTH_TITLE: 'Buy something in the store "Secret Wealth"',
		/* Misc */
		BOTTOM_URLS: '<a href="https://t.me/+0oMwICyV1aQ1MDAy" target="_blank">tg</a>',
		GIFTS_SENT: 'Gifts sent!',
		DO_YOU_WANT: "Do you really want to do this?",
		BTN_RUN: 'Run',
		BTN_CANCEL: 'Cancel',
		BTN_OK: 'OK',
		MSG_HAVE_BEEN_DEFEATED: 'You have been defeated!',
		BTN_AUTO: 'Auto',
		MSG_YOU_APPLIED: 'You applied',
		MSG_DAMAGE: 'damage',
		MSG_CANCEL_AND_STAT: 'Cancel and show statistic',
		MSG_REPEAT_MISSION: 'Repeat the mission?',
		BTN_REPEAT: 'Repeat',
		BTN_NO: 'No',
		MSG_SPECIFY_QUANT: 'Specify Quantity:',
		BTN_OPEN: 'Open',
		QUESTION_COPY: 'Question copied to clipboard',
		ANSWER_KNOWN: 'The answer is known',
		ANSWER_NOT_KNOWN: 'ATTENTION THE ANSWER IS NOT KNOWN',
		BEING_RECALC: 'The battle is being recalculated',
		THIS_TIME: 'This time',
		VICTORY: 'VICTORY',
		DEFEAT: 'DEFEAT',
		CHANCE_TO_WIN: "Chance to win",
		OPEN_DOLLS: 'nesting dolls recursively',
		SENT_QUESTION: 'Question sent',
		SETTINGS: 'Settings',
		MSG_BAN_ATTENTION: '<p style="color:red;">Using this feature may result in a ban.</p> Continue?',
		BTN_YES_I_AGREE: 'Yes, I understand the risks!',
		BTN_NO_I_AM_AGAINST: 'No, I refuse it!',
		VALUES: 'Values',
		EXPEDITIONS_SENT: 'Expeditions sent',
		TITANIT: 'Titanit',
		COMPLETED: 'completed',
		FLOOR: 'Floor',
		LEVEL: 'Level',
		BATTLES: 'battles',
		EVENT: 'Event',
		NOT_AVAILABLE: 'not available',
		NO_HEROES: 'No heroes',
		DAMAGE_AMOUNT: 'Damage amount',
		NOTHING_TO_COLLECT: 'Nothing to collect',
		COLLECTED: 'Collected',
		REWARD: 'rewards',
		REMAINING_ATTEMPTS: 'Remaining attempts',
		BATTLES_CANCELED: 'Battles canceled',
		MINION_RAID: 'Minion Raid',
		STOPPED: 'Stopped',
		REPETITIONS: 'Repetitions',
		MISSIONS_PASSED: 'Missions passed',
		STOP: 'stop',
		TOTAL_OPEN: 'Total open',
		OPEN: 'Open',
		ROUND_STAT: 'Damage statistics for ',
		BATTLE: 'battles',
		MINIMUM: 'Minimum',
		MAXIMUM: 'Maximum',
		AVERAGE: 'Average',
		NOT_THIS_TIME: 'Not this time',
		RETRY_LIMIT_EXCEEDED: 'Retry limit exceeded',
		SUCCESS: 'Success',
		RECEIVED: 'Received',
		LETTERS: 'letters',
		PORTALS: 'portals',
		ATTEMPTS: 'attempts',
		/* Quests */
		QUEST_10001: 'Upgrade the skills of heroes 3 times',
		QUEST_10002: 'Complete 10 missions',
		QUEST_10003: 'Complete 3 heroic missions',
		QUEST_10004: 'Fight 3 times in the Arena or Grand Arena',
		QUEST_10006: 'Use the exchange of emeralds 1 time',
		QUEST_10007: 'Open 1 chest',
		QUEST_10016: 'Send gifts to guildmates',
		QUEST_10018: 'Use an experience potion',
		QUEST_10019: 'Open 1 chest in the Tower',
		QUEST_10020: 'Open 3 chests in Outland',
		QUEST_10021: 'Collect 75 Titanite in the Guild Dungeon',
		QUEST_10021: 'Collect 150 Titanite in the Guild Dungeon',
		QUEST_10023: 'Upgrade Gift of the Elements by 1 level',
		QUEST_10024: 'Level up any artifact once',
		QUEST_10025: 'Start Expedition 1',
		QUEST_10026: 'Start 4 Expeditions',
		QUEST_10027: 'Win 1 battle of the Tournament of Elements',
		QUEST_10028: 'Level up any titan artifact',
		QUEST_10029: 'Unlock the Orb of Titan Artifacts',
		QUEST_10030: 'Upgrade any Skin of any hero 1 time',
		QUEST_10031: 'Win 6 battles of the Tournament of Elements',
		QUEST_10043: 'Start or Join an Adventure',
		QUEST_10044: 'Use Summon Pets 1 time',
		QUEST_10046: 'Open 3 chests in Adventure',
		QUEST_10047: 'Get 150 Guild Activity Points',
		NOTHING_TO_DO: 'Nothing to do',
		YOU_CAN_COMPLETE: 'You can complete quests',
		BTN_DO_IT: 'Do it',
		NOT_QUEST_COMPLETED: 'Not a single quest completed',
		COMPLETED_QUESTS: 'Completed quests',
		/* everything button */
		ASSEMBLE_OUTLAND: 'Assemble Outland',
		PASS_THE_TOWER: 'Pass the tower',
		CHECK_EXPEDITIONS: 'Check Expeditions',
		COMPLETE_TOE: 'Complete ToE',
		COMPLETE_DUNGEON: 'Complete the dungeon',
		COLLECT_MAIL: 'Collect mail',
		COLLECT_MISC: 'Collect Easter Eggs, Skin Gems, Arena Keys and Coins',
		COLLECT_QUEST_REWARDS: 'Collect quest rewards',
		MAKE_A_SYNC: 'Make a sync',

		RUN_FUNCTION: 'Run the following functions?',
		BTN_GO: 'Go!',
		PERFORMED: 'Performed',
		DONE: 'Done',
		ERRORS_OCCURRES: 'Errors occurred while executing',
		COPY_ERROR: 'Copy error information to clipboard',
		BTN_YES: 'Yes',
		ALL_TASK_COMPLETED: 'All tasks completed',

		UNKNOWN: 'unknown',
		ENTER_THE_PATH: 'Enter the path of adventure using commas or dashes',
		START_ADVENTURE: 'Start your adventure along this path!',
		BTN_CANCELED: 'Canceled',
		MUST_TWO_POINTS: 'The path must contain at least 2 points.',
		MUST_ONLY_NUMBERS: 'The path must contain only numbers and commas',
		NOT_ON_AN_ADVENTURE: 'You are not on an adventure',
		YOU_IN_NOT_ON_THE_WAY: 'Your location is not on the way',
		ATTEMPTS_NOT_ENOUGH: 'Your attempts are not enough to complete the path, continue?',
		YES_CONTINUE: 'Yes, continue!',
		NOT_ENOUGH_AP: 'Not enough action points',
		ATTEMPTS_ARE_OVER: 'The attempts are over',
		MOVES: 'Moves',
		BUFF_GET_ERROR: 'Buff getting error',
		BATTLE_END_ERROR: 'Battle end error',
		AUTOBOT: 'Autobot',
		FAILED_TO_WIN_AUTO: 'Failed to win the auto battle',
		ERROR_OF_THE_BATTLE_COPY: 'An error occurred during the passage of the battle<br>Copy the error to the clipboard?',
		ERROR_DURING_THE_BATTLE: 'Error during the battle',
		NO_CHANCE_WIN: 'No chance of winning this fight: 0/',
		LOST_HEROES: 'You have won, but you have lost one or several heroes',
		VICTORY_IMPOSSIBLE: 'Is victory impossible, should we focus on the result?',
		FIND_COEFF: 'Find the coefficient greater than',
		BTN_PASS: 'PASS',
		BRAWLS: 'Brawls',
		BRAWLS_TITLE: 'Activates the ability to auto-brawl',
		START_AUTO_BRAWLS: 'Start Auto Brawls?',
		LOSSES: 'Losses',
		WINS: 'Wins',
		FIGHTS: 'Fights',
		STAGE: 'Stage',
		DONT_HAVE_LIVES: 'You don\'t have lives',
		SECRET_WEALTH_ALREADY: 'Secret Wealth: Item for Pet Potions already purchased',
		SECRET_WEALTH_NOT_ENOUGH: 'Secret Wealth: Not Enough Pet Potion, You Have {available}, Need {need}',
		SECRET_WEALTH_UPGRADE_NEW_PET: 'Secret Wealth: After purchasing the Pet Potion, it will not be enough to upgrade a new pet',
		SECRET_WEALTH_PURCHASED: 'Secret wealth: Purchased {count} {name}',
		SECRET_WEALTH_CANCELED: 'Secret Wealth: Purchase Canceled',
		SECRET_WEALTH_BUY: 'You have {available} Pet Potion.<br>Do you want to buy {countBuy} {name} for {price} Pet Potion?',
	},
	ru: {
		/* Чекбоксы */
		SKIP_FIGHTS: 'Пропуск боев',
		SKIP_FIGHTS_TITLE: 'Пропуск боев в запределье и арене титанов, автопропуск в башне и кампании',
		ENDLESS_CARDS: 'Бесконечные карты',
		ENDLESS_CARDS_TITLE: 'Бесконечные карты предсказаний',
		AUTO_EXPEDITION: 'АвтоЭкспедиции',
		AUTO_EXPEDITION_TITLE: 'Автоотправка экспедиций',
		CANCEL_FIGHT: 'Отмена боя',
		CANCEL_FIGHT_TITLE: 'Возможность отмены боя на ВГ, СМ и в Асгарде',
		GIFTS: 'Подарки',
		GIFTS_TITLE: 'Собирать подарки автоматически',
		BATTLE_RECALCULATION: 'Прерасчет боя',
		BATTLE_RECALCULATION_TITLE: 'Предварительный расчет боя',
		QUANTITY_CONTROL: 'Контроль кол-ва',
		QUANTITY_CONTROL_TITLE: 'Возможность указывать количество открываемых "лутбоксов"',
		REPEAT_CAMPAIGN: 'Повтор в компании',
		REPEAT_CAMPAIGN_TITLE: 'Автоповтор боев в кампании',
		DISABLE_DONAT: 'Отключить донат',
		DISABLE_DONAT_TITLE: 'Убирает все предложения доната',
		DAILY_QUESTS: 'Ежедневные квесты',
		DAILY_QUESTS_TITLE: 'Выполнять ежедневные квесты',
		AUTO_QUIZ: 'АвтоВикторина',
		AUTO_QUIZ_TITLE: 'Автоматическое получение правильных ответов на вопросы викторины',
		SECRET_WEALTH_CHECKBOX: 'Автоматическая покупка в магазине "Тайное Богатство" при заходе в игру',
		/* Поля ввода */
		HOW_MUCH_TITANITE: 'Сколько фармим титанита',
		COMBAT_SPEED: 'Множитель ускорения боя',
		NUMBER_OF_TEST: 'Количество тестовых боев',
		NUMBER_OF_AUTO_BATTLE: 'Количество попыток автобоев',
		/* Кнопки */
		RUN_SCRIPT: 'Запустить скрипт',
		TO_DO_EVERYTHING: 'Сделать все',
		TO_DO_EVERYTHING_TITLE: 'Выполнить несколько действий',
		OUTLAND: 'Запределье',
		OUTLAND_TITLE: 'Собрать Запределье',
		TITAN_ARENA: 'Турнир Стихий',
		TITAN_ARENA_TITLE: 'Автопрохождение Турнира Стихий',
		DUNGEON: 'Подземелье',
		DUNGEON_TITLE: 'Автопрохождение подземелья',
		TOWER: 'Башня',
		TOWER_TITLE: 'Автопрохождение башни',
		EXPEDITIONS: 'Экспедиции',
		EXPEDITIONS_TITLE: 'Отправка и сбор экспедиций',
		SYNC: 'Синхронизация',
		SYNC_TITLE: 'Частичная синхронизация данных игры без перезагрузки сатраницы',
		ARCHDEMON: 'Архидемон',
		ARCHDEMON_TITLE: 'Набивает килы и собирает награду',
		ESTER_EGGS: 'Пасхалки',
		ESTER_EGGS_TITLE: 'Собрать все пасхалки или награды',
		REWARDS: 'Награды',
		REWARDS_TITLE: 'Собрать все награды за задания',
		MAIL: 'Почта',
		MAIL_TITLE: 'Собрать всю почту, кроме писем с энергией и зарядами портала',
		MINIONS: 'Прислужники',
		MINIONS_TITLE: 'Атакует прислужников сохраннеными пачками',
		ADVENTURE: 'Приключение',
		ADVENTURE_TITLE: 'Проходит приключение по указанному маршруту',
		STORM: 'Буря',
		STORM_TITLE: 'Проходит бурю по указанному маршруту',
		SANCTUARY: 'Святилище',
		SANCTUARY_TITLE: 'Быстрый переход к Святилищу',
		GUILD_WAR: 'Война гильдий',
		GUILD_WAR_TITLE: 'Быстрый переход к Войне гильдий',
		SECRET_WEALTH: 'Тайное богатство',
		SECRET_WEALTH_TITLE: 'Купить что-то в магазине "Тайное богатство"',
		/* Разное */
		BOTTOM_URLS: '<a href="https://t.me/+q6gAGCRpwyFkNTYy" target="_blank">tg</a> <a href="https://vk.com/invite/YNPxKGX" target="_blank">vk</a>',
		GIFTS_SENT: 'Подарки отправлены!',
		DO_YOU_WANT: "Вы действительно хотите это сделать?",
		BTN_RUN: 'Запускай',
		BTN_CANCEL: 'Отмена',
		BTN_OK: 'Ок',
		MSG_HAVE_BEEN_DEFEATED: 'Вы потерпели поражение!',
		BTN_AUTO: 'Авто',
		MSG_YOU_APPLIED: 'Вы нанесли',
		MSG_DAMAGE: 'урона',
		MSG_CANCEL_AND_STAT: 'Отменить и показать Статистику',
		MSG_REPEAT_MISSION: 'Повторить миссию?',
		BTN_REPEAT: 'Повторить',
		BTN_NO: 'Нет',
		MSG_SPECIFY_QUANT: 'Указать количество:',
		BTN_OPEN: 'Открыть',
		QUESTION_COPY: 'Вопрос скопирован в буфер обмена',
		ANSWER_KNOWN: 'Ответ известен',
		ANSWER_NOT_KNOWN: 'ВНИМАНИЕ ОТВЕТ НЕ ИЗВЕСТЕН',
		BEING_RECALC: 'Идет прерасчет боя',
		THIS_TIME: 'На этот раз',
		VICTORY: 'ПОБЕДА',
		DEFEAT: 'ПОРАЖЕНИЕ',
		CHANCE_TO_WIN: 'Шансы на победу',
		OPEN_DOLLS: 'матрешек рекурсивно',
		SENT_QUESTION: 'Вопрос отправлен',
		SETTINGS: 'Настройки',
		MSG_BAN_ATTENTION: '<p style="color:red;">Использование этой функции может привести к бану.</p> Продолжить?',
		BTN_YES_I_AGREE: 'Да, я беру на себя все риски!',
		BTN_NO_I_AM_AGAINST: 'Нет, я отказываюсь от этого!',
		VALUES: 'Значения',
		EXPEDITIONS_SENT: 'Экспедиции отправлены',
		TITANIT: 'Титанит',
		COMPLETED: 'завершено',
		FLOOR: 'Этаж',
		LEVEL: 'Уровень',
		BATTLES: 'бои',
		EVENT: 'Эвент',
		NOT_AVAILABLE: 'недоступен',
		NO_HEROES: 'Нет героев',
		DAMAGE_AMOUNT: 'Количество урона',
		NOTHING_TO_COLLECT: 'Нечего собирать',
		COLLECTED: 'Собрано',
		REWARD: 'наград',
		REMAINING_ATTEMPTS: 'Осталось попыток',
		BATTLES_CANCELED: 'Битв отменено',
		MINION_RAID: 'Рейд прислужников',
		STOPPED: 'Остановлено',
		REPETITIONS: 'Повторений',
		MISSIONS_PASSED: 'Миссий пройдено',
		STOP: 'остановить',
		TOTAL_OPEN: 'Всего открыто',
		OPEN: 'Открыто',
		ROUND_STAT: 'Статистика урона за',
		BATTLE: 'боев',
		MINIMUM: 'Минимальный',
		MAXIMUM: 'Максимальный',
		AVERAGE: 'Средний',
		NOT_THIS_TIME: 'Не в этот раз',
		RETRY_LIMIT_EXCEEDED: 'Превышен лимит попыток',
		SUCCESS: 'Успех',
		RECEIVED: 'Получено',
		LETTERS: 'писем',
		PORTALS: 'порталов',
		ATTEMPTS: 'попыток',
		QUEST_10001: 'Улучши умения героев 3 раза',
		QUEST_10002: 'Пройди 10 миссий',
		QUEST_10003: 'Пройди 3 героические миссии',
		QUEST_10004: 'Сразись 3 раза на Арене или Гранд Арене',
		QUEST_10006: 'Используй обмен изумрудов 1 раз',
		QUEST_10007: 'Открой 1 сундук',
		QUEST_10016: 'Отправь подарки согильдийцам',
		QUEST_10018: 'Используй зелье опыта',
		QUEST_10019: 'Открой 1 сундук в Башне',
		QUEST_10020: 'Открой 3 сундука в Запределье',
		QUEST_10021: 'Собери 75 Титанита в Подземелье Гильдии',
		QUEST_10021: 'Собери 150 Титанита в Подземелье Гильдии',
		QUEST_10023: 'Прокачай Дар Стихий на 1 уровень',
		QUEST_10024: 'Повысь уровень любого артефакта один раз',
		QUEST_10025: 'Начни 1 Экспедицию',
		QUEST_10026: 'Начни 4 Экспедиции',
		QUEST_10027: 'Победи в 1 бою Турнира Стихий',
		QUEST_10028: 'Повысь уровень любого артефакта титанов',
		QUEST_10029: 'Открой сферу артефактов титанов',
		QUEST_10030: 'Улучши облик любого героя 1 раз',
		QUEST_10031: 'Победи в 6 боях Турнира Стихий',
		QUEST_10043: 'Начни или присоеденись к Приключению',
		QUEST_10044: 'Воспользуйся призывом питомцев 1 раз',
		QUEST_10046: 'Открой 3 сундука в Приключениях',
		QUEST_10047: 'Набери 150 очков активности в Гильдии',
		NOTHING_TO_DO: 'Нечего выполнять',
		YOU_CAN_COMPLETE: 'Можно выполнить квесты',
		BTN_DO_IT: 'Выполняй',
		NOT_QUEST_COMPLETED: 'Ни одного квеста не выполенно',
		COMPLETED_QUESTS: 'Выполенно квестов',
		/* everything button */
		ASSEMBLE_OUTLAND: 'Собрать Запределье',
		PASS_THE_TOWER: 'Пройти башню',
		CHECK_EXPEDITIONS: 'Проверить экспедиции',
		COMPLETE_TOE: 'Пройти Турнир Стихий',
		COMPLETE_DUNGEON: 'Пройти подземелье',
		COLLECT_MAIL: 'Собрать почту',
		COLLECT_MISC: 'Собрать пасхалки, камни облика, ключи и монеты арены',
		COLLECT_QUEST_REWARDS: 'Собрать награды за квесты',
		MAKE_A_SYNC: 'Сделать синхронизацю',

		RUN_FUNCTION: 'Выполнить следующие функции?',
		BTN_GO: 'Погнали!',
		PERFORMED: 'Выполняется',
		DONE: 'Выполено',
		ERRORS_OCCURRES: 'Призошли ошибки при выполнении',
		COPY_ERROR: 'Скопировать в буфер информацию об ошибке',
		BTN_YES: 'Да',
		ALL_TASK_COMPLETED: 'Все задачи выполнены',

		UNKNOWN: 'Неизвестно',
		ENTER_THE_PATH: 'Введите путь приключения через запятые или дефисы',
		START_ADVENTURE: 'Начать приключение по этому пути!',
		BTN_CANCELED: 'Отменено',
		MUST_TWO_POINTS: 'Путь должен состоять минимум из 2х точек',
		MUST_ONLY_NUMBERS: 'Путь должен содержать только цифры и запятые',
		NOT_ON_AN_ADVENTURE: 'Вы не в приключении',
		YOU_IN_NOT_ON_THE_WAY: 'Указанный путь должен включать точку вашего положения',
		ATTEMPTS_NOT_ENOUGH: 'Ваших попыток не достаточно для завершения пути, продолжить?',
		YES_CONTINUE: 'Да, продолжай!',
		NOT_ENOUGH_AP: 'Попыток не достаточно',
		ATTEMPTS_ARE_OVER: 'Попытки закончились',
		MOVES: 'Ходы',
		BUFF_GET_ERROR: 'Ошибка при получении бафа',
		BATTLE_END_ERROR: 'Ошибка завершения боя',
		AUTOBOT: 'АвтоБой',
		FAILED_TO_WIN_AUTO: 'Не удалось победить в автобою',
		ERROR_OF_THE_BATTLE_COPY: 'Призошли ошибка в процессе прохождения боя<br>Скопировать ошибку в буфер обмена?',
		ERROR_DURING_THE_BATTLE: 'Ошибка в процессе прохождения боя',
		NO_CHANCE_WIN: 'Нет шансов победить в этом бою: 0/',
		LOST_HEROES: 'Вы победили, но потеряли одного или несколько героев!',
		VICTORY_IMPOSSIBLE: 'Победа не возможна, бъем на результат?',
		FIND_COEFF: 'Поиск коэффициента больше чем',
		BTN_PASS: 'ПРОПУСК',
		BRAWLS: 'Потасовки',
		BRAWLS_TITLE: 'Включает возможность автопотасовок',
		START_AUTO_BRAWLS: 'Запустить Автопотасовки?',
		LOSSES: 'Поражений',
		WINS: 'Побед',
		FIGHTS: 'Боев',
		STAGE: 'Стадия',
		DONT_HAVE_LIVES: 'У Вас нет жизней',
		SECRET_WEALTH_ALREADY: 'Тайное богатство: товар за Зелья питомцев уже куплен',
		SECRET_WEALTH_NOT_ENOUGH: 'Тайное богатство: Не достаточно Зелье Питомца, у Вас {available}, нужно {need}',
		SECRET_WEALTH_UPGRADE_NEW_PET: 'Тайное богатство: После покупки Зелье Питомца будет не достаточно для прокачки нового питомца',
		SECRET_WEALTH_PURCHASED: 'Тайное богатство: Куплено {count} {name}',
		SECRET_WEALTH_CANCELED: 'Тайное богатство: покупка отменена',
		SECRET_WEALTH_BUY: 'У вас {available} Зелье Питомца.<br>Вы хотите купить {countBuy} {name} за {price} Зелье Питомца?',
	}
}

let selectLang = 'en';
function checkLang() {
	const html = document.querySelector('html')
	const isFacebook = location.pathname.includes('facebook');
	const browserLang = (navigator.language || navigator.userLanguage).substr(0, 2);
	if (
		(html && html.lang == 'ru') || 
		(isFacebook && browserLang == 'ru')
	) {
		selectLang = 'ru';
	} else {
		selectLang = 'en';
	}
}
checkLang();

this.I18N = function (constant, replace) {
	if (constant && constant in i18nLangData[selectLang]) {
		const result = i18nLangData[selectLang][constant];
		if (replace) {
			return result.sprintf(replace);
		}
		return result;
	}
	return `% ${constant} %`;
};

String.prototype.sprintf = String.prototype.sprintf ||
	function () {
		"use strict";
		var str = this.toString();
		if (arguments.length) {
			var t = typeof arguments[0];
			var key;
			var args = ("string" === t || "number" === t) ?
				Array.prototype.slice.call(arguments)
				: arguments[0];

			for (key in args) {
				str = str.replace(new RegExp("\\{" + key + "\\}", "gi"), args[key]);
			}
		}

		return str;
	};

/**
 * Checkboxes
 *
 * Чекбоксы
 */
const checkboxes = {
	passBattle: {
		label: I18N('SKIP_FIGHTS'),
		cbox: null,
		title: I18N('SKIP_FIGHTS_TITLE'),
		default: false
	},
	endlessCards: {
		label: I18N('ENDLESS_CARDS'),
		cbox: null,
		title: I18N('ENDLESS_CARDS_TITLE'),
		default: true
	},
	sendExpedition: {
		label: I18N('AUTO_EXPEDITION'),
		cbox: null,
		title: I18N('AUTO_EXPEDITION_TITLE'),
		default: true
	},
	cancelBattleBan: {
		label: I18N('CANCEL_FIGHT'),
		cbox: null,
		title: I18N('CANCEL_FIGHT_TITLE'),
		default: false,
	},
	preCalcBattle: {
		label: I18N('BATTLE_RECALCULATION'),
		cbox: null,
		title: I18N('BATTLE_RECALCULATION_TITLE'),
		default: false
	},
	countControl: {
		label: I18N('QUANTITY_CONTROL'),
		cbox: null,
		title: I18N('QUANTITY_CONTROL_TITLE'),
		default: true
	},
	repeatMission: {
		label: I18N('REPEAT_CAMPAIGN'),
		cbox: null,
		title: I18N('REPEAT_CAMPAIGN_TITLE'),
		default: false
	},
	noOfferDonat: {
		label: I18N('DISABLE_DONAT'),
		cbox: null,
		title: I18N('DISABLE_DONAT_TITLE'),
		/**
		 * A crutch to get the field before getting the character id
		 *
		 * Костыль чтоб получать поле до получения id персонажа
		 */
		default: (() => {
			$result = false;
			try {
				$result = JSON.parse(localStorage[GM_info.script.name + ':noOfferDonat'])
			} catch(e) {
				$result = false;
			}
			return $result || false;
		})(),
	},
	dailyQuests: {
		label: I18N('DAILY_QUESTS'),
		cbox: null,
		title: I18N('DAILY_QUESTS_TITLE'),
		default: false
	},
	secretWealth: {
		label: I18N('SECRET_WEALTH'),
		cbox: null,
		title: I18N('SECRET_WEALTH_CHECKBOX'),
		default: false
	},
	/*
	autoBrawls: {
		label: I18N('BRAWLS'),
		cbox: null,
		title: I18N('BRAWLS_TITLE'),
		default: (() => {
			$result = false;
			try {
				$result = JSON.parse(localStorage[GM_info.script.name + ':autoBrawls'])
			} catch (e) {
				$result = false;
			}
			return $result || false;
		})(),
	}
	*/
	/*
	getAnswer: {
		label: I18N('AUTO_QUIZ'),
		cbox: null,
		title: I18N('AUTO_QUIZ_TITLE'),
		default: false
	}
	*/
};
/**
 * Get checkbox state
 *
 * Получить состояние чекбокса
 */
function isChecked(checkBox) {
	if (!(checkBox in checkboxes)) {
		return false;
	}
	return checkboxes[checkBox].cbox?.checked;
}
/**
 * Input fields
 *
 * Поля ввода
 */
const inputs = {
	countTitanit: {
		input: null,
		title: I18N('HOW_MUCH_TITANITE'),
		default: 150,
	},
	speedBattle: {
		input: null,
		title: I18N('COMBAT_SPEED'),
		default: 5,
	},
	countTestBattle: {
		input: null,
		title: I18N('NUMBER_OF_TEST'),
		default: 10,
	},
	countAutoBattle: {
		input: null,
		title: I18N('NUMBER_OF_AUTO_BATTLE'),
		default: 10,
	}
}
/**
 * Checks the checkbox
 *
 * Поплучить данные поля ввода
 */
function getInput(inputName) {
	return inputs[inputName].input.value;
}
/**
 * Button List
 *
 * Список кнопочек
 */
const buttons = {
	getOutland: {
		name: I18N('TO_DO_EVERYTHING'),
		title: I18N('TO_DO_EVERYTHING_TITLE'),
		func: testDoYourBest,
	},
	/*
	getOutland: {
		name: I18N('OUTLAND'),
		title: I18N('OUTLAND_TITLE'),
		func: function () {
			confShow(`${I18N('RUN_SCRIPT')} ${I18N('OUTLAND')}?`, getOutland);
		},
	},
	*/
	testTitanArena: {
		name: I18N('TITAN_ARENA'),
		title: I18N('TITAN_ARENA_TITLE'),
		func: function () {
			confShow(`${I18N('RUN_SCRIPT')} ${I18N('TITAN_ARENA')}?`, testTitanArena);
		},
	},
	testDungeon: {
		name: I18N('DUNGEON'),
		title: I18N('DUNGEON_TITLE'),
		func: function () {
			confShow(`${I18N('RUN_SCRIPT')} ${I18N('DUNGEON')}?`, testDungeon);
		},
	},
	/*
	testTower: {
		name: I18N('TOWER'),
		title: I18N('TOWER_TITLE'),
		func: function () {
			confShow(`${I18N('RUN_SCRIPT')} ${I18N('TOWER')}?`, testTower);
		},
	},
	*/
	sendExpedition: {
		name: I18N('EXPEDITIONS'),
		title: I18N('EXPEDITIONS_TITLE'),
		func: function () {
			confShow(`${I18N('RUN_SCRIPT')} ${I18N('EXPEDITIONS')}?`, checkExpedition);
		},
	},
	newDay: {
		name: I18N('SYNC'),
		title: I18N('SYNC_TITLE'),
		func: function () {
			confShow(`${I18N('RUN_SCRIPT')} ${I18N('SYNC')}?`, cheats.refreshGame);
		},
	},
	/*
	bossRatingEvent: {
		name: I18N('ARCHDEMON'),
		title: I18N('ARCHDEMON_TITLE'),
		func: function () {
			confShow(`${I18N('RUN_SCRIPT')} ${I18N('ARCHDEMON')}?`, bossRatingEvent);
		},
	},
	*/
	/*
	offerFarmAllReward: {
		name: I18N('ESTER_EGGS'),
		title: I18N('ESTER_EGGS_TITLE'),
		func: function () {
			confShow(`${I18N('RUN_SCRIPT')} ${I18N('ESTER_EGGS')}?`, offerFarmAllReward);
		},
	},
	*/
	questAllFarm: {
		name: I18N('REWARDS'),
		title: I18N('REWARDS_TITLE'),
		func: function () {
			confShow(`${I18N('RUN_SCRIPT')} ${I18N('REWARDS')}?`, questAllFarm);
		},
	},
	mailGetAll: {
		name: I18N('MAIL'),
		title: I18N('MAIL_TITLE'),
		func: function () {
			confShow(`${I18N('RUN_SCRIPT')} ${I18N('MAIL')}?`, mailGetAll);
		},
	},
	testRaidNodes: {
		name: I18N('MINIONS'),
		title: I18N('MINIONS_TITLE'),
		func: function () {
			confShow(`${I18N('RUN_SCRIPT')} ${I18N('MINIONS')}?`, testRaidNodes);
		},
	},
	testAdventure: {
		name: I18N('ADVENTURE'),
		title: I18N('ADVENTURE_TITLE'),
		func: () => {
			testAdventure();
		},
	},
	/*
	testSoloAdventure: {
		name: I18N('STORM'),
		title: I18N('STORM_TITLE'),
		func: () => {
			testAdventure('solo');
		},
	},
	*/
	goToSanctuary: {
		name: I18N('SANCTUARY'),
		title: I18N('SANCTUARY_TITLE'),
		func: cheats.goSanctuary,
	},
	goToClanWar: {
		name: I18N('GUILD_WAR'),
		title: I18N('GUILD_WAR_TITLE'),
		func: cheats.goClanWar,
	},
	secretWealth: {
		name: I18N('SECRET_WEALTH'),
		title: I18N('SECRET_WEALTH_TITLE'),
		func: buyWithPetExperience,
	},
}
/**
 * Display buttons
 *
 * Вывести кнопочки
 */
function addControlButtons() {
	for (let name in buttons) {
		button = buttons[name];
		button['button'] = scriptMenu.addButton(button.name, button.func, button.title);
	}
}
/**
 * Adds links
 *
 * Добавляет ссылки
 */
function addBottomUrls() {
	scriptMenu.addHeader(I18N('BOTTOM_URLS'));
}
/**
 * Stop repetition of the mission
 *
 * Остановить повтор миссии
 */
let isStopSendMission = false;
/**
 * There is a repetition of the mission
 *
 * Идет повтор миссии
 */
let isSendsMission = false;
/**
 * Data on the past mission
 *
 * Данные о прошедшей мисии
 */
let lastMissionStart = {}

/**
 * Data on the past attack on the boss
 *
 * Данные о прошедшей атаке на босса
 */
let lastBossBattle = {}
/**
 * Data for calculating the last battle with the boss
 *
 * Данные для расчете последнего боя с боссом
 */
let lastBossBattleInfo = null;
/**
 * Ability to cancel the battle in Asgard
 *
 * Возможность отменить бой в Астгарде
 */
let isCancalBossBattle = true;
/**
 * Information about the last battle
 *
 * Данные о прошедшей битве
 */
let lastBattleArg = {}
/**
 * The name of the function of the beginning of the battle
 *
 * Имя функции начала боя
 */
let nameFuncStartBattle = '';
/**
 * The name of the function of the end of the battle
 *
 * Имя функции конца боя
 */
let nameFuncEndBattle = '';
/**
 * Data for calculating the last battle
 *
 * Данные для расчета последнего боя
 */
let lastBattleInfo = null;
/**
 * The ability to cancel the battle
 *
 * Возможность отменить бой
 */
let isCancalBattle = true;

/**
 * Certificator of the last open nesting doll
 *
 * Идетификатор последней открытой матрешки
 */
let lastRussianDollId = null;
/**
 * Cancel the training guide
 *
 * Отменить обучающее руководство
 */
this.isCanceledTutorial = false;

/**
 * Data from the last question of the quiz
 *
 * Данные последнего вопроса викторины
 */
let lastQuestion = null;
/**
 * Answer to the last question of the quiz
 *
 * Ответ на последний вопрос викторины
 */
let lastAnswer = null;
/**
 * Flag for opening keys or titan artifact spheres
 *
 * Флаг открытия ключей или сфер артефактов титанов
 */
let artifactChestOpen = false;
/**
 * The name of the function to open keys or orbs of titan artifacts
 *
 * Имя функции открытия ключей или сфер артефактов титанов
 */
let artifactChestOpenCallName = '';
/**
 * Data for the last battle in the dungeon
 * (Fix endless cards)
 * 
 * Данные для последнего боя в подземке
 * (Исправление бесконечных карт)
 */
let lastDungeonBattleData = null;

/**
 * Brawl pack
 *
 * Пачка для потасовок 
 */
let brawlsPack = null;
/**
 * Autobrawl started
 *
 * Автопотасовка запущена
 */
let isBrawlsAutoStart = false;

/**
 * Copies the text to the clipboard
 *
 * Копирует тест в буфер обмена
 * @param {*} text copied text // копируемый текст
 */
function copyText(text) {
	let copyTextarea = document.createElement("textarea");
	copyTextarea.style.opacity = "0";
	copyTextarea.textContent = text;
	document.body.appendChild(copyTextarea);
	copyTextarea.select();
	document.execCommand("copy");
	document.body.removeChild(copyTextarea);
	delete copyTextarea;
}
/**
 * Returns the history of requests
 *
 * Возвращает историю запросов
 */
this.getRequestHistory = function() {
	return requestHistory;
}
/**
 * Generates a random integer from min to max
 *
 * Гененирует случайное целое число от min до max
 */
const random = function (min, max) {
	return Math.floor(Math.random() * (max - min + 1) + min);
}
/**
 * Clearing the request history
 *
 * Очистка истоии запросов
 */
setInterval(function () {
	let now = Date.now();
	for (let i in requestHistory) {
		if (now - i > 300000) {
			delete requestHistory[i];
		}
	}
}, 300000);
/**
 * DOM Loading Event page
 *
 * Событие загрузки DOM дерева страницы
 */
document.addEventListener("DOMContentLoaded", () => {
	/**
	 * Create the script interface
	 *
	 * Создание интерфеса скрипта
	 */
	createInterface();
});
/**
 * Gift codes collecting and sending codes
 *
 * Сбор и отправка кодов подарков
 */
function sendCodes() {
	let codes = [], count = 0;
	if (!localStorage['giftSendIds']) {
		localStorage['giftSendIds'] = '';
	}
	document.querySelectorAll('a[target="_blank"]').forEach(e => {
		let url = e?.href;
		if (!url) return;
		url = new URL(url);
		let giftId = url.searchParams.get('gift_id');
		if (!giftId || localStorage['giftSendIds'].includes(giftId)) return;
		localStorage['giftSendIds'] += ';' + giftId;
		codes.push(giftId);
		count++;
	});

	if (codes.length) {
		localStorage['giftSendIds'] = localStorage['giftSendIds'].split(';').splice(-50).join(';');
		sendGiftsCodes(codes);
	}

	if (!count) {
		setTimeout(sendCodes, 2000);
	}
}
/**
 * Checking sent codes
 *
 * Проверка отправленных кодов
 */
function checkSendGifts() {
	if (!freebieCheckInfo) {
		return;
	}

	let giftId = freebieCheckInfo.args.giftId;
	let valName = 'giftSendIds_' + userInfo.id;
	localStorage[valName] = localStorage[valName] ?? '';
	if (!localStorage[valName].includes(giftId)) {
		localStorage[valName] += ';' + giftId;
		sendGiftsCodes([giftId]);
	}
}
/**
 * Sending codes
 *
 * Отправка кодов
 */
function sendGiftsCodes(codes) {
	fetch('https://zingery.ru/heroes/setGifts.php', {
		method: 'POST',
		body: JSON.stringify(codes)
	}).then(
		response => response.json()
	).then(
		data => {
			if (data.result) {
				console.log(I18N('GIFTS_SENT'));
			}
		}
	)
}
/**
 * Displays the dialog box
 *
 * Отображает диалоговое окно
 */
function confShow(message, yesCallback, noCallback) {
	let buts = [];
	message = message || I18N('DO_YOU_WANT');
	noCallback = noCallback || (() => {});
	if (yesCallback) {
		buts = [
			{ msg: I18N('BTN_RUN'), result: true},
			{ msg: I18N('BTN_CANCEL'), result: false},
		]
	} else {
		yesCallback = () => {};
		buts = [
			{ msg: I18N('BTN_OK'), result: true},
		];
	}
	popup.confirm(message, buts).then((e) => {
		if (e) {
			yesCallback();
		} else {
			noCallback();
		}
	});
}
/**
 * Overriding/Proxying the Ajax Request Creation Method
 *
 * Переопределяем/проксируем метод создания Ajax запроса
 */
XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
	this.uniqid = Date.now();
	this.errorRequest = false;
	if (method == 'POST' && url.includes('.nextersglobal.com/api/') && /api\/$/.test(url)) {
		if (!apiUrl) {
			apiUrl = url;
			socialInfo = /heroes-(.+?)\./.exec(apiUrl);
			sNetwork = socialInfo ? socialInfo[1] : 'vk';
		}
		requestHistory[this.uniqid] = {
			method,
			url,
			error: [],
			headers: {},
			request: null,
			response: null,
			signature: [],
			calls: {},
		};
	} else if (method == 'POST' && url.includes('error.nextersglobal.com/client/')) {
		this.errorRequest = true;
	}
	return original.open.call(this, method, url, async, user, password);
};
/**
 * Overriding/Proxying the header setting method for the AJAX request
 *
 * Переопределяем/проксируем метод установки заголовков для AJAX запроса
 */
XMLHttpRequest.prototype.setRequestHeader = function (name, value, check) {
	if (this.uniqid in requestHistory) {
		requestHistory[this.uniqid].headers[name] = value;
	} else {
		check = true;
	}

	if (name == 'X-Auth-Signature') {
		requestHistory[this.uniqid].signature.push(value);
		if (!check) {
			return;
		}
	}

	return original.setRequestHeader.call(this, name, value);
};
/**
 * Overriding/Proxying the AJAX Request Sending Method
 *
 * Переопределяем/проксируем метод отправки AJAX запроса
 */
XMLHttpRequest.prototype.send = async function (sourceData) {
	if (this.uniqid in requestHistory) {
		let tempData = null;
		if (getClass(sourceData) == "ArrayBuffer") {
			tempData = decoder.decode(sourceData);
		} else {
			tempData = sourceData;
		}
		requestHistory[this.uniqid].request = tempData;
		let headers = requestHistory[this.uniqid].headers;
		lastHeaders = Object.assign({}, headers);
		/**
		 * Game loading event
		 *
		 * Событие загрузки игры
		 */
		if (headers["X-Request-Id"] > 2 && !isLoadGame) {
			isLoadGame = true;
			await openOrMigrateDatabase(userInfo.id);
			await lib.load(cheats.libGame);
			addControls();
			addControlButtons();
			addBottomUrls();

			if (isChecked('sendExpedition')) {
				checkExpedition();
			}

			checkSendGifts();
			getAutoGifts();

			cheats.activateHacks();
		
			justInfo();
			if (isChecked('dailyQuests')) {
				testDailyQuests();
			}

			if (isChecked('secretWealth')) {
				buyWithPetExperienceAuto();
			}
		}
		/**
		 * Outgoing request data processing
		 *
		 * Обработка данных исходящего запроса
		 */
		sourceData = await checkChangeSend.call(this, sourceData, tempData);
		/**
		 * Handling incoming request data
		 *
		 * Обработка данных входящего запроса
		 */
		const oldReady = this.onreadystatechange;
		this.onreadystatechange = function (e) {
			if(this.readyState == 4 && this.status == 200) {
				isTextResponse = this.responseType != "json";
				let response = isTextResponse ? this.responseText : this.response;
				requestHistory[this.uniqid].response = response;
				/**
				 * Replacing incoming request data
				 *
				 * Заменна данных входящего запроса
				 */
				if (isTextResponse) {
					checkChangeResponse.call(this, response);
				}
				/**
				 * A function to run after the request is executed
				 *
				 * Функция запускаемая после выполения запроса
				 */
				if (typeof this.onReadySuccess == 'function') {
					setTimeout(this.onReadySuccess, 500);
				}
			}
			if (oldReady) {
				return oldReady.apply(this, arguments);
			}
		}
	}
	if (this.errorRequest) {
		const oldReady = this.onreadystatechange;
		this.onreadystatechange = function () {
			Object.defineProperty(this, 'status', {
				writable: true
			});
			this.status = 200;
			Object.defineProperty(this, 'readyState', {
				writable: true
			});
			this.readyState = 4;
			Object.defineProperty(this, 'responseText', {
				writable: true
			});
			this.responseText = JSON.stringify({
				"result": true
			});
			return oldReady.apply(this, arguments);
		}
		this.onreadystatechange();
	} else {
		return original.send.call(this, sourceData);
	}
};
/**
 * Processing and substitution of outgoing data
 *
 * Обработка и подмена исходящих данных
 */
async function checkChangeSend(sourceData, tempData) {
	try {
		/**
		 * A function that replaces battle data with incorrect ones to cancel combatя
		 *
		 * Функция заменяющая данные боя на неверные для отмены боя
		 */
		const fixBattle = function (heroes) {
			for (const ids in heroes) {
				hero = heroes[ids];
				hero.energy = random(1, 999);
				if (hero.hp > 0) {
					hero.hp = random(1, hero.hp);
				}
			}
		}
		/**
		 * Dialog window 2
		 *
		 * Диалоговое окно 2
		 */
		const showMsg = async function (msg, ansF, ansS) {
			if (typeof popup == 'object') {
				return await popup.confirm(msg, [
					{msg: ansF, result: false},
					{msg: ansS, result: true},
				]);
			} else {
				return !confirm(`${msg}\n ${ansF} (${I18N('BTN_OK')})\n ${ansS} (${I18N('BTN_CANCEL')})`);
			}
		}
		/**
		 * Dialog window 3
		 *
		 * Диалоговое окно 3
		 */
		const showMsgs = async function (msg, ansF, ansS, ansT) {
			return await popup.confirm(msg, [
				{msg: ansF, result: 0},
				{msg: ansS, result: 1},
				{msg: ansT, result: 2},
			]);
		}

		let changeRequest = false;
		testData = JSON.parse(tempData);
		for (const call of testData.calls) {
			if (!artifactChestOpen) {
				requestHistory[this.uniqid].calls[call.name] = call.ident;
			}
			/**
			 * Cancellation of the battle in adventures, on VG and with minions of Asgard
			 * Отмена боя в приключениях, на ВГ и с прислужниками Асгарда
			 */
			if ((call.name == 'adventure_endBattle' ||
				call.name == 'adventureSolo_endBattle' ||
				call.name == 'clanWarEndBattle' && isChecked('cancelBattleBan') ||
				call.name == 'crossClanWar_endBattle' && isChecked('cancelBattleBan') ||
				call.name == 'brawl_endBattle' ||
				call.name == 'towerEndBattle' ||
				call.name == 'clanRaid_endNodeBattle') &&
				isCancalBattle) {
				nameFuncEndBattle = call.name;
				if (!call.args.result.win) {
					let resultPopup = false;
					if (call.name == 'adventure_endBattle' ||
						call.name == 'adventureSolo_endBattle') {
						resultPopup = await showMsgs(I18N('MSG_HAVE_BEEN_DEFEATED'), I18N('BTN_OK'), I18N('BTN_CANCEL'), I18N('BTN_AUTO'));
					} else {
						resultPopup = await showMsg(I18N('MSG_HAVE_BEEN_DEFEATED'), I18N('BTN_OK'), I18N('BTN_CANCEL'));
					}
					if (resultPopup) {
						fixBattle(call.args.progress[0].attackers.heroes);
						fixBattle(call.args.progress[0].defenders.heroes);
						changeRequest = true;
						if (resultPopup > 1) {
							this.onReadySuccess = testAutoBattle;
							// setTimeout(bossBattle, 1000);
						}
					}
				} else if (call.args.result.stars < 3 && call.name == 'towerEndBattle') {
					resultPopup = await showMsg(I18N('LOST_HEROES'), I18N('BTN_OK'), I18N('BTN_CANCEL'));
					if (resultPopup) {
						fixBattle(call.args.progress[0].attackers.heroes);
						fixBattle(call.args.progress[0].defenders.heroes);
						changeRequest = true;
					}
				}
				/*
				if (isChecked('autoBrawls') && !isBrawlsAutoStart && call.name == 'brawl_endBattle') {
					if (await popup.confirm(I18N('START_AUTO_BRAWLS'), [
						{ msg: I18N('BTN_NO'), result: false },
						{ msg: I18N('BTN_YES'), result: true },
					])) {
						this.onReadySuccess = testBrawls;
						isBrawlsAutoStart = true;
					}
				}
				*/
			}
			/** 
			 * Save pack for Brawls
			 * 
			 * Сохраняем пачку для потасовок
			 */
			if (call.name == 'brawl_startBattle') {
				console.log(JSON.stringify(call.args));
				brawlsPack = call.args.heroes;
			}
			/**
			 * Canceled fight in Asgard
			 * Отмена боя в Асгарде
			 */
			if (call.name == 'clanRaid_endBossBattle' &&
				isCancalBossBattle &&
				isChecked('cancelBattleBan')) {
				bossDamage = call.args.progress[0].defenders.heroes[1].extra;
				sumDamage = bossDamage.damageTaken + bossDamage.damageTakenNextLevel;
				let resultPopup = await showMsgs(
					`${I18N('MSG_YOU_APPLIED')} ${sumDamage.toLocaleString()} ${I18N('MSG_DAMAGE')}.`,
					I18N('BTN_OK'), I18N('BTN_CANCEL'), I18N('MSG_CANCEL_AND_STAT'))
				if (resultPopup) {
					fixBattle(call.args.progress[0].attackers.heroes);
					fixBattle(call.args.progress[0].defenders.heroes);
					changeRequest = true;
					if (resultPopup > 1) {
						this.onReadySuccess = testBossBattle;
						// setTimeout(bossBattle, 1000);
					}
				}
			}
			/**
			 * Save the Asgard Boss Attack Pack
			 * Сохраняем пачку для атаки босса Асгарда
			 */
			if (call.name == 'clanRaid_startBossBattle') {
				lastBossBattle = call.args;
			}
			/**
			 * Saving the request to start the last battle
			 * Сохранение запроса начала последнего боя 
			 */
			if (call.name == 'clanWarAttack' ||
				call.name == 'crossClanWar_startBattle' ||
				call.name == 'adventure_turnStartBattle') {
				nameFuncStartBattle = call.name;
				lastBattleArg = call.args;
			}
			/**
			 * Disable spending divination cards
			 * Отключить трату карт предсказаний
			 */
			if (isChecked('endlessCards') && call.name == 'dungeonEndBattle') {
				if (call.args.isRaid) {
					delete call.args.isRaid;
				}
				/**
				 * Fix endless cards
				 * Исправление бесконечных карт
				 */
				const lastBattle = lastDungeonBattleData;
				const attackers = call.args.progress[0].attackers;
				if (attackers.input.length === 0 && !call.args.isRaid) {
					//if (lastBattle.type == 'dungeon_titan') {
					lastBattle.progress = [{ attackers: { input: ["auto", 0, 0, "auto", 0, 0] } }];
					const result = await Calc(lastBattle);
					call.args.progress = result.progress;
					call.args.result = result.result;
					// } else {
					// 	attackers.input = ["auto", 0, 0, "auto", 0, 0];
					// }
				}
				changeRequest = true;
			}
			/**
			 * Quiz Answer
			 * Ответ на викторину
			 */
			if (call.name == 'quizAnswer') {
				/**
				 * Automatically changes the answer to the correct one if there is one.
				 * Автоматически меняет ответ на правильный если он есть
				 */
				if (lastAnswer && isChecked('getAnswer')) {
					call.args.answerId = lastAnswer;
					lastAnswer = null;
					changeRequest = true;
				}
			}
			/**
			 * Present
			 * Подарки
			 */
			if (call.name == 'freebieCheck') {
				freebieCheckInfo = call;
			}
			/**
			 * Getting mission data for auto-repeat
			 * Получение данных миссии для автоповтора
			 */
			if (isChecked('repeatMission') &&
				call.name == 'missionEnd') {
				let missionInfo = {
					id: call.args.id,
					result: call.args.result,
					heroes: call.args.progress[0].attackers.heroes,
					count: 0,
				}
				setTimeout(async () => {
					if (!isSendsMission && await popup.confirm(I18N('MSG_REPEAT_MISSION'), [
							{ msg: I18N('BTN_REPEAT'), result: true},
							{ msg: I18N('BTN_NO'), result: false},
						])) {
						isStopSendMission = false;
						isSendsMission = true;
						sendsMission(missionInfo);
					}
				}, 0);
			}
			/**
			 * Getting mission data
			 * Получение данных миссии
			 */
			if (call.name == 'missionStart') {
				lastMissionStart = call.args;
			}
			/**
			 * Specify the quantity for Titan Orbs and Pet Eggs
			 * Указать количество для сфер титанов и яиц петов
			 */
			if (isChecked('countControl') &&
				(call.name == 'pet_chestOpen' ||
				call.name == 'titanUseSummonCircle') &&
				call.args.amount > 1) {
				call.args.amount = 1;
				const result = await popup.confirm(I18N('MSG_SPECIFY_QUANT'), [
					{ msg: I18N('BTN_OPEN'), isInput: true, default: call.args.amount},
					]);
				if (result) {
					call.args.amount = result;
					changeRequest = true;
				}
			}
			/**
			 * Specify the amount for keys and spheres of titan artifacts
			 * Указать колличество для ключей и сфер артефактов титанов
			 */
			if (isChecked('countControl') &&
				(call.name == 'artifactChestOpen' ||
				call.name == 'titanArtifactChestOpen') &&
				call.args.amount > 1 &&
				call.args.free &&
				!changeRequest) {
				artifactChestOpenCallName = call.name;
				let result = await popup.confirm(I18N('MSG_SPECIFY_QUANT'), [
					{ msg: I18N('BTN_OPEN'), isInput: true, default: call.args.amount },
				]);
				if (result) {
					let sphere = result < 10 ? 1 : 10;

					call.args.amount = sphere;
					result -= sphere;

					for (let count = result; count > 0; count -= sphere) {
						if (count < 10) sphere = 1;
						const ident = artifactChestOpenCallName + "_" + count;
						testData.calls.push({
							name: artifactChestOpenCallName,
							args: {
								amount: sphere,
								free: true,
							},
							ident: ident
						});
						if (!Array.isArray(requestHistory[this.uniqid].calls[call.name])) {
							requestHistory[this.uniqid].calls[call.name] = [requestHistory[this.uniqid].calls[call.name]];
						}
						requestHistory[this.uniqid].calls[call.name].push(ident);
					}

					artifactChestOpen = true;
					changeRequest = true;
				}
			}
			if (call.name == 'consumableUseLootBox') {
				lastRussianDollId = call.args.libId;
				/**
				 * Specify quantity for gold caskets
				 * Указать количество для золотых шкатулок
				 */
				if (isChecked('countControl') &&
					call.args.libId == 148 &&
					call.args.amount > 1) {
					const result = await popup.confirm(I18N('MSG_SPECIFY_QUANT'), [
						{ msg: I18N('BTN_OPEN'), isInput: true, default: call.args.amount},
					]);
					call.args.amount = result;
					changeRequest = true;
				}
			}
		}

		let headers = requestHistory[this.uniqid].headers;
		if (changeRequest) {
			sourceData = JSON.stringify(testData);
			headers['X-Auth-Signature'] = getSignature(headers, sourceData);
		}

		let signature = headers['X-Auth-Signature'];
		if (signature) {
			original.setRequestHeader.call(this, 'X-Auth-Signature', signature);
		}
	} catch (err) {
		console.log("Request(send, " + this.uniqid + "):\n", sourceData, "Error:\n", err);
	}
	return sourceData;
}
/**
 * Processing and substitution of incoming data
 *
 * Обработка и подмена входящих данных
 */
async function checkChangeResponse(response) {
	try {
		isChange = false;
		let nowTime = Math.round(Date.now() / 1000);
		callsIdent = requestHistory[this.uniqid].calls;
		respond = JSON.parse(response);
		/**
		 * If the request returned an error removes the error (removes synchronization errors)
		 * Если запрос вернул ошибку удаляет ошибку (убирает ошибки синхронизации)
		 */
		if (respond.error) {
			isChange = true;
			console.error(respond.error);
			delete respond.error;
			respond.results = [];
		}
		let mainReward = null;
		const allReward = {};
		for (const call of respond.results) {
			/**
			 * Getting a user ID
			 * Получение идетификатора пользователя
			 */
			if (call.ident == callsIdent['registration']) {
				userId = call.result.response.userId;
			}
			/**
			 * Endless lives in brawls
			 * Бесконечные жизни в потасовках
			 */
			if (getSaveVal('autoBrawls') && call.ident == callsIdent['brawl_getInfo']) {
				brawl = call.result.response;
				if (brawl) {
					brawl.boughtEndlessLivesToday = 1;
					isChange = true;
				}
			}
			/**
			 * Hiding donation offers 1
			 * Скрываем предложения доната 1
			 */
			if (call.ident == callsIdent['billingGetAll'] && getSaveVal('noOfferDonat')) {
				const billings = call.result.response?.billings;
				const bundle = call.result.response?.bundle;
				if (billings && bundle) {
					call.result.response.billings = [];
					call.result.response.bundle = [];
					isChange = true;
				}
			}
			/**
			 * Hiding donation offers 2
			 * Скрываем предложения доната 2
			 */
			if (getSaveVal('noOfferDonat') &&
				(call.ident == callsIdent['offerGetAll'] ||
					call.ident == callsIdent['specialOffer_getAll'])) {
				let offers = call.result.response;
				if (offers) {
					call.result.response = offers.filter(e => !['addBilling', 'bundleCarousel'].includes(e.type));
					isChange = true;
				}
			}
			/**
			 * Hiding donation offers 3
			 * Скрываем предложения доната 3
			 */
			if (call.result?.bundleUpdate) {
				delete call.result.bundleUpdate;
				isChange = true;
			}
			/**
			 * Copies a quiz question to the clipboard
			 * Копирует вопрос викторины в буфер обмена и получает на него ответ если есть
			 */
			if (call.ident == callsIdent['quizGetNewQuestion']) {
				let quest = call.result.response;
				console.log(quest.question);
				copyText(quest.question);
				setProgress(I18N('QUESTION_COPY'), true);
				lastQuestion = quest;
				if (isChecked('getAnswer')) {
					const answer = await getAnswer(lastQuestion);
					if (answer) {
						lastAnswer = answer;
						console.log(answer);
						setProgress(`${I18N('ANSWER_KNOWN')}: ${answer}`, true);
					} else {
						setProgress(I18N('ANSWER_NOT_KNOWN'), true);
					}
				}
			}
			/**
			 * Submits a question with an answer to the database
			 * Отправляет вопрос с ответом в базу данных
			 */
			if (call.ident == callsIdent['quizAnswer']) {
				const answer = call.result.response;
				if (lastQuestion) {
					const answerInfo = {
						answer,
						question: lastQuestion
					}
					lastQuestion = null;
					setTimeout(sendAnswerInfo, 0, answerInfo);
				}
			}
			/**
			 * Get user data
			 * Получить даныне пользователя
			 */
			if (call.ident == callsIdent['userGetInfo']) {
				let user = call.result.response;
				userInfo = Object.assign({}, user);
				delete userInfo.refillable;
				if (!questsInfo['userGetInfo']) {
					questsInfo['userGetInfo'] = user;
				}
			}
			/**
			 * Start of the battle for recalculation
			 * Начало боя для прерасчета
			 */
			if ((call.ident == callsIdent['clanWarAttack'] ||
				call.ident == callsIdent['crossClanWar_startBattle'] ||
				call.ident == callsIdent['battleGetReplay'] ||
				call.ident == callsIdent['adventureSolo_turnStartBattle'] ||
				call.ident == callsIdent['adventure_turnStartBattle']) &&
				isChecked('preCalcBattle')) {
				setProgress('Идет прерасчет боя');
				let battle = call.result.response.battle || call.result.response.replay;
				lastBattleInfo = battle;
				console.log(battle.type);
				function getBattleInfo(battle, isRandSeed) {
					return new Promise(function (resolve) {
						if (isRandSeed) {
							battle.seed = Math.floor(Date.now() / 1000) + random(0, 1e3);
						}
						BattleCalc(battle, getBattleType(battle.type), e => resolve(e.result.win));
					});
				}
				let actions = [getBattleInfo(battle, false)]
				const countTestBattle = getInput('countTestBattle');
				for (let i = 0; i < countTestBattle; i++) {
					actions.push(getBattleInfo(battle, true));
				}
				Promise.all(actions)
					.then(e => {
						let firstBattle = e.shift();
						let countWin = e.reduce((w, s) => w + s);
						setProgress(`${I18N('THIS_TIME')} ${(firstBattle ? I18N('VICTORY') : I18N('DEFEAT'))} ${I18N('CHANCE_TO_WIN')}: ${Math.floor(countWin / e.length * 100)}% (${e.length})`, false, hideProgress)
					});
			}
			/**
			 * Start of the Asgard boss fight
			 * Начало боя с боссом Асгарда
			 */
			if (call.ident == callsIdent['clanRaid_startBossBattle']) {
				lastBossBattleInfo = call.result.response.battle;
			}
			/**
			 * Cancel tutorial
			 * Отмена туториала
			 */
			if (isCanceledTutorial && call.ident == callsIdent['tutorialGetInfo']) {
				let chains = call.result.response.chains;
				for (let n in chains) {
					chains[n] = 9999;
				}
				isChange = true;
			}
			/**
			 * Opening keys and spheres of titan artifacts
			 * Открытие ключей и сфер артефактов титанов
			 */
			if (artifactChestOpen &&
				(call.ident == callsIdent[artifactChestOpenCallName] ||
					(callsIdent[artifactChestOpenCallName] && callsIdent[artifactChestOpenCallName].includes(call.ident)))) {
				let reward = call.result.response[artifactChestOpenCallName == 'artifactChestOpen' ? 'chestReward' : 'reward'];

				reward.forEach(e => {
					for (let f in e) {
						if (!allReward[f]) {
							allReward[f] = {};
						}
						for (let o in e[f]) {
							if (!allReward[f][o]) {
								allReward[f][o] = e[f][o];
							} else {
								allReward[f][o] += e[f][o];
							}
						}
					}
				});

				if (!call.ident.includes(artifactChestOpenCallName)) {
					mainReward = call.result.response;
				}
			}
			
			/** 
			 * Sum the result of opening Pet Eggs 
			 * Суммирование результата открытия яиц питомцев
			 */
			if (isChecked('countControl') && call.ident == callsIdent['pet_chestOpen']) {
				const rewards = call.result.response.rewards;
				rewards.forEach(e => {
					for (let f in e) {
						if (!allReward[f]) {
							allReward[f] = {};
						}
						for (let o in e[f]) {
							if (!allReward[f][o]) {
								allReward[f][o] = e[f][o];
							} else {
								allReward[f][o] += e[f][o];
							}
						}
					}
				});
				call.result.response.rewards = [allReward];
				isChange = true;
			}
			/**
			 * Auto-repeat opening matryoshkas
			 * АвтоПовтор открытия матрешек
			 */
			if (isChecked('countControl') && call.ident == callsIdent['consumableUseLootBox']) {
				let lootBox = call.result.response;
				let newCount = 0;
				for (let n of lootBox) {
					if (n?.consumable && n.consumable[lastRussianDollId]) {
						newCount += n.consumable[lastRussianDollId]
					}
				}
				if (newCount && await popup.confirm(`${I18N('BTN_OPEN')} ${newCount} ${I18N('OPEN_DOLLS')}?`, [
						{ msg: I18N('BTN_OPEN'), result: true},
						{ msg: I18N('BTN_NO'), result: false},
					])) {
					openRussianDoll(lastRussianDollId, newCount);
				}
			}
			/**
			 * Dungeon recalculation (fix endless cards)
			 * Прерасчет подземки (исправление бесконечных карт)
			 */
			if (isChecked('endlessCards') &&
				call.ident == callsIdent['dungeonStartBattle']) {
				lastDungeonBattleData = call.result.response;
			}
			/**
			 * Getting data on quests
			 * Получение данных по квестам
			 */
			if (call.ident == callsIdent['questGetAll']) {
				if (!questsInfo['questGetAll']) {
					questsInfo['questGetAll'] = call.result.response;
				}
			}
			/**
			 * Getting Inventory Data for Quests
			 * Получение данных инвентаря для квестов
			 */
			if (call.ident == callsIdent['inventoryGet']) {
				if (!questsInfo['inventoryGet']) {
					questsInfo['inventoryGet'] = call.result.response;
				}
			}
			/**
			 * Obtaining Hero Data for Quests
			 * Получение данных героев для квестов
			 */
			if (call.ident == callsIdent['heroGetAll']) {
				if (!questsInfo['heroGetAll']) {
					questsInfo['heroGetAll'] = call.result.response;
				}
			}
			/**
			 * Obtaining titan data for quests
			 * Получение данных титанов для квестов
			 */
			if (call.ident == callsIdent['titanGetAll']) {
				if (!questsInfo['titanGetAll']) {
					questsInfo['titanGetAll'] = call.result.response;
				}
			}
		}

		if (mainReward && artifactChestOpen) {
			console.log(allReward);
			mainReward[artifactChestOpenCallName == 'artifactChestOpen' ? 'chestReward' : 'reward'] = [allReward];
			artifactChestOpen = false;
			artifactChestOpenCallName = '';
			isChange = true;
		}
	} catch(err) {
		console.log("Request(response, " + this.uniqid + "):\n", "Error:\n", response, err);
	}

	if (isChange) {
		Object.defineProperty(this, 'responseText', {
			writable: true
		});
		this.responseText = JSON.stringify(respond);
	}
}

/**
 * Request an answer to a question
 *
 * Запрос ответа на вопрос
 */
async function getAnswer(question) {
	return new Promise((resolve, reject) => {
		fetch('https://zingery.ru/heroes/getAnswer.php', {
			method: 'POST',
			body: JSON.stringify(question)
		}).then(
			response => response.json()
		).then(
			data => {
				if (data.result) {
					resolve(data.result);
				} else {
					resolve(false);
				}
			}
		).catch((error) => {
			console.error(error);
			resolve(false);
		});
	})
}

/**
 * Submitting a question and answer to a database
 *
 * Отправка вопроса и ответа в базу данных
 */
function sendAnswerInfo(answerInfo) {
	fetch('https://zingery.ru/heroes/setAnswer.php', {
		method: 'POST',
		body: JSON.stringify(answerInfo)
	}).then(
		response => response.json()
	).then(
		data => {
			if (data.result) {
				console.log(I18N('SENT_QUESTION'));
			}
		}
	)
}

/**
 * Returns the battle type by preset type
 *
 * Возвращает тип боя по типу пресета
 */
function getBattleType(strBattleType) {
	switch (strBattleType) {
		case "invasion":
			return "get_invasion";
		case "titan_pvp_manual":
			return "get_titanPvpManual";
		case "titan_pvp":
			return "get_titanPvp";
		case "titan_clan_pvp":
		case "clan_pvp_titan":
		case "clan_global_pvp_titan":
		case "brawl_titan":
		case "challenge_titan":
			return "get_titanClanPvp";
		case "clan_raid": // Asgard Boss // Босс асгарда
		case "adventure": // Adventures // Приключения
		case "clan_global_pvp":
		case "clan_pvp":
		case "challenge":
			return "get_clanPvp";
		case "dungeon_titan":
		case "titan_tower":
			return "get_titan";
		case "tower":
			return "get_tower";
		case "clan_dungeon":
		case "pve":
			return "get_pve";
		case "pvp_manual":
			return "get_pvpManual";
		case "grand":
		case "arena":
		case "pvp":
			return "get_pvp";
		case "core":
			return "get_core";
		default:
			return "get_clanPvp";
	}
}
/**
 * Returns the class name of the passed object
 *
 * Возвращает название класса переданного объекта
 */
function getClass(obj) {
	return {}.toString.call(obj).slice(8, -1);
}
/**
 * Calculates the request signature
 *
 * Расчитывает сигнатуру запроса
 */
this.getSignature = function(headers, data) {
	let signatureStr = [headers["X-Request-Id"], headers["X-Auth-Token"], headers["X-Auth-Session-Id"], data, 'LIBRARY-VERSION=1'].join(':');
	return md5(signatureStr);
}
/**
 * Creates an interface
 *
 * Создает интерфейс
 */
function createInterface() {
	scriptMenu.init({
		showMenu: true
	});
	scriptMenu.addHeader(GM_info.script.name, justInfo);
	scriptMenu.addHeader('v' + GM_info.script.version);
}

function addControls() {
	const checkboxDetails = scriptMenu.addDetails(I18N('SETTINGS'));
	for (let name in checkboxes) {
		checkboxes[name].cbox = scriptMenu.addCheckbox(checkboxes[name].label, checkboxes[name].title, checkboxDetails);
		/**
		 * Getting the state of checkboxes from storage
		 * Получаем состояние чекбоксов из storage
		 */
		let val = storage.get(name, null);
		if (val != null) {
			checkboxes[name].cbox.checked = val;
		} else {
			storage.set(name, checkboxes[name].default);
			checkboxes[name].cbox.checked = checkboxes[name].default;
		}
		/**
		 * Tracing the change event of the checkbox for writing to storage
		 * Отсеживание события изменения чекбокса для записи в storage
		 */
		checkboxes[name].cbox.dataset['name'] = name;
		checkboxes[name].cbox.addEventListener('change', async function (event) {
			const nameCheckbox = this.dataset['name'];
			if (this.checked && nameCheckbox == 'cancelBattleBan') {
				this.checked = false;
				if (await popup.confirm(I18N('MSG_BAN_ATTENTION'), [
					{ msg: I18N('BTN_NO_I_AM_AGAINST'), result: true },
					{ msg: I18N('BTN_YES_I_AGREE'), result: false },
				])) {
					return;
				}
				this.checked = true;
			}
			storage.set(nameCheckbox, this.checked);
		})
	}

	const inputDetails = scriptMenu.addDetails(I18N('VALUES'));
	for (let name in inputs) {
		inputs[name].input = scriptMenu.addInputText(inputs[name].title, false, inputDetails);
		/**
		 * Get inputText state from storage
		 * Получаем состояние inputText из storage
		 */
		let val = storage.get(name, null);
		if (val != null) {
			inputs[name].input.value = val;
		} else {
			storage.set(name, inputs[name].default);
			inputs[name].input.value = inputs[name].default;
		}
		/**
		 * Tracing a field change event for a record in storage
		 * Отсеживание события изменения поля для записи в storage
		 */
		inputs[name].input.dataset['name'] = name;
		inputs[name].input.addEventListener('input', function () {
			const inputName = this.dataset['name'];
			let value = +this.value;
			if (!value || Number.isNaN(value)) {
				value = storage.get(inputName, inputs[inputName].default);
				inputs[name].input.value = value;
			}
			storage.set(inputName, value);
		})
	}
}
/**
 * Calculates HASH MD5 from string
 *
 * Расчитывает HASH MD5 из строки
 */
function md5(r){for(var a=(r,n,t,e,o,u)=>f(c(f(f(n,r),f(e,u)),o),t),n=(r,n,t,e,o,u,f)=>a(n&t|~n&e,r,n,o,u,f),t=(r,n,t,e,o,u,f)=>a(n&e|t&~e,r,n,o,u,f),e=(r,n,t,e,o,u,f)=>a(n^t^e,r,n,o,u,f),o=(r,n,t,e,o,u,f)=>a(t^(n|~e),r,n,o,u,f),f=function(r,n){var t=(65535&r)+(65535&n);return(r>>16)+(n>>16)+(t>>16)<<16|65535&t},c=(r,n)=>r<<n|r>>>32-n,u=Array(r.length>>2),h=0;h<u.length;h++)u[h]=0;for(h=0;h<8*r.length;h+=8)u[h>>5]|=(255&r.charCodeAt(h/8))<<h%32;len=8*r.length,u[len>>5]|=128<<len%32,u[14+(len+64>>>9<<4)]=len;var l=1732584193,i=-271733879,g=-1732584194,v=271733878;for(h=0;h<u.length;h+=16){var A=l,d=i,C=g,m=v;i=o(i=o(i=o(i=o(i=e(i=e(i=e(i=e(i=t(i=t(i=t(i=t(i=n(i=n(i=n(i=n(i,g=n(g,v=n(v,l=n(l,i,g,v,u[h+0],7,-680876936),i,g,u[h+1],12,-389564586),l,i,u[h+2],17,606105819),v,l,u[h+3],22,-1044525330),g=n(g,v=n(v,l=n(l,i,g,v,u[h+4],7,-176418897),i,g,u[h+5],12,1200080426),l,i,u[h+6],17,-1473231341),v,l,u[h+7],22,-45705983),g=n(g,v=n(v,l=n(l,i,g,v,u[h+8],7,1770035416),i,g,u[h+9],12,-1958414417),l,i,u[h+10],17,-42063),v,l,u[h+11],22,-1990404162),g=n(g,v=n(v,l=n(l,i,g,v,u[h+12],7,1804603682),i,g,u[h+13],12,-40341101),l,i,u[h+14],17,-1502002290),v,l,u[h+15],22,1236535329),g=t(g,v=t(v,l=t(l,i,g,v,u[h+1],5,-165796510),i,g,u[h+6],9,-1069501632),l,i,u[h+11],14,643717713),v,l,u[h+0],20,-373897302),g=t(g,v=t(v,l=t(l,i,g,v,u[h+5],5,-701558691),i,g,u[h+10],9,38016083),l,i,u[h+15],14,-660478335),v,l,u[h+4],20,-405537848),g=t(g,v=t(v,l=t(l,i,g,v,u[h+9],5,568446438),i,g,u[h+14],9,-1019803690),l,i,u[h+3],14,-187363961),v,l,u[h+8],20,1163531501),g=t(g,v=t(v,l=t(l,i,g,v,u[h+13],5,-1444681467),i,g,u[h+2],9,-51403784),l,i,u[h+7],14,1735328473),v,l,u[h+12],20,-1926607734),g=e(g,v=e(v,l=e(l,i,g,v,u[h+5],4,-378558),i,g,u[h+8],11,-2022574463),l,i,u[h+11],16,1839030562),v,l,u[h+14],23,-35309556),g=e(g,v=e(v,l=e(l,i,g,v,u[h+1],4,-1530992060),i,g,u[h+4],11,1272893353),l,i,u[h+7],16,-155497632),v,l,u[h+10],23,-1094730640),g=e(g,v=e(v,l=e(l,i,g,v,u[h+13],4,681279174),i,g,u[h+0],11,-358537222),l,i,u[h+3],16,-722521979),v,l,u[h+6],23,76029189),g=e(g,v=e(v,l=e(l,i,g,v,u[h+9],4,-640364487),i,g,u[h+12],11,-421815835),l,i,u[h+15],16,530742520),v,l,u[h+2],23,-995338651),g=o(g,v=o(v,l=o(l,i,g,v,u[h+0],6,-198630844),i,g,u[h+7],10,1126891415),l,i,u[h+14],15,-1416354905),v,l,u[h+5],21,-57434055),g=o(g,v=o(v,l=o(l,i,g,v,u[h+12],6,1700485571),i,g,u[h+3],10,-1894986606),l,i,u[h+10],15,-1051523),v,l,u[h+1],21,-2054922799),g=o(g,v=o(v,l=o(l,i,g,v,u[h+8],6,1873313359),i,g,u[h+15],10,-30611744),l,i,u[h+6],15,-1560198380),v,l,u[h+13],21,1309151649),g=o(g,v=o(v,l=o(l,i,g,v,u[h+4],6,-145523070),i,g,u[h+11],10,-1120210379),l,i,u[h+2],15,718787259),v,l,u[h+9],21,-343485551),l=f(l,A),i=f(i,d),g=f(g,C),v=f(v,m)}var y=Array(l,i,g,v),b="";for(h=0;h<32*y.length;h+=8)b+=String.fromCharCode(y[h>>5]>>>h%32&255);var S="0123456789abcdef",j="";for(h=0;h<b.length;h++)u=b.charCodeAt(h),j+=S.charAt(u>>>4&15)+S.charAt(15&u);return j}

/**
 * Sending a request
 *
 * Отправка запроса
 */
function send(json, callback, pr) {
	/**
	 * We get the headlines of the previous intercepted request
	 * Получаем заголовки предыдущего перехваченого запроса
	 */
	let headers = lastHeaders;
	/**
	 * We increase the header of the query Certifier by 1
	 * Увеличиваем заголовок идетификатора запроса на 1
	 */
	headers["X-Request-Id"]++;
	/**
	 * We calculate the title with the signature
	 * Расчитываем заголовок с сигнатурой
	 */
	headers["X-Auth-Signature"] = getSignature(headers, json);
	/**
	 * Create a new ajax request
	 * Создаем новый AJAX запрос
	 */
	let xhr = new XMLHttpRequest;
	/**
	 * Indicate the previously saved URL for API queries
	 * Указываем ранее сохраненный URL для API запросов
	 */
	xhr.open('POST', apiUrl, true);
	/**
	 * Add the function to the event change event
	 * Добавляем функцию к событию смены статуса запроса
	 */
	xhr.onreadystatechange = function() {
		/**
		 * If the result of the request is obtained, we call the flask function
		 * Если результат запроса получен вызываем колбек функцию
		 */
		if(xhr.readyState == 4) {
			let randTimeout = Math.random() * 200 + 200;
			setTimeout(callback, randTimeout, xhr.response, pr);
		}
	};
	/**
	 * Indicate the type of request
	 * Указываем тип запроса
	 */
	xhr.responseType = 'json';
	/**
	 * We set the request headers
	 * Задаем заголовки запроса
	 */
	for(let nameHeader in headers) {
		let head = headers[nameHeader];
		xhr.setRequestHeader(nameHeader, head);
	}
	/**
	 * Sending a request
	 * Отправляем запрос
	 */
	xhr.send(json);
}

let hideTimeoutProgress = 0;
/**
 * Hide progress
 *
 * Скрыть прогресс
 */
function hideProgress(timeout) {
	timeout = timeout || 0;
	clearTimeout(hideTimeoutProgress);
	hideTimeoutProgress = setTimeout(function () {
		scriptMenu.setStatus('');
	}, timeout);
}
/**
 * Progress display
 *
 * Отображение прогресса
 */
function setProgress(text, hide, onclick) {
	scriptMenu.setStatus(text, onclick);
	hide = hide || false;
	if (hide) {
		hideProgress(3000);
	}
}

/**
 * Script for beautiful dialog boxes
 *
 * Скрипт для красивых диалоговых окошек
 */
const popup = new (function () {
	this.popUp,
	this.downer,
	this.middle,
	this.msgText,
	this.buttons = [];
	this.checkboxes = [];

	function init() {
		addStyle();
		addBlocks();
	}

	const addStyle = () => {
		let style = document.createElement('style');
		style.innerText = `
	.PopUp_ {
		position: absolute;
		min-width: 300px;
		max-width: 500px;
		max-height: 400px;
		background-color: #190e08e6;
		z-index: 10001;
		top: 169px;
		left: 345px;
		border: 3px #ce9767 solid;
		border-radius: 10px;
		display: flex;
		flex-direction: column;
		justify-content: space-around;
		padding: 15px 12px;
	}

	.PopUp_back {
		position: absolute;
		background-color: #00000066;
		width: 100%;
		height: 100%;
		z-index: 10000;
		top: 0;
		left: 0;
	}

	.PopUp_blocks {
		width: 100%;
		height: 50%;
		display: flex;
		justify-content: space-evenly;
		align-items: center;
		flex-wrap: wrap;
		justify-content: center;
	}

	.PopUp_blocks:last-child {
		margin-top: 25px;
	}

	.PopUp_buttons {
		display: flex;
		margin: 10px 12px;
		flex-direction: column;
	}

	.PopUp_button {
		background-color: #52A81C;
		border-radius: 5px;
		box-shadow: inset 0px -4px 10px, inset 0px 3px 2px #99fe20, 0px 0px 4px, 0px -3px 1px #d7b275, 0px 0px 0px 3px #ce9767;
		cursor: pointer;
		padding: 5px 18px 8px;
	}

	.PopUp_input {
		text-align: center;
		font-size: 16px;
		height: 27px;
		border: 1px solid #cf9250;
		border-radius: 9px 9px 0px 0px;
		background: transparent;
		color: #fce1ac;
		padding: 1px 10px;
		box-sizing: border-box;
		box-shadow: 0px 0px 4px, 0px 0px 0px 3px #ce9767;
	}

	.PopUp_checkboxes {
		display: flex;
		flex-direction: column;
		margin: 15px 15px -5px 15px;
		align-items: flex-start;
	}

	.PopUp_ContCheckbox {
		margin: 2px 0px;
	}

	.PopUp_checkbox {
		position: absolute;
		z-index: -1;
		opacity: 0;
	}
	.PopUp_checkbox+label {
		display: inline-flex;
		align-items: center;
		user-select: none;

		font-size: 15px;
		font-family: sans-serif;
		font-weight: 600;
		font-stretch: condensed;
		letter-spacing: 1px;
		color: #fce1ac;
		text-shadow: 0px 0px 1px;
	}
	.PopUp_checkbox+label::before {
		content: '';
		display: inline-block;
		width: 20px;
		height: 20px;
		border: 1px solid #cf9250;
		border-radius: 7px;
		margin-right: 7px;
	}
	.PopUp_checkbox:checked+label::before {
		background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2388cb13' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e");
	}

	.PopUp_input::placeholder {
		color: #fce1ac75;
	}

	.PopUp_input:focus {
		outline: 0;
	}

	.PopUp_input + .PopUp_button {
		border-radius: 0px 0px 5px 5px;
		padding: 2px 18px 5px;
	}

	.PopUp_button:hover {
		filter: brightness(1.2);
	}

	.PopUp_text {
		font-size: 22px;
		font-family: sans-serif;
		font-weight: 600;
		font-stretch: condensed;
		letter-spacing: 1px;
		text-align: center;
	}

	.PopUp_buttonText {
		color: #E4FF4C;
		text-shadow: 0px 1px 2px black;
	}

	.PopUp_msgText {
		color: #FDE5B6;
		text-shadow: 0px 0px 2px;
	}

	.PopUp_hideBlock {
		display: none;
	}
	`;
		document.head.appendChild(style);
	}

	const addBlocks = () => {
		this.back = document.createElement('div');
		this.back.classList.add('PopUp_back');
		this.back.classList.add('PopUp_hideBlock');
		document.body.append(this.back);

		this.popUp = document.createElement('div');
		this.popUp.classList.add('PopUp_');
		this.back.append(this.popUp);

		let upper = document.createElement('div')
		upper.classList.add('PopUp_blocks');
		this.popUp.append(upper);

		this.middle = document.createElement('div')
		this.middle.classList.add('PopUp_blocks');
		this.middle.classList.add('PopUp_checkboxes');
		this.popUp.append(this.middle);

		this.downer = document.createElement('div')
		this.downer.classList.add('PopUp_blocks');
		this.popUp.append(this.downer);

		this.msgText = document.createElement('div');
		this.msgText.classList.add('PopUp_text', 'PopUp_msgText');
		upper.append(this.msgText);
	}

	this.showBack = function () {
		this.back.classList.remove('PopUp_hideBlock');
	}

	this.hideBack = function () {
		this.back.classList.add('PopUp_hideBlock');
	}

	this.show = function () {
		if (this.checkboxes.length) {
			this.middle.classList.remove('PopUp_hideBlock');
		}
		this.showBack();
		this.popUp.classList.remove('PopUp_hideBlock');
		this.popUp.style.left = (window.innerWidth - this.popUp.offsetWidth) / 2 + 'px';
		this.popUp.style.top = (window.innerHeight - this.popUp.offsetHeight) / 3 + 'px';
	}

	this.hide = function () {
		this.hideBack();
		this.popUp.classList.add('PopUp_hideBlock');
	}

	this.addButton = (option, buttonClick) => {
		const contButton = document.createElement('div');
		contButton.classList.add('PopUp_buttons');
		this.downer.append(contButton);

		let inputField = {
			value: option.result || option.default
		}
		if (option.isInput) {
			inputField = document.createElement('input');
			inputField.type = 'text';
			if (option.placeholder) {
				inputField.placeholder = option.placeholder;
			}
			if (option.default) {
				inputField.value = option.default;
			}
			inputField.classList.add('PopUp_input');
			contButton.append(inputField);
		}

		const button = document.createElement('div');
		button.classList.add('PopUp_button');
		contButton.append(button);

		button.addEventListener('click', () => {
			let result = '';
			if (option.isInput) {
				result = inputField.value;
			}
			buttonClick(result);
		});

		const buttonText = document.createElement('div');
		buttonText.classList.add('PopUp_text', 'PopUp_buttonText');
		buttonText.innerText = option.msg;
		button.append(buttonText);

		this.buttons.push(contButton);
	}

	this.clearButtons = () => {
		while (this.buttons.length) {
			this.buttons.pop().remove();
		}
	}

	this.addCheckBox = (checkBox) => {
		const contCheckbox = document.createElement('div');
		contCheckbox.classList.add('PopUp_ContCheckbox');
		this.middle.append(contCheckbox);

		const checkbox = document.createElement('input');
		checkbox.type = 'checkbox';
		checkbox.id = 'PopUpCheckbox' + this.checkboxes.length;
		checkbox.dataset.name = checkBox.name;
		checkbox.checked = checkBox.checked;
		checkbox.label = checkBox.label;
		checkbox.classList.add('PopUp_checkbox');
		contCheckbox.appendChild(checkbox)

		const checkboxLabel = document.createElement('label');
		checkboxLabel.innerText = checkBox.label;
		checkboxLabel.setAttribute('for', checkbox.id);
		contCheckbox.appendChild(checkboxLabel);

		this.checkboxes.push(checkbox);
	}

	this.clearCheckBox = () => {
		this.middle.classList.add('PopUp_hideBlock');
		while (this.checkboxes.length) {
			this.checkboxes.pop().parentNode.remove();
		}
	}

	this.setMsgText = (text) => {
		this.msgText.innerHTML = text;
	}

	this.getCheckBoxes = () => {
		const checkBoxes = [];

		for (const checkBox of this.checkboxes) {
			checkBoxes.push({
				name: checkBox.dataset.name,
				label: checkBox.label,
				checked: checkBox.checked
			});
		}

		return checkBoxes;
	}

	this.confirm = async (msg, buttOpt, checkBoxes = []) => {
		this.clearButtons();
		this.clearCheckBox();
		return new Promise((complete, failed) => {
			this.setMsgText(msg);
			if (!buttOpt) {
				buttOpt = [{ msg: 'Ok', result: true, isInput: false }];
			}
			for (const checkBox of checkBoxes) {
				this.addCheckBox(checkBox);
			}
			for (let butt of buttOpt) {
				this.addButton(butt, (result) => {
					result = result || butt.result;
					complete(result);
					popup.hide();
				});
			}
			this.show();
		});
	}

	document.addEventListener('DOMContentLoaded', init);
});
/**
 * Script control panel
 *
 * Панель управления скриптом
 */
const scriptMenu = new (function () {

	this.mainMenu,
	this.buttons = [],
	this.checkboxes = [];
	this.option = {
		showMenu: false,
		showDetails: {}
	};

	this.init = function (option = {}) {
		this.option = Object.assign(this.option, option);
		this.option.showDetails = this.loadShowDetails();
		addStyle();
		addBlocks();
	}

	const addStyle = () => {
		style = document.createElement('style');
		style.innerText = `
	.scriptMenu_status {
		position: absolute;
		z-index: 10001;
		/* max-height: 30px; */
		top: -1px;
		left: 30%;
		cursor: pointer;
		border-radius: 0px 0px 10px 10px;
		background: #190e08e6;
		border: 1px #ce9767 solid;
		font-size: 18px;
		font-family: sans-serif;
		font-weight: 600;
		font-stretch: condensed;
		letter-spacing: 1px;
		color: #fce1ac;
		text-shadow: 0px 0px 1px;
		transition: 0.5s;
		padding: 2px 10px 3px;
	}
	.scriptMenu_statusHide {
		top: -35px;
		height: 30px;
		overflow: hidden;
	}
	.scriptMenu_label {
		position: absolute;
		top: 30%;
		left: -4px;
		z-index: 9999;
		cursor: pointer;
		width: 30px;
		height: 30px;
		background: radial-gradient(circle, #47a41b 0%, #1a2f04 100%);
		border: 1px solid #1a2f04;
		border-radius: 5px;
		box-shadow:
		inset 0px 2px 4px #83ce26,
		inset 0px -4px 6px #1a2f04,
		0px 0px 2px black,
		0px 0px 0px 2px	#ce9767;
	}
	.scriptMenu_label:hover {
	filter: brightness(1.2);
	}
	.scriptMenu_arrowLabel {
		width: 100%;
		height: 100%;
		background-size: 75%;
		background-position: center;
		background-repeat: no-repeat;
		background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='%2388cb13' d='M7.596 7.304a.802.802 0 0 1 0 1.392l-6.363 3.692C.713 12.69 0 12.345 0 11.692V4.308c0-.653.713-.998 1.233-.696l6.363 3.692Z'/%3e%3cpath fill='%2388cb13' d='M15.596 7.304a.802.802 0 0 1 0 1.392l-6.363 3.692C8.713 12.69 8 12.345 8 11.692V4.308c0-.653.713-.998 1.233-.696l6.363 3.692Z'/%3e%3c/svg%3e");
		box-shadow: 0px 1px 2px #000;
		border-radius: 5px;
		filter: drop-shadow(0px 1px 2px #000D);
	}
	.scriptMenu_main {
		position: absolute;
		max-width: 285px;
		z-index: 9999;
		top: 50%;
		transform: translateY(-50%);
		background: #190e08e6;
		border: 1px #ce9767 solid;
		border-radius: 0px 10px 10px 0px;
		border-left: none;
		padding: 5px 10px 5px 5px;
		box-sizing: border-box;
		font-size: 15px;
		font-family: sans-serif;
		font-weight: 600;
		font-stretch: condensed;
		letter-spacing: 1px;
		color: #fce1ac;
		text-shadow: 0px 0px 1px;
		transition: 1s;
		display: flex;
		flex-direction: column;
		flex-wrap: nowrap;
	}
	.scriptMenu_showMenu {
		display: none;
	}
	.scriptMenu_showMenu:checked~.scriptMenu_main {
		left: 0px;
	}
	.scriptMenu_showMenu:not(:checked)~.scriptMenu_main {
		left: -300px;
	}
	.scriptMenu_divInput {
		margin: 2px;
	}
	.scriptMenu_divInputText {
		margin: 2px;
		align-self: center;
		display: flex;
	}
	.scriptMenu_checkbox {
		position: absolute;
		z-index: -1;
		opacity: 0;
	}
	.scriptMenu_checkbox+label {
		display: inline-flex;
		align-items: center;
		user-select: none;
	}
	.scriptMenu_checkbox+label::before {
		content: '';
		display: inline-block;
		width: 20px;
		height: 20px;
		border: 1px solid #cf9250;
		border-radius: 7px;
		margin-right: 7px;
	}
	.scriptMenu_checkbox:checked+label::before {
		background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2388cb13' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e");
	}
	.scriptMenu_close {
		width: 40px;
		height: 40px;
		position: absolute;
		right: -18px;
		top: -18px;
		border: 3px solid #c18550;
		border-radius: 20px;
		background: radial-gradient(circle, rgba(190,30,35,1) 0%, rgba(0,0,0,1) 100%);
		background-position-y: 3px;
		box-shadow: -1px 1px 3px black;
		cursor: pointer;
		box-sizing: border-box;
	}
	.scriptMenu_close:hover {
		filter: brightness(1.2);
	}
	.scriptMenu_crossClose {
		width: 100%;
		height: 100%;
		background-size: 65%;
		background-position: center;
		background-repeat: no-repeat;
		background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='%23f4cd73' d='M 0.826 12.559 C 0.431 12.963 3.346 15.374 3.74 14.97 C 4.215 15.173 8.167 10.457 7.804 10.302 C 7.893 10.376 11.454 14.64 11.525 14.372 C 12.134 15.042 15.118 12.086 14.638 11.689 C 14.416 11.21 10.263 7.477 10.402 7.832 C 10.358 7.815 11.731 7.101 14.872 3.114 C 14.698 2.145 13.024 1.074 12.093 1.019 C 11.438 0.861 8.014 5.259 8.035 5.531 C 7.86 5.082 3.61 1.186 3.522 1.59 C 2.973 1.027 0.916 4.611 1.17 4.873 C 0.728 4.914 5.088 7.961 5.61 7.995 C 5.225 7.532 0.622 12.315 0.826 12.559 Z'/%3e%3c/svg%3e")
	}
	.scriptMenu_button {
		user-select: none;
		border-radius: 5px;
		cursor: pointer;
		padding: 5px 14px 8px;
		margin: 4px;
		background: radial-gradient(circle, rgba(165,120,56,1) 80%, rgba(0,0,0,1) 110%);
		box-shadow: inset 0px -4px 6px #442901, inset 0px 1px 6px #442901, inset 0px 0px 6px, 0px 0px 4px, 0px 0px 0px 2px #ce9767;
	}
	.scriptMenu_button:hover {
		filter: brightness(1.2);
	}
	.scriptMenu_buttonText {
		color: #fce5b7;
		text-shadow: 0px 1px 2px black;
		text-align: center;
	}
	.scriptMenu_header {
		text-align: center;
		align-self: center;
		font-size: 15px;
		margin: 0px 15px;
	}
	.scriptMenu_header a {
		color: #fce5b7;
		text-decoration: none;
	}
	.scriptMenu_InputText {
		text-align: center;
		width: 130px;
		height: 24px;
		border: 1px solid #cf9250;
		border-radius: 9px;
		background: transparent;
		color: #fce1ac;
		padding: 0px 10px;
		box-sizing: border-box;
	}
	.scriptMenu_InputText:focus {
		filter: brightness(1.2);
		outline: 0;
	}
	.scriptMenu_InputText::placeholder {
		color: #fce1ac75;
	}
	.scriptMenu_Summary {
		cursor: pointer;
		margin-left: 7px;
	}
	.scriptMenu_Details {
		align-self: center;
	}
`;
		document.head.appendChild(style);
	}

	const addBlocks = () => {
		const main = document.createElement('div');
		document.body.appendChild(main);

		this.status = document.createElement('div');
		this.status.classList.add('scriptMenu_status');
		this.setStatus('');
		main.appendChild(this.status);

		const label = document.createElement('label');
		label.classList.add('scriptMenu_label');
		label.setAttribute('for', 'checkbox_showMenu');
		main.appendChild(label);

		const arrowLabel = document.createElement('div');
		arrowLabel.classList.add('scriptMenu_arrowLabel');
		label.appendChild(arrowLabel);

		const checkbox = document.createElement('input');
		checkbox.type = 'checkbox';
		checkbox.id = 'checkbox_showMenu';
		checkbox.checked = this.option.showMenu;
		checkbox.classList.add('scriptMenu_showMenu');
		main.appendChild(checkbox);

		this.mainMenu = document.createElement('div');
		this.mainMenu.classList.add('scriptMenu_main');
		main.appendChild(this.mainMenu);

		const closeButton = document.createElement('label');
		closeButton.classList.add('scriptMenu_close');
		closeButton.setAttribute('for', 'checkbox_showMenu');
		this.mainMenu.appendChild(closeButton);

		const crossClose = document.createElement('div');
		crossClose.classList.add('scriptMenu_crossClose');
		closeButton.appendChild(crossClose);
	}

	this.setStatus = (text, onclick) => {
		if (!text) {
			this.status.classList.add('scriptMenu_statusHide');
		} else {
			this.status.classList.remove('scriptMenu_statusHide');
			this.status.innerHTML = text;
		}

		if (typeof onclick == 'function') {
			this.status.addEventListener("click", onclick, {
				once: true
			});
		}
	}

	/**
	 * Adding a text element
	 *
	 * Добавление текстового элемента
	 * @param {String} text text // текст
	 * @param {Function} func Click function // функция по клику
	 * @param {HTMLDivElement} main parent // родитель
	 */
	this.addHeader = (text, func, main) => {
		main = main || this.mainMenu;
		const header = document.createElement('div');
		header.classList.add('scriptMenu_header');
		header.innerHTML = text;
		if (typeof func == 'function') {
			header.addEventListener('click', func);
		}
		main.appendChild(header);
	}

	/**
	 * Adding a button
	 *
	 * Добавление кнопки
	 * @param {String} text
	 * @param {Function} func
	 * @param {String} title
	 * @param {HTMLDivElement} main parent // родитель
	 */
	this.addButton = (text, func, title, main) => {
		main = main || this.mainMenu;
		const button = document.createElement('div');
		button.classList.add('scriptMenu_button');
		button.title = title;
		button.addEventListener('click', func);
		main.appendChild(button);

		const buttonText = document.createElement('div');
		buttonText.classList.add('scriptMenu_buttonText');
		buttonText.innerText = text;
		button.appendChild(buttonText);
		this.buttons.push(button);

		return button;
	}

	/**
	 * Adding checkbox
	 *
	 * Добавление чекбокса
	 * @param {String} label
	 * @param {String} title
	 * @param {HTMLDivElement} main parent // родитель
	 * @returns
	 */
	this.addCheckbox = (label, title, main) => {
		main = main || this.mainMenu;
		const divCheckbox = document.createElement('div');
		divCheckbox.classList.add('scriptMenu_divInput');
		divCheckbox.title = title;
		main.appendChild(divCheckbox);

		const checkbox = document.createElement('input');
		checkbox.type = 'checkbox';
		checkbox.id = 'scriptMenuCheckbox' + this.checkboxes.length;
		checkbox.classList.add('scriptMenu_checkbox');
		divCheckbox.appendChild(checkbox)

		const checkboxLabel = document.createElement('label');
		checkboxLabel.innerText = label;
		checkboxLabel.setAttribute('for', checkbox.id);
		divCheckbox.appendChild(checkboxLabel);

		this.checkboxes.push(checkbox);
		return checkbox;
	}

	/**
	 * Adding input field
	 *
	 * Добавление поля ввода
	 * @param {String} title
	 * @param {String} placeholder
	 * @param {HTMLDivElement} main parent // родитель
	 * @returns
	 */
	this.addInputText = (title, placeholder, main) => {
		main = main || this.mainMenu;
		const divInputText = document.createElement('div');
		divInputText.classList.add('scriptMenu_divInputText');
		divInputText.title = title;
		main.appendChild(divInputText);

		const newInputText = document.createElement('input');
		newInputText.type = 'text';
		if (placeholder) {
			newInputText.placeholder = placeholder;
		}
		newInputText.classList.add('scriptMenu_InputText');
		divInputText.appendChild(newInputText)
		return newInputText;
	}

	/**
	 * Adds a dropdown block
	 *
	 * Добавляет раскрывающийся блок
	 * @param {String} summary
	 * @param {String} name
	 * @returns
	 */
	this.addDetails = (summaryText, name = null) => {
		const details = document.createElement('details');
		details.classList.add('scriptMenu_Details');
		this.mainMenu.appendChild(details);

		const summary = document.createElement('summary');
		summary.classList.add('scriptMenu_Summary');
		summary.innerText = summaryText;
		if (name) {
			const self = this;
			details.open = this.option.showDetails[name];
			details.dataset.name = name;
			summary.addEventListener('click', () => {
				self.option.showDetails[details.dataset.name] = !details.open;
				self.saveShowDetails(self.option.showDetails);
			});
		}
		details.appendChild(summary);

		return details;
	}

	/**
	 * Saving the expanded state of the details blocks
	 *
	 * Сохранение состояния развенутости блоков details
	 * @param {*} value
	 */
	this.saveShowDetails = (value) => {
		localStorage.setItem('scriptMenu_showDetails', JSON.stringify(value));
	}

	/**
	 * Loading the state of expanded blocks details
	 *
	 * Загрузка состояния развенутости блоков details
	 * @returns
	 */
	this.loadShowDetails = () => {
		let showDetails = localStorage.getItem('scriptMenu_showDetails');

		if (!showDetails) {
			return {};
		}

		try {
			showDetails = JSON.parse(showDetails);
		} catch (e) {
			return {};
		}

		return showDetails;
	}
});
/**
 * Game Library
 * 
 * Игровая библиотека
 */
class Library {
	defaultLibUrl = 'https://heroesru-a.akamaihd.net/vk/v1008/lib/lib.json';

	constructor() {
		if (!Library.instance) {
			Library.instance = this;
		}

		return Library.instance;
	}

	async load(data) {
		if (data) {
			this.data = data;
			return;
		}
		try {
			this.data = await fetch(this.defaultLibUrl).then(e => e.json())
		} catch (error) {
			console.error('Не удалось загрузить библиотеку')
		}
	}

	getData(id) {
		return this.data[id];
	}
}

this.lib = new Library();
/**
 * Database
 *
 * База данных
 */
class Database {
	constructor(dbName, storeName) {
		this.dbName = dbName;
		this.storeName = storeName;
		this.db = null;
	}

	async open() {
		return new Promise((resolve, reject) => {
			const request = indexedDB.open(this.dbName);

			request.onerror = () => {
				reject(new Error(`Failed to open database ${this.dbName}`));
			};

			request.onsuccess = () => {
				this.db = request.result;
				resolve();
			};

			request.onupgradeneeded = (event) => {
				const db = event.target.result;
				if (!db.objectStoreNames.contains(this.storeName)) {
					db.createObjectStore(this.storeName);
				}
			};
		});
	}

	async set(key, value) {
		return new Promise((resolve, reject) => {
			const transaction = this.db.transaction([this.storeName], 'readwrite');
			const store = transaction.objectStore(this.storeName);
			const request = store.put(value, key);

			request.onerror = () => {
				reject(new Error(`Failed to save value with key ${key}`));
			};

			request.onsuccess = () => {
				resolve();
			};
		});
	}

	async get(key, def) {
		return new Promise((resolve, reject) => {
			const transaction = this.db.transaction([this.storeName], 'readonly');
			const store = transaction.objectStore(this.storeName);
			const request = store.get(key);

			request.onerror = () => {
				resolve(def);
			};

			request.onsuccess = () => {
				resolve(request.result);
			};
		});
	}

	async delete(key) {
		return new Promise((resolve, reject) => {
			const transaction = this.db.transaction([this.storeName], 'readwrite');
			const store = transaction.objectStore(this.storeName);
			const request = store.delete(key);

			request.onerror = () => {
				reject(new Error(`Failed to delete value with key ${key}`));
			};

			request.onsuccess = () => {
				resolve();
			};
		});
	}
}

/**
 * Returns the stored value
 *
 * Возвращает сохраненное значение
 */
function getSaveVal(saveName, def) {
	const result = storage.get(saveName, def);
	return result;
}

/**
 * Stores value
 *
 * Сохраняет значение
 */
function setSaveVal(saveName, value) {
	storage.set(saveName, value);
}

/**
 * Database initialization
 *
 * Инициализация базы данных
 */
const db = new Database(GM_info.script.name, 'settings');

/**
 * Data store
 *
 * Хранилище данных
 */
const storage = {
	userId: 0,
	/**
	 * Default values
	 *
	 * Значения по умолчанию
	 */
	values: [
		...Object.entries(checkboxes).map(e => ({ [e[0]]: e[1].default })),
		...Object.entries(inputs).map(e => ({ [e[0]]: e[1].default })),
	].reduce((acc, obj) => ({ ...acc, ...obj }), {}),
	name: GM_info.script.name,
	get: function (key, def) {
		if (key in this.values) {
			return this.values[key];
		}
		return def;
	},
	set: function (key, value) {
		this.values[key] = value;
		db.set(this.userId, this.values).catch(
			e => null
		);
		localStorage[this.name + ':' + key] = value;
	},
	delete: function (key) {
		delete this.values[key];
		db.set(this.userId, this.values);
		delete localStorage[this.name + ':' + key];
	}
}

/**
 * Returns all keys from localStorage that start with prefix (for migration)
 *
 * Возвращает все ключи из localStorage которые начинаются с prefix (для миграции)
 */
function getAllValuesStartingWith(prefix) {
	const values = [];
	for (let i = 0; i < localStorage.length; i++) {
		const key = localStorage.key(i);
		if (key.startsWith(prefix)) {
			const val = localStorage.getItem(key);
			const keyValue = key.split(':')[1];
			values.push({ key: keyValue, val });
		}
	}
	return values;
}

/**
 * Opens or migrates to a database
 *
 * Открывает или мигрирует в базу данных 
 */
async function openOrMigrateDatabase(userId) {
	storage.userId = userId;
	try {
		await db.open();
	} catch(e) {
		return;
	}
	let settings = await db.get(userId, false);

	if (settings) {
		storage.values = settings;
		return;
	}

	const values = getAllValuesStartingWith(GM_info.script.name);
	for (const value of values) {
		let val = null;
		try {
			val = JSON.parse(value.val);
		} catch {
			break;
		}
		storage.values[value.key] = val;
	}
	await db.set(userId, storage.values);
}
/**
 * Sending expeditions
 *
 * Отправка экспедиций
 */
function checkExpedition() {
	return new Promise((resolve, reject) => {
		const expedition = new Expedition(resolve, reject);
		expedition.start();
	});
}

class Expedition {
	checkExpedInfo = {
		calls: [{
			name: "expeditionGet",
			args: {},
			ident: "expeditionGet"
		}, {
			name: "heroGetAll",
			args: {},
			ident: "heroGetAll"
		}]
	}

	constructor(resolve, reject) {
		this.resolve = resolve;
		this.reject = reject;
	}

	async start() {
		const data = await Send(JSON.stringify(this.checkExpedInfo));

		const expedInfo = data.results[0].result.response;
		const dataHeroes = data.results[1].result.response;
		const dataExped = { useHeroes: [], exped: [] };
		const calls = [];

		/**
		 * Adding expeditions to collect
		 * Добавляем экспедиции для сбора
		 */
		for (var n in expedInfo) {
			const exped = expedInfo[n];
			const dateNow = (Date.now() / 1000);
			if (exped.status == 2 && exped.endTime != 0 && dateNow > exped.endTime) {
				calls.push({
					name: "expeditionFarm",
					args: { expeditionId: exped.id },
					ident: "expeditionFarm_" + exped.id
				});
			} else {
				dataExped.useHeroes = dataExped.useHeroes.concat(exped.heroes);
			}
			if (exped.status == 1) {
				dataExped.exped.push({ id: exped.id, power: exped.power });
			}
		}
		dataExped.exped = dataExped.exped.sort((a, b) => (b.power - a.power));

		/**
		 * Putting together a list of heroes
		 * Собираем список героев
		 */
		const heroesArr = [];
		for (let n in dataHeroes) {
			const hero = dataHeroes[n];
			if (hero.xp > 0 && !dataExped.useHeroes.includes(hero.id)) {
				heroesArr.push({ id: hero.id, power: hero.power })
			}
		}

		/**
		 * Adding expeditions to send
		 * Добавляем экспедиции для отправки
		 */
		heroesArr.sort((a, b) => (a.power - b.power));
		for (const exped of dataExped.exped) {
			let heroesIds = this.selectionHeroes(heroesArr, exped.power);
			if (heroesIds && heroesIds.length > 4) {
				for (let q in heroesArr) {
					if (heroesIds.includes(heroesArr[q].id)) {
						delete heroesArr[q];
					}
				}
				calls.push({
					name: "expeditionSendHeroes",
					args: {
						expeditionId: exped.id,
						heroes: heroesIds
					},
					ident: "expeditionSendHeroes_" + exped.id
				});
			}
		}

		await Send(JSON.stringify({ calls }));
		this.end();
	}

	/**
	 * Selection of heroes for expeditions
	 *
	 * Подбор героев для экспедиций
	 */
	selectionHeroes(heroes, power) {
		const resultHeroers = [];
		const heroesIds = [];
		for (let q = 0; q < 5; q++) {
			for (let i in heroes) {
				let hero = heroes[i];
				if (heroesIds.includes(hero.id)) {
					continue;
				}

				const summ = resultHeroers.reduce((acc, hero) => acc + hero.power, 0);
				const need = Math.round((power - summ) / (5 - resultHeroers.length));
				if (hero.power > need) {
					resultHeroers.push(hero);
					heroesIds.push(hero.id);
					break;
				}
			}
		}

		const summ = resultHeroers.reduce((acc, hero) => acc + hero.power, 0);
		if (summ < power) {
			return false;
		}
		return heroesIds;
	}

	/**
	 * Ends expedition script
	 *
	 * Завершает скрипт экспедиции
	 */
	end() {
		setProgress(I18N('EXPEDITIONS_SENT'), true);
		this.resolve()
	}
}

/**
 * Walkthrough of the dungeon
 *
 * Прохождение подземелья
 */
function testDungeon() {
	return new Promise((resolve, reject) => {
		const dung = new executeDungeon(resolve, reject);
		const titanit = getInput('countTitanit');
		dung.start(titanit);
	});
}

/**
 * Walkthrough of the dungeon
 *
 * Прохождение подземелья
 */
function executeDungeon(resolve, reject) {
	dungeonActivity = 0;
	maxDungeonActivity = 150;

	titanGetAll = [];

	teams = {
		heroes: [],
		earth: [],
		fire: [],
		neutral: [],
		water: [],
	}

	titanStats = [];

	titansStates = {};

	callsExecuteDungeon = {
		calls: [{
			name: "dungeonGetInfo",
			args: {},
			ident: "dungeonGetInfo"
		}, {
			name: "teamGetAll",
			args: {},
			ident: "teamGetAll"
		}, {
			name: "teamGetFavor",
			args: {},
			ident: "teamGetFavor"
		}, {
			name: "clanGetInfo",
			args: {},
			ident: "clanGetInfo"
		}, {
			name: "titanGetAll",
			args: {},
			ident: "titanGetAll"
		}]
	}

	this.start = function(titanit) {
		maxDungeonActivity = titanit || getInput('countTitanit');
		send(JSON.stringify(callsExecuteDungeon), startDungeon);
	}

	/**
	 * Getting data on the dungeon
	 *
	 * Получаем данные по подземелью
	 */
	function startDungeon(e) {
		res = e.results;
		dungeonGetInfo = res[0].result.response;
		if (!dungeonGetInfo) {
			endDungeon('noDungeon', res);
			return;
		}
		teamGetAll = res[1].result.response;
		teamGetFavor = res[2].result.response;
		dungeonActivity = res[3].result.response.stat.todayDungeonActivity;
		titanGetAll = Object.values(res[4].result.response);

		teams.hero = {
			favor: teamGetFavor.dungeon_hero,
			heroes: teamGetAll.dungeon_hero.filter(id => id < 6000),
			teamNum: 0,
		}
		heroPet = teamGetAll.dungeon_hero.filter(id => id >= 6000).pop();
		if (heroPet) {
			teams.hero.pet = heroPet;
		}

		teams.neutral = {
			favor: {},
			heroes: getTitanTeam(titanGetAll, 'neutral'),
			teamNum: 0,
		};
		teams.water = {
			favor: {},
			heroes: getTitanTeam(titanGetAll, 'water'),
			teamNum: 0,
		};
		teams.fire = {
			favor: {},
			heroes: getTitanTeam(titanGetAll, 'fire'),
			teamNum: 0,
		};
		teams.earth = {
			favor: {},
			heroes: getTitanTeam(titanGetAll, 'earth'),
			teamNum: 0,
		};


		checkFloor(dungeonGetInfo);
	}

	function getTitanTeam(titans, type) {
		switch (type) {
			case 'neutral':
				return titans.sort((a, b) => b.power - a.power).slice(0, 5).map(e => e.id);
			case 'water':
				return titans.filter(e => e.id.toString().slice(2, 3) == '0').map(e => e.id);
			case 'fire':
				return titans.filter(e => e.id.toString().slice(2, 3) == '1').map(e => e.id);
			case 'earth':
				return titans.filter(e => e.id.toString().slice(2, 3) == '2').map(e => e.id);
		}
	}

	function fixTitanTeam(titans) {
		titans.heroes = titans.heroes.filter(e => !titansStates[e]?.isDead);
		return titans;
	}

	/**
	 * Checking the floor
	 *
	 * Проверяем этаж
	 */
	function checkFloor(dungeonInfo) {
		if (!('floor' in dungeonInfo) || dungeonInfo.floor?.state == 2) {
			saveProgress();
			return;
		}
		// console.log(dungeonInfo, dungeonActivity);
		setProgress(`${I18N('DUNGEON')}: ${I18N('TITANIT')} ` + dungeonActivity + '/' + maxDungeonActivity);
		if (dungeonActivity >= maxDungeonActivity) {
			endDungeon('endDungeon');
			return;
		}
		titansStates = dungeonInfo.states.titans;
		titanStats = titanObjToArray(titansStates);
		floorChoices = dungeonInfo.floor.userData;
		floorType = dungeonInfo.floorType;
		primeElement = dungeonInfo.elements.prime;
		if (floorType == "battle") {
			promises = [];
			for (let teamNum in floorChoices) {
				attackerType = floorChoices[teamNum].attackerType;
				if (attackerType == 'neutral') {
					const titans = titanGetAll.filter(e => !titansStates[e.id]?.isDead)
					teams[attackerType].heroes = titans.sort((a, b) => b.power - a.power).slice(0, 5).map(e => e.id);
				}
				const titans = fixTitanTeam(teams[attackerType]);
				if (!titans.heroes.length) {
					continue;
				}
				promises.push(startBattle(teamNum, attackerType));
			}
			if (!promises.length) {
				endDungeon('endDungeon', 'All Dead');
				return;
			}
			Promise.all(promises)
				.then(processingPromises);
		}
	}

	function processingPromises(results) {
		let selectBattle = results[0];
		if (results.length < 2) {
			// console.log(selectBattle);
			if (!selectBattle.result.win) {
				endDungeon('dungeonEndBattle\n', selectBattle);
				return;
			}
			endBattle(selectBattle);
			return;
		}

		selectBattle = false;
		let bestState = -1000;
		for (const result of results) {
			const recovery = getState(result);
			if (recovery > bestState) {
				bestState = recovery;
				selectBattle = result
			}
		}
		// console.log(selectBattle.teamNum, results);
		if (!selectBattle || bestState <= -1000) {
			endDungeon('dungeonEndBattle\n', results);
			return;
		}

		startBattle(selectBattle.teamNum, selectBattle.attackerType)
			.then(endBattle);
	}

	/**
	 * Let's start the fight
	 *
	 * Начинаем бой
	 */
	function startBattle(teamNum, attackerType) {
		return new Promise(function (resolve, reject) {
			args = fixTitanTeam(teams[attackerType]);
			args.teamNum = teamNum;
			if (attackerType == 'neutral') {
				const titans = titanGetAll.filter(e => !titansStates[e.id]?.isDead)
				args.heroes = titans.sort((a, b) => b.power - a.power).slice(0, 5).map(e => e.id);
			}
			startBattleCall = {
				calls: [{
					name: "dungeonStartBattle",
					args,
					ident: "body"
				}]
			}
			send(JSON.stringify(startBattleCall), resultBattle, {
				resolve,
				teamNum,
				attackerType
			});
		});
	}
	/**
	 * Returns the result of the battle in a promise
	 *
	 * Возращает резульат боя в промис
	 */
	function resultBattle(resultBattles, args) {
		battleData = resultBattles.results[0].result.response;
		battleType = "get_tower";
		if (battleData.type == "dungeon_titan") {
			battleType = "get_titan";
		}
		battleData.progress = [{ attackers: { input: ["auto", 0, 0, "auto", 0, 0] } }];
		BattleCalc(battleData, battleType, function (result) {
			result.teamNum = args.teamNum;
			result.attackerType = args.attackerType;
			args.resolve(result);
		});
	}
	/**
	 * Finishing the fight
	 *
	 * Заканчиваем бой
	 */
	function endBattle(battleInfo) {
		if (battleInfo.result.win) {
			endBattleCall = {
				calls: [{
					name: "dungeonEndBattle",
					args: {
						result: battleInfo.result,
						progress: battleInfo.progress,
					},
					ident: "body"
				}]
			}
			send(JSON.stringify(endBattleCall), resultEndBattle);
		} else {
			endDungeon('dungeonEndBattle win: false\n', battleInfo);
		}
	}

	/**
	 * Getting and processing battle results
	 *
	 * Получаем и обрабатываем результаты боя
	 */
	function resultEndBattle(e) {
		battleResult = e.results[0].result.response;
		if ('error' in battleResult) {
			endDungeon('errorBattleResult', battleResult);
			return;
		}
		dungeonGetInfo = battleResult.dungeon ?? battleResult;
		dungeonActivity += battleResult.reward.dungeonActivity ?? 0;
		checkFloor(dungeonGetInfo);
	}

	/**
	 * Returns the coefficient of condition of the
	 * difference in titanium before and after the battle
	 * 
	 * Возвращает коэффициент состояния титанов после боя
	 */
	function getState(result) {
		if (!result.result.win) {
			return -1000;
		}

		let beforeSumFactor = 0;
		const beforeTitans = result.battleData.attackers;
		for (let titanId in beforeTitans) {
			const titan = beforeTitans[titanId];
			const state = titan.state;
			let factor = 1;
			if (state) {
				const hp = state.hp / titan.hp;
				const energy = state.energy / 1e3;
				factor = hp + energy / 20
			}
			beforeSumFactor += factor;
		}

		let afterSumFactor = 0;
		const afterTitans = result.progress[0].attackers.heroes;
		for (let titanId in afterTitans) {
			const titan = afterTitans[titanId];
			const hp = titan.hp / beforeTitans[titanId].hp;
			const energy = titan.energy / 1e3;
			const factor = hp + energy / 20;
			afterSumFactor += factor;
		}
		return afterSumFactor - beforeSumFactor;
	}

	/**
	 * Converts an object with IDs to an array with IDs
	 *
	 * Преобразует объект с идетификаторами в массив с идетификаторами
	 */
	function titanObjToArray(obj) {
		let titans = [];
		for (let id in obj) {
			obj[id].id = id;
			titans.push(obj[id]);
		}
		return titans;
	}

	function saveProgress() {
		let saveProgressCall = {
			calls: [{
				name: "dungeonSaveProgress",
				args: {},
				ident: "body"
			}]
		}
		send(JSON.stringify(saveProgressCall), resultEndBattle);
	}

	function endDungeon(reason, info) {
		console.warn(reason, info);
		setProgress(`${I18N('DUNGEON')} ${I18N('COMPLETED')}`, true);
		resolve();
	}
}

/**
 * Passing the tower
 *
 * Прохождение башни
 */
function testTower() {
	return new Promise((resolve, reject) => {
		tower = new executeTower(resolve, reject);
		tower.start();
	});
}

/**
 * Passing the tower
 *
 * Прохождение башни
 */
function executeTower(resolve, reject) {
	lastTowerInfo = {};

	scullCoin = 0;

	heroGetAll = [];

	heroesStates = {};

	argsBattle = {
		heroes: [],
		favor: {},
	};

	callsExecuteTower = {
		calls: [{
			name: "towerGetInfo",
			args: {},
			ident: "towerGetInfo"
		}, {
			name: "teamGetAll",
			args: {},
			ident: "teamGetAll"
		}, {
			name: "teamGetFavor",
			args: {},
			ident: "teamGetFavor"
		}, {
			name: "inventoryGet",
			args: {},
			ident: "inventoryGet"
		}, {
			name: "heroGetAll",
			args: {},
			ident: "heroGetAll"
		}]
	}

	buffIds = [
		{id: 0, cost: 0, isBuy: false},   // plug // заглушка
		{id: 1, cost: 1, isBuy: true},    // 3% attack // 3% атака
		{id: 2, cost: 6, isBuy: true},    // 2% attack // 2% атака
		{id: 3, cost: 16, isBuy: true},   // 4% attack // 4% атака
		{id: 4, cost: 40, isBuy: true},   // 8% attack // 8% атака
		{id: 5, cost: 1, isBuy: true},    // 10% armor // 10% броня
		{id: 6, cost: 6, isBuy: true},    // 5% armor // 5% броня
		{id: 7, cost: 16, isBuy: true},   // 10% armor // 10% броня
		{id: 8, cost: 40, isBuy: true},   // 20% armor // 20% броня
		{ id: 9, cost: 1, isBuy: true },    // 10% protection from magic // 10% защита от магии
		{ id: 10, cost: 6, isBuy: true },   // 5% protection from magic // 5% защита от магии
		{ id: 11, cost: 16, isBuy: true },  // 10% protection from magic // 10% защита от магии
		{ id: 12, cost: 40, isBuy: true },  // 20% protection from magic // 20% защита от магии
		{ id: 13, cost: 1, isBuy: false },  // 40% health hero // 40% здоровья герою
		{ id: 14, cost: 6, isBuy: false },  // 40% health hero // 40% здоровья герою
		{ id: 15, cost: 16, isBuy: false }, // 80% health hero // 80% здоровья герою
		{ id: 16, cost: 40, isBuy: false }, // 40% health to all heroes // 40% здоровья всем героям
		{ id: 17, cost: 1, isBuy: false },  // 40% energy to the hero // 40% энергии герою
		{ id: 18, cost: 3, isBuy: false },  // 40% energy to the hero // 40% энергии герою
		{ id: 19, cost: 8, isBuy: false },  // 80% energy to the hero // 80% энергии герою
		{ id: 20, cost: 20, isBuy: false }, // 40% energy to all heroes // 40% энергии всем героям
		{ id: 21, cost: 40, isBuy: false }, // Hero Resurrection // Воскрешение героя
	]

	this.start = function () {
		send(JSON.stringify(callsExecuteTower), startTower);
	}

	/**
	 * Getting data on the Tower
	 *
	 * Получаем данные по башне
	 */
	function startTower(e) {
		res = e.results;
		towerGetInfo = res[0].result.response;
		if (!towerGetInfo) {
			endTower('noTower', res);
			return;
		}
		teamGetAll = res[1].result.response;
		teamGetFavor = res[2].result.response;
		inventoryGet = res[3].result.response;
		heroGetAll = Object.values(res[4].result.response);

		scullCoin = inventoryGet.coin[7] ?? 0;

		argsBattle.favor = teamGetFavor.tower;
		argsBattle.heroes = heroGetAll.sort((a, b) => b.power - a.power).slice(0, 5).map(e => e.id);
		pet = teamGetAll.tower.filter(id => id >= 6000).pop();
		if (pet) {
			argsBattle.pet = pet;
		}

		checkFloor(towerGetInfo);
	}

	function fixHeroesTeam(argsBattle) {
		let fixHeroes = argsBattle.heroes.filter(e => !heroesStates[e]?.isDead);
		if (fixHeroes.length < 5) {
			heroGetAll = heroGetAll.filter(e => !heroesStates[e.id]?.isDead);
			fixHeroes = heroGetAll.sort((a, b) => b.power - a.power).slice(0, 5).map(e => e.id);
			Object.keys(argsBattle.favor).forEach(e => {
				if (!fixHeroes.includes(+e)) {
					delete argsBattle.favor[e];
				}
			})
		}
		argsBattle.heroes = fixHeroes;
		return argsBattle;
	}

	/**
	 * Check the floor
	 *
	 * Проверяем этаж
	 */
	function checkFloor(towerInfo) {
		lastTowerInfo = towerInfo;
		maySkipFloor = +towerInfo.maySkipFloor;
		floorNumber = +towerInfo.floorNumber;
		heroesStates = towerInfo.states.heroes;
		floorInfo = towerInfo.floor;

		/**
		 * Is there at least one chest open on the floor
		 * Открыт ли на этаже хоть один сундук
		 */
		isOpenChest = false;
		if (towerInfo.floorType == "chest") {
			isOpenChest = towerInfo.floor.chests.reduce((n, e) => n + e.opened, 0);
		}

		setProgress(`${I18N('TOWER')}: ${I18N('FLOOR')} ${floorNumber}`);
		if (floorNumber > 49) {
			if (isOpenChest) {
				endTower('alreadyOpenChest 50 floor', floorNumber);
				return;
			}
		}
		/**
		 * If the chest is open and you can skip floors, then move on
		 * Если сундук открыт и можно скипать этажи, то переходим дальше
		 */
		if (towerInfo.mayFullSkip && +towerInfo.teamLevel == 130) {
			if (isOpenChest) {
				nextOpenChest(floorNumber);
			} else {
				nextChestOpen(floorNumber);
			}
			return;
		}

		// console.log(towerInfo, scullCoin);
		switch (towerInfo.floorType) {
			case "battle":
				if (floorNumber <= maySkipFloor) {
					skipFloor();
					return;
				}
				if (floorInfo.state == 2) {
					nextFloor();
					return;
				}
				startBattle().then(endBattle);
				return;
			case "buff":
				checkBuff(towerInfo);
				return;
			case "chest":
				openChest(floorNumber);
				return;
			default:
				console.log('!', towerInfo.floorType, towerInfo);
				break;
		}
	}

	/**
	 * Let's start the fight
	 *
	 * Начинаем бой
	 */
	function startBattle() {
		return new Promise(function (resolve, reject) {
			towerStartBattle = {
				calls: [{
					name: "towerStartBattle",
					args: fixHeroesTeam(argsBattle),
					ident: "body"
				}]
			}
			send(JSON.stringify(towerStartBattle), resultBattle, resolve);
		});
	}
	/**
	 * Returns the result of the battle in a promise
	 *
	 * Возращает резульат боя в промис
	 */
	function resultBattle(resultBattles, resolve) {
		battleData = resultBattles.results[0].result.response;
		battleType = "get_tower";
		BattleCalc(battleData, battleType, function (result) {
			resolve(result);
		});
	}
	/**
	 * Finishing the fight
	 *
	 * Заканчиваем бой
	 */
	function endBattle(battleInfo) {
		if (battleInfo.result.stars >= 3) {
			endBattleCall = {
				calls: [{
					name: "towerEndBattle",
					args: {
						result: battleInfo.result,
						progress: battleInfo.progress,
					},
					ident: "body"
				}]
			}
			send(JSON.stringify(endBattleCall), resultEndBattle);
		} else {
			endTower('towerEndBattle win: false\n', battleInfo);
		}
	}

	/**
	 * Getting and processing battle results
	 *
	 * Получаем и обрабатываем результаты боя
	 */
	function resultEndBattle(e) {
		battleResult = e.results[0].result.response;
		if ('error' in battleResult) {
			endTower('errorBattleResult', battleResult);
			return;
		}
		if ('reward' in battleResult) {
			scullCoin += battleResult.reward?.coin[7] ?? 0;
		}
		nextFloor();
	}

	function nextFloor() {
		nextFloorCall = {
			calls: [{
				name: "towerNextFloor",
				args: {},
				ident: "body"
			}]
		}
		send(JSON.stringify(nextFloorCall), checkDataFloor);
	}

	function openChest(floorNumber) {
		floorNumber = floorNumber || 0;
		openChestCall = {
			calls: [{
				name: "towerOpenChest",
				args: {
					num: 2
				},
				ident: "body"
			}]
		}
		send(JSON.stringify(openChestCall), floorNumber < 50 ? nextFloor : lastChest);
	}

	function lastChest() {
		endTower('openChest 50 floor', floorNumber);
	}

	function skipFloor() {
		skipFloorCall = {
			calls: [{
				name: "towerSkipFloor",
				args: {},
				ident: "body"
			}]
		}
		send(JSON.stringify(skipFloorCall), checkDataFloor);
	}

	function checkBuff(towerInfo) {
		buffArr = towerInfo.floor;
		promises = [];
		for (let buff of buffArr) {
			buffInfo = buffIds[buff.id];
			if (buffInfo.isBuy && buffInfo.cost <= scullCoin) {
				scullCoin -= buffInfo.cost;
				promises.push(buyBuff(buff.id));
			}
		}
		Promise.all(promises).then(nextFloor);
	}

	function buyBuff(buffId) {
		return new Promise(function (resolve, reject) {
			buyBuffCall = {
				calls: [{
					name: "towerBuyBuff",
					args: {
						buffId
					},
					ident: "body"
				}]
			}
			send(JSON.stringify(buyBuffCall), resolve);
		});
	}

	function checkDataFloor(result) {
		towerInfo = result.results[0].result.response;
		if ('reward' in towerInfo && towerInfo.reward?.coin) {
			scullCoin += towerInfo.reward?.coin[7] ?? 0;
		}
		if ('tower' in towerInfo) {
			towerInfo = towerInfo.tower;
		}
		if ('skullReward' in towerInfo) {
			scullCoin += towerInfo.skullReward?.coin[7] ?? 0;
		}
		checkFloor(towerInfo);
	}
	/**
	 * Getting tower rewards
	 *
	 * Получаем награды башни
	 */
	function farmTowerRewards(reason) {
		let { pointRewards, points } = lastTowerInfo;
		let pointsAll = Object.getOwnPropertyNames(pointRewards);
		let farmPoints = pointsAll.filter(e => +e <= +points && !pointRewards[e]);
		if (!farmPoints.length) {
			return;
		}
		let farmTowerRewardsCall = {
			calls: [{
				name: "tower_farmPointRewards",
				args: {
					points: farmPoints
				},
				ident: "tower_farmPointRewards"
			}]
		}

		if (scullCoin > 0 && reason == 'openChest 50 floor') {
			farmTowerRewardsCall.calls.push({
				name: "tower_farmSkullReward",
				args: {},
				ident: "tower_farmSkullReward"
			});
		}

		send(JSON.stringify(farmTowerRewardsCall), () => { });
	}

	function fullSkipTower() {
		/**
		 * Next chest
		 *
		 * Следующий сундук
		 */
		function nextChest(n) {
			return {
				name: "towerNextChest",
				args: {},
				ident: "group_" + n + "_body"
			}
		}
		/**
		 * Open chest
		 *
		 * Открыть сундук
		 */
		function openChest(n) {
			return {
				name: "towerOpenChest",
				args: {
					"num": 2
				},
				ident: "group_" + n + "_body"
			}
		}

		const fullSkipTowerCall = {
			calls: []
		}

		let n = 0;
		for (let i = 0; i < 15; i++) {
			fullSkipTowerCall.calls.push(nextChest(++n));
			fullSkipTowerCall.calls.push(openChest(++n));
		}

		send(JSON.stringify(fullSkipTowerCall), data => {
			data.results[0] = data.results[28];
			checkDataFloor(data);
		});
	}

	function nextChestOpen(floorNumber) {
		const calls = [{
			name: "towerOpenChest",
			args: {
				num: 2
			},
			ident: "towerOpenChest"
		}];

		Send(JSON.stringify({ calls })).then(e => {
			nextOpenChest(floorNumber);
		});
	}

	function nextOpenChest(floorNumber) {
		if (floorNumber > 49) {
			endTower('openChest 50 floor', floorNumber);
			return;
		}
		if (floorNumber == 1) {
			fullSkipTower();
			return;
		}

		let nextOpenChestCall = {
			calls: [{
				name: "towerNextChest",
				args: {},
				ident: "towerNextChest"
			}, {
				name: "towerOpenChest",
				args: {
					num: 2
				},
				ident: "towerOpenChest"
			}]
		}
		send(JSON.stringify(nextOpenChestCall), checkDataFloor);
	}

	function endTower(reason, info) {
		console.log(reason, info);
		if (reason != 'noTower') {
			farmTowerRewards(reason);
		}
		setProgress(`${I18N('TOWER')} ${I18N('COMPLETED')}!`, true);
		resolve();
	}
}

/**
 * Passage of the arena of the titans
 *
 * Прохождение арены титанов
 */
function testTitanArena() {
	return new Promise((resolve, reject) => {
		titAren = new executeTitanArena(resolve, reject);
		titAren.start();
	});
}

/**
 * Passage of the arena of the titans
 *
 * Прохождение арены титанов
 */
function executeTitanArena(resolve, reject) {
	let titan_arena = [];
	let finishListBattle = [];
	/**
	 * ID of the current batch
	 *
	 * Идетификатор текущей пачки
	 */
	let currentRival = 0;
	/**
	 * Number of attempts to finish off the pack
	 *
	 * Количество попыток добития пачки
	 */
	let attempts = 0;
	/**
	 * Was there an attempt to finish off the current shooting range
	 *
	 * Была ли попытка добития текущего тира
	 */
	let isCheckCurrentTier = false;
	/**
	 * Current shooting range
	 *
	 * Текущий тир
	 */
	let currTier = 0;
	/**
	 * Number of battles on the current dash
	 *
	 * Количество битв на текущем тире
	 */
	let countRivalsTier = 0;

	let callsStart = {
		calls: [{
			name: "titanArenaGetStatus",
			args: {},
			ident: "titanArenaGetStatus"
		}, {
			name: "teamGetAll",
			args: {},
			ident: "teamGetAll"
		}]
	}

	this.start = function () {
		send(JSON.stringify(callsStart), startTitanArena);
	}

	function startTitanArena(data) {
		let titanArena = data.results[0].result.response;
		if (titanArena.status == 'disabled') {
			endTitanArena('disabled', titanArena);
			return;
		}

		let teamGetAll = data.results[1].result.response;
		titan_arena = teamGetAll.titan_arena;

		checkTier(titanArena)
	}

	function checkTier(titanArena) {
		if (titanArena.status == "peace_time") {
			endTitanArena('Peace_time', titanArena);
			return;
		}
		currTier = titanArena.tier;
		if (currTier) {
			setProgress(`${I18N('TITAN_ARENA')}: ${I18N('LEVEL')} ${currTier}`);
		}

		if (titanArena.status == "completed_tier") {
			titanArenaCompleteTier();
			return;
		}
		/**
		 * Checking for the possibility of a raid
		 * Проверка на возможность рейда
		 */
		if (titanArena.canRaid) {
			titanArenaStartRaid();
			return;
		}
		/**
		 * Check was an attempt to achieve the current shooting range
		 * Проверка была ли попытка добития текущего тира
		 */
		if (!isCheckCurrentTier) {
			checkRivals(titanArena.rivals);
			return;
		}

		endTitanArena('Done or not canRaid', titanArena);
	}
	/**
	 * Submit dash information for verification
	 *
	 * Отправка информации о тире на проверку
	 */
	function checkResultInfo(data) {
		let titanArena = data.results[0].result.response;
		checkTier(titanArena);
	}
	/**
	 * Finish the current tier
	 *
	 * Завершить текущий тир
	 */
	function titanArenaCompleteTier() {
		isCheckCurrentTier = false;
		let calls = [{
			name: "titanArenaCompleteTier",
			args: {},
			ident: "body"
		}];
		send(JSON.stringify({calls}), checkResultInfo);
	}
	/**
	 * Gathering points to be completed
	 *
	 * Собираем точки которые нужно добить
	 */
	function checkRivals(rivals) {
		finishListBattle = [];
		for (let n in rivals) {
			if (rivals[n].attackScore < 250) {
				finishListBattle.push(n);
			}
		}
		console.log('checkRivals', finishListBattle);
		countRivalsTier = finishListBattle.length;
		roundRivals();
	}
	/**
	 * Selecting the next point to finish off
	 *
	 * Выбор следующей точки для добития
	 */
	function roundRivals() {
		let countRivals = finishListBattle.length;
		if (!countRivals) {
			/**
			 * Whole range checked
			 *
			 * Весь тир проверен
			 */
			isCheckCurrentTier = true;
			titanArenaGetStatus();
			return;
		}
		// setProgress('TitanArena: Уровень ' + currTier + ' Бои: ' + (countRivalsTier - countRivals + 1) + '/' + countRivalsTier);
		currentRival = finishListBattle.pop();
		attempts = +currentRival;
		// console.log('roundRivals', currentRival);
		titanArenaStartBattle(currentRival);
	}
	/**
	 * The start of a solo battle
	 *
	 * Начало одиночной битвы
	 */
	function titanArenaStartBattle(rivalId) {
		let calls = [{
			name: "titanArenaStartBattle",
			args: {
				rivalId: rivalId,
				titans: titan_arena
			},
			ident: "body"
		}];
		send(JSON.stringify({calls}), calcResult);
	}
	/**
	 * Calculation of the results of the battle
	 *
	 * Расчет результатов боя
	 */
	function calcResult(data) {
		let battlesInfo = data.results[0].result.response.battle;
		/**
		 * If attempts are equal to the current battle number we make
		 * Если попытки равны номеру текущего боя делаем прерасчет
		 */
		if (attempts == currentRival) {
			preCalcBattle(battlesInfo);
			return;
		}
		/**
		 * If there are still attempts, we calculate a new battle
		 * Если попытки еще есть делаем расчет нового боя
		 */
		if (attempts > 0) {
			attempts--;
			calcBattleResult(battlesInfo)
				.then(resultCalcBattle);
			return;
		}
		/**
		 * Otherwise, go to the next opponent
		 * Иначе переходим к следующему сопернику
		 */
		roundRivals();
	}
	/**
	 * Processing the results of the battle calculation
	 *
	 * Обработка результатов расчета битвы
	 */
	function resultCalcBattle(resultBattle) {
		// console.log('resultCalcBattle', currentRival, attempts, resultBattle.result.win);
		/**
		 * If the current calculation of victory is not a chance or the attempt ended with the finish the battle
		 * Если текущий расчет победа или шансов нет или попытки кончились завершаем бой
		 */
		if (resultBattle.result.win || !attempts) {
			titanArenaEndBattle({
				progress: resultBattle.progress,
				result: resultBattle.result,
				rivalId: resultBattle.battleData.typeId
			});
			return;
		}
		/**
		 * If not victory and there are attempts we start a new battle
		 * Если не победа и есть попытки начинаем новый бой
		 */
		titanArenaStartBattle(resultBattle.battleData.typeId);
	}
	/**
	 * Returns the promise of calculating the results of the battle
	 *
	 * Возращает промис расчета результатов битвы
	 */
	function getBattleInfo(battle, isRandSeed) {
		return new Promise(function (resolve) {
			if (isRandSeed) {
				battle.seed = Math.floor(Date.now() / 1000) + random(0, 1e3);
			}
			// console.log(battle.seed);
			BattleCalc(battle, "get_titanClanPvp", e => resolve(e));
		});
	}
	/**
	 * Recalculate battles
	 *
	 * Прерасчтет битвы
	 */
	function preCalcBattle(battle) {
		let actions = [getBattleInfo(battle, false)];
		const countTestBattle = getInput('countTestBattle');
		for (let i = 0; i < countTestBattle; i++) {
			actions.push(getBattleInfo(battle, true));
		}
		Promise.all(actions)
			.then(resultPreCalcBattle);
	}
	/**
	 * Processing the results of the battle recalculation
	 *
	 * Обработка результатов прерасчета битвы
	 */
	function resultPreCalcBattle(e) {
		let wins = e.map(n => n.result.win);
		let firstBattle = e.shift();
		let countWin = wins.reduce((w, s) => w + s);
		let numReval = countRivalsTier - finishListBattle.length;
		// setProgress('TitanArena: Уровень ' + currTier + ' Бои: ' + numReval + '/' + countRivalsTier + ' - ' + countWin + '/11');
		console.log('resultPreCalcBattle', countWin + '/11' )
		if (countWin > 0) {
			attempts = getInput('countAutoBattle');
		} else {
			attempts = 0;
		}
		resultCalcBattle(firstBattle);
	}

	/**
	 * Complete an arena battle
	 *
	 * Завершить битву на арене
	 */
	function titanArenaEndBattle(args) {
		let calls = [{
			name: "titanArenaEndBattle",
			args,
			ident: "body"
		}];
		send(JSON.stringify({calls}), resultTitanArenaEndBattle);
	}

	function resultTitanArenaEndBattle(e) {
		let attackScore = e.results[0].result.response.attackScore;
		let numReval = countRivalsTier - finishListBattle.length;
		setProgress(`${I18N('TITAN_ARENA')}: ${I18N('LEVEL')} ${currTier} </br>${I18N('BATTLES')}: ${numReval}/${countRivalsTier} - ${attackScore}`);
		/**
		 * TODO: Might need to improve the results.
		 * TODO: Возможно стоит сделать улучшение результатов
		 */
		// console.log('resultTitanArenaEndBattle', e)
		console.log('resultTitanArenaEndBattle', numReval + '/' + countRivalsTier, attempts)
		roundRivals();
	}
	/**
	 * Arena State
	 *
	 * Состояние арены
	 */
	function titanArenaGetStatus() {
		let calls = [{
			name: "titanArenaGetStatus",
			args: {},
			ident: "body"
		}];
		send(JSON.stringify({calls}), checkResultInfo);
	}
	/**
	 * Arena Raid Request
	 *
	 * Запрос рейда арены
	 */
	function titanArenaStartRaid() {
		let calls = [{
			name: "titanArenaStartRaid",
			args: {
				titans: titan_arena
			},
			ident: "body"
		}];
		send(JSON.stringify({calls}), calcResults);
	}

	function calcResults(data) {
		let battlesInfo = data.results[0].result.response;
		let {attackers, rivals} = battlesInfo;

		let promises = [];
		for (let n in rivals) {
			rival = rivals[n];
			promises.push(calcBattleResult({
				attackers: attackers,
				defenders: [rival.team],
				seed: rival.seed,
				typeId: n,
			}));
		}

		Promise.all(promises)
			.then(results => {
				const endResults = {};
				for (let info of results) {
					let id = info.battleData.typeId;
					endResults[id] = {
						progress: info.progress,
						result: info.result,
					}
				}
				titanArenaEndRaid(endResults);
			});
	}

	function calcBattleResult(battleData) {
		return new Promise(function (resolve, reject) {
			BattleCalc(battleData, "get_titanClanPvp", resolve);
		});
	}

	/**
	 * Sending Raid Results
	 *
	 * Отправка результатов рейда
	 */
	function titanArenaEndRaid(results) {
		titanArenaEndRaidCall = {
			calls: [{
				name: "titanArenaEndRaid",
				args: {
					results
				},
				ident: "body"
			}]
		}
		send(JSON.stringify(titanArenaEndRaidCall), checkRaidResults);
	}

	function checkRaidResults(data) {
		results = data.results[0].result.response.results;
		isSucsesRaid = true;
		for (let i in results) {
			isSucsesRaid &&= (results[i].attackScore >= 250);
		}

		if (isSucsesRaid) {
			titanArenaCompleteTier();
		} else {
			titanArenaGetStatus();
		}
	}

	function titanArenaFarmDailyReward() {
		titanArenaFarmDailyRewardCall = {
			calls: [{
				name: "titanArenaFarmDailyReward",
				args: {},
				ident: "body"
			}]
		}
		send(JSON.stringify(titanArenaFarmDailyRewardCall), () => {console.log('Done farm daily reward')});
	}

	function endTitanArena(reason, info) {
		if (!['Peace_time', 'disabled'].includes(reason)) {
			titanArenaFarmDailyReward();
		}
		console.log(reason, info);
		setProgress(`${I18N('TITAN_ARENA')} ${I18N('COMPLETED')}!`, true);
		resolve();
	}
}

function hackGame() {
	self = this;
	selfGame = null;
	bindId = 1e9;
	this.libGame = null;

	/**
	 * List of correspondence of used classes to their names
	 *
	 * Список соответствия используемых классов их названиям
	 */
	ObjectsList = [
		{name:"BattlePresets", prop:"game.battle.controller.thread.BattlePresets"},
		{name:"DataStorage", prop:"game.data.storage.DataStorage"},
		{name:"BattleConfigStorage", prop:"game.data.storage.battle.BattleConfigStorage"},
		{name:"BattleInstantPlay", prop:"game.battle.controller.instant.BattleInstantPlay"},
		{name:"MultiBattleResult", prop:"game.battle.controller.MultiBattleResult"},

		{name:"PlayerMissionData", prop:"game.model.user.mission.PlayerMissionData"},
		{name:"PlayerMissionBattle", prop:"game.model.user.mission.PlayerMissionBattle"},
		{name:"GameModel", prop:"game.model.GameModel"},
		{name:"CommandManager", prop:"game.command.CommandManager"},
		{name:"MissionCommandList", prop:"game.command.rpc.mission.MissionCommandList"},
		{name:"RPCCommandBase", prop:"game.command.rpc.RPCCommandBase"},
		{name:"PlayerTowerData", prop:"game.model.user.tower.PlayerTowerData"},
		{name:"TowerCommandList", prop:"game.command.tower.TowerCommandList"},
		{name:"PlayerHeroTeamResolver", prop:"game.model.user.hero.PlayerHeroTeamResolver"},
		{name:"BattlePausePopup", prop:"game.view.popup.battle.BattlePausePopup"},
		{name:"BattlePopup", prop:"game.view.popup.battle.BattlePopup"},
		{name:"DisplayObjectContainer", prop:"starling.display.DisplayObjectContainer"},
		{name:"GuiClipContainer", prop:"engine.core.clipgui.GuiClipContainer"},
		{name:"BattlePausePopupClip", prop:"game.view.popup.battle.BattlePausePopupClip"},
		{name:"ClipLabel", prop:"game.view.gui.components.ClipLabel"},
		{name:"ClipLabelBase", prop:"game.view.gui.components.ClipLabelBase"},
		{name:"Translate", prop:"com.progrestar.common.lang.Translate"},
		{name:"ClipButtonLabeledCentered", prop:"game.view.gui.components.ClipButtonLabeledCentered"},
		{name:"BattlePausePopupMediator", prop:"game.mediator.gui.popup.battle.BattlePausePopupMediator"},
		{name:"SettingToggleButton", prop:"game.mechanics.settings.popup.view.SettingToggleButton"},
		{name:"PlayerDungeonData", prop:"game.mechanics.dungeon.model.PlayerDungeonData"},
		{name:"NextDayUpdatedManager", prop:"game.model.user.NextDayUpdatedManager"},
		{name:"BattleController", prop:"game.battle.controller.BattleController"},
		{name:"BattleSettingsModel", prop:"game.battle.controller.BattleSettingsModel"},
		{name:"BooleanProperty", prop:"engine.core.utils.property.BooleanProperty"},
		{name:"RuleStorage", prop:"game.data.storage.rule.RuleStorage"},
		{name:"BattleConfig", prop:"battle.BattleConfig"},
		{name:"SpecialShopModel", prop:"game.model.user.shop.SpecialShopModel"},
		{name:"BattleGuiMediator", prop:"game.battle.gui.BattleGuiMediator"},
		{name:"BooleanPropertyWriteable", prop:"engine.core.utils.property.BooleanPropertyWriteable"},
	];

	/**
	 * Contains the game classes needed to write and override game methods
	 *
	 * Содержит классы игры необходимые для написания и подмены методов игры
	 */
	Game = {
		/**
		 * Function 'e'
		 * Функция 'e'
		 */
		bindFunc: function (a, b) {
			if (null == b)
				return null;
			null == b.__id__ && (b.__id__ = bindId++);
			var c;
			null == a.hx__closures__ ? a.hx__closures__ = {} :
				c = a.hx__closures__[b.__id__];
			null == c && (c = b.bind(a), a.hx__closures__[b.__id__] = c);
			return c
		},
	};

	/**
	 * Connects to game objects via the object creation event
	 *
	 * Подключается к объектам игры через событие создания объекта
	 */
	function connectGame() {
		for (let obj of ObjectsList) {
			/**
			 * https: //stackoverflow.com/questions/42611719/how-to-intercept-and-modify-a-specific-property-for-any-object
			 */
			Object.defineProperty(Object.prototype, obj.prop, {
				set: function (value) {
					if (!selfGame) {
						selfGame = this;
					}
					if (!Game[obj.name]) {
						Game[obj.name] = value;
					}
					// console.log('set ' + obj.prop, this, value);
					this[obj.prop + '_'] = value;
				},
				get: function () {
					// console.log('get ' + obj.prop, this);
					return this[obj.prop + '_'];
				}
			});
		}
	}

	/**
	 * Game.BattlePresets
	 * @param {bool} a isReplay
	 * @param {bool} b autoToggleable
	 * @param {bool} c auto On Start
	 * @param {object} d config
	 * @param {bool} f showBothTeams
	 */
	/**
	 * Returns the results of the battle to the callback function
	 * Возвращает в функцию callback результаты боя
	 * @param {*} battleData battle data данные боя
	 * @param {*} battleConfig combat configuration type options:
	 *
	 * тип конфигурации боя варианты:
	 *
	 * "get_invasion", "get_titanPvpManual", "get_titanPvp",
	 * "get_titanClanPvp","get_clanPvp","get_titan","get_boss",
	 * "get_tower","get_pve","get_pvpManual","get_pvp","get_core"
	 *
	 * You can specify the xYc function in the game.assets.storage.BattleAssetStorage class
	 *
	 * Можно уточнить в классе game.assets.storage.BattleAssetStorage функция xYc
	 * @param {*} callback функция в которую вернуться результаты боя
	 */
	this.BattleCalc = function (battleData, battleConfig, callback) {
		// battleConfig = battleConfig || getBattleType(battleData.type)
		if (!Game.BattlePresets) throw Error('Use connectGame');
		battlePresets = new Game.BattlePresets(!!battleData.progress, !1, !0, Game.DataStorage[getFn(Game.DataStorage, 22)][getF(Game.BattleConfigStorage, battleConfig)](), !1);
		battleInstantPlay = new Game.BattleInstantPlay(battleData, battlePresets);
		battleInstantPlay[getProtoFn(Game.BattleInstantPlay, 8)].add((battleInstant) => {
			battleResult = battleInstant[getF(Game.BattleInstantPlay, 'get_result')]();
			battleData = battleInstant[getF(Game.BattleInstantPlay, 'get_rawBattleInfo')]();
			callback({
				battleData,
				progress: battleResult[getF(Game.MultiBattleResult, 'get_progress')](),
				result: battleResult[getF(Game.MultiBattleResult, 'get_result')]()
			})
		});
		battleInstantPlay.start();
	}

	/**
	 * Returns a function with the specified name from the class
	 *
	 * Возвращает из класса функцию с указанным именем
	 * @param {Object} classF Class // класс
	 * @param {String} nameF function name // имя функции
	 * @param {String} pos name and alias order // порядок имени и псевдонима
	 * @returns
	 */
	function getF(classF, nameF, pos) {
		pos = pos || false;
		let prop = Object.entries(classF.prototype.__properties__)
		if (!pos) {
			return prop.filter((e) => e[1] == nameF).pop()[0];
		} else {
			return prop.filter((e) => e[0] == nameF).pop()[1];
		}
	}

	/**
	 * Returns a function with the specified name from the class
	 *
	 * Возвращает из класса функцию с указанным именем
	 * @param {Object} classF Class // класс
	 * @param {String} nameF function name // имя функции
	 * @returns
	 */
	function getFnP(classF, nameF) {
		let prop = Object.entries(classF.__properties__)
		return prop.filter((e) => e[1] == nameF).pop()[0];
	}

	/**
	 * Returns the function name with the specified ordinal from the class
	 *
	 * Возвращает имя функции с указаным порядковым номером из класса
	 * @param {Object} classF Class // класс
	 * @param {Number} nF Order number of function // порядковый номер функции
	 * @returns
	 */
	function getFn(classF, nF) {
		let prop = Object.keys(classF);
		return prop[nF];
	}

	/**
	 * Returns the name of the function with the specified serial number from the prototype of the class
	 *
	 * Возвращает имя функции с указаным порядковым номером из прототипа класса
	 * @param {Object} classF Class // класс
	 * @param {Number} nF Order number of function // порядковый номер функции
	 * @returns
	 */
	function getProtoFn(classF, nF) {
		let prop = Object.keys(classF.prototype);
		return prop[nF];
	}
	/**
	 * Description of replaced functions
	 *
	 * Описание подменяемых функций
	 */
	replaceFunction = {
		company: function() {
			let PMD_12 = getProtoFn(Game.PlayerMissionData, 12);
			let oldSkipMisson = Game.PlayerMissionData.prototype[PMD_12];
			Game.PlayerMissionData.prototype[PMD_12] = function (a, b, c) {
				if (isChecked('passBattle')) {
					this[getProtoFn(Game.PlayerMissionData, 9)] = new Game.PlayerMissionBattle(a, b, c);

					var a = new Game.BattlePresets(!1, !1, !0, Game.DataStorage[getFn(Game.DataStorage, 22)][getProtoFn(Game.BattleConfigStorage, 17)](), !1);
					a = new Game.BattleInstantPlay(c, a);
					a[getProtoFn(Game.BattleInstantPlay, 8)].add(Game.bindFunc(this, this.P$h));
					a.start()
				} else {
					oldSkipMisson.call(this, a, b, c);
				}
			}

			Game.PlayerMissionData.prototype.P$h = function (a) {
				let GM_2 = getFn(Game.GameModel, 2);
				let GM_P2 = getProtoFn(Game.GameModel, 2);
				let CM_20 = getProtoFn(Game.CommandManager, 20);
				let MCL_2 = getProtoFn(Game.MissionCommandList, 2);
				let MBR_15 = getProtoFn(Game.MultiBattleResult, 15);
				let RPCCB_15 = getProtoFn(Game.RPCCommandBase, 15);
				let PMD_32 = getProtoFn(Game.PlayerMissionData, 32);
				Game.GameModel[GM_2]()[GM_P2][CM_20][MCL_2](a[MBR_15]())[RPCCB_15](Game.bindFunc(this, this[PMD_32]))
			}
		},
		tower: function() {
			let PTD_67 = getProtoFn(Game.PlayerTowerData, 67);
			let oldSkipTower = Game.PlayerTowerData.prototype[PTD_67];
			Game.PlayerTowerData.prototype[PTD_67] = function (a) {
				if (isChecked('passBattle')) {
					var p = new Game.BattlePresets(!1, !1, !0, Game.DataStorage[getFn(Game.DataStorage, 22)][getProtoFn(Game.BattleConfigStorage,17)](), !1);
					a = new Game.BattleInstantPlay(a, p);
					a[getProtoFn(Game.BattleInstantPlay,8)].add(Game.bindFunc(this, this.P$h));
					a.start()
				} else {
					oldSkipTower.call(this, a);;
				}
			}

			Game.PlayerTowerData.prototype.P$h = function (a) {
				let GM_2 = getFn(Game.GameModel, 2);
				let GM_P2 = getProtoFn(Game.GameModel, 2);
				let CM_29 = getProtoFn(Game.CommandManager, 29);
				let TCL_5 = getProtoFn(Game.TowerCommandList, 5);
				let MBR_15 = getProtoFn(Game.MultiBattleResult, 15);
				let RPCCB_15 = getProtoFn(Game.RPCCommandBase, 15);
				let PTD_78 = getProtoFn(Game.PlayerTowerData, 78);
				Game.GameModel[GM_2]()[GM_P2][CM_29][TCL_5](a[MBR_15]())[RPCCB_15](Game.bindFunc(this, this[PTD_78]))
			}
		},
		// skipSelectHero: function() {
		// 	if (!HOST) throw Error('Use connectGame');
		// 	Game.PlayerHeroTeamResolver.prototype[getProtoFn(Game.PlayerHeroTeamResolver, 3)] = () => false;
		// },
		passBattle: function() {
			let BPP_4 = getProtoFn(Game.BattlePausePopup, 4);
			let oldPassBattle = Game.BattlePausePopup.prototype[BPP_4];
			Game.BattlePausePopup.prototype[BPP_4] = function (a) {
				if (isChecked('passBattle')) {
					Game.BattlePopup.prototype[getProtoFn(Game.BattlePausePopup, 4)].call(this, a);
					this[getProtoFn(Game.BattlePausePopup, 3)]();
					this[getProtoFn(Game.DisplayObjectContainer, 3)](this.clip[getProtoFn(Game.GuiClipContainer, 2)]());
					this.clip[getProtoFn(Game.BattlePausePopupClip, 1)][getProtoFn(Game.ClipLabelBase, 9)](Game.Translate.translate("UI_POPUP_BATTLE_PAUSE"));

					this.clip[getProtoFn(Game.BattlePausePopupClip, 2)][getProtoFn(Game.ClipButtonLabeledCentered, 2)](Game.Translate.translate("UI_POPUP_BATTLE_RETREAT"), (q = this[getProtoFn(Game.BattlePausePopup, 1)], Game.bindFunc(q, q[getProtoFn(Game.BattlePausePopupMediator, 17)])));
					this.clip[getProtoFn(Game.BattlePausePopupClip, 5)][getProtoFn(Game.ClipButtonLabeledCentered, 2)](
						this[getProtoFn(Game.BattlePausePopup, 1)][getProtoFn(Game.BattlePausePopupMediator, 14)](),
						this[getProtoFn(Game.BattlePausePopup, 1)][getProtoFn(Game.BattlePausePopupMediator, 13)]() ?
						(q = this[getProtoFn(Game.BattlePausePopup, 1)], Game.bindFunc(q, q[getProtoFn(Game.BattlePausePopupMediator, 18)])) :
						(q = this[getProtoFn(Game.BattlePausePopup, 1)], Game.bindFunc(q, q[getProtoFn(Game.BattlePausePopupMediator, 18)]))
					);

					this.clip[getProtoFn(Game.BattlePausePopupClip, 5)][getProtoFn(Game.ClipButtonLabeledCentered, 0)][getProtoFn(Game.ClipLabelBase, 24)]();
					this.clip[getProtoFn(Game.BattlePausePopupClip, 3)][getProtoFn(Game.SettingToggleButton, 3)](this[getProtoFn(Game.BattlePausePopup, 1)][getProtoFn(Game.BattlePausePopupMediator, 9)]());
					this.clip[getProtoFn(Game.BattlePausePopupClip, 4)][getProtoFn(Game.SettingToggleButton, 3)](this[getProtoFn(Game.BattlePausePopup, 1)][getProtoFn(Game.BattlePausePopupMediator, 10)]());
					this.clip[getProtoFn(Game.BattlePausePopupClip, 6)][getProtoFn(Game.SettingToggleButton, 3)](this[getProtoFn(Game.BattlePausePopup, 1)][getProtoFn(Game.BattlePausePopupMediator, 11)]());
					/*
					Какая-то ненужная фигня
					if (!HC.lSb()) {
						this.clip.r6b.g().B(!1);
						a = this.clip.r6b.g().X() + 7;
						var b = this.clip.ba.g();
						b.$(b.X() - a);
						b = this.clip.IS.g();
						b.sa(b.Fa() - a)
					}
					*/
				} else {
					oldPassBattle.call(this, a);
				}
			}

			let retreatButtonLabel = getF(Game.BattlePausePopupMediator, "get_retreatButtonLabel");
			let oldFunc = Game.BattlePausePopupMediator.prototype[retreatButtonLabel];
			Game.BattlePausePopupMediator.prototype[retreatButtonLabel] = function () {
				if (isChecked('passBattle')) {
					return I18N('BTN_PASS');
				} else {
					return oldFunc.call(this);
				}
			}
		},
		endlessCards: function() {
			let PDD_15 = getProtoFn(Game.PlayerDungeonData, 15);
			let oldEndlessCards = Game.PlayerDungeonData.prototype[PDD_15];
			Game.PlayerDungeonData.prototype[PDD_15] = function () {
				if (isChecked('endlessCards')) {
					return true;
				} else {
					return oldEndlessCards.call(this);
				}
			}
		},
		speedBattle: function () {
			const get_timeScale = getF(Game.BattleController, "get_timeScale");
			const oldSpeedBattle = Game.BattleController.prototype[get_timeScale];
			Game.BattleController.prototype[get_timeScale] = function () {
				const speedBattle = Number.parseFloat(getInput('speedBattle'));
				if (speedBattle) {
					const BC_11 = getProtoFn(Game.BattleController, 11);
					const BSM_11 = getProtoFn(Game.BattleSettingsModel, 11);
					const BP_get_value = getF(Game.BooleanProperty, "get_value");
					if (this[BC_11][BSM_11][BP_get_value]()) {
						return 0;
					}
					const BSM_2 = getProtoFn(Game.BattleSettingsModel, 2);
					const BC_44 = getProtoFn(Game.BattleController, 44);
					const BSM_1 = getProtoFn(Game.BattleSettingsModel, 1);
					const BC_13 = getProtoFn(Game.BattleController, 13);
					const BC_3 = getFn(Game.BattleController, 3);
					if (this[BC_11][BSM_2][BP_get_value]()) {
						var a = speedBattle * this[BC_44]();
					} else {
						a = this[BC_11][BSM_1][BP_get_value]();
						//const multiple = a == 1 ? speedBattle : this[BC_13][a];
						a = this[BC_13][a] * Game.BattleController[BC_3][BP_get_value]() * this[BC_44]();
					}
					const BSM_22 = getProtoFn(Game.BattleSettingsModel, 22);
					a > this[BC_11][BSM_22][BP_get_value]() && (a = this[BC_11][BSM_22][BP_get_value]());
					const DS_21 = getFn(Game.DataStorage, 21);
					const get_battleSpeedMultiplier = getF(Game.RuleStorage, "get_battleSpeedMultiplier", true);
					// const RS_167 = getProtoFn(Game.RuleStorage, 167); // get_battleSpeedMultiplier
					var b = Game.DataStorage[DS_21][get_battleSpeedMultiplier]();
					const R_1 = getFn(selfGame.Reflect, 1);
					const BC_1 = getFn(Game.BattleController, 1);
					const get_config = getF(Game.BattlePresets, "get_config");
					// const BC_0 = getProtoFn(Game.BattleConfig, 0); // .ident
					null != b && (a = selfGame.Reflect[R_1](b, this[BC_1][get_config]().ident) ? a * selfGame.Reflect[R_1](b, this[BC_1][get_config]().ident) : a * selfGame.Reflect[R_1](b, "default"));
					return a
				} else {
					return oldSpeedBattle.call(this);
				}
			}
		},

		/**
		 * Remove the rare shop
		 *
		 * Удаление торговца редкими товарами
		 */
		removeWelcomeShop: function () {
			let SSM_3 = getProtoFn(Game.SpecialShopModel, 3);
			const oldWelcomeShop = Game.SpecialShopModel.prototype[SSM_3];
			Game.SpecialShopModel.prototype[SSM_3] = function () {
				if (isChecked('noOfferDonat')) {
					return null;
				} else {
					return oldWelcomeShop.call(this);
				}
			}
		},

		/**
		 * Acceleration button without Valkyries favor
		 *
		 * Кнопка ускорения без Покровительства Валькирий
		 */
		battleFastKey: function () {
			const BGM_39 = getProtoFn(Game.BattleGuiMediator, 39);
			const oldBattleFastKey = Game.BattleGuiMediator.prototype[BGM_39];
			Game.BattleGuiMediator.prototype[BGM_39] = function () {
				if (true) {
					const BGM_8 = getProtoFn(Game.BattleGuiMediator, 8);
					const BGM_9 = getProtoFn(Game.BattleGuiMediator, 9);
					const BPW_0 = getProtoFn(Game.BooleanPropertyWriteable, 0);
					this[BGM_8][BPW_0](true);
					this[BGM_9][BPW_0](true);
				} else {
					return oldBattleFastKey.call(this);
				}
			}
		}
	}

	/**
	 * Starts replacing recorded functions
	 *
	 * Запускает замену записанных функций
	 */
	this.activateHacks = function () {
		if (!selfGame) throw Error('Use connectGame');
		for (let func in replaceFunction) {
			replaceFunction[func]();
		}
	}

	/**
	 * Returns the game object
	 *
	 * Возвращает объект игры
	 */
	this.getSelfGame = function () {
		return selfGame;
	}

	/**
	 * Updates game data
	 *
	 * Обновляет данные игры
	 */
	this.refreshGame = function () {
		(new Game.NextDayUpdatedManager)[getProtoFn(Game.NextDayUpdatedManager, 5)]();
	}

	/**
	 * Change the play screen on windowName
	 *
	 * Сменить экран игры на windowName
	 *
	 * Possible options:
	 *
	 * Возможные варианты:
	 *
	 * MISSION, ARENA, GRAND, CHEST, SKILLS, SOCIAL_GIFT, CLAN, ENCHANT, TOWER, RATING, CHALLENGE, BOSS, CHAT, CLAN_DUNGEON, CLAN_CHEST, TITAN_GIFT, CLAN_RAID, ASGARD, HERO_ASCENSION, ROLE_ASCENSION, ASCENSION_CHEST, TITAN_MISSION, TITAN_ARENA, TITAN_ARTIFACT, TITAN_ARTIFACT_CHEST, TITAN_VALLEY, TITAN_SPIRITS, TITAN_ARTIFACT_MERCHANT, TITAN_ARENA_HALL_OF_FAME, CLAN_PVP, CLAN_PVP_MERCHANT, CLAN_GLOBAL_PVP, CLAN_GLOBAL_PVP_TITAN, ARTIFACT, ZEPPELIN, ARTIFACT_CHEST, ARTIFACT_MERCHANT, EXPEDITIONS, SUBSCRIPTION, NY2018_GIFTS, NY2018_TREE, NY2018_WELCOME, ADVENTURE, ADVENTURESOLO, SANCTUARY, PET_MERCHANT, PET_LIST, PET_SUMMON, BOSS_RATING_EVENT, BRAWL
	 */
	this.goNavigtor = function (windowName) {
		let mechanicStorage = selfGame["game.data.storage.mechanic.MechanicStorage"];
		let window = mechanicStorage[windowName];
		let event = new selfGame["game.mediator.gui.popup.PopupStashEventParams"];
		let Game = selfGame['Game'];
		let navigator = getF(Game, "get_navigator")
		let navigate = getProtoFn(selfGame["game.screen.navigator.GameNavigator"], 15)
		let instance = getFnP(Game, 'get_instance');
		Game[instance]()[navigator]()[navigate](window, event);
	}

	/**
	 * Move to the sanctuary cheats.goSanctuary()
	 *
	 * Переместиться в святилище cheats.goSanctuary()
	 */
	this.goSanctuary = () => {
		this.goNavigtor("SANCTUARY");
	}

	/**
	 * Go to Guild War
	 *
	 * Перейти к Войне Гильдий
	 */
	this.goClanWar = function() {
		let instance = getFnP(selfGame["game.model.GameModel"], 'get_instance')
		let player = selfGame["game.model.GameModel"][instance]().A;
		let clanWarSelect = selfGame["game.mechanics.cross_clan_war.popup.selectMode.CrossClanWarSelectModeMediator"];
		new clanWarSelect(player).open();
	}

	/** 
	 * Go to BrawlShop
	 * 
	 * Переместиться в BrawlShop
	 */
	this.goBrawlShop = () => {
		const instance = getFnP(selfGame["game.model.GameModel"], 'get_instance')
		const P_36 = getProtoFn(selfGame["game.model.user.Player"], 36);
		const PSD_0 = getProtoFn(selfGame["game.model.user.shop.PlayerShopData"], 0);
		const IM_0 = getProtoFn(selfGame["haxe.ds.IntMap"], 0);
		const PSDE_4 = getProtoFn(selfGame["game.model.user.shop.PlayerShopDataEntry"], 4);

		const player = selfGame["game.model.GameModel"][instance]().A;
		const shop = player[P_36][PSD_0][IM_0][1038][PSDE_4];
		const shopPopup = new selfGame["game.mechanics.brawl.mediator.BrawlShopPopupMediator"](player, shop)
		shopPopup.open(new selfGame["game.mediator.gui.popup.PopupStashEventParams"])
	}

	/**
	 * Game library availability tracker
	 *
	 * Отслеживание доступности игровой библиотеки
	 */
	function checkLibLoad() {
		timeout = setTimeout(() => {
			if (Game.GameModel) {
				changeLib();
			} else {
				checkLibLoad();
			}
		}, 100)
	}

	/**
	 * Game library data spoofing
	 *
	 * Подмена данных игровой библиотеки
	 */
	function changeLib() {
		console.log('lib connect');
		const originalStartFunc = Game.GameModel.prototype.start;
		Game.GameModel.prototype.start = function (a, b, c) {
			self.libGame = b.raw;
			try {
				b.raw.shop[26].requirements = null;
			} catch (e) {
				console.warn(e);
			}
			originalStartFunc.call(this, a, b, c);
		}
	}

	/**
	 * Returns the value of a language constant
	 * 
	 * Возвращает значение языковой константы
	 * @param {*} langConst language constant // языковая константа
	 * @returns 
	 */
	this.translate = function (langConst) {
		return Game.Translate.translate(langConst);
	}

	connectGame();
	checkLibLoad();
}

/**
 * Auto collection of gifts
 *
 * Автосбор подарков
 */
function getAutoGifts() {
	let valName = 'giftSendIds_' + userInfo.id;

	if (!localStorage['clearGift' + userInfo.id]) {
		localStorage[valName] = '';
		localStorage['clearGift' + userInfo.id] = '+';
	}

	if (!localStorage[valName]) {
		localStorage[valName] = '';
	}

	/**
	 * Submit a request to receive gift codes
	 *
	 * Отправка запроса для получения кодов подарков
	 */
	fetch('https://zingery.ru/heroes/getGifts.php', {
			method: 'POST',
			body: JSON.stringify({scriptInfo, userInfo})
	}).then(
		response => response.json()
	).then(
		data => {
			let freebieCheckCalls = {
				calls: []
			}
			data.forEach((giftId, n) => {
				if (localStorage[valName].includes(giftId)) return;
				//localStorage[valName] += ';' + giftId;
				freebieCheckCalls.calls.push({
					name: "freebieCheck",
					args: {
						giftId
					},
					ident: giftId
				});
			});

			if (!freebieCheckCalls.calls.length) {
				return;
			}

			send(JSON.stringify(freebieCheckCalls), e => {
				let countGetGifts = 0;
				const gifts = [];
				for (check of e.results) {
					gifts.push(check.ident);
					if (check.result.response != null) {
						countGetGifts++;
					}
				}
				const saveGifts = localStorage[valName].split(';');
				localStorage[valName] = [...saveGifts, ...gifts].slice(-50).join(';');
				console.log(`${I18N('GIFTS')}: ${countGetGifts}`);
			});
		}
	)
}

/**
 * To fill the kills in the Forge of Souls
 *
 * Набить килов в горниле душ
 */
async function bossRatingEvent() {
	const topGet = await Send(JSON.stringify({ calls: [{ name: "topGet", args: { type: "bossRatingTop", extraId: 0 }, ident: "body" }] }));
	if (!topGet) {
		setProgress(`${I18N('EVENT')} ${I18N('NOT_AVAILABLE')}`, true);
		return;
	}
	const replayId = topGet.results[0].result.response[0].userData.replayId;
	const result = await Send(JSON.stringify({
		calls: [
			{ name: "battleGetReplay", args: { id: replayId }, ident: "battleGetReplay" },
			{ name: "heroGetAll", args: {}, ident: "heroGetAll" },
			{ name: "pet_getAll", args: {}, ident: "pet_getAll" },
			{ name: "offerGetAll", args: {}, ident: "offerGetAll" }
		]
	}));
	const bossEventInfo = result.results[3].result.response.find(e => e.offerType == "bossEvent");
	if (!bossEventInfo) {
		setProgress(`${I18N('EVENT')} ${I18N('NOT_AVAILABLE')}`, true);
		return;
	}
	const usedHeroes = bossEventInfo.progress.usedHeroes;
	const party = Object.values(result.results[0].result.response.replay.attackers);
	const availableHeroes = Object.values(result.results[1].result.response).map(e => e.id);
	const availablePets = Object.values(result.results[2].result.response).map(e => e.id);
	const calls = [];
	/**
	 * First pack
	 *
	 * Первая пачка
	 */
	const args = {
		heroes: [],
		favor: {}
	}
	for (let hero of party) {
		if (hero.id >= 6000 && availablePets.includes(hero.id)) {
			args.pet = hero.id;
			continue;
		}
		if (!availableHeroes.includes(hero.id) || usedHeroes.includes(hero.id)) {
			continue;
		}
		args.heroes.push(hero.id);
		if (hero.favorPetId) {
			args.favor[hero.id] = hero.favorPetId;
		}
	}
	if (args.heroes.length) {
		calls.push({
			name: "bossRatingEvent_startBattle",
			args,
			ident: "body_0"
		});
	}
	/**
	 * Other packs
	 *
	 * Другие пачки
	 */
	let heroes = [];
	let count = 1;
	while (heroId = availableHeroes.pop()) {
		if (args.heroes.includes(heroId) || usedHeroes.includes(heroId)) {
			continue;
		}
		heroes.push(heroId);
		if (heroes.length == 5) {
			calls.push({
				name: "bossRatingEvent_startBattle",
				args: {
					heroes: [...heroes],
					pet: availablePets[Math.floor(Math.random() * availablePets.length)]
				},
				ident: "body_" + count
			});
			heroes = [];
			count++;
		}
	}

	if (!calls.length) {
		setProgress(`${I18N('NO_HEROES')}`, true);
		return;
	}

	const resultBattles = await Send(JSON.stringify({ calls }));
	console.log(resultBattles);
	rewardBossRatingEvent();
}

/**
 * Collecting Rewards from the Forge of Souls
 *
 * Сбор награды из Горнила Душ
 */
function rewardBossRatingEvent() {
	let rewardBossRatingCall = '{"calls":[{"name":"offerGetAll","args":{},"ident":"offerGetAll"}]}';
	send(rewardBossRatingCall, function (data) {
		let bossEventInfo = data.results[0].result.response.find(e => e.offerType == "bossEvent");
		if (!bossEventInfo) {
			setProgress(`${I18N('EVENT')} ${I18N('NOT_AVAILABLE')}`, true);
			return;
		}

		let farmedChests = bossEventInfo.progress.farmedChests;
		let score = bossEventInfo.progress.score;
		setProgress(`${I18N('DAMAGE_AMOUNT')}: ${score}`);
		let revard = bossEventInfo.reward;

		let getRewardCall = {
			calls: []
		}

		let count = 0;
		for (let i = 1; i < 10; i++) {
			if (farmedChests.includes(i)) {
				continue;
			}
			if (score < revard[i].score) {
				break;
			}
			getRewardCall.calls.push({
				name: "bossRatingEvent_getReward",
				args: {
					rewardId: i
				},
				ident: "body_" + i
			});
			count++;
		}
		if (!count) {
			setProgress(`${I18N('NOTHING_TO_COLLECT')}`, true);
			return;
		}

		send(JSON.stringify(getRewardCall), e => {
			console.log(e);
			setProgress(`${I18N('COLLECTED')} ${e?.results?.length} ${I18N('REWARD')}`, true);
		});
	});
}

/**
 * Collect Easter eggs and event rewards
 *
 * Собрать пасхалки и награды событий
 */
function offerFarmAllReward() {
	const offerGetAllCall = '{"calls":[{"name":"offerGetAll","args":{},"ident":"offerGetAll"}]}';
	return Send(offerGetAllCall).then((data) => {
		const offerGetAll = data.results[0].result.response.filter(e => e.type == "reward" && !e?.freeRewardObtained && e.reward);
		if (!offerGetAll.length) {
			setProgress(`${I18N('NOTHING_TO_COLLECT')}`, true);
			return;
		}

		const calls = [];
		for (let reward of offerGetAll) {
			calls.push({
				name: "offerFarmReward",
				args: {
					offerId: reward.id
				},
				ident: "offerFarmReward_" + reward.id
			});
		}

		return Send(JSON.stringify({ calls })).then(e => {
			console.log(e);
			setProgress(`${I18N('COLLECTED')} ${e?.results?.length} ${I18N('REWARD')}`, true);
		});
	});
}

/**
 * Assemble Outland
 *
 * Собрать запределье
 */
function getOutland() {
	return new Promise(function (resolve, reject) {
		send('{"calls":[{"name":"bossGetAll","args":{},"ident":"bossGetAll"}]}', e => {
			let bosses = e.results[0].result.response;

			let bossRaidOpenChestCall = {
				calls: []
			};

			for (let boss of bosses) {
				if (boss.mayRaid) {
					bossRaidOpenChestCall.calls.push({
						name: "bossRaid",
						args: {
							bossId: boss.id
						},
						ident: "bossRaid_" + boss.id
					});
					bossRaidOpenChestCall.calls.push({
						name: "bossOpenChest",
						args: {
							bossId: boss.id,
							amount: 1,
							starmoney: 0
						},
						ident: "bossOpenChest_" + boss.id
					});
				} else if (boss.chestId == 1) {
					bossRaidOpenChestCall.calls.push({
						name: "bossOpenChest",
						args: {
							bossId: boss.id,
							amount: 1,
							starmoney: 0
						},
						ident: "bossOpenChest_" + boss.id
					});
				}
			}

			if (!bossRaidOpenChestCall.calls.length) {
				setProgress(`${I18N('OUTLAND')} ${I18N('NOTHING_TO_COLLECT')}`, true);
				resolve();
				return;
			}

			send(JSON.stringify(bossRaidOpenChestCall), e => {
				setProgress(`${I18N('OUTLAND')} ${I18N('COLLECTED')}`, true);
				resolve();
			});
		});
	});
}

/**
 * Collect all rewards
 *
 * Собрать все награды
 */
function questAllFarm() {
	return new Promise(function (resolve, reject) {
		let questGetAllCall = {
			calls: [{
				name: "questGetAll",
				args: {},
				ident: "body"
			}]
		}
		send(JSON.stringify(questGetAllCall), function (data) {
			let questGetAll = data.results[0].result.response;
			const questAllFarmCall = {
				calls: []
			}
			let number = 0;
			for (let quest of questGetAll) {
				if (quest.id < 1e6 && quest.state == 2) {
					questAllFarmCall.calls.push({
						name: "questFarm",
						args: {
							questId: quest.id
						},
						ident: `group_${number}_body`
					});
					number++;
				}
			}

			if (!questAllFarmCall.calls.length) {
				setProgress(`${I18N('COLLECTED')} ${number} ${I18N('REWARD')}`, true);
				resolve();
				return;
			}

			send(JSON.stringify(questAllFarmCall), function (res) {
				console.log(res);
				setProgress(`${I18N('COLLECTED')} ${number} ${I18N('REWARD')}`, true);
				resolve();
			});
		});
	})
}

/**
 * Mission auto repeat
 *
 * Автоповтор миссии
 * isStopSendMission = false;
 * isSendsMission = true;
 **/
this.sendsMission = async function (param) {
	if (isStopSendMission) {
		isSendsMission = false;
		console.log(I18N('STOPPED'));
		setProgress('');
		await popup.confirm(`${I18N('STOPPED')}<br>${I18N('REPETITIONS')}: ${param.count}`, [{
			msg: 'Ok',
			result: true
		}, ])
		return;
	}

	let missionStartCall = {
		"calls": [{
			"name": "missionStart",
			"args": lastMissionStart,
			"ident": "body"
		}]
	}
	/**
	 * Mission Request
	 *
	 * Запрос на выполнение мисии
	 */
	SendRequest(JSON.stringify(missionStartCall), async e => {
		if (e['error']) {
			isSendsMission = false;
			console.log(e['error']);
			setProgress('');
			let msg = e['error'].name + ' ' + e['error'].description + `<br>${I18N('REPETITIONS')}: ${param.count}`;
			await popup.confirm(msg, [
				{msg: 'Ok', result: true},
			])
			return;
		}
		/**
		 * Mission data calculation
		 *
		 * Расчет данных мисии
		 */
		BattleCalc(e.results[0].result.response, 'get_tower', async r => {

			let missionEndCall = {
				"calls": [{
					"name": "missionEnd",
					"args": {
						"id": param.id,
						"result": r.result,
						"progress": r.progress
					},
					"ident": "body"
				}]
			}
			/**
			 * Mission Completion Request
			 *
			 * Запрос на завершение миссии
			 */
			SendRequest(JSON.stringify(missionEndCall), async (e) => {
				if (e['error']) {
					isSendsMission = false;
					console.log(e['error']);
					setProgress('');
					let msg = e['error'].name + ' ' + e['error'].description + `<br>${I18N('REPETITIONS')}: ${param.count}`;
					await popup.confirm(msg, [
						{msg: 'Ok', result: true},
					])
					return;
				}
				r = e.results[0].result.response;
				if (r['error']) {
					isSendsMission = false;
					console.log(r['error']);
					setProgress('');
					await popup.confirm(`<br>${I18N('REPETITIONS')}: ${param.count}` + ' 3 ' + r['error'], [
						{msg: 'Ok', result: true},
					])
					return;
				}

				param.count++;
				setProgress(`${I18N('MISSIONS_PASSED')}: ${param.count} (${I18N('STOP')})`, false, () => {
					isStopSendMission = true;
				});
				setTimeout(sendsMission, 1, param);
			});
		})
	});
}

/**
 * Recursive opening of matryoshka dolls
 *
 * Рекурсивное открытие матрешек
 */
function openRussianDoll(id, count, sum) {
	sum = sum || 0;
	sum += count;
	send('{"calls":[{"name":"consumableUseLootBox","args":{"libId":'+id+',"amount":'+count+'},"ident":"body"}]}', e => {
		setProgress(`${I18N('OPEN')} ${count}`, true);
		let result = e.results[0].result.response;
		let newCount = 0;
		for(let n of result) {
			if (n?.consumable && n.consumable[id]) {
				newCount += n.consumable[id]
			}
		}
		if (newCount) {
			openRussianDoll(id, newCount, sum);
		} else {
			popup.confirm(`${I18N('TOTAL_OPEN')} ${sum}`);
		}
	})
}

/**
 * Collect all mail, except letters with energy and charges of the portal
 *
 * Собрать всю почту, кроме писем с энергией и зарядами портала
 */
function mailGetAll() {
	const getMailInfo = '{"calls":[{"name":"mailGetAll","args":{},"ident":"body"}]}';

	return Send(getMailInfo).then(dataMail => {
		const letters = dataMail.results[0].result.response.letters;
		const letterIds = lettersFilter(letters);
		if (!letterIds.length) {
			setProgress(I18N('NOTHING_TO_COLLECT'), true);
			return;
		}

		const calls = [
			{ name: "mailFarm", args: { letterIds }, ident: "body" }
		];

		return Send(JSON.stringify({ calls })).then(res => {
			const lettersIds = res.results[0].result.response;
			if (lettersIds) {
				const countLetters = Object.keys(lettersIds).length;
				setProgress(`${I18N('RECEIVED')} ${countLetters} ${I18N('LETTERS')}`, true);
			}
		});
	});
}

/**
 * Filters received emails
 *
 * Фильтрует получаемые письма
 */
function lettersFilter(letters) {
	const lettersIds = [];
	for (let l in letters) {
		letter = letters[l];
		const reward = letter.reward;
		/**
		 * Mail Collection Exceptions
		 *
		 * Исключения на сбор писем
		 */
		const isFarmLetter = !(
			/** Portals // сферы портала */
			(reward?.refillable ? reward.refillable[45] : false) ||
			/** Energy // энергия */
			(reward?.stamina ? reward.stamina : false) ||
			/** accelerating energy gain // ускорение набора энергии */
			(reward?.buff ? true : false) ||
			/** VIP Points // вип очки */
			(reward?.vipPoints ? reward.vipPoints : false) ||
			/** souls of heroes // душы героев */
			(reward?.fragmentHero ? true : false) ||
			/** heroes // герои */
			(reward?.bundleHeroReward ? true : false)
		);
		if (isFarmLetter) {
			lettersIds.push(~~letter.id);
		}
	}
	return lettersIds;
}

/**
 * Displaying information about the areas of the portal and attempts on the VG
 *
 * Отображение информации о сферах портала и попытках на ВГ
 */
async function justInfo() {
	return new Promise(async (resolve, reject) => {
		const calls = [{
			name: "userGetInfo",
			args: {},
			ident: "userGetInfo"
		},
		{
			name: "clanWarGetInfo",
			args: {},
			ident: "clanWarGetInfo"
		}];
		const result = await Send(JSON.stringify({ calls }));
		const infos = result.results;
		const portalSphere = infos[0].result.response.refillable.find(n => n.id == 45);
		const clanWarMyTries = infos[1].result.response?.myTries ?? 0;
		const sanctuaryButton = buttons['goToSanctuary'].button;
		const clanWarButton = buttons['goToClanWar'].button;
		if (portalSphere.amount) {
			sanctuaryButton.style.color = portalSphere.amount >= 3 ? 'red' : 'brown';
			sanctuaryButton.title = `${I18N('SANCTUARY_TITLE')}\n${portalSphere.amount} ${I18N('PORTALS')}`;
		} else {
			sanctuaryButton.style.color = '';
			sanctuaryButton.title = I18N('SANCTUARY_TITLE');
		}
		if (clanWarMyTries) {
			clanWarButton.style.color = 'red';
			clanWarButton.title = `${I18N('GUILD_WAR_TITLE')}\n${clanWarMyTries}${I18N('ATTEMPTS')}`;
		} else {
			clanWarButton.style.color = '';
			clanWarButton.title = I18N('GUILD_WAR_TITLE');
		}
		setProgress('<img src="https://zingery.ru/heroes/portal.png" style="height: 25px;position: relative;top: 5px;"> ' + `${portalSphere.amount} </br> ${I18N('GUILD_WAR')}: ${clanWarMyTries}`, true);
		resolve();
	});
}

async function buyWithPetExperience() {
	const itemLib = lib.getData('inventoryItem');
	const result = await Send('{"calls":[{"name":"inventoryGet","args":{},"ident":"inventoryGet"},{"name":"shopGet","args":{"shopId":"26"},"ident":"shopGet"}]}').then(e => e.results.map(n => n.result.response));
	const inventory = result[0];
	const shop = result[1];
	const slot = shop.slots[2];

	const currentCount = inventory.consumable[85];
	const price = slot.cost.consumable[85];

	const typeBuyItem = Object.keys(slot.reward).pop();
	const itemIdBuyItem = Object.keys(slot.reward[typeBuyItem]).pop();
	const countBuyItem = slot.reward[typeBuyItem][itemIdBuyItem];
	const itemName = cheats.translate(`LIB_${typeBuyItem.toUpperCase()}_NAME_${itemIdBuyItem}`);

	if (slot.bought) {
		await popup.confirm(I18N('SECRET_WEALTH_ALREADY'), [
			{ msg: 'Ok', result: true },
		]);
		return;
	}

	const purchaseMsg = I18N('SECRET_WEALTH_BUY', { available: currentCount, countBuy: countBuyItem, name: itemName, price })
	const answer = await popup.confirm(purchaseMsg, [
		{ msg: I18N('BTN_NO'), result: false },
		{ msg: I18N('BTN_YES'), result: true },
	]);

	if (!answer) {
		setProgress(I18N('SECRET_WEALTH_CANCELED'), true);
		return;
	}

	if (currentCount < price) {
		const msg = I18N('SECRET_WEALTH_NOT_ENOUGH', { available: currentCount, need: price });
		await popup.confirm(msg, [
			{ msg: 'Ok', result: true },
		]);
		return;
	}

	const calls = [{
		name: "shopBuy",
		args: {
			shopId: 26,
			slot: 2,
			cost: slot.cost,
			reward: slot.reward
		},
		ident: "body"
	}];
	const bought = await Send(JSON.stringify({ calls })).then(e => e.results[0].result.response);

	const type = Object.keys(bought).pop();
	const itemId = Object.keys(bought[type]).pop();
	const count = bought[type][itemId];
	
	const resultMsg = I18N('SECRET_WEALTH_PURCHASED', { count, name: itemName });
	await popup.confirm(resultMsg, [
		{ msg: 'Ok', result: true },
	]);
}

async function buyWithPetExperienceAuto() {
	const itemLib = lib.getData('inventoryItem');
	const minCount = 450551;
	const result = await Send('{"calls":[{"name":"inventoryGet","args":{},"ident":"inventoryGet"},{"name":"shopGet","args":{"shopId":"26"},"ident":"shopGet"}]}').then(e => e.results.map(n => n.result.response));
	const inventory = result[0];
	const shop = result[1];
	const slot = shop.slots[2];

	const currentCount = inventory.consumable[85];
	const price = slot.cost.consumable[85];

	if (slot.bought) {
		console.log(I18N('SECRET_WEALTH_ALREADY'));
		setProgress(I18N('SECRET_WEALTH_ALREADY'), true);
		return;
	}

	if (currentCount < price) {
		const msg = I18N('SECRET_WEALTH_NOT_ENOUGH', { available: currentCount, need: price });
		console.log(msg);
		setProgress(msg, true);
		return;
	}

	if ((currentCount - price) < minCount) {
		console.log(I18N('SECRET_WEALTH_UPGRADE_NEW_PET'));
		setProgress(I18N('SECRET_WEALTH_UPGRADE_NEW_PET'), true);
		return;
	}

	const calls = [{
		name: "shopBuy",
		args: {
			shopId: 26,
			slot: 2,
			cost: slot.cost,
			reward: slot.reward
		},
		ident: "body"
	}];
	const bought = await Send(JSON.stringify({ calls })).then(e => e.results[0].result.response);

	const type = Object.keys(bought).pop();
	const itemId = Object.keys(bought[type]).pop();
	const count = bought[type][itemId];
	const itemName = itemLib[type][itemId].label;
	
	const resultMsg = I18N('SECRET_WEALTH_PURCHASED', { count, name: itemName });
	console.log(resultMsg, bought);
	setProgress(resultMsg, true);
}
/**
 * Attack of the minions of Asgard
 *
 * Атака прислужников Асгарда
 */
function testRaidNodes() {
	return new Promise((resolve, reject) => {
		const tower = new executeRaidNodes(resolve, reject);
		tower.start();
	});
}

/**
 * Attack of the minions of Asgard
 *
 * Атака прислужников Асгарда
 */
function executeRaidNodes(resolve, reject) {
	let raidData = {
		teams: [],
		favor: {},
		nodes: [],
		attempts: 0,
		countExecuteBattles: 0,
		cancelBattle: 0,
	}

	callsExecuteRaidNodes = {
		calls: [{
			name: "clanRaid_getInfo",
			args: {},
			ident: "clanRaid_getInfo"
		}, {
			name: "teamGetAll",
			args: {},
			ident: "teamGetAll"
		}, {
			name: "teamGetFavor",
			args: {},
			ident: "teamGetFavor"
		}]
	}

	this.start = function () {
		send(JSON.stringify(callsExecuteRaidNodes), startRaidNodes);
	}

	function startRaidNodes(data) {
		res = data.results;
		clanRaidInfo = res[0].result.response;
		teamGetAll = res[1].result.response;
		teamGetFavor = res[2].result.response;

		let index = 0;
		for (let team of teamGetAll.clanRaid_nodes) {
			raidData.teams.push({
				data: {},
				heroes: team.filter(id => id < 6000),
				pet: team.filter(id => id >= 6000).pop(),
				battleIndex: index++
			});
		}
		raidData.favor = teamGetFavor.clanRaid_nodes;

		raidData.nodes = clanRaidInfo.nodes;
		raidData.attempts = clanRaidInfo.attempts;
		isCancalBattle = false;

		checkNodes();
	}

	function getAttackNode() {
		for (let nodeId in raidData.nodes) {
			let node = raidData.nodes[nodeId];
			let points = 0
			for (team of node.teams) {
				points += team.points;
			}
			let now = Date.now() / 1000;
			if (!points && now > node.timestamps.start && now < node.timestamps.end) {
				let countTeam = node.teams.length;
				delete raidData.nodes[nodeId];
				return {
					nodeId,
					countTeam
				};
			}
		}
		return null;
	}

	function checkNodes() {
		setProgress(`${I18N('REMAINING_ATTEMPTS')}: ${raidData.attempts}`);
		let nodeInfo = getAttackNode();
		if (nodeInfo && raidData.attempts) {
			startNodeBattles(nodeInfo);
			return;
		}

		endRaidNodes('EndRaidNodes');
	}

	function startNodeBattles(nodeInfo) {
		let {nodeId, countTeam} = nodeInfo;
		let teams = raidData.teams.slice(0, countTeam);
		let heroes = raidData.teams.map(e => e.heroes).flat();
		let favor = {...raidData.favor};
		for (let heroId in favor) {
			if (!heroes.includes(+heroId)) {
				delete favor[heroId];
			}
		}

		let calls = [{
			name: "clanRaid_startNodeBattles",
			args: {
				nodeId,
				teams,
				favor
			},
			ident: "body"
		}];

		send(JSON.stringify({calls}), resultNodeBattles);
	}

	function resultNodeBattles(e) {
		if (e['error']) {
			endRaidNodes('nodeBattlesError', e['error']);
			return;
		}

		console.log(e);
		let battles = e.results[0].result.response.battles;
		let promises = [];
		let battleIndex = 0;
		for (let battle of battles) {
			battle.battleIndex = battleIndex++;
			promises.push(calcBattleResult(battle));
		}

		Promise.all(promises)
			.then(results => {
				const endResults = {};
				let isAllWin = true;
				for (let r of results) {
					isAllWin &&= r.result.win;
				}
				if (!isAllWin) {
					cancelEndNodeBattle(results[0]);
					return;
				}
				raidData.countExecuteBattles = results.length;
				let timeout = 500;
				for (let r of results) {
					setTimeout(endNodeBattle, timeout, r);
					timeout += 500;
				}
			});
	}
	/**
	 * Returns the battle calculation promise
	 *
	 * Возвращает промис расчета боя
	 */
	function calcBattleResult(battleData) {
		return new Promise(function (resolve, reject) {
			BattleCalc(battleData, "get_clanPvp", resolve);
		});
	}
	/**
	 * Cancels the fight
	 *
	 * Отменяет бой
	 */
	function cancelEndNodeBattle(r) {
		const fixBattle = function (heroes) {
			for (const ids in heroes) {
				hero = heroes[ids];
				hero.energy = random(1, 999);
				if (hero.hp > 0) {
					hero.hp = random(1, hero.hp);
				}
			}
		}
		fixBattle(r.progress[0].attackers.heroes);
		fixBattle(r.progress[0].defenders.heroes);
		endNodeBattle(r);
	}
	/**
	 * Ends the fight
	 *
	 * Завершает бой
	 */
	function endNodeBattle(r) {
		let nodeId = r.battleData.result.nodeId;
		let battleIndex = r.battleData.battleIndex;
		let calls = [{
			name: "clanRaid_endNodeBattle",
			args: {
				nodeId,
				battleIndex,
				result: r.result,
				progress: r.progress
			},
			ident: "body"
		}]

		SendRequest(JSON.stringify({calls}), battleResult);
	}
	/**
	 * Processing the results of the battle
	 *
	 * Обработка результатов боя
	 */
	function battleResult(e) {
		if (e['error']) {
			endRaidNodes('missionEndError', e['error']);
			return;
		}
		r = e.results[0].result.response;
		if (r['error']) {
			if (r.reason == "invalidBattle") {
				raidData.cancelBattle++;
				checkNodes();
			} else {
				endRaidNodes('missionEndError', e['error']);
			}
			return;
		}

		if (!(--raidData.countExecuteBattles)) {
			raidData.attempts--;
			checkNodes();
		}
	}
	/**
	 * Completing a task
	 *
	 * Завершение задачи
	 */
	function endRaidNodes(reason, info) {
		isCancalBattle = true;
		let textCancel = raidData.cancelBattle ? ` ${I18N('BATTLES_CANCELED')}: ${raidData.cancelBattle}` : '';
		setProgress(`${I18N('MINION_RAID')} ${I18N('COMPLETED')}! ${textCancel}`, true);
		console.log(reason, info);
		resolve();
	}
}

/**
 * Asgard Boss Attack Replay
 *
 * Повтор атаки босса Асгарда
 */
function testBossBattle() {
	return new Promise((resolve, reject) => {
		const bossBattle = new executeBossBattle(resolve, reject);
		bossBattle.start(lastBossBattle, lastBossBattleInfo);
	});
}

/**
 * Asgard Boss Attack Replay
 *
 * Повтор атаки босса Асгарда
 */
function executeBossBattle(resolve, reject) {
	let lastBossBattleArgs = {};
	let reachDamage = 0;
	let countBattle = 0;
	let countMaxBattle = 10;
	let lastDamage = 0;

	this.start = function (battleArg, battleInfo) {
		lastBossBattleArgs = battleArg;
		preCalcBattle(battleInfo);
	}

	function getBattleInfo(battle) {
		return new Promise(function (resolve) {
			battle.seed = Math.floor(Date.now() / 1000) + random(0, 1e3);
			BattleCalc(battle, getBattleType(battle.type), e => {
				let extra = e.progress[0].defenders.heroes[1].extra;
				resolve(extra.damageTaken + extra.damageTakenNextLevel);
			});
		});
	}

	function preCalcBattle(battle) {
		let actions = [];
		const countTestBattle = getInput('countTestBattle');
		for (let i = 0; i < countTestBattle; i++) {
			actions.push(getBattleInfo(battle, true));
		}
		Promise.all(actions)
			.then(resultPreCalcBattle);
	}

	function fixDamage(damage) {
		for (let i = 1e6; i > 1; i /= 10) {
			if (damage > i) {
				let n = i / 10;
				damage = Math.ceil(damage / n) * n;
				break;
			}
		}
		return damage;
	}

	async function resultPreCalcBattle(damages) {
		let maxDamage = 0;
		let minDamage = 1e10;
		let avgDamage = 0;
		for (let damage of damages) {
			avgDamage += damage
			if (damage > maxDamage) {
				maxDamage = damage;
			}
			if (damage < minDamage) {
				minDamage = damage;
			}
		}
		avgDamage /= damages.length;
		console.log(damages.map(e => e.toLocaleString()).join('\n'), avgDamage, maxDamage);

		reachDamage = fixDamage(avgDamage);
		const result = await popup.confirm(
			`${I18N('ROUND_STAT')} ${damages.length} ${I18N('BATTLE')}:` +
			`<br>${I18N('MINIMUM')}: ` + minDamage.toLocaleString() +
			`<br>${I18N('MAXIMUM')}: ` + maxDamage.toLocaleString() +
			`<br>${I18N('AVERAGE')}: ` + avgDamage.toLocaleString()
			/*+ '<br>Поиск урона больше чем ' + reachDamage.toLocaleString()*/
			, [
				{ msg: I18N('BTN_OK'), result: 0},
			/* {msg: 'Погнали', isInput: true, default: reachDamage}, */
		])
		if (result) {
			reachDamage = result;
			isCancalBossBattle = false;
			startBossBattle();
			return;
		}
		endBossBattle(I18N('BTN_CANCEL'));
	}

	function startBossBattle() {
		countBattle++;
		countMaxBattle = getInput('countAutoBattle');
		if (countBattle > countMaxBattle) {
			setProgress('Превышен лимит попыток: ' + countMaxBattle, true);
			endBossBattle('Превышен лимит попыток: ' + countMaxBattle);
			return;
		}
		let calls = [{
			name: "clanRaid_startBossBattle",
			args: lastBossBattleArgs,
			ident: "body"
		}];
		send(JSON.stringify({calls}), calcResultBattle);
	}

	function calcResultBattle(e) {
		BattleCalc(e.results[0].result.response.battle, "get_clanPvp", resultBattle);
	}

	async function resultBattle(e) {
		let extra = e.progress[0].defenders.heroes[1].extra
		resultDamage = extra.damageTaken + extra.damageTakenNextLevel
		console.log(resultDamage);
		scriptMenu.setStatus(countBattle + ') ' + resultDamage.toLocaleString());
		lastDamage = resultDamage;
		if (resultDamage > reachDamage && await popup.confirm(countBattle + ') Урон ' + resultDamage.toLocaleString(), [
			{msg: 'Ок', result: true},
			{msg: 'Не пойдет', result: false},
		]))  {
			endBattle(e, false);
			return;
		}
		cancelEndBattle(e);
	}

	function cancelEndBattle (r) {
		const fixBattle = function (heroes) {
			for (const ids in heroes) {
				hero = heroes[ids];
				hero.energy = random(1, 999);
				if (hero.hp > 0) {
					hero.hp = random(1, hero.hp);
				}
			}
		}
		fixBattle(r.progress[0].attackers.heroes);
		fixBattle(r.progress[0].defenders.heroes);
		endBattle(r, true);
	}

	function endBattle(battleResult, isCancal) {
		let calls = [{
			name: "clanRaid_endBossBattle",
			args: {
				result: battleResult.result,
				progress: battleResult.progress
			},
			ident: "body"
		}];

		send(JSON.stringify({calls}), e => {
			console.log(e);
			if (isCancal) {
				startBossBattle();
				return;
			}
			scriptMenu.setStatus('Босс пробит нанесен урон: ' + lastDamage);
			setTimeout(() => {
				scriptMenu.setStatus('');
			}, 5000);
			endBossBattle('Узпех!');
		});
	}

	/**
	 * Completing a task
	 *
	 * Завершение задачи
	 */
	function endBossBattle(reason, info) {
		isCancalBossBattle = true;
		console.log(reason, info);
		resolve();
	}
}

/**
 * Auto-repeat attack
 *
 * Автоповтор атаки
 */
function testAutoBattle() {
	return new Promise((resolve, reject) => {
		const bossBattle = new executeAutoBattle(resolve, reject);
		bossBattle.start(lastBattleArg, lastBattleInfo);
	});
}

/**
 * Auto-repeat attack
 *
 * Автоповтор атаки
 */
function executeAutoBattle(resolve, reject) {
	let battleArg = {};
	let countBattle = 0;
	let findCoeff = 0;

	this.start = function (battleArgs, battleInfo) {
		battleArg = battleArgs;
		preCalcBattle(battleInfo);
	}
	/**
	 * Returns a promise for combat recalculation
	 *
	 * Возвращает промис для прерасчета боя
	 */
	function getBattleInfo(battle) {
		return new Promise(function (resolve) {
			battle.seed = Math.floor(Date.now() / 1000) + random(0, 1e3);
			Calc(battle).then(e => {
				e.coeff = calcCoeff(e, 'defenders');
				resolve(e);
			});
		});
	}
	/**
	 * Battle recalculation
	 *
	 * Прерасчет боя
	 */
	function preCalcBattle(battle) {
		let actions = [];
		const countTestBattle = getInput('countTestBattle');
		for (let i = 0; i < countTestBattle; i++) {
			actions.push(getBattleInfo(battle));
		}
		Promise.all(actions)
			.then(resultPreCalcBattle);
	}
	/**
	 * Processing the results of the battle recalculation
	 *
	 * Обработка результатов прерасчета боя
	 */
	async function resultPreCalcBattle(results) {
		let countWin = results.reduce((s, w) => w.result.win + s, 0);
		setProgress(`${I18N('CHANCE_TO_WIN')} ${Math.floor(countWin / results.length * 100)}% (${results.length})`, true);
		if (countWin > 0) {
			isCancalBattle = false;
			startBattle();
			return;
		}

		let minCoeff = 100;
		let maxCoeff = -100;
		let avgCoeff = 0;
		results.forEach(e => {
			if (e.coeff < minCoeff) minCoeff = e.coeff;
			if (e.coeff > maxCoeff) maxCoeff = e.coeff;
			avgCoeff += e.coeff;
		});
		avgCoeff /= results.length;

		const result = await popup.confirm(
			I18N('VICTORY_IMPOSSIBLE') +
			`<br>${I18N('ROUND_STAT')} ${results.length} ${I18N('BATTLE')}:` +
			`<br>${I18N('MINIMUM')}: ` + minCoeff.toLocaleString() +
			`<br>${I18N('MAXIMUM')}: ` + maxCoeff.toLocaleString() +
			`<br>${I18N('AVERAGE')}: ` + avgCoeff.toLocaleString() +
			`<br>${I18N('FIND_COEFF')} ` + avgCoeff.toLocaleString(), [
			{ msg: I18N('BTN_CANCEL'), result: 0 },
			{ msg: I18N('BTN_GO'), isInput: true, default: Math.round(avgCoeff * 1000) / 1000 },
		])
		if (result) {
			findCoeff = result;
			isCancalBattle = false;
			startBattle();
			return;
		}
		setProgress(I18N('NOT_THIS_TIME'), true);
		endAutoBattle(I18N('NOT_THIS_TIME'));
	}

	/**
	 * Calculation of the combat result coefficient
	 *
	 * Расчет коэфициента результата боя
	 */
	function calcCoeff(result, packType) {
		let beforeSumFactor = 0;
		const beforePack = result.battleData[packType][0];
		for (let heroId in beforePack) {
			const hero = beforePack[heroId];
			const state = hero.state;
			let factor = 1;
			if (state) {
				const hp = state.hp / state.maxHp;
				const energy = state.energy / 1e3;
				factor = hp + energy / 20;
			}
			beforeSumFactor += factor;
		}

		let afterSumFactor = 0;
		const afterPack = result.progress[0][packType].heroes;
		for (let heroId in afterPack) {
			const hero = afterPack[heroId];
			const hp = hero.hp / beforePack[heroId].state.hp;
			const energy = hero.energy / 1e3;
			const factor = hp + energy / 20;
			afterSumFactor += factor;
		}
		const resultCoeff = -(afterSumFactor - beforeSumFactor);
		return Math.round(resultCoeff * 1000) / 1000;
	}
	/**
	 * Start battle
	 *
	 * Начало боя
	 */
	function startBattle() {
		countBattle++;
		const countMaxBattle = getInput('countAutoBattle');
		setProgress(countBattle + '/' + countMaxBattle);
		if (countBattle > countMaxBattle) {
			setProgress(`${I18N('RETRY_LIMIT_EXCEEDED')}: ${countMaxBattle}`, true);
			endAutoBattle(`${I18N('RETRY_LIMIT_EXCEEDED')}: ${countMaxBattle}`)
			return;
		}
		let calls = [{
			name: nameFuncStartBattle,
			args: battleArg,
			ident: "body"
		}];
		send(JSON.stringify({
			calls
		}), calcResultBattle);
	}
	/**
	 * Battle calculation
	 *
	 * Расчет боя
	 */
	function calcResultBattle(e) {
		let battle = e.results[0].result.response.battle
		BattleCalc(battle, getBattleType(battle.type), resultBattle);
	}
	/**
	 * Processing the results of the battle
	 *
	 * Обработка результатов боя
	 */
	function resultBattle(e) {
		const isWin = e.result.win;
		console.log(isWin);
		if (isWin) {
			endBattle(e, false);
			return;
		}
		if (findCoeff) {
			const coeff = calcCoeff(e, 'defenders');
			console.log(coeff);
			setProgress(coeff, true);
			if (coeff > findCoeff) {
				endBattle(e, false);
				return;
			}
		}
		cancelEndBattle(e);
	}
	/**
	 * Cancel fight
	 *
	 * Отмена боя
	 */
	function cancelEndBattle(r) {
		const fixBattle = function (heroes) {
			for (const ids in heroes) {
				hero = heroes[ids];
				hero.energy = random(1, 999);
				if (hero.hp > 0) {
					hero.hp = random(1, hero.hp);
				}
			}
		}
		fixBattle(r.progress[0].attackers.heroes);
		fixBattle(r.progress[0].defenders.heroes);
		endBattle(r, true);
	}
	/**
	 * End of the fight
	 *
	 * Завершение боя */
	function endBattle(battleResult, isCancal) {
		let calls = [{
			name: nameFuncEndBattle,
			args: {
				result: battleResult.result,
				progress: battleResult.progress
			},
			ident: "body"
		}];

		send(JSON.stringify({
			calls
		}), e => {
			console.log(e);
			if (isCancal) {
				startBattle();
				return;
			}
			scriptMenu.setStatus(`${I18N('SUCCESS')}!`);
			setTimeout(() => {
				scriptMenu.setStatus('');
			}, 5000)
			endAutoBattle(`${I18N('SUCCESS')}!`)
		});
	}
	/**
	 * Completing a task
	 *
	 * Завершение задачи
	 */
	function endAutoBattle(reason, info) {
		isCancalBattle = true;
		console.log(reason, info);
		resolve();
	}
}

function testDailyQuests() {
	return new Promise((resolve, reject) => {
		const bossBattle = new dailyQuests(resolve, reject, questsInfo);
		bossBattle.start();
	});
}

/**
 * Automatic completion of daily quests
 *
 * Автоматическое выполнение ежедневных квестов
 */
class dailyQuests {
	/**
	 * Send(' {"calls":[{"name":"heroGetAll","args":{},"ident":"body"}]}').then(e => console.log(e))
	 * Send(' {"calls":[{"name":"titanGetAll","args":{},"ident":"body"}]}').then(e => console.log(e))
	 * Send(' {"calls":[{"name":"inventoryGet","args":{},"ident":"body"}]}').then(e => console.log(e))
	 * Send(' {"calls":[{"name":"questGetAll","args":{},"ident":"body"}]}').then(e => console.log(e))
	 */
	dataQuests = {
		10001: {
			/**
			 * TODO: Watch heroes and money
			 * TODO: Смотреть героев и деньги
			 */
			description: 'Улучши умения героев 3 раза', //
			isWeCanDo: () => false,
		},
		10002: {
			description: 'Пройди 10 миссий', // --------------
			isWeCanDo: () => false,
		},
		10003: {
			description: 'Пройди 3 героические миссии', // --------------
			isWeCanDo: () => false,
		},
		10004: {
			description: 'Сразись 3 раза на Арене или Гранд Арене', // --------------
			isWeCanDo: () => false,
		},
		10006: {
			description: 'Используй обмен изумрудов 1 раз',
			doItCall: [{ name: "refillableAlchemyUse", args: { multi: false }, ident: "refillableAlchemyUse" }],
			isWeCanDo: () => false,
		},
		10007: {
			description: 'Открой 1 сундук', // ++++++++++++++++
			doItCall: [{ name: "chestBuy", args: { chest: "town", free: true, pack: false }, ident: "chestBuy" }],
			isWeCanDo: (info) => {
				const chestInfo = info['userGetInfo'].refillable.find(e => e.id == 37);
				return chestInfo.amount > 0;
			},
		},
		10016: {
			description: 'Отправь подарки согильдийцам', // ++++++++++++++++
			doItCall: [{ name: "clanSendDailyGifts", args: {}, ident: "clanSendDailyGifts" }],
			isWeCanDo: () => true,
		},
		10018: {
			/**
			 * TODO: Watch heroes, watch potions (consumable 9, 10, 11, 12)
			 * TODO: Смотреть героев, смотреть зелья (consumable 9, 10, 11, 12)
			 */
			description: 'Используй зелье опыта',
			/**
			 * Spends a bank of experience on Galahard
			 * Тратит банку опыта на Галахарда
			 */
			doItCall: [{ name: "consumableUseHeroXp", args: { heroId: 2, libId: 10, amount: 1 }, ident: "consumableUseHeroXp" }],
			isWeCanDo: () => false,
		},
		10019: {
			description: 'Открой 1 сундук в Башне',
			doItFunc: testTower,
			isWeCanDo: () => false,
		},
		10020: {
			description: 'Открой 3 сундука в Запределье',
			isWeCanDo: () => false,
		},
		10021: {
			description: 'Собери 75 Титанита в Подземелье Гильдии',
			isWeCanDo: () => false,
		},
		10022: {
			description: 'Собери 150 Титанита в Подземелье Гильдии',
			doItFunc: testDungeon,
			isWeCanDo: () => false,
		},
		10023: {
			/**
			 * TODO: Watch heroes, watch sparks (24 consumable, 250 at level 0 and 7000 gold)
			 * TODO: Смотреть героев, смотреть искры (consumable 24, 250 на 0 уровне и золото 7000)
			 */
			description: 'Прокачай Дар Стихий на 1 уровень',
			/**
			 * Upgrade and Reset Gift of the Elements to Galahard
			 * Улучшение и сброс дара стихий Галахарду
			*/
			doItCall: [
				{ name: "heroTitanGiftLevelUp", args: { heroId: 2 }, ident: "heroTitanGiftLevelUp" },
				{ name: "heroTitanGiftDrop", args: { heroId: 2 }, ident: "heroTitanGiftDrop" }
			],
			isWeCanDo: () => false,
		},
		10024: {
			/**
			 * TODO: Watch Heroes
			 * TODO: Смотреть героев
			 */
			description: 'Повысь уровень любого артефакта один раз',
			isWeCanDo: () => false,
		},
		10025: {
			description: 'Начни 1 Экспедицию',
			doItFunc: checkExpedition,
			isWeCanDo: () => false,
		},
		10026: {
			description: 'Начни 4 Экспедиции', // --------------
			doItFunc: checkExpedition,
			isWeCanDo: () => false,
		},
		10027: {
			description: 'Победи в 1 бою Турнира Стихий',
			doItFunc: testTitanArena,
			isWeCanDo: () => false,
		},
		10028: {
			/**
			 * TODO: Смотреть титанов, можно качать арты за золото если золота больше 5 лямов
			 * TODO: Watch titans, you can download arts for gold if there is more than 5kk gold
			 */
			description: 'Повысь уровень любого артефакта титанов',
			isWeCanDo: () => false,
		},
		10029: {
			description: 'Открой сферу артефактов титанов', // ++++++++++++++++
			doItCall: [{ name: "titanArtifactChestOpen", args: { amount: 1, free: true }, ident: "titanArtifactChestOpen" }],
			isWeCanDo: (info) => {
				return info['inventoryGet']?.consumable[55] > 0
			},
		},
		10030: {
			/**
			 * TODO: Watch Heroes
			 * TODO: Смотреть героев
			 */
			description: 'Улучши облик любого героя 1 раз',
			isWeCanDo: () => false,
		},
		10031: {
			description: 'Победи в 6 боях Турнира Стихий', // --------------
			doItFunc: testTitanArena,
			isWeCanDo: () => false,
		},
		10043: {
			description: 'Начни или присоеденись к Приключению', // --------------
			isWeCanDo: () => false,
		},
		10044: {
			description: 'Воспользуйся призывом питомцев 1 раз', // ++++++++++++++++
			doItCall: [{ name: "pet_chestOpen", args: { amount: 1, paid: false }, ident: "pet_chestOpen" }],
			isWeCanDo: (info) => {
				return info['inventoryGet']?.consumable[90] > 0
			},
		},
		10046: {
			/**
			 * TODO: Watch Adventure
			 * TODO: Смотреть приключение
			 */
			description: 'Открой 3 сундука в Приключениях',
			isWeCanDo: () => false,
		},
		10047: {
			/**
			 * TODO: Watch heroes and runes consumable 1, 2, 3, 4
			 * TODO: Смотреть героев и руны consumable 1, 2, 3, 4
			 */
			description: 'Набери 150 очков активности в Гильдии',
			/**
			 * Upgrade the rune Galahard
			 * Прокачать руну Галахарду
			 */
			doItCall: [{ name: "heroEnchantRune", args: { heroId: 2, tier: 0, items: { consumable: { '1': 1 } } }, ident: "heroEnchantRune" }],
			isWeCanDo: () => false,
		},
	};

	constructor(resolve, reject, questInfo) {
		this.resolve = resolve;
		this.reject = reject;
		this.questInfo = questInfo
	}

	async start() {
		/**
		 * TODO may not be needed
		 *
		 * TODO возожно не нужна
		 */
		let countQuest = 0;
		const weCanDo = [];
		const selectedActions = getSaveVal('selectedActions', {});
		for (let quest of this.questInfo['questGetAll']) {
			if (quest.id in this.dataQuests && quest.state == 1) {
				if (!selectedActions[quest.id]) {
					selectedActions[quest.id] = {
						checked: false
					}
				}
				if (!this.dataQuests[quest.id].isWeCanDo(this.questInfo)) {
					continue;
				}
			
				weCanDo.push({
					name: quest.id,
					label: I18N(`QUEST_${quest.id}`),
					checked: selectedActions[quest.id].checked
				});
				countQuest++;
			}
		}

		if (!weCanDo.length) {
			this.end(I18N('NOTHING_TO_DO'));
			return;
		}

		console.log(weCanDo);
		const answer = await popup.confirm(`${I18N('YOU_CAN_COMPLETE') }:`, [
			{ msg: I18N('BTN_DO_IT'), result: true },
			{ msg: I18N('BTN_CANCEL'), result: false },
		], weCanDo);
		if (!answer) {
			this.end('');
			return;
		}
		const taskList = popup.getCheckBoxes();
		taskList.forEach(e => {
			selectedActions[e.name].checked = e.checked;
		});
		setSaveVal('selectedActions', selectedActions);
		const calls = [];
		let countChecked = 0;
		for (const task of taskList) {
			if (task.checked) {
				countChecked++;
				const quest = this.dataQuests[task.name]
				console.log(quest.description);

				if (quest.doItCall) {
					calls.push(...quest.doItCall);
				}
			}
		}

		if (!countChecked) {
			this.end(I18N('NOT_QUEST_COMPLETED'));
			return;
		}

		await Send(JSON.stringify({ calls }));
		this.end(`${I18N('COMPLETED_QUESTS')}: ${countChecked}`);
	}

	errorHandling(error) {
		//console.error(error);
		let errorInfo = error.toString() + '\n';
		try {
			const errorStack = error.stack.split('\n');
			const endStack = errorStack.map(e => e.split('@')[0]).indexOf("testDoYourBest");
			errorInfo += errorStack.slice(0, endStack).join('\n');
		} catch (e) {
			errorInfo += error.stack;
		}
		copyText(errorInfo);
	}

	end(status) {
		setProgress(status, true);
		this.resolve();
	}
}

function testDoYourBest() {
	return new Promise((resolve, reject) => {
		const doIt = new doYourBest(resolve, reject);
		doIt.start();
	});
}

/**
 * Do everything button
 *
 * Кнопка сделать все
 */
class doYourBest {

	funcList = [
		{
			name: 'getOutland',
			label: I18N('ASSEMBLE_OUTLAND'),
			checked: false
		},
		{
			name: 'testTower',
			label: I18N('PASS_THE_TOWER'),
			checked: false
		},
		{
			name: 'checkExpedition',
			label: I18N('CHECK_EXPEDITIONS'),
			checked: false
		},
		{
			name: 'testTitanArena',
			label: I18N('COMPLETE_TOE'),
			checked: false
		},
		{
			name: 'testDungeon',
			label: I18N('COMPLETE_DUNGEON'),
			checked: false
		},
		{
			name: 'mailGetAll',
			label: I18N('COLLECT_MAIL'),
			checked: false
		},
		{
			name: 'collectAllStuff',
			label: I18N('COLLECT_MISC'),
			checked: false
		},
		{
			name: 'questAllFarm',
			label: I18N('COLLECT_QUEST_REWARDS'),
			checked: false
		},
		{
			name: 'synchronization',
			label: I18N('MAKE_A_SYNC'),
			checked: false
		},
	];

	functions = {
		getOutland,
		testTower,
		checkExpedition,
		testTitanArena,
		testDungeon,
		mailGetAll,
		collectAllStuff: async () => {
			await offerFarmAllReward();
			await Send('{"calls":[{"name":"subscriptionFarm","args":{},"ident":"body"},{"name":"zeppelinGiftFarm","args":{},"ident":"zeppelinGiftFarm"},{"name":"grandFarmCoins","args":{},"ident":"grandFarmCoins"}]}');
		},
		questAllFarm,
		synchronization: async () => {
			cheats.refreshGame();
		}
	}

	constructor(resolve, reject, questInfo) {
		this.resolve = resolve;
		this.reject = reject;
		this.questInfo = questInfo
	}

	async start() {
		const selectedDoIt = getSaveVal('selectedDoIt', {});

		this.funcList.forEach(task => {
			if (!selectedDoIt[task.name]) {
				selectedDoIt[task.name] = {
					checked: task.checked
				}
			} else {
				task.checked = selectedDoIt[task.name].checked
			}
		});

		const answer = await popup.confirm(I18N('RUN_FUNCTION'), [
			{ msg: I18N('BTN_CANCEL'), result: false },
			{ msg: I18N('BTN_GO'), result: true },
		], this.funcList);

		if (!answer) {
			this.end('');
			return;
		}

		const taskList = popup.getCheckBoxes();
		taskList.forEach(task => {
			selectedDoIt[task.name].checked = task.checked;
		});
		setSaveVal('selectedDoIt', selectedDoIt);
		for (const task of popup.getCheckBoxes()) {
			if (task.checked) {
				try {
					setProgress(`${task.label} <br>${I18N('PERFORMED')}!`);
					await this.functions[task.name]();
					setProgress(`${task.label} <br>${I18N('DONE')}!`);
				} catch (error) {
					if (await popup.confirm(`${I18N('ERRORS_OCCURRES')}:<br> ${task.label} <br>${I18N('COPY_ERROR')}?`, [
						{ msg: I18N('BTN_NO'), result: false },
						{ msg: I18N('BTN_YES'), result: true },
					])) {
						this.errorHandling(error);
					}
				}
			}
		}
		setTimeout((msg) => {
			this.end(msg);
		}, 2000, I18N('ALL_TASK_COMPLETED'));
		return;
	}

	errorHandling(error) {
		//console.error(error);
		let errorInfo = error.toString() + '\n';
		try {
			const errorStack = error.stack.split('\n');
			const endStack = errorStack.map(e => e.split('@')[0]).indexOf("testDoYourBest");
			errorInfo += errorStack.slice(0, endStack).join('\n');
		} catch (e) {
			errorInfo += error.stack;
		}
		copyText(errorInfo);
	}

	end(status) {
		setProgress(status, true);
		this.resolve();
	}
}

/**
 * Passing the adventure along the specified route
 *
 * Прохождение приключения по указанному маршруту
 */
function testAdventure(type) {
	return new Promise((resolve, reject) => {
		const bossBattle = new executeAdventure(resolve, reject);
		bossBattle.start(type);
	});
}

/**
 * Passing the adventure along the specified route
 *
 * Прохождение приключения по указанному маршруту
 */
class executeAdventure {

	type = 'default';

	actions = {
		default: {
			getInfo: "adventure_getInfo",
			startBattle: 'adventure_turnStartBattle',
			endBattle: 'adventure_endBattle',
			collectBuff: 'adventure_turnCollectBuff'
		},
		solo: {
			getInfo: "adventureSolo_getInfo",
			startBattle: 'adventureSolo_turnStartBattle',
			endBattle: 'adventureSolo_endBattle',
			collectBuff: 'adventureSolo_turnCollectBuff'
		}
	}

	terminatеReason = I18N('UNKNOWN');
	callAdventureInfo = {
		name: "adventure_getInfo",
		args: {},
		ident: "adventure_getInfo"
	}
	callTeamGetAll = {
		name: "teamGetAll",
		args: {},
		ident: "teamGetAll"
	}
	callTeamGetFavor = {
		name: "teamGetFavor",
		args: {},
		ident: "teamGetFavor"
	}
	callStartBattle = {
		name: "adventure_turnStartBattle",
		args: {},
		ident: "body"
	}
	callEndBattle = {
		name: "adventure_endBattle",
		args: {
			result: {},
			progress: {},
		},
		ident: "body"
	}
	callCollectBuff = {
		name: "adventure_turnCollectBuff",
		args: {},
		ident: "body"
	}

	constructor(resolve, reject) {
		this.resolve = resolve;
		this.reject = reject;
	}

	async start(type) {
		this.type = type || this.type;
		this.path = await this.getPath();
		if (!this.path) {
			this.end();
			return;
		}
		this.callAdventureInfo.name = this.actions[this.type].getInfo;
		const data = await Send(JSON.stringify({
			calls: [
				this.callAdventureInfo,
				this.callTeamGetAll,
				this.callTeamGetFavor
			]
		}));
		return this.checkAdventureInfo(data.results);
	}

	async getPath() {
		const answer = await popup.confirm(I18N('ENTER_THE_PATH'), [
			{
				msg: I18N('START_ADVENTURE'),
				placeholder: '1,2,3,4,5,6',
				isInput: true,
				default: getSaveVal('adventurePath', '')
			},
			{
				msg: I18N('BTN_CANCEL'),
				result: false
			},
		]);
		if (!answer) {
			this.terminatеReason = I18N('BTN_CANCELED');
			return false;
		}

		let path = answer.split(',');
		if (path.length < 2) {
			path = answer.split('-');
		}
		if (path.length < 2) {
			this.terminatеReason = I18N('MUST_TWO_POINTS');
			return false;
		}

		for (let p in path) {
			path[p] = +path[p].trim()
			if (Number.isNaN(path[p])) {
				this.terminatеReason = I18N('MUST_ONLY_NUMBERS');
				return false;
			}
		}
		setSaveVal('adventurePath', answer);
		return path;
	}

	checkAdventureInfo(data) {
		this.advInfo = data[0].result.response;
		if (!this.advInfo) {
			this.terminatеReason = I18N('NOT_ON_AN_ADVENTURE') ;
			return this.end();
		}
		const heroesTeam = data[1].result.response.adventure_hero;
		const favor = data[2]?.result.response.adventure_hero;
		const heroes = heroesTeam.slice(0, 5);
		const pet = heroesTeam[5];
		this.args = {
			pet,
			heroes,
			favor,
			path: [],
			broadcast: false
		}
		const advUserInfo = this.advInfo.users[userInfo.id];
		this.turnsLeft = advUserInfo.turnsLeft;
		this.currentNode = advUserInfo.currentNode;
		this.nodes = this.advInfo.nodes;

		if (this.currentNode == 1 && this.path[0] != 1) {
			this.path.unshift(1);
		}

		return this.loop();
	}

	async loop() {
		const position = this.path.indexOf(+this.currentNode);
		if (!(~position)) {
			this.terminatеReason = I18N('YOU_IN_NOT_ON_THE_WAY');
			return this.end();
		}
		this.path = this.path.slice(position);
		if ((this.path.length - 1) > this.turnsLeft &&
			await popup.confirm(I18N('ATTEMPTS_NOT_ENOUGH'), [
				{ msg: I18N('YES_CONTINUE'), result: false },
				{ msg: I18N('BTN_NO'), result: true },
			])) {
			this.terminatеReason = I18N('NOT_ENOUGH_AP');
			return this.end();
		}
		const toPath = [];
		for (const nodeId of this.path) {
			if (!this.turnsLeft) {
				this.terminatеReason = I18N('ATTEMPTS_ARE_OVER');
				return this.end();
			}
			toPath.push(nodeId);
			console.log(toPath);
			if (toPath.length > 1) {
				setProgress(toPath.join(' > ') + ` ${I18N('MOVES')}: ` + this.turnsLeft);
			}
			if (nodeId == this.currentNode) {
				continue;
			}

			const nodeInfo = this.getNodeInfo(nodeId);
			if (nodeInfo.type == 'TYPE_COMBAT') {
				if (nodeInfo.state == 'empty') {
					this.turnsLeft--;
					continue;
				}

				/**
				 * Disable regular battle cancellation
				 *
				 * Отключаем штатную отменую боя
				 */
				isCancalBattle = false;
				if (await this.battle(toPath)) {
					this.turnsLeft--;
					toPath.splice(0, toPath.indexOf(nodeId));
					nodeInfo.state = 'empty';
					isCancalBattle = true;
					continue;
				}
				isCancalBattle = true;
				return this.end()
			}

			if (nodeInfo.type == 'TYPE_PLAYERBUFF') {
				const buff = this.checkBuff(nodeInfo);
				if (buff == null) {
					continue;
				}

				if (await this.collectBuff(buff, toPath)) {
					this.turnsLeft--;
					toPath.splice(0, toPath.indexOf(nodeId));
					continue;
				}
				this.terminatеReason = I18N('BUFF_GET_ERROR');
				return this.end();
			}
		}
		this.terminatеReason = I18N('SUCCESS');
		return this.end();
	}

	/**
	 * Carrying out a fight
	 *
	 * Проведение боя
	 */
	async battle(path, preCalc = true) {
		const data = await this.startBattle(path);
		try {
			const battle = data.results[0].result.response.battle;
			const result = await Calc(battle);
			if (result.result.win) {
				const info = await this.endBattle(result);
				if (info.results[0].result.response?.error) {
					this.terminatеReason = I18N('BATTLE_END_ERROR');
					return false;
				}
			} else {
				await this.cancelBattle(result);

				if (preCalc && await this.preCalcBattle(battle)) {
					path = path.slice(-2);
					for (let i = 1; i <= getInput('countAutoBattle'); i++) {
						setProgress(`${I18N('AUTOBOT')}: ${i}/${getInput('countAutoBattle')}`);
						const result = await this.battle(path, false);
						if (result) {
							setProgress(I18N('VICTORY'));
							return true;
						}
					}
					this.terminatеReason = I18N('FAILED_TO_WIN_AUTO');
					return false;
				}
				return false;
			}
		} catch (error) {
			console.error(error);
			if (await popup.confirm(I18N('ERROR_OF_THE_BATTLE_COPY'), [
				{ msg: I18N('BTN_NO'), result: false },
				{ msg: I18N('BTN_YES'), result: true },
			])) {
				this.errorHandling(error, data);
			}
			this.terminatеReason = I18N('ERROR_DURING_THE_BATTLE');
			return false;
		}
		return true;
	}

	/**
	 * Recalculate battles
	 *
	 * Прерасчтет битвы
	 */
	async preCalcBattle(battle) {
		const countTestBattle = getInput('countTestBattle');
		for (let i = 0; i < countTestBattle; i++) {
			battle.seed = Math.floor(Date.now() / 1000) + random(0, 1e3);
			const result = await Calc(battle);
			if (result.result.win) {
				console.log(i, countTestBattle);
				return true;
			}
		}
		this.terminatеReason = I18N('NO_CHANCE_WIN') + countTestBattle;
		return false;
	}

	/**
	 * Starts a fight
	 *
	 * Начинает бой
	 */
	startBattle(path) {
		this.args.path = path;
		this.callStartBattle.name = this.actions[this.type].startBattle;
		this.callStartBattle.args = this.args
		const calls = [this.callStartBattle];
		return Send(JSON.stringify({ calls }));
	}

	cancelBattle(battle) {
		const fixBattle = function (heroes) {
			for (const ids in heroes) {
				const hero = heroes[ids];
				hero.energy = random(1, 999);
				if (hero.hp > 0) {
					hero.hp = random(1, hero.hp);
				}
			}
		}
		fixBattle(battle.progress[0].attackers.heroes);
		fixBattle(battle.progress[0].defenders.heroes);
		return this.endBattle(battle);
	}

	/**
	 * Ends the fight
	 *
	 * Заканчивает бой
	 */
	endBattle(battle) {
		this.callEndBattle.name = this.actions[this.type].endBattle;
		this.callEndBattle.args.result = battle.result
		this.callEndBattle.args.progress = battle.progress
		const calls = [this.callEndBattle];
		return Send(JSON.stringify({ calls }));
	}

	/**
	 * Checks if you can get a buff
	 *
	 * Проверяет можно ли получить баф
	 */
	checkBuff(nodeInfo) {
		let id = null;
		let value = 0;
		for (const buffId in nodeInfo.buffs) {
			const buff = nodeInfo.buffs[buffId];
			if (buff.owner == null && buff.value > value) {
				id = buffId;
				value = buff.value;
			}
		}
		nodeInfo.buffs[id].owner = 'Я';
		return id;
	}

	/**
	 * Collects a buff
	 *
	 * Собирает баф
	 */
	async collectBuff(buff, path) {
		this.callCollectBuff.name = this.actions[this.type].collectBuff;
		this.callCollectBuff.args = { buff, path };
		const calls = [this.callCollectBuff];
		return Send(JSON.stringify({ calls }));
	}

	getNodeInfo(nodeId) {
		return this.nodes.find(node => node.id == nodeId);
	}

	errorHandling(error, data) {
		//console.error(error);
		let errorInfo = error.toString() + '\n';
		try {
			const errorStack = error.stack.split('\n');
			const endStack = errorStack.map(e => e.split('@')[0]).indexOf("testAdventure");
			errorInfo += errorStack.slice(0, endStack).join('\n');
		} catch (e) {
			errorInfo += error.stack;
		}
		if (data) {
			errorInfo += '\nData: ' + JSON.stringify(data);
		}
		copyText(errorInfo);
	}

	end() {
		isCancalBattle = true;
		setProgress(this.terminatеReason, true);
		console.log(this.terminatеReason);
		this.resolve();
	}
}

/**
 * Passage of brawls
 *
 * Прохождение потасовок
 */
function testBrawls() {
	return new Promise((resolve, reject) => {
		const brawls = new executeBrawls(resolve, reject);
		brawls.start(brawlsPack);
	});
}
/**
 * Passage of brawls
 *
 * Прохождение потасовок
 */
class executeBrawls {
	callBrawlQuestGetInfo = {
		name: "brawl_questGetInfo",
		args: {},
		ident: "brawl_questGetInfo"
	}
	callBrawlFindEnemies = {
		name: "brawl_findEnemies",
		args: {},
		ident: "brawl_findEnemies"
	}
	callBrawlQuestFarm = {
		name: "brawl_questFarm",
		args: {},
		ident: "brawl_questFarm"
	}
	callUserGetInfo = {
		name: "userGetInfo",
		args: {},
		ident: "userGetInfo"
	}

	stats = {
		win: 0,
		loss: 0,
		count: 0,
	}

	stage = {
		'3': 1,
		'7': 2,
		'12': 3,
	}

	constructor(resolve, reject) {
		this.resolve = resolve;
		this.reject = reject;
	}

	async start(heroes) {
		this.heroes = heroes;
		isCancalBattle = false;
		this.brawlInfo = await this.getBrawlInfo();

		if (!this.brawlInfo.attempts) {
			this.end(I18N('DONT_HAVE_LIVES'))
			return;
		}

		while (1) {
			if (!isBrawlsAutoStart) {
				this.end(I18N('BTN_CANCELED'))
				return;
			}

			const maxStage = this.brawlInfo.questInfo.stage;
			const stage = this.stage[maxStage];
			const progress = this.brawlInfo.questInfo.progress;

			setProgress(`${I18N('STAGE')} ${stage}: ${progress}/${maxStage}<br>${I18N('FIGHTS')}: ${this.stats.count}<br>${I18N('WINS')}: ${this.stats.win}<br>${I18N('LOSSES')}: ${this.stats.loss}<br>${I18N('STOP')}`, false, function () {
				isBrawlsAutoStart = false;
			});

			if (this.brawlInfo.questInfo.canFarm) {
				const result = await this.questFarm();
				console.log(result);
			}

			if (this.brawlInfo.questInfo.stage == 12 && this.brawlInfo.questInfo.progress == 12) {
				this.end(I18N('SUCCESS'))
				return;
			}

			const enemie = Object.values(this.brawlInfo.findEnemies).shift();

			const result = await this.battle(enemie.userId);
			this.brawlInfo = {
				questInfo: result[1].result.response,
				findEnemies: result[2].result.response,
			}
		}
	}

	async questFarm() {
		const calls = [this.callBrawlQuestFarm];
		const result = await Send(JSON.stringify({ calls }));
		return result.results[0].result.response;
	}

	async getBrawlInfo() {
		const data = await Send(JSON.stringify({
			calls: [
				this.callUserGetInfo,
				this.callBrawlQuestGetInfo,
				this.callBrawlFindEnemies,
			]
		}));

		let attempts = data.results[0].result.response.refillable.find(n => n.id == 48);
		return {
			attempts: attempts.amount,
			questInfo: data.results[1].result.response,
			findEnemies: data.results[2].result.response,
		}
	}

	/**
	 * Carrying out a fight
	 *
	 * Проведение боя
	 */
	async battle(userId) {
		this.stats.count++;
		const battle = await this.startBattle(userId, this.heroes);
		const result = await Calc(battle);
		console.log(result.result);
		if (result.result.win) {
			this.stats.win++;
			return await this.endBattle(result);
		}
		this.stats.loss++;
		return await this.cancelBattle(result);
	}

	/**
	 * Starts a fight
	 *
	 * Начинает бой
	 */
	async startBattle(userId, heroes) {
		const calls = [{
			name: "brawl_startBattle",
			args: {
				userId,
				heroes,
				favor: {},
			},
			ident: "brawl_startBattle"
		}];
		const result = await Send(JSON.stringify({ calls }));
		return result.results[0].result.response;
	}

	cancelBattle(battle) {
		const fixBattle = function (heroes) {
			for (const ids in heroes) {
				const hero = heroes[ids];
				hero.energy = random(1, 999);
				if (hero.hp > 0) {
					hero.hp = random(1, hero.hp);
				}
			}
		}
		fixBattle(battle.progress[0].attackers.heroes);
		fixBattle(battle.progress[0].defenders.heroes);
		return this.endBattle(battle);
	}

	/**
	 * Ends the fight
	 *
	 * Заканчивает бой
	 */
	async endBattle(battle) {
		battle.progress[0].attackers.input = ['auto', 0, 0, 'auto', 0, 0];
		const calls = [{
			name: "brawl_endBattle",
			args: {
				result: battle.result,
				progress: battle.progress
			},
			ident: "brawl_endBattle"
		},
		this.callBrawlQuestGetInfo,
		this.callBrawlFindEnemies,
		];
		const result = await Send(JSON.stringify({ calls }));
		return result.results;
	}

	end(endReason) {
		isCancalBattle = true;
		isBrawlsAutoStart = false;
		setProgress(endReason, true);
		console.log(endReason);
		this.resolve();
	}
}
})();

/**
 * TODO:
 * Получение всех уровней при сборе всех наград (квест на титанит и на энку)
 * Добавить проверку правильности пути для приключения
 * Добивание на арене титанов
 * Сбор ежедневных календарных наград
 * Добавить в подземку проверку варианта когда одна пачка из 2х мертва
 * Кнопку Турнир стихий красить в красный цвет если не дошел до 7 этапа
 * добавить отображение урона на авто перед боем для босса Асгарда
 * Добавить ввод количества для рейдов в кампании
 * Удалить progress боя при прерасчете реплеев
 */