MZ Tactics Manager

Lets you manage your tactics in ManagerZone

当前为 2025-02-04 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name MZ Tactics Manager
  3. // @namespace douglaskampl
  4. // @version 10.1.0
  5. // @description Lets you manage your tactics in ManagerZone
  6. // @author Douglas Vieira
  7. // @match https://www.managerzone.com/?p=tactics
  8. // @match https://www.managerzone.com/?p=national_teams&sub=tactics&type=*
  9. // @icon https://yt3.googleusercontent.com/ytc/AIdro_mDHaJkwjCgyINFM7cdUV2dWPPnL9Q58vUsrhOmRqkatg=s160-c-k-c0x00ffffff-no-rj
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_addStyle
  13. // @require https://cdnjs.cloudflare.com/ajax/libs/jsSHA/3.3.1/sha256.js
  14. // @require https://cdnjs.cloudflare.com/ajax/libs/i18next/23.7.16/i18next.min.js
  15. // @require https://cdnjs.cloudflare.com/ajax/libs/sweetalert2/11.10.2/sweetalert2.min.js
  16. // @license MIT
  17. // ==/UserScript==
  18.  
  19. (function () {
  20. 'use strict';
  21.  
  22. GM_addStyle(`@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Dancing+Script:wght@500&display=swap"); #mz_tactics_panel {font-family: "Space Grotesk", -apple-system, sans-serif; background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border-radius: 12px; padding: 20px; margin: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); border: 1px solid #1e293b; transition: opacity 0.3s ease, max-height 0.3s ease; max-height: 1000px; opacity: 1; color: #f8fafc; } .mz-group {background: linear-gradient(135deg, #334155 0%, #1e293b 100%); border-radius: 8px; padding: 16px; margin: 8px; border: 1px solid #334155; position: relative; } .mz-group-main-title {color: #f8fafc; font-size: 16px; font-weight: 500; margin: -4px 0 12px 0; padding-bottom: 8px; border-bottom: 1px solid rgba(248, 250, 252, 0.1); } .mz-main-title {color: #f8fafc; font-family: "Space Grotesk", sans-serif; font-size: 18px; font-weight: 500; margin: 0; padding: 0; text-align: center; letter-spacing: 0.2px; } .mz-version-text {color: #ff9933; font-family: "Dancing Script", cursive; font-size: 0.9em; font-weight: 500; margin-left: 4px; } .mz-divider {width: 40px; height: 2px; background: #64748b; margin: 8px auto 0; opacity: 0.3; } #mz_tactics_panel .mzbtn {display: inline-flex; align-items: center; padding: 8px 14px; margin: 4px; font-family: "Space Grotesk", sans-serif; font-size: 13px; font-weight: 500; color: #f8fafc; background: #334155; border: none; border-radius: 6px; cursor: pointer; transition: all 0.2s ease; } #mz_tactics_panel .mzbtn:hover {background: #475569; transform: translateY(-1px); } #mz_tactics_panel .mzbtn:focus {outline: 2px solid #94a3b8; outline-offset: 1px; } #mz_tactics_panel select {font-family: "Space Grotesk", sans-serif; font-size: 13px; color: #f8fafc; padding: 8px 14px; border: 1px solid #334155; border-radius: 6px; background-color: #1e293b; cursor: pointer; margin: 4px; transition: all 0.2s ease; } #mz_tactics_panel select:focus {outline: none; border-color: #64748b; box-shadow: 0 0 0 2px rgba(100, 116, 139, 0.1); } #language_flag {height: 15px; width: 25px; margin: 6px 0 6px 6px; border: 1px solid #e2e8f0; border-radius: 22px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); } #info_modal, #useful_links_modal {background: #1e293b; padding: 20px; border-radius: 12px; color: #f8fafc; width: 90%; max-width: 500px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); } #info_modal a, #useful_links_modal a {color: #60a5fa; } #info_modal ul, #useful_links_modal ul {list-style: none; padding: 0; } #info_modal ul li, #useful_links_modal ul li {margin: 10px 0; } .swal2-popup.swal-mz-popup {font-family: "Space Grotesk", sans-serif; border-radius: 8px; background: #1e293b; color: #f8fafc; } .swal2-popup.swal-mz-popup .swal2-title {font-size: 18px; color: #f8fafc; } .swal2-popup.swal-mz-popup .swal2-html-container {font-size: 14px; color: #f8fafc; } .swal2-popup.swal-mz-popup .swal2-input {font-size: 14px; border: 1px solid #334155; border-radius: 6px; background: #0f172a; color: #f8fafc; } .swal2-popup.swal-mz-popup .swal2-textarea {background: #0f172a; color: #f8fafc; border: 1px solid #334155; } .swal2-popup.swal-mz-popup .swal2-confirm {background: #475569 !important; } .swal2-popup.swal-mz-popup .swal2-cancel {background: #64748b !important; } .swal2-container.swal2-backdrop-show {background: rgba(0, 0, 0, 0.6); } #hidden_trigger_button {position: absolute; visibility: hidden; pointer-events: none; }`);
  23.  
  24. const OUTFIELD_PLAYERS_SELECTOR = ".fieldpos.fieldpos-ok.ui-draggable:not(.substitute):not(.goalkeeper):not(.substitute.goalkeeper), .fieldpos.fieldpos-collision.ui-draggable:not(.substitute):not(.goalkeeper):not(.substitute.goalkeeper)";
  25. const GOALKEEPER_SELECTOR = ".fieldpos.fieldpos-ok.goalkeeper.ui-draggable";
  26. const FORMATION_TEXT_SELECTOR = "#formation_text";
  27. const TACTIC_SLOT_SELECTOR = ".ui-state-default.ui-corner-top.ui-tabs-selected.ui-state-active.invalid";
  28. const MIN_OUTFIELD_PLAYERS = 10;
  29. const MAX_TACTIC_NAME_LENGTH = 50;
  30. const DEFAULT_TACTICS_DATA_URL = "https://pub-02de1c06eac643f992bb26daeae5c7a0.r2.dev/json/defaultTactics.json";
  31. const LANG_DATA_BASE_URL = "https://pub-02de1c06eac643f992bb26daeae5c7a0.r2.dev/json/lang/";
  32. const BASE_FLAG_URL = "https://flagcdn.com/w320/";
  33. const LANGUAGES = [
  34. { code: "en", name: "English", flag: BASE_FLAG_URL + "gb.png" },
  35. { code: "pt", name: "Português", flag: BASE_FLAG_URL + "br.png" },
  36. { code: "zh", name: "中文", flag: BASE_FLAG_URL + "cn.png" },
  37. { code: "sv", name: "Svenska", flag: BASE_FLAG_URL + "se.png" },
  38. { code: "no", name: "Norsk", flag: BASE_FLAG_URL + "no.png" },
  39. { code: "da", name: "Dansk", flag: BASE_FLAG_URL + "dk.png" },
  40. { code: "es", name: "Español", flag: BASE_FLAG_URL + "ar.png" },
  41. { code: "pl", name: "Polski", flag: BASE_FLAG_URL + "pl.png" },
  42. { code: "nl", name: "Nederlands", flag: BASE_FLAG_URL + "nl.png" },
  43. { code: "id", name: "Bahasa Indonesia", flag: BASE_FLAG_URL + "id.png" },
  44. { code: "de", name: "Deutsch", flag: BASE_FLAG_URL + "de.png" },
  45. { code: "it", name: "Italiano", flag: BASE_FLAG_URL + "it.png" },
  46. { code: "fr", name: "Français", flag: BASE_FLAG_URL + "fr.png" },
  47. { code: "ro", name: "Română", flag: BASE_FLAG_URL + "ro.png" },
  48. { code: "tr", name: "Türkçe", flag: BASE_FLAG_URL + "tr.png" },
  49. { code: "ko", name: "한국어", flag: BASE_FLAG_URL + "kr.png" },
  50. { code: "ru", name: "Русский", flag: BASE_FLAG_URL + "ru.png" },
  51. { code: "ar", name: "العربية", flag: BASE_FLAG_URL + "sa.png" },
  52. { code: "jp", name: "日本語", flag: BASE_FLAG_URL + "jp.png" }
  53. ];
  54. const VERSION = "10.1.0";
  55. const VERSION_KEY = "mz_tactics_version";
  56. const SWAL_CONSTANTS = {
  57. ICONS: {
  58. SUCCESS: 'success',
  59. ERROR: 'error',
  60. WARNING: 'warning'
  61. }
  62. };
  63. const SWAL_CUSTOM_CLASS = {
  64. popup: 'swal-mz-popup',
  65. title: 'swal-mz-title',
  66. htmlContainer: 'swal-mz-html-container',
  67. input: 'swal-mz-input',
  68. validationMessage: 'swal-mz-validation',
  69. actions: 'swal-mz-actions',
  70. confirmButton: 'swal-mz-confirm',
  71. cancelButton: 'swal-mz-cancel',
  72. closeButton: 'swal-mz-close'
  73. };
  74. const USERSCRIPT_STRINGS = {
  75. addButton: "Add Tactic",
  76. addWithXmlButton: "Add Tactic via XML",
  77. deleteButton: "Delete Tactic",
  78. renameButton: "Rename Tactic",
  79. updateButton: "Update Tactic",
  80. clearButton: "Clear Tactics",
  81. resetButton: "Reset Tactics",
  82. importButton: "Import Tactics",
  83. exportButton: "Export Tactics",
  84. usefulLinksButton: "Useful Links",
  85. aboutButton: "About",
  86. tacticNamePrompt: "Tactic Name:",
  87. addAlert: "Tactic {} added successfully.",
  88. deleteAlert: "Tactic {} deleted successfully.",
  89. renameAlert: "Tactic {} renamed to {} successfully.",
  90. updateAlert: "Tactic {} updated successfully.",
  91. clearAlert: "Tactics cleared successfully.",
  92. resetAlert: "Tactics reset successfully.",
  93. importAlert: "Tactics imported successfully.",
  94. exportAlert: "Tactics exported successfully.",
  95. deleteConfirmation: "Do you really want to delete {}?",
  96. updateConfirmation: "Do you really want to update {}?",
  97. clearConfirmation: "Do you really want to clear tactics?",
  98. resetConfirmation: "Do you really want to reset tactics?",
  99. invalidTacticError: "Invalid tactic.",
  100. noTacticNameProvidedError: "No tactic name provided.",
  101. alreadyExistingTacticNameError: "Tactic name already exists.",
  102. tacticNameMaxLengthError: "Tactic name is too long.",
  103. noTacticSelectedError: "No tactic selected.",
  104. duplicateTacticError: "Duplicate tactic.",
  105. noChangesMadeError: "No changes made.",
  106. invalidImportError: "Invalid import file.",
  107. modalContentInfoText: "This is the tactic selector.",
  108. modalContentFeedbackText: "Send your feedback.",
  109. usefulContent: "Some useful resources:",
  110. tacticsDropdownMenuLabel: "Tactics:",
  111. languageDropdownMenuLabel: "Language:",
  112. errorTitle: "Error",
  113. doneTitle: "Done",
  114. confirmationTitle: "Confirmation",
  115. deleteTacticConfirmButton: "Delete",
  116. cancelConfirmButton: "Cancel",
  117. updateConfirmButton: "Update",
  118. clearTacticsConfirmButton: "Clear",
  119. resetTacticsConfirmButton: "Reset",
  120. addConfirmButton: "Add",
  121. xmlValidationError: "Invalid XML.",
  122. xmlParsingError: "Error parsing XML.",
  123. xmlPlaceholder: "Paste XML here",
  124. tacticNamePlaceholder: "Name",
  125. managerTitle: "MZ Tactics Manager",
  126. tacticActionsTitle: "Actions",
  127. otherActionsTitle: "Other",
  128. welcomeMessage: "Welcome to MZ Tactics Manager! Enjoy using it!<br><br>If you got any questions or suggestions, feel free to message douglaskampl via chat or guestbook.",
  129. welcomeGotIt: "Got it!"
  130. };
  131. const ELEMENT_STRING_KEYS = {
  132. add_tactic_button: "addButton",
  133. add_tactic_with_xml_button: "addWithXmlButton",
  134. delete_tactic_button: "deleteButton",
  135. rename_tactic_button: "renameButton",
  136. update_tactic_button: "updateButton",
  137. clear_tactics_button: "clearButton",
  138. reset_tactics_button: "resetButton",
  139. import_tactics_button: "importButton",
  140. export_tactics_button: "exportButton",
  141. about_button: "aboutButton",
  142. tactics_dropdown_menu_label: "tacticsDropdownMenuLabel",
  143. language_dropdown_menu_label: "languageDropdownMenuLabel",
  144. info_modal_info_text: "modalContentInfoText",
  145. info_modal_feedback_text: "modalContentFeedbackText",
  146. useful_links_button: "usefulLinksButton",
  147. };
  148.  
  149. let dropdownMenuTactics = [];
  150. let activeLanguage;
  151. let infoModal;
  152. let usefulLinksModal;
  153.  
  154. function isFootball() {
  155. const element = document.querySelector("div#tactics_box.soccer.clearfix");
  156. return !!element;
  157. }
  158.  
  159. function sha256Hash(str) {
  160. const shaObj = new jsSHA("SHA-256", "TEXT");
  161. shaObj.update(str);
  162. return shaObj.getHash("HEX");
  163. }
  164.  
  165. function showAlert(options) {
  166. const defaultOptions = {
  167. customClass: SWAL_CUSTOM_CLASS,
  168. buttonsStyling: true,
  169. showClass: {
  170. popup: 'swal-mz-popup modalFadeIn'
  171. },
  172. hideClass: {
  173. popup: 'modalFadeOut'
  174. },
  175. allowOutsideClick: true,
  176. allowEscapeKey: true,
  177. width: options.html?.includes('swal-xml-input') ? '600px' : '300px',
  178. padding: '20px'
  179. };
  180. if (options.customClass) {
  181. options.customClass = { ...SWAL_CUSTOM_CLASS, ...options.customClass };
  182. }
  183. return Swal.fire({
  184. ...defaultOptions,
  185. ...options
  186. });
  187. }
  188.  
  189. function showSuccessMessage(title, text) {
  190. return showAlert({
  191. title,
  192. text,
  193. icon: SWAL_CONSTANTS.ICONS.SUCCESS
  194. });
  195. }
  196.  
  197. function showErrorMessage(title, text) {
  198. return showAlert({
  199. title,
  200. text,
  201. icon: SWAL_CONSTANTS.ICONS.ERROR
  202. });
  203. }
  204.  
  205. async function fetchTacticsFromGMStorage() {
  206. const storedTactics = GM_getValue("ls_tactics");
  207. if (storedTactics) {
  208. return storedTactics;
  209. } else {
  210. const jsonTactics = await fetchTacticsFromJson();
  211. storeTacticsInGMStorage(jsonTactics);
  212. return jsonTactics;
  213. }
  214. }
  215.  
  216. async function fetchTacticsFromJson() {
  217. const response = await fetch(DEFAULT_TACTICS_DATA_URL);
  218. return await response.json();
  219. }
  220.  
  221. function storeTacticsInGMStorage(data) {
  222. GM_setValue("ls_tactics", data);
  223. }
  224.  
  225. async function validateDuplicateTactic(id) {
  226. const tacticsData = (await GM_getValue("ls_tactics")) || { tactics: [] };
  227. return tacticsData.tactics.some((tactic) => tactic.id === id);
  228. }
  229.  
  230. async function saveTacticToStorage(tactic) {
  231. const tacticsData = (await GM_getValue("ls_tactics")) || { tactics: [] };
  232. tacticsData.tactics.push(tactic);
  233. await GM_setValue("ls_tactics", tacticsData);
  234. }
  235.  
  236. async function validateDuplicateTacticWithUpdatedCoord(newId, selectedTac, tacticsData) {
  237. if (newId === selectedTac.id) {
  238. return "unchanged";
  239. } else if (tacticsData.tactics.some((tac) => tac.id === newId)) {
  240. return "duplicate";
  241. } else {
  242. return "unique";
  243. }
  244. }
  245.  
  246. function handleTacticsSelection(tactic) {
  247. const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR));
  248. const selectedTactic = dropdownMenuTactics.find((tacticData) => tacticData.name === tactic);
  249. if (selectedTactic) {
  250. if (outfieldPlayers.length < MIN_OUTFIELD_PLAYERS) {
  251. const hiddenTriggerButton = document.getElementById("hidden_trigger_button");
  252. hiddenTriggerButton.click();
  253. setTimeout(() => rearrangePlayers(selectedTactic.coordinates), 1);
  254. } else {
  255. rearrangePlayers(selectedTactic.coordinates);
  256. }
  257. }
  258. }
  259.  
  260. function rearrangePlayers(coordinates) {
  261. const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR));
  262. findBestPositions(outfieldPlayers, coordinates);
  263. for (let i = 0; i < outfieldPlayers.length; ++i) {
  264. outfieldPlayers[i].style.left = coordinates[i][0] + "px";
  265. outfieldPlayers[i].style.top = coordinates[i][1] + "px";
  266. removeCollision(outfieldPlayers[i]);
  267. }
  268. removeTacticSlotInvalidStatus();
  269. updateFormationText(getFormation(coordinates));
  270. }
  271.  
  272. function findBestPositions(players, coordinates) {
  273. players.sort((a, b) => parseInt(a.style.top) - parseInt(b.style.top));
  274. coordinates.sort((a, b) => a[1] - b[1]);
  275. }
  276.  
  277. function removeCollision(player) {
  278. if (player.classList.contains("fieldpos-collision")) {
  279. player.classList.remove("fieldpos-collision");
  280. player.classList.add("fieldpos-ok");
  281. }
  282. }
  283.  
  284. function removeTacticSlotInvalidStatus() {
  285. const slot = document.querySelector(TACTIC_SLOT_SELECTOR);
  286. if (slot) {
  287. slot.classList.remove("invalid");
  288. }
  289. }
  290.  
  291. function updateFormationText(formation) {
  292. const formationTextElement = document.querySelector(FORMATION_TEXT_SELECTOR);
  293. formationTextElement.querySelector(".defs").textContent = formation.defenders;
  294. formationTextElement.querySelector(".mids").textContent = formation.midfielders;
  295. formationTextElement.querySelector(".atts").textContent = formation.strikers;
  296. }
  297.  
  298. function getFormation(coordinates) {
  299. let strikers = 0;
  300. let midfielders = 0;
  301. let defenders = 0;
  302. for (const coo of coordinates) {
  303. const y = coo[1];
  304. if (y < 103) {
  305. strikers++;
  306. } else if (y <= 204) {
  307. midfielders++;
  308. } else {
  309. defenders++;
  310. }
  311. }
  312. return { strikers, midfielders, defenders };
  313. }
  314.  
  315. function validateTacticPlayerCount(outfieldPlayers) {
  316. const isGoalkeeper = document.querySelector(GOALKEEPER_SELECTOR);
  317. outfieldPlayers = outfieldPlayers.filter((player) => !player.classList.contains("fieldpos-collision"));
  318. if (outfieldPlayers.length < MIN_OUTFIELD_PLAYERS || !isGoalkeeper) {
  319. showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidTacticError);
  320. return false;
  321. }
  322. return true;
  323. }
  324.  
  325. async function addNewTactic() {
  326. const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR));
  327. const tacticsDropdownMenu = document.getElementById("tactics_dropdown_menu");
  328. const tacticCoordinates = outfieldPlayers.map((player) => [parseInt(player.style.left), parseInt(player.style.top)]);
  329. if (!validateTacticPlayerCount(outfieldPlayers)) {
  330. return;
  331. }
  332. const tacticId = generateUniqueId(tacticCoordinates);
  333. const isDuplicate = await validateDuplicateTactic(tacticId);
  334. if (isDuplicate) {
  335. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.duplicateTacticError);
  336. return;
  337. }
  338. const result = await showAlert({
  339. title: USERSCRIPT_STRINGS.tacticNamePrompt,
  340. input: 'text',
  341. inputValue: '',
  342. inputValidator: (value) => {
  343. if (!value) {
  344. return USERSCRIPT_STRINGS.noTacticNameProvidedError;
  345. }
  346. if (value.length > MAX_TACTIC_NAME_LENGTH) {
  347. return USERSCRIPT_STRINGS.tacticNameMaxLengthError;
  348. }
  349. if (dropdownMenuTactics.some((t) => t.name === value)) {
  350. return USERSCRIPT_STRINGS.alreadyExistingTacticNameError;
  351. }
  352. },
  353. showCancelButton: true,
  354. confirmButtonText: USERSCRIPT_STRINGS.addConfirmButton,
  355. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton
  356. });
  357. const tacticName = result.value;
  358. if (!tacticName) {
  359. return;
  360. }
  361. const tactic = {
  362. name: tacticName,
  363. coordinates: tacticCoordinates,
  364. id: tacticId
  365. };
  366. await saveTacticToStorage(tactic);
  367. addTacticsToDropdownMenu(tacticsDropdownMenu, [tactic]);
  368. dropdownMenuTactics.push(tactic);
  369. const placeholderOption = tacticsDropdownMenu.querySelector('option[value=""]');
  370. if (placeholderOption) {
  371. placeholderOption.remove();
  372. }
  373. if (tacticsDropdownMenu.disabled) {
  374. tacticsDropdownMenu.disabled = false;
  375. }
  376. tacticsDropdownMenu.value = tactic.name;
  377. const changeEvent = new Event('change', { bubbles: true });
  378. tacticsDropdownMenu.dispatchEvent(changeEvent);
  379. handleTacticsSelection(tactic.name);
  380. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.addAlert.replace("{}", tactic.name));
  381. }
  382.  
  383. async function addNewTacticWithXml() {
  384. const result = await showAlert({
  385. title: USERSCRIPT_STRINGS.addWithXmlButton,
  386. html:
  387. `<textarea id="swal-xml-input" class="swal2-textarea swal-mz-input" placeholder="${USERSCRIPT_STRINGS.xmlPlaceholder}" style="height: 200px;"></textarea>` +
  388. `<input id="swal-name-input" class="swal2-input swal-mz-input" placeholder="${USERSCRIPT_STRINGS.tacticNamePlaceholder}">`,
  389. focusConfirm: false,
  390. showCancelButton: true,
  391. confirmButtonText: USERSCRIPT_STRINGS.addConfirmButton,
  392. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton,
  393. preConfirm: () => {
  394. const xml = document.getElementById('swal-xml-input').value;
  395. const name = document.getElementById('swal-name-input').value;
  396. if (!xml) {
  397. Swal.showValidationMessage(USERSCRIPT_STRINGS.xmlValidationError);
  398. return false;
  399. }
  400. if (!name) {
  401. Swal.showValidationMessage(USERSCRIPT_STRINGS.noTacticNameProvidedError);
  402. return false;
  403. }
  404. if (name.length > MAX_TACTIC_NAME_LENGTH) {
  405. Swal.showValidationMessage(USERSCRIPT_STRINGS.tacticNameMaxLengthError);
  406. return false;
  407. }
  408. if (dropdownMenuTactics.some((t) => t.name === name)) {
  409. Swal.showValidationMessage(USERSCRIPT_STRINGS.alreadyExistingTacticNameError);
  410. return false;
  411. }
  412. return { xml, name };
  413. }
  414. });
  415. if (!result.value) {
  416. return;
  417. }
  418. try {
  419. const { xml, name } = result.value;
  420. const newTactic = await convertXmlToTacticJson(xml, name);
  421. const tacticId = generateUniqueId(newTactic.coordinates);
  422. const isDuplicate = await validateDuplicateTactic(tacticId);
  423. if (isDuplicate) {
  424. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.duplicateTacticError);
  425. return;
  426. }
  427. newTactic.id = tacticId;
  428. await saveTacticToStorage(newTactic);
  429. const tacticsDropdownMenu = document.getElementById('tactics_dropdown_menu');
  430. addTacticsToDropdownMenu(tacticsDropdownMenu, [newTactic]);
  431. dropdownMenuTactics.push(newTactic);
  432. tacticsDropdownMenu.value = newTactic.name;
  433. handleTacticsSelection(newTactic.name);
  434. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.addAlert.replace('{}', newTactic.name));
  435. } catch (e) {
  436. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.xmlParsingError);
  437. }
  438. }
  439.  
  440. async function deleteTactic() {
  441. const tacticsDropdownMenu = document.getElementById("tactics_dropdown_menu");
  442. const selectedTactic = dropdownMenuTactics.find((tactic) => tactic.name === tacticsDropdownMenu.value);
  443. if (!selectedTactic) {
  444. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError);
  445. return;
  446. }
  447. const result = await showAlert({
  448. text: USERSCRIPT_STRINGS.deleteConfirmation.replace("{}", selectedTactic.name),
  449. icon: SWAL_CONSTANTS.ICONS.WARNING,
  450. showCancelButton: true,
  451. confirmButtonText: USERSCRIPT_STRINGS.deleteTacticConfirmButton,
  452. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton
  453. });
  454. if (!result.isConfirmed) {
  455. return;
  456. }
  457. const tacticsData = (await GM_getValue("ls_tactics")) || { tactics: [] };
  458. tacticsData.tactics = tacticsData.tactics.filter((tactic) => tactic.id !== selectedTactic.id);
  459. await GM_setValue("ls_tactics", tacticsData);
  460. dropdownMenuTactics = dropdownMenuTactics.filter((tactic) => tactic.id !== selectedTactic.id);
  461. const selectedOption = Array.from(tacticsDropdownMenu.options).find((option) => option.value === selectedTactic.name);
  462. tacticsDropdownMenu.remove(selectedOption.index);
  463. if (tacticsDropdownMenu.options[0]?.disabled) {
  464. tacticsDropdownMenu.selectedIndex = 0;
  465. }
  466. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.deleteAlert.replace("{}", selectedTactic.name));
  467. }
  468.  
  469. async function renameTactic() {
  470. const tacticsDropdownMenu = document.getElementById("tactics_dropdown_menu");
  471. const selectedTactic = dropdownMenuTactics.find((tactic) => tactic.name === tacticsDropdownMenu.value);
  472. if (!selectedTactic) {
  473. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError);
  474. return;
  475. }
  476. const oldName = selectedTactic.name;
  477. const result = await showAlert({
  478. title: USERSCRIPT_STRINGS.tacticNamePrompt,
  479. input: 'text',
  480. inputValue: oldName,
  481. inputValidator: (value) => {
  482. if (!value) {
  483. return USERSCRIPT_STRINGS.noTacticNameProvidedError;
  484. }
  485. if (value.length > MAX_TACTIC_NAME_LENGTH) {
  486. return USERSCRIPT_STRINGS.tacticNameMaxLengthError;
  487. }
  488. if (value !== oldName && dropdownMenuTactics.some((t) => t.name === value)) {
  489. return USERSCRIPT_STRINGS.alreadyExistingTacticNameError;
  490. }
  491. },
  492. showCancelButton: true,
  493. confirmButtonText: USERSCRIPT_STRINGS.updateConfirmButton,
  494. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton
  495. });
  496. const newName = result.value;
  497. if (!newName) {
  498. return;
  499. }
  500. const selectedOption = Array.from(tacticsDropdownMenu.options).find((option) => option.value === selectedTactic.name);
  501. const tacticsData = (await GM_getValue("ls_tactics")) || { tactics: [] };
  502. tacticsData.tactics = tacticsData.tactics.map((tactic) => {
  503. if (tactic.id === selectedTactic.id) {
  504. tactic.name = newName;
  505. }
  506. return tactic;
  507. });
  508. await GM_setValue("ls_tactics", tacticsData);
  509. dropdownMenuTactics = dropdownMenuTactics.map((tactic) => {
  510. if (tactic.id === selectedTactic.id) {
  511. tactic.name = newName;
  512. }
  513. return tactic;
  514. });
  515. selectedOption.value = newName;
  516. selectedOption.textContent = newName;
  517. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.renameAlert.replace("{}", oldName).replace("{}", newName));
  518. }
  519.  
  520. async function updateTactic() {
  521. const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR));
  522. const tacticsDropdownMenu = document.getElementById("tactics_dropdown_menu");
  523. const selectedTactic = dropdownMenuTactics.find((tactic) => tactic.name === tacticsDropdownMenu.value);
  524. if (!selectedTactic) {
  525. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError);
  526. return;
  527. }
  528. const updatedCoordinates = outfieldPlayers.map((player) => [parseInt(player.style.left), parseInt(player.style.top)]);
  529. const newId = generateUniqueId(updatedCoordinates);
  530. const tacticsData = (await GM_getValue("ls_tactics")) || { tactics: [] };
  531. const validationOutcome = await validateDuplicateTacticWithUpdatedCoord(newId, selectedTactic, tacticsData);
  532. if (validationOutcome === "unchanged") {
  533. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noChangesMadeError);
  534. return;
  535. } else if (validationOutcome === "duplicate") {
  536. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.duplicateTacticError);
  537. return;
  538. }
  539. const result = await showAlert({
  540. text: USERSCRIPT_STRINGS.updateConfirmation.replace("{}", selectedTactic.name),
  541. icon: SWAL_CONSTANTS.ICONS.WARNING,
  542. showCancelButton: true,
  543. confirmButtonText: USERSCRIPT_STRINGS.updateConfirmButton,
  544. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton
  545. });
  546. if (!result.isConfirmed) {
  547. return;
  548. }
  549. for (const tactic of tacticsData.tactics) {
  550. if (tactic.id === selectedTactic.id) {
  551. tactic.coordinates = updatedCoordinates;
  552. tactic.id = newId;
  553. }
  554. }
  555. for (const tactic of dropdownMenuTactics) {
  556. if (tactic.id === selectedTactic.id) {
  557. tactic.coordinates = updatedCoordinates;
  558. tactic.id = newId;
  559. }
  560. }
  561. await GM_setValue("ls_tactics", tacticsData);
  562. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.updateAlert.replace("{}", selectedTactic.name));
  563. }
  564.  
  565. async function clearTactics() {
  566. const result = await showAlert({
  567. text: USERSCRIPT_STRINGS.clearConfirmation,
  568. icon: SWAL_CONSTANTS.ICONS.WARNING,
  569. showCancelButton: true,
  570. confirmButtonText: USERSCRIPT_STRINGS.clearTacticsConfirmButton,
  571. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton
  572. });
  573. if (!result.isConfirmed) {
  574. return;
  575. }
  576. await GM_setValue("ls_tactics", { tactics: [] });
  577. dropdownMenuTactics = [];
  578. const tacticsDropdownMenu = document.getElementById("tactics_dropdown_menu");
  579. tacticsDropdownMenu.innerHTML = "";
  580. tacticsDropdownMenu.disabled = true;
  581. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.clearAlert);
  582. }
  583.  
  584. async function resetTactics() {
  585. const result = await showAlert({
  586. text: USERSCRIPT_STRINGS.resetConfirmation,
  587. icon: SWAL_CONSTANTS.ICONS.WARNING,
  588. showCancelButton: true,
  589. confirmButtonText: USERSCRIPT_STRINGS.resetTacticsConfirmButton,
  590. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton
  591. });
  592. if (!result.isConfirmed) {
  593. return;
  594. }
  595. const response = await fetch(DEFAULT_TACTICS_DATA_URL);
  596. const data = await response.json();
  597. const defaultTactics = data.tactics;
  598. await GM_setValue("ls_tactics", { tactics: defaultTactics });
  599. dropdownMenuTactics = defaultTactics;
  600. const tacticsDropdownMenu = document.getElementById("tactics_dropdown_menu");
  601. tacticsDropdownMenu.innerHTML = "";
  602. tacticsDropdownMenu.appendChild(createPlaceholderOption());
  603. addTacticsToDropdownMenu(tacticsDropdownMenu, dropdownMenuTactics);
  604. tacticsDropdownMenu.disabled = false;
  605. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.resetAlert);
  606. }
  607.  
  608. async function importTactics() {
  609. const input = document.createElement("input");
  610. input.type = "file";
  611. input.accept = ".json";
  612. input.onchange = async function (event) {
  613. const file = event.target.files[0];
  614. const reader = new FileReader();
  615. reader.onload = async function (event) {
  616. let importedData;
  617. try {
  618. importedData = JSON.parse(event.target.result);
  619. } catch (e) {
  620. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidImportError);
  621. return;
  622. }
  623. if (!importedData || !Array.isArray(importedData.tactics)) {
  624. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidImportError);
  625. return;
  626. }
  627. const importedTactics = importedData.tactics;
  628. let existingTactics = await GM_getValue("ls_tactics", { tactics: [] });
  629. existingTactics = existingTactics.tactics;
  630. const mergedTactics = [...existingTactics];
  631. for (const importedTactic of importedTactics) {
  632. if (!existingTactics.some((tactic) => tactic.id === importedTactic.id)) {
  633. mergedTactics.push(importedTactic);
  634. }
  635. }
  636. await GM_setValue("ls_tactics", { tactics: mergedTactics });
  637. mergedTactics.sort((a, b) => a.name.localeCompare(b.name));
  638. dropdownMenuTactics = mergedTactics;
  639. const tacticsDropdownMenu = document.getElementById("tactics_dropdown_menu");
  640. tacticsDropdownMenu.innerHTML = "";
  641. tacticsDropdownMenu.append(createPlaceholderOption());
  642. addTacticsToDropdownMenu(tacticsDropdownMenu, dropdownMenuTactics);
  643. tacticsDropdownMenu.disabled = false;
  644. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.importAlert);
  645. };
  646. reader.readAsText(file);
  647. };
  648. input.click();
  649. }
  650.  
  651. function exportTactics() {
  652. const tactics = GM_getValue("ls_tactics", { tactics: [] });
  653. const tacticsJson = JSON.stringify(tactics);
  654. const blob = new Blob([tacticsJson], { type: "application/json" });
  655. const url = URL.createObjectURL(blob);
  656. const link = document.createElement("a");
  657. link.href = url;
  658. link.download = "tactics.json";
  659. const onFocus = () => {
  660. window.removeEventListener('focus', onFocus);
  661. URL.revokeObjectURL(url);
  662. };
  663. window.addEventListener('focus', onFocus, { once: true });
  664. link.click();
  665. }
  666.  
  667. async function convertXmlToTacticJson(xmlString, tacticName) {
  668. const parser = new DOMParser();
  669. const xmlDoc = parser.parseFromString(xmlString, 'text/xml');
  670. const parserError = xmlDoc.getElementsByTagName('parsererror');
  671. if (parserError.length > 0) {
  672. throw new Error('Invalid XML');
  673. }
  674. const posElements = Array.from(xmlDoc.getElementsByTagName('Pos'));
  675. const normalPosElements = posElements.filter(el => el.getAttribute('pos') === 'normal');
  676. const coordinates = normalPosElements.map(el => {
  677. const x = parseInt(el.getAttribute('x'));
  678. const y = parseInt(el.getAttribute('y'));
  679. const htmlLeft = x - 7;
  680. const htmlTop = y - 9;
  681. return [htmlLeft, htmlTop];
  682. });
  683. return {
  684. name: tacticName,
  685. coordinates: coordinates
  686. };
  687. }
  688.  
  689. function createAddNewTacticButton() {
  690. const button = document.createElement("button");
  691. setUpButton(button, "add_tactic_button", USERSCRIPT_STRINGS.addButton);
  692. button.addEventListener("click", function () {
  693. addNewTactic().catch((_) => { });
  694. });
  695. return button;
  696. }
  697.  
  698. function createAddNewTacticWithXmlButton() {
  699. const button = document.createElement("button");
  700. setUpButton(button, "add_tactic_with_xml_button", USERSCRIPT_STRINGS.addWithXmlButton);
  701. button.addEventListener("click", function () {
  702. addNewTacticWithXml().catch((_) => { });
  703. });
  704. return button;
  705. }
  706.  
  707. function createDeleteTacticButton() {
  708. const button = document.createElement("button");
  709. setUpButton(button, "delete_tactic_button", USERSCRIPT_STRINGS.deleteButton);
  710. button.addEventListener("click", function () {
  711. deleteTactic().catch((_) => { });
  712. });
  713. return button;
  714. }
  715.  
  716. function createRenameTacticButton() {
  717. const button = document.createElement("button");
  718. setUpButton(button, "rename_tactic_button", USERSCRIPT_STRINGS.renameButton);
  719. button.addEventListener("click", function () {
  720. renameTactic().catch((_) => { });
  721. });
  722. return button;
  723. }
  724.  
  725. function createUpdateTacticButton() {
  726. const button = document.createElement("button");
  727. setUpButton(button, "update_tactic_button", USERSCRIPT_STRINGS.updateButton);
  728. button.addEventListener("click", function () {
  729. updateTactic().catch((_) => { });
  730. });
  731. return button;
  732. }
  733.  
  734. function createClearTacticsButton() {
  735. const button = document.createElement("button");
  736. setUpButton(button, "clear_tactics_button", USERSCRIPT_STRINGS.clearButton);
  737. button.addEventListener("click", function () {
  738. clearTactics().catch((_) => { });
  739. });
  740. return button;
  741. }
  742.  
  743. function createResetTacticsButton() {
  744. const button = document.createElement("button");
  745. setUpButton(button, "reset_tactics_button", USERSCRIPT_STRINGS.resetButton);
  746. button.addEventListener("click", function () {
  747. resetTactics().catch((_) => { });
  748. });
  749. return button;
  750. }
  751.  
  752. function createImportTacticsButton() {
  753. const button = document.createElement("button");
  754. setUpButton(button, "import_tactics_button", USERSCRIPT_STRINGS.importButton);
  755. button.addEventListener("click", function () {
  756. importTactics().catch((_) => { });
  757. });
  758. return button;
  759. }
  760.  
  761. function createExportTacticsButton() {
  762. const button = document.createElement("button");
  763. setUpButton(button, "export_tactics_button", USERSCRIPT_STRINGS.exportButton);
  764. button.addEventListener("click", function () {
  765. exportTactics();
  766. });
  767. return button;
  768. }
  769.  
  770. async function checkVersion() {
  771. const storedVersion = GM_getValue(VERSION_KEY, null);
  772.  
  773. if (!storedVersion || storedVersion !== VERSION) {
  774. await showWelcomeMessage();
  775. GM_setValue(VERSION_KEY, VERSION);
  776. }
  777. }
  778.  
  779. async function showWelcomeMessage() {
  780. await showAlert({
  781. html: `
  782. <div style="text-align: left; margin: 1em 0;">
  783. <p>${USERSCRIPT_STRINGS.welcomeMessage}</p>
  784. </div>
  785. `,
  786. icon: 'info',
  787. confirmButtonText: USERSCRIPT_STRINGS.welcomeGotIt,
  788. customClass: {
  789. popup: 'swal-mz-popup',
  790. confirmButton: 'swal-mz-confirm'
  791. },
  792. showClass: {
  793. popup: 'swal-mz-popup modalFadeIn'
  794. },
  795. hideClass: {
  796. popup: 'modalFadeOut'
  797. }
  798. });
  799. }
  800.  
  801. function playRandomAudio(audios) {
  802. if (audios.length === 0) {
  803. return;
  804. }
  805. const randomIdx = Math.floor(Math.random() * audios.length);
  806. const activeAudio = audios.splice(randomIdx, 1)[0];
  807. playAudio(activeAudio, audios);
  808. return activeAudio;
  809. }
  810.  
  811. function playAudio(currAudio, audios) {
  812. currAudio.play();
  813. currAudio.onended = function () {
  814. playRandomAudio(audios);
  815. };
  816. }
  817.  
  818. function pauseAudio(audio) {
  819. if (audio) {
  820. audio.pause();
  821. audio.currentTime = 0;
  822. }
  823. }
  824.  
  825. function updateAudioIcon(button, isPlaying) {
  826. button.textContent = isPlaying ? "⏸️" : "🔊";
  827. }
  828.  
  829. function createAudioButton() {
  830. const button = document.createElement("button");
  831. setUpButton(button, "audio_button", "🔊");
  832.  
  833. const audioUrls = [
  834. "https://ia801901.us.archive.org/31/items/corp.-palm-mall-01-palm-mall/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20Palm%20Mall%20-%2003%20Special%20Discount.mp3",
  835. "https://ia801901.us.archive.org/31/items/corp.-palm-mall-01-palm-mall/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20Palm%20Mall%20-%2004%20First%20Floor.mp3",
  836. "https://ia801901.us.archive.org/31/items/corp.-palm-mall-01-palm-mall/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20Palm%20Mall%20-%2006%20Second%20Floor.mp3",
  837. "https://ia801901.us.archive.org/7/items/palm-mall-mars-remastered/%E7%8C%AB%20%E3%82%B7%20Corp.%20%26%20SEPHORA%E8%84%B3%E3%83%90%E3%82%A4%E3%83%96%E3%82%B9%20-%20Palm%20Mall%20Mars%20%28remastered%29%20-%2006%20Second%20floor-%20%ED%99%98%EB%8C%80%20%26%20%EC%9D%8C%EC%95%85.mp3",
  838. "https://ia801901.us.archive.org/7/items/palm-mall-mars-remastered/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20Palm%20Mall%20Mars%20%28remastered%29%20-%2001%20%E3%82%B9%E3%82%AD%E3%83%9D%E3%83%BC%E3%83%AB%E7%A9%BA%E6%B8%AFPlaza.mp3",
  839. "https://ia801901.us.archive.org/7/items/palm-mall-mars-remastered/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20Palm%20Mall%20Mars%20%28remastered%29%20-%2009%20Sembikiya%20Restaurant.mp3",
  840. "https://ia804504.us.archive.org/20/items/5-wn9896/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20%E3%82%B7%E3%83%A7%E3%83%83%E3%83%97%20%40%20%E3%83%98%E3%83%AB%E3%82%B7%E3%83%B3%E3%82%AD%20-%2001%20FORUM%20%E6%B6%88%E8%B2%BB%E8%80%85-kuluttaja-.mp3",
  841. "https://ia904504.us.archive.org/20/items/5-wn9896/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20%E3%82%B7%E3%83%A7%E3%83%83%E3%83%97%20%40%20%E3%83%98%E3%83%AB%E3%82%B7%E3%83%B3%E3%82%AD%20-%2002%20Pelican%20Self%20Storage%20-Tilaa%20Kaikelle-.mp3",
  842. "https://ia904504.us.archive.org/20/items/5-wn9896/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20%E3%82%B7%E3%83%A7%E3%83%83%E3%83%97%20%40%20%E3%83%98%E3%83%AB%E3%82%B7%E3%83%B3%E3%82%AD%20-%2003%20%E8%B2%B7%E3%81%86%40JUMBO%20-Kauppakeskus-.mp3",
  843. "https://ia904504.us.archive.org/20/items/5-wn9896/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20%E3%82%B7%E3%83%A7%E3%83%83%E3%83%97%20%40%20%E3%83%98%E3%83%AB%E3%82%B7%E3%83%B3%E3%82%AD%20-%2005%20Hesburger%20%E6%98%A0%E7%94%BB%E9%A4%A8%20-hampurilainen-.mp3",
  844. "https://ia804504.us.archive.org/20/items/5-wn9896/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20%E3%82%B7%E3%83%A7%E3%83%83%E3%83%97%20%40%20%E3%83%98%E3%83%AB%E3%82%B7%E3%83%B3%E3%82%AD%20-%2006%20%E9%83%BD%E5%B8%82%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A9%E3%83%A0%20Consumer%20-kahvi-.mp3"
  845. ];
  846. const audios = audioUrls.map(url => new Audio(url));
  847. let isPlaying = false;
  848. let currentAudio = null;
  849.  
  850. button.addEventListener("click", function () {
  851. if (!isPlaying) {
  852. currentAudio = playRandomAudio(audios);
  853. isPlaying = true;
  854. } else {
  855. pauseAudio(currentAudio);
  856. isPlaying = false;
  857. }
  858. updateAudioIcon(button, isPlaying);
  859. });
  860.  
  861. return button;
  862. }
  863.  
  864. function createMainContainer() {
  865. const container = document.createElement("div");
  866. container.id = "mz_tactics_panel";
  867. container.classList.add("mz-panel");
  868.  
  869. const tacticGroup = document.createElement("div");
  870. tacticGroup.classList.add("mz-group");
  871.  
  872. const mainTitle = document.createElement("h2");
  873. mainTitle.classList.add("mz-group-main-title");
  874. const titleText = document.createElement("span");
  875. titleText.textContent = "MZ Tactics Manager ";
  876. mainTitle.appendChild(titleText);
  877. const authorText = document.createElement("span");
  878. authorText.textContent = "v10.1.0";
  879. authorText.classList.add("mz-version-text");
  880. mainTitle.appendChild(authorText);
  881.  
  882. const dropdownSection = document.createElement("div");
  883. const tacticsDropdownMenuLabel = createDropdownMenuLabel("tactics_dropdown_menu_label");
  884. const tacticsDropdownMenu = createTacticsDropdownMenu();
  885. const tacticsDropdownGroup = createLabelDropdownMenuGroup(tacticsDropdownMenuLabel, tacticsDropdownMenu);
  886. dropdownSection.appendChild(tacticsDropdownGroup);
  887.  
  888. const buttonsSection = document.createElement("div");
  889. buttonsSection.style.marginTop = "10px";
  890.  
  891. const addNewTacticBtn = createAddNewTacticButton();
  892. const addNewTacticWithXmlBtn = createAddNewTacticWithXmlButton();
  893. const deleteTacticBtn = createDeleteTacticButton();
  894. const renameTacticBtn = createRenameTacticButton();
  895. const updateTacticBtn = createUpdateTacticButton();
  896. const clearTacticsBtn = createClearTacticsButton();
  897. const resetTacticsBtn = createResetTacticsButton();
  898. const importTacticsBtn = createImportTacticsButton();
  899. const exportTacticsBtn = createExportTacticsButton();
  900.  
  901. appendChildren(buttonsSection, [
  902. addNewTacticBtn,
  903. addNewTacticWithXmlBtn,
  904. deleteTacticBtn,
  905. renameTacticBtn,
  906. updateTacticBtn,
  907. clearTacticsBtn,
  908. resetTacticsBtn,
  909. importTacticsBtn,
  910. exportTacticsBtn
  911. ]);
  912.  
  913. appendChildren(tacticGroup, [
  914. mainTitle,
  915. dropdownSection,
  916. buttonsSection,
  917. createHiddenTriggerButton()
  918. ]);
  919.  
  920. const otherGroup = document.createElement("div");
  921. otherGroup.classList.add("mz-group");
  922.  
  923. const otherContainer = document.createElement("div");
  924. otherContainer.style.display = "flex";
  925. otherContainer.style.justifyContent = "space-between";
  926. otherContainer.style.alignItems = "center";
  927. otherContainer.style.width = "100%";
  928.  
  929. const otherLeftGroup = document.createElement("div");
  930. otherLeftGroup.style.display = "flex";
  931. otherLeftGroup.style.alignItems = "center";
  932.  
  933. const usefulLinksBtn = createUsefulLinksButton();
  934. const aboutBtn = createAboutButton();
  935. const audioBtn = createAudioButton();
  936.  
  937. appendChildren(otherLeftGroup, [usefulLinksBtn, aboutBtn, audioBtn]);
  938.  
  939. const otherRightGroup = document.createElement("div");
  940. otherRightGroup.style.display = "flex";
  941. otherRightGroup.style.alignItems = "center";
  942.  
  943. const languageDropdownMenuLabel = createDropdownMenuLabel("language_dropdown_menu_label");
  944. const languageDropdownMenu = createLanguageDropdownMenu();
  945. const languageDropdownGroup = createLabelDropdownMenuGroup(languageDropdownMenuLabel, languageDropdownMenu);
  946. const flagImage = createFlagImage();
  947.  
  948. appendChildren(otherRightGroup, [languageDropdownGroup, flagImage]);
  949. appendChildren(otherContainer, [otherLeftGroup, otherRightGroup]);
  950. appendChildren(otherGroup, [otherContainer]);
  951.  
  952. appendChildren(container, [tacticGroup, otherGroup]);
  953.  
  954. return container;
  955. }
  956.  
  957. function createHiddenTriggerButton() {
  958. const button = document.createElement("button");
  959. button.id = "hidden_trigger_button";
  960. button.textContent = "";
  961. button.style.visibility = "hidden";
  962. button.addEventListener("click", function () {
  963. const tacticsPresetInfo = {
  964. elem: document.getElementById("tactics_preset"),
  965. resetValue: "5-3-2"
  966. }
  967. tacticsPresetInfo.elem.value = tacticsPresetInfo.resetValue;
  968. tacticsPresetInfo.elem.dispatchEvent(new Event("change"));
  969. });
  970. return button;
  971. }
  972.  
  973. function insertAfterElement(something, element) {
  974. element.parentNode.insertBefore(something, element.nextSibling);
  975. }
  976.  
  977. function appendChildren(parent, children) {
  978. children.forEach((ch) => {
  979. parent.appendChild(ch);
  980. });
  981. }
  982.  
  983. function setUpButton(button, id, textContent) {
  984. button.id = id;
  985. button.classList.add('mzbtn');
  986. button.textContent = textContent;
  987. }
  988.  
  989. function createTacticsDropdownMenu() {
  990. const dropdown = document.createElement("select");
  991. setUpDropdownMenu(dropdown, "tactics_dropdown_menu");
  992. appendChildren(dropdown, [createPlaceholderOption()]);
  993. return dropdown;
  994. }
  995.  
  996. function createDropdownMenuLabel(labelId) {
  997. const label = document.createElement("span");
  998. setUpDropdownMenuLabel(label, labelId, USERSCRIPT_STRINGS.languageDropdownMenuLabel);
  999. return label;
  1000. }
  1001.  
  1002. function createLabelDropdownMenuGroup(label, dropdown) {
  1003. const group = document.createElement("div");
  1004. group.classList.add('dropdown-group');
  1005. group.appendChild(label);
  1006. group.appendChild(dropdown);
  1007. return group;
  1008. }
  1009.  
  1010. function setUpDropdownMenu(dropdown, id) {
  1011. dropdown.id = id;
  1012. }
  1013.  
  1014. function createPlaceholderOption() {
  1015. const placeholderOption = document.createElement("option");
  1016. placeholderOption.value = "";
  1017. placeholderOption.text = "";
  1018. placeholderOption.disabled = true;
  1019. placeholderOption.selected = true;
  1020. return placeholderOption;
  1021. }
  1022.  
  1023. function addTacticsToDropdownMenu(dropdown, tactics) {
  1024. for (const tactic of tactics) {
  1025. const option = document.createElement("option");
  1026. option.value = tactic.name;
  1027. option.text = tactic.name;
  1028. dropdown.appendChild(option);
  1029. }
  1030. }
  1031.  
  1032. function setUpDropdownMenuLabel(description, id, textContent) {
  1033. description.id = id;
  1034. description.textContent = textContent;
  1035. }
  1036.  
  1037. function createLanguageDropdownMenu() {
  1038. const dropdown = document.createElement("select");
  1039. setUpDropdownMenu(dropdown, "language_dropdown_menu");
  1040. for (const lang of LANGUAGES) {
  1041. const option = document.createElement("option");
  1042. option.value = lang.code;
  1043. option.textContent = lang.name;
  1044. if (lang.code === activeLanguage) {
  1045. option.selected = true;
  1046. }
  1047. dropdown.appendChild(option);
  1048. }
  1049. dropdown.addEventListener("change", function () {
  1050. changeLanguage(this.value).catch((_) => { });
  1051. });
  1052. return dropdown;
  1053. }
  1054.  
  1055. function createFlagImage() {
  1056. const img = document.createElement("img");
  1057. img.id = "language_flag";
  1058. const activeLang = LANGUAGES.find((lang) => lang.code === activeLanguage);
  1059. if (activeLang) {
  1060. img.src = activeLang.flag;
  1061. }
  1062. return img;
  1063. }
  1064.  
  1065. function getActiveLanguage() {
  1066. let language = GM_getValue("language");
  1067. if (!language) {
  1068. let browserLanguage = navigator.language || "en";
  1069. browserLanguage = browserLanguage.split("-")[0];
  1070. const languageExists = LANGUAGES.some((lang) => lang.code === browserLanguage);
  1071. language = languageExists ? browserLanguage : "en";
  1072. }
  1073. return language;
  1074. }
  1075.  
  1076. function updateTranslation() {
  1077. for (const key in USERSCRIPT_STRINGS) {
  1078. USERSCRIPT_STRINGS[key] = i18next.t(key);
  1079. }
  1080. for (const id in ELEMENT_STRING_KEYS) {
  1081. const element = document.getElementById(id);
  1082. if (id === "info_modal_info_text" || id === "info_modal_feedback_text") {
  1083. if (element) element.innerHTML = USERSCRIPT_STRINGS[ELEMENT_STRING_KEYS[id]];
  1084. } else if (element) {
  1085. element.textContent = USERSCRIPT_STRINGS[ELEMENT_STRING_KEYS[id]];
  1086. }
  1087. }
  1088. }
  1089.  
  1090. async function changeLanguage(languageCode) {
  1091. try {
  1092. const translationDataUrl = LANG_DATA_BASE_URL + languageCode + ".json";
  1093. const translations = await (await fetch(translationDataUrl)).json();
  1094. i18next.changeLanguage(languageCode);
  1095. i18next.addResourceBundle(languageCode, "translation", translations);
  1096. GM_setValue("language", languageCode);
  1097. updateTranslation();
  1098. const language = LANGUAGES.find((lang) => lang.code === languageCode);
  1099. if (language) {
  1100. const flagImage = document.getElementById("language_flag");
  1101. if (flagImage) flagImage.src = language.flag;
  1102. }
  1103. } catch (_e) { }
  1104. }
  1105.  
  1106. function generateUniqueId(coordinates) {
  1107. const sortedCoordinates = coordinates.sort((a, b) => a[1] - b[1] || a[0] - b[0]);
  1108. const coordString = sortedCoordinates.map((coord) => coord[1] + "_" + coord[0]).join("_");
  1109. return sha256Hash(coordString);
  1110. }
  1111.  
  1112. function createUsefulLinksModal() {
  1113. const modal = document.createElement("div");
  1114. setUpModal(modal, "useful_links_modal");
  1115. const modalContent = createUsefulLinksModalContent();
  1116. modal.appendChild(modalContent);
  1117. return modal;
  1118. }
  1119.  
  1120. function createUsefulLinksModalContent() {
  1121. const modalContent = document.createElement("div");
  1122. styleModalContent(modalContent);
  1123. const usefulContent = createUsefulContent();
  1124. const resources = new Map([
  1125. ["gewlaht - BoooM", "https://www.managerzone.com/?p=forum&sub=topic&topic_id=11415137&forum_id=49&sport=soccer"],
  1126. ["taktikskola by honken91", "https://www.managerzone.com/?p=forum&sub=topic&topic_id=12653892&forum_id=4&sport=soccer"],
  1127. ["peto - mix de dibujos", "https://www.managerzone.com/?p=forum&sub=topic&topic_id=12196312&forum_id=255&sport=soccer"],
  1128. ["The Zone Chile", "https://www.managerzone.com/thezone/paper.php?paper_id=18036&page=9&sport=soccer"],
  1129. ["Tactics guide by lukasz87o/filipek4", "https://www.managerzone.com/?p=forum&sub=topic&topic_id=12766444&forum_id=12&sport=soccer&share_sport=soccer"],
  1130. ["MZExtension/van.mz.playerAdvanced by vanjoge", "https://greasyfork.org/pt-BR/scripts/373382-van-mz-playeradvanced"],
  1131. ["Mazyar Userscript", "https://greasyfork.org/pt-BR/scripts/476290-mazyar"],
  1132. ["Stats Xente Userscript", "https://greasyfork.org/pt-BR/scripts/491442-stats-xente-script"],
  1133. ["More userscripts", "https://greasyfork.org/pt-BR/users/1088808-douglasdotv"]
  1134. ]);
  1135. const usefulLinksList = createLinksList(resources);
  1136. modalContent.appendChild(usefulContent);
  1137. modalContent.appendChild(usefulLinksList);
  1138. return modalContent;
  1139. }
  1140.  
  1141. function createUsefulContent() {
  1142. const usefulContent = document.createElement("p");
  1143. usefulContent.id = "useful_content";
  1144. usefulContent.textContent = "";
  1145. return usefulContent;
  1146. }
  1147.  
  1148. function createLinksList(hrefs) {
  1149. const list = document.createElement("ul");
  1150. hrefs.forEach((href, title) => {
  1151. const listItem = document.createElement("li");
  1152. const link = document.createElement("a");
  1153. link.href = href;
  1154. link.textContent = title;
  1155. listItem.appendChild(link);
  1156. list.appendChild(listItem);
  1157. });
  1158. return list;
  1159. }
  1160.  
  1161. function setUsefulLinksModal() {
  1162. usefulLinksModal = createUsefulLinksModal();
  1163. document.body.appendChild(usefulLinksModal);
  1164. }
  1165.  
  1166. function createInfoModal() {
  1167. const modal = document.createElement("div");
  1168. setUpModal(modal, "info_modal");
  1169. const modalContent = createModalContent();
  1170. modal.appendChild(modalContent);
  1171. return modal;
  1172. }
  1173.  
  1174. function createModalContent() {
  1175. const modalContent = document.createElement("div");
  1176. styleModalContent(modalContent);
  1177. const title = createTitle();
  1178. const infoText = createInfoText();
  1179. const feedbackText = createFeedbackText();
  1180. modalContent.appendChild(title);
  1181. modalContent.appendChild(infoText);
  1182. modalContent.appendChild(feedbackText);
  1183. return modalContent;
  1184. }
  1185.  
  1186. function createTitle() {
  1187. const title = document.createElement("h2");
  1188. title.id = "info_modal_title";
  1189. title.textContent = "";
  1190. title.style.fontSize = "24px";
  1191. title.style.fontWeight = "bold";
  1192. title.style.marginBottom = "20px";
  1193. return title;
  1194. }
  1195.  
  1196. function createInfoText() {
  1197. const infoText = document.createElement("p");
  1198. infoText.id = "info_modal_info_text";
  1199. infoText.innerHTML = USERSCRIPT_STRINGS.modalContentInfoText;
  1200. return infoText;
  1201. }
  1202.  
  1203. function createFeedbackText() {
  1204. const feedbackText = document.createElement("p");
  1205. feedbackText.id = "info_modal_feedback_text";
  1206. feedbackText.innerHTML = USERSCRIPT_STRINGS.modalContentFeedbackText;
  1207. return feedbackText;
  1208. }
  1209.  
  1210. function setInfoModal() {
  1211. infoModal = createInfoModal();
  1212. document.body.appendChild(infoModal);
  1213. }
  1214.  
  1215. function setUpModal(modal, id) {
  1216. modal.id = id;
  1217. modal.style.display = "none";
  1218. modal.style.position = "fixed";
  1219. modal.style.zIndex = "1";
  1220. modal.style.left = "50%";
  1221. modal.style.top = "50%";
  1222. modal.style.transform = "translate(-50%, -50%)";
  1223. modal.style.opacity = "0";
  1224. modal.style.transition = "opacity 0.5s ease-in-out";
  1225. }
  1226.  
  1227. function styleModalContent(content) {
  1228. content.classList.add('swal-mz-content');
  1229. }
  1230.  
  1231. function toggleModal(modal) {
  1232. if (modal.style.display === "none" || modal.style.opacity === "0") {
  1233. showModal(modal);
  1234. } else {
  1235. hideModal(modal);
  1236. }
  1237. }
  1238.  
  1239. function showModal(modal) {
  1240. modal.style.display = "block";
  1241. setTimeout(function () {
  1242. modal.style.opacity = "1";
  1243. }, 0);
  1244. }
  1245.  
  1246. function hideModal(modal) {
  1247. modal.style.opacity = "0";
  1248. setTimeout(function () {
  1249. modal.style.display = "none";
  1250. }, 500);
  1251. }
  1252.  
  1253. function setUpModalsWindowClickListener() {
  1254. window.addEventListener("click", function (event) {
  1255. if (usefulLinksModal.style.display === "block" && !usefulLinksModal.contains(event.target)) {
  1256. hideModal(usefulLinksModal);
  1257. }
  1258. if (infoModal.style.display === "block" && !infoModal.contains(event.target)) {
  1259. hideModal(infoModal);
  1260. }
  1261. });
  1262. }
  1263.  
  1264. function createUsefulLinksButton() {
  1265. const button = document.createElement("button");
  1266. setUpButton(button, "useful_links_button", USERSCRIPT_STRINGS.usefulLinksButton);
  1267. button.addEventListener("click", function (event) {
  1268. event.stopPropagation();
  1269. toggleModal(usefulLinksModal);
  1270. });
  1271. return button;
  1272. }
  1273.  
  1274. function createAboutButton() {
  1275. const button = document.createElement("button");
  1276. setUpButton(button, "about_button", USERSCRIPT_STRINGS.aboutButton);
  1277. button.addEventListener("click", function (event) {
  1278. event.stopPropagation();
  1279. toggleModal(infoModal);
  1280. });
  1281. return button;
  1282. }
  1283.  
  1284. function initialize() {
  1285. const tacticsBox = document.getElementById("tactics_box");
  1286. if (tacticsBox) {
  1287. activeLanguage = getActiveLanguage();
  1288. i18next.init({
  1289. lng: activeLanguage,
  1290. resources: {
  1291. [activeLanguage]: {
  1292. translation: {}
  1293. }
  1294. }
  1295. }).then(async () => {
  1296. const res = await fetch(LANG_DATA_BASE_URL + activeLanguage + ".json");
  1297. const json = await res.json();
  1298. i18next.addResourceBundle(activeLanguage, "translation", json);
  1299. await checkVersion();
  1300. const mainContainer = createMainContainer();
  1301.  
  1302. if (isFootball()) {
  1303. insertAfterElement(mainContainer, tacticsBox);
  1304. }
  1305.  
  1306. fetchTacticsFromGMStorage().then((data) => {
  1307. const tacticsDropdownMenu = document.getElementById("tactics_dropdown_menu");
  1308. tacticsDropdownMenu.addEventListener('click', function () {
  1309. if (this.value) {
  1310. handleTacticsSelection(this.value);
  1311. }
  1312. });
  1313. dropdownMenuTactics = data.tactics;
  1314. dropdownMenuTactics.sort((a, b) => a.name.localeCompare(b.name));
  1315. addTacticsToDropdownMenu(tacticsDropdownMenu, dropdownMenuTactics);
  1316. tacticsDropdownMenu.addEventListener("change", function () {
  1317. handleTacticsSelection(this.value);
  1318. });
  1319. }).catch((_) => { });
  1320. setInfoModal();
  1321. setUsefulLinksModal();
  1322. setUpModalsWindowClickListener();
  1323. updateTranslation();
  1324. });
  1325. }
  1326. }
  1327.  
  1328. window.addEventListener("load", function () {
  1329. initialize();
  1330. });
  1331. })();