MZ Tactics Selector

Adds a dropdown menu with overused tactics.

Per 14-06-2023. Zie de nieuwste versie.

  1. // ==UserScript==
  2. // @name MZ Tactics Selector
  3. // @namespace essenfc
  4. // @version 6.1
  5. // @description Adds a dropdown menu with overused tactics.
  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. // @match https://www.managerzone.com/?p=players
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=managerzone.com
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_addStyle
  14. // @require https://unpkg.com/jssha@3.3.0/dist/sha256.js
  15. // @require https://unpkg.com/i18next@21.6.3/i18next.min.js
  16. // @license MIT
  17. // ==/UserScript==
  18.  
  19. GM_addStyle(
  20. "@import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap');"
  21. );
  22.  
  23. (function () {
  24. "use strict";
  25.  
  26. let dropdownTactics = [];
  27.  
  28. const defaultTacticsDataUrl =
  29. "https://raw.githubusercontent.com/douglasdotv/tactics-selector/main/json/tactics.json?callback=?";
  30.  
  31. let activeLanguage;
  32.  
  33. const flagsDataUrl = {
  34. gb: "https://raw.githubusercontent.com/lipis/flag-icons/d6785f2434e54e775d55a304733d17b048eddfb5/flags/4x3/gb.svg",
  35. br: "https://raw.githubusercontent.com/lipis/flag-icons/d6785f2434e54e775d55a304733d17b048eddfb5/flags/4x3/br.svg",
  36. cn: "https://raw.githubusercontent.com/lipis/flag-icons/d6785f2434e54e775d55a304733d17b048eddfb5/flags/4x3/cn.svg",
  37. se: "https://raw.githubusercontent.com/lipis/flag-icons/d6785f2434e54e775d55a304733d17b048eddfb5/flags/4x3/se.svg",
  38. no: "https://raw.githubusercontent.com/lipis/flag-icons/d6785f2434e54e775d55a304733d17b048eddfb5/flags/4x3/no.svg",
  39. dk: "https://raw.githubusercontent.com/lipis/flag-icons/d6785f2434e54e775d55a304733d17b048eddfb5/flags/4x3/dk.svg",
  40. ar: "https://raw.githubusercontent.com/lipis/flag-icons/d6785f2434e54e775d55a304733d17b048eddfb5/flags/4x3/ar.svg",
  41. pl: "https://raw.githubusercontent.com/lipis/flag-icons/d6785f2434e54e775d55a304733d17b048eddfb5/flags/4x3/pl.svg",
  42. nl: "https://raw.githubusercontent.com/lipis/flag-icons/d6785f2434e54e775d55a304733d17b048eddfb5/flags/4x3/nl.svg",
  43. id: "https://raw.githubusercontent.com/lipis/flag-icons/d6785f2434e54e775d55a304733d17b048eddfb5/flags/4x3/id.svg",
  44. de: "https://raw.githubusercontent.com/lipis/flag-icons/d6785f2434e54e775d55a304733d17b048eddfb5/flags/4x3/de.svg",
  45. it: "https://raw.githubusercontent.com/lipis/flag-icons/d6785f2434e54e775d55a304733d17b048eddfb5/flags/4x3/it.svg",
  46. fr: "https://raw.githubusercontent.com/lipis/flag-icons/d6785f2434e54e775d55a304733d17b048eddfb5/flags/4x3/fr.svg",
  47. ro: "https://raw.githubusercontent.com/lipis/flag-icons/d6785f2434e54e775d55a304733d17b048eddfb5/flags/4x3/ro.svg",
  48. tr: "https://raw.githubusercontent.com/lipis/flag-icons/d6785f2434e54e775d55a304733d17b048eddfb5/flags/4x3/tr.svg",
  49. kr: "https://raw.githubusercontent.com/lipis/flag-icons/d6785f2434e54e775d55a304733d17b048eddfb5/flags/4x3/kr.svg",
  50. ru: "https://raw.githubusercontent.com/lipis/flag-icons/d6785f2434e54e775d55a304733d17b048eddfb5/flags/4x3/ru.svg",
  51. sa: "https://raw.githubusercontent.com/lipis/flag-icons/d6785f2434e54e775d55a304733d17b048eddfb5/flags/4x3/sa.svg",
  52. };
  53.  
  54. const languages = [
  55. { code: "en", name: "English", flag: flagsDataUrl.gb },
  56. { code: "pt", name: "Português", flag: flagsDataUrl.br },
  57. { code: "zh", name: "中文", flag: flagsDataUrl.cn },
  58. { code: "sv", name: "Svenska", flag: flagsDataUrl.se },
  59. { code: "no", name: "Norsk", flag: flagsDataUrl.no },
  60. { code: "da", name: "Dansk", flag: flagsDataUrl.dk },
  61. { code: "es", name: "Español", flag: flagsDataUrl.ar },
  62. { code: "pl", name: "Polski", flag: flagsDataUrl.pl },
  63. { code: "nl", name: "Nederlands", flag: flagsDataUrl.nl },
  64. { code: "id", name: "Bahasa Indonesia", flag: flagsDataUrl.id },
  65. { code: "de", name: "Deutsch", flag: flagsDataUrl.de },
  66. { code: "it", name: "Italiano", flag: flagsDataUrl.it },
  67. { code: "fr", name: "Français", flag: flagsDataUrl.fr },
  68. { code: "ro", name: "Română", flag: flagsDataUrl.ro },
  69. { code: "tr", name: "Türkçe", flag: flagsDataUrl.tr },
  70. { code: "ko", name: "한국어", flag: flagsDataUrl.kr },
  71. { code: "ru", name: "Русский", flag: flagsDataUrl.ru },
  72. { code: "ar", name: "العربية", flag: flagsDataUrl.sa },
  73. ];
  74.  
  75. const strings = {
  76. addButton: "",
  77. deleteButton: "",
  78. renameButton: "",
  79. updateButton: "",
  80. aboutButton: "",
  81. clearButton: "",
  82. resetButton: "",
  83. importButton: "",
  84. exportButton: "",
  85. tacticNamePrompt: "",
  86. addAlert: "",
  87. deleteAlert: "",
  88. renameAlert: "",
  89. updateAlert: "",
  90. clearAlert: "",
  91. resetAlert: "",
  92. deleteConfirmation: "",
  93. updateConfirmation: "",
  94. clearConfirmation: "",
  95. resetConfirmation: "",
  96. invalidTacticError: "",
  97. noTacticNameProvidedError: "",
  98. alreadyExistingTacticNameError: "",
  99. tacticNameLengthError: "",
  100. noTacticSelectedError: "",
  101. duplicateTacticError: "",
  102. modalContentInfoText: "",
  103. modalContentFeedbackText: "",
  104. tacticsDropdownMenuLabel: "",
  105. languageDropdownMenuLabel: "",
  106. };
  107.  
  108. let infoModal;
  109.  
  110. const tacticsBox = document.getElementById("tactics_box");
  111.  
  112. const tacticsPreset = document.getElementById("tactics_preset");
  113.  
  114. const isFootball = function () {
  115. return tacticsBox.classList.contains("soccer");
  116. };
  117.  
  118. const outfieldPlayersSelector =
  119. ".fieldpos.fieldpos-ok.ui-draggable:not(.substitute):not(.goalkeeper):not(.substitute.goalkeeper), .fieldpos.fieldpos-collision.ui-draggable:not(.substitute):not(.goalkeeper):not(.substitute.goalkeeper)";
  120.  
  121. const goalkeeperSelector = ".fieldpos.fieldpos-ok.goalkeeper.ui-draggable";
  122.  
  123. const formationTextSelector = "#formation_text";
  124.  
  125. const tacticSlotSelector =
  126. ".ui-state-default.ui-corner-top.ui-tabs-selected.ui-state-active.invalid";
  127.  
  128. const minOutfieldPlayers = 10;
  129.  
  130. const maxTacticNameLength = 50;
  131.  
  132. async function initialize() {
  133. if (tacticsBox) {
  134. activeLanguage = getActiveLanguage();
  135. i18next
  136. .init({
  137. lng: activeLanguage,
  138. resources: {
  139. [activeLanguage]: {
  140. translation: await (
  141. await fetch(
  142. `https://raw.githubusercontent.com/douglasdotv/tactics-selector/main/json/lang/${activeLanguage}.json?callback=?`
  143. )
  144. ).json(),
  145. },
  146. },
  147. })
  148. .then(() => {
  149. const tacticsSelectorDiv = createMainDiv();
  150. const firstRow = createMainDivFirstRow();
  151. const secondRow = createMainDivSecondRow();
  152.  
  153. const tacticsDropdownMenuLabel = createTacticsDropdownMenuLabel();
  154. const tacticsDropdownMenu = createTacticsDropdownMenu();
  155. const tacticsDropdownGroup = createLabelDropdownMenuGroup(
  156. tacticsDropdownMenuLabel,
  157. tacticsDropdownMenu
  158. );
  159.  
  160. const languageDropdownMenuLabel = createLanguageDropdownMenuLabel();
  161. const languageDropdownMenu = createLanguageDropdownMenu();
  162. const languageDropdownGroup = createLabelDropdownMenuGroup(
  163. languageDropdownMenuLabel,
  164. languageDropdownMenu
  165. );
  166. const flagImage = createFlagImage();
  167. languageDropdownGroup.appendChild(flagImage);
  168.  
  169. appendChildren(firstRow, [
  170. tacticsDropdownGroup,
  171. languageDropdownGroup,
  172. ]);
  173.  
  174. const addNewTacticBtn = createAddNewTacticButton();
  175. const deleteTacticBtn = createDeleteTacticButton();
  176. const renameTacticBtn = createRenameTacticButton();
  177. const updateTacticBtn = createUpdateTacticButton();
  178. const clearTacticsBtn = createClearTacticsButton();
  179. const resetTacticsBtn = createResetTacticsButton();
  180. const importTacticsBtn = createImportTacticsButton();
  181. const exportTacticsBtn = createExportTacticsButton();
  182. const aboutBtn = createAboutButton();
  183. const hiddenTriggerBtn = createHiddenTriggerButton();
  184.  
  185. appendChildren(secondRow, [
  186. addNewTacticBtn,
  187. deleteTacticBtn,
  188. renameTacticBtn,
  189. updateTacticBtn,
  190. clearTacticsBtn,
  191. resetTacticsBtn,
  192. importTacticsBtn,
  193. exportTacticsBtn,
  194. aboutBtn,
  195. ]);
  196.  
  197. appendChildren(tacticsSelectorDiv, [
  198. firstRow,
  199. secondRow,
  200. hiddenTriggerBtn,
  201. ]);
  202.  
  203. if (isFootball) {
  204. insertAfterElement(tacticsSelectorDiv, tacticsBox);
  205. }
  206.  
  207. fetchTacticsFromGMStorage()
  208. .then((data) => {
  209. dropdownTactics = data.tactics;
  210.  
  211. dropdownTactics.sort((a, b) => {
  212. return a.name.localeCompare(b.name);
  213. });
  214.  
  215. addTacticsToDropdown(tacticsDropdownMenu, dropdownTactics);
  216.  
  217. tacticsDropdownMenu.addEventListener("change", function () {
  218. handleTacticSelection(this.value);
  219. });
  220. })
  221. .catch((err) => {
  222. console.error("Couldn't fetch data from json: ", err);
  223. });
  224. setInfoModal();
  225. updateTranslation();
  226. });
  227. }
  228. applyUxxFilter();
  229. }
  230.  
  231. window.addEventListener("load", function () {
  232. initialize().catch((err) => {
  233. console.error("Init error: ", err);
  234. });
  235. });
  236.  
  237. // _____Tactics Dropdown Menu_____
  238.  
  239. function createTacticsDropdownMenu() {
  240. const dropdown = document.createElement("select");
  241. setupDropdownMenu(dropdown, "tactics_dropdown_menu");
  242. appendChildren(dropdown, [createPlaceholderOption()]);
  243. return dropdown;
  244. }
  245.  
  246. function createTacticsDropdownMenuLabel() {
  247. const label = document.createElement("span");
  248. setupDropdownMenuLabel(
  249. label,
  250. "tactics_dropdown_menu_label",
  251. strings.tacticsDropdownMenuLabel
  252. );
  253. return label;
  254. }
  255.  
  256. function createHiddenTriggerButton() {
  257. const button = document.createElement("button");
  258. button.id = "hidden_trigger_button";
  259. button.textContent = "";
  260. button.style.visibility = "hidden";
  261.  
  262. button.addEventListener("click", function () {
  263. tacticsPreset.value = "5-3-2";
  264. tacticsPreset.dispatchEvent(new Event("change"));
  265. });
  266.  
  267. return button;
  268. }
  269.  
  270. async function fetchTacticsFromGMStorage() {
  271. const storedTactics = GM_getValue("ls_tactics");
  272. if (storedTactics) {
  273. return storedTactics;
  274. } else {
  275. const jsonTactics = await fetchTacticsFromJson();
  276. storeTacticsInGMStorage(jsonTactics);
  277. return jsonTactics;
  278. }
  279. }
  280.  
  281. async function fetchTacticsFromJson() {
  282. const response = await fetch(defaultTacticsDataUrl);
  283. return await response.json();
  284. }
  285.  
  286. function storeTacticsInGMStorage(data) {
  287. GM_setValue("ls_tactics", data);
  288. }
  289.  
  290. function addTacticsToDropdown(dropdown, tactics) {
  291. for (const tactic of tactics) {
  292. const option = document.createElement("option");
  293. option.value = tactic.name;
  294. option.text = tactic.name;
  295. dropdown.appendChild(option);
  296. }
  297. }
  298.  
  299. function handleTacticSelection(tactic) {
  300. const outfieldPlayers = Array.from(
  301. document.querySelectorAll(outfieldPlayersSelector)
  302. );
  303.  
  304. const selectedTactic = dropdownTactics.find(
  305. (tacticData) => tacticData.name === tactic
  306. );
  307.  
  308. if (selectedTactic) {
  309. if (outfieldPlayers.length < minOutfieldPlayers) {
  310. const hiddenTriggerButton = document.getElementById(
  311. "hidden_trigger_button"
  312. );
  313. hiddenTriggerButton.click();
  314. setTimeout(() => rearrangePlayers(selectedTactic.coordinates), 1);
  315. } else {
  316. rearrangePlayers(selectedTactic.coordinates);
  317. }
  318. }
  319. }
  320.  
  321. function rearrangePlayers(coordinates) {
  322. const outfieldPlayers = Array.from(
  323. document.querySelectorAll(outfieldPlayersSelector)
  324. );
  325.  
  326. findBestPositions(outfieldPlayers, coordinates);
  327.  
  328. for (let i = 0; i < outfieldPlayers.length; ++i) {
  329. outfieldPlayers[i].style.left = coordinates[i][0] + "px";
  330. outfieldPlayers[i].style.top = coordinates[i][1] + "px";
  331. removeCollision(outfieldPlayers[i]);
  332. }
  333.  
  334. removeTacticSlotInvalidStatus();
  335. updateFormationText(getFormation(coordinates));
  336. }
  337.  
  338. function findBestPositions(players, coordinates) {
  339. players.sort((a, b) => parseInt(a.style.top) - parseInt(b.style.top));
  340. coordinates.sort((a, b) => a[1] - b[1]);
  341. }
  342.  
  343. function removeCollision(player) {
  344. if (player.classList.contains("fieldpos-collision")) {
  345. player.classList.remove("fieldpos-collision");
  346. player.classList.add("fieldpos-ok");
  347. }
  348. }
  349.  
  350. function removeTacticSlotInvalidStatus() {
  351. const slot = document.querySelector(tacticSlotSelector);
  352. if (slot) {
  353. slot.classList.remove("invalid");
  354. }
  355. }
  356.  
  357. function updateFormationText(formation) {
  358. const formationTextElement = document.querySelector(formationTextSelector);
  359. formationTextElement.querySelector(".defs").textContent =
  360. formation.defenders;
  361. formationTextElement.querySelector(".mids").textContent =
  362. formation.midfielders;
  363. formationTextElement.querySelector(".atts").textContent =
  364. formation.strikers;
  365. }
  366.  
  367. function getFormation(coordinates) {
  368. let strikers = 0;
  369. let midfielders = 0;
  370. let defenders = 0;
  371.  
  372. for (const coo of coordinates) {
  373. const y = coo[1];
  374. if (y < 103) {
  375. strikers++;
  376. } else if (y <= 204) {
  377. midfielders++;
  378. } else {
  379. defenders++;
  380. }
  381. }
  382.  
  383. return { strikers, midfielders, defenders };
  384. }
  385.  
  386. // _____Add new tactic_____
  387.  
  388. function createAddNewTacticButton() {
  389. const button = document.createElement("button");
  390. setupButton(button, "add_tactic_button", strings.addButton);
  391.  
  392. button.addEventListener("click", function () {
  393. addNewTactic().catch(console.error);
  394. });
  395.  
  396. return button;
  397. }
  398.  
  399. async function addNewTactic() {
  400. const outfieldPlayers = Array.from(
  401. document.querySelectorAll(outfieldPlayersSelector)
  402. );
  403.  
  404. const tacticsDropdownMenu = document.getElementById(
  405. "tactics_dropdown_menu"
  406. );
  407.  
  408. const tacticCoordinates = outfieldPlayers.map((player) => [
  409. parseInt(player.style.left),
  410. parseInt(player.style.top),
  411. ]);
  412.  
  413. if (!validateTacticPlayerCount(outfieldPlayers)) {
  414. return;
  415. }
  416.  
  417. const tacticId = generateUniqueId(tacticCoordinates);
  418. const isDuplicate = await validateDuplicateTactic(tacticId);
  419. if (isDuplicate) {
  420. alert(strings.duplicateTacticError);
  421. return;
  422. }
  423.  
  424. const tacticName = prompt(strings.tacticNamePrompt);
  425. const isValidName = await validateTacticName(tacticName);
  426. if (!isValidName) {
  427. return;
  428. }
  429.  
  430. const tactic = {
  431. name: tacticName,
  432. coordinates: tacticCoordinates,
  433. id: tacticId,
  434. };
  435.  
  436. saveTacticToStorage(tactic).catch(console.error);
  437. addTacticsToDropdown(tacticsDropdownMenu, [tactic]);
  438. dropdownTactics.push(tactic);
  439.  
  440. tacticsDropdownMenu.value = tactic.name;
  441. handleTacticSelection(tactic.name);
  442.  
  443. alert(strings.addAlert.replace("{}", tactic.name));
  444. }
  445.  
  446. function validateTacticPlayerCount(outfieldPlayers) {
  447. const isGoalkeeper = document.querySelector(goalkeeperSelector);
  448.  
  449. outfieldPlayers = outfieldPlayers.filter(
  450. (player) => !player.classList.contains("fieldpos-collision")
  451. );
  452.  
  453. if (outfieldPlayers.length < minOutfieldPlayers || !isGoalkeeper) {
  454. alert(strings.invalidTacticError);
  455. return false;
  456. }
  457.  
  458. return true;
  459. }
  460.  
  461. async function validateDuplicateTactic(id) {
  462. const tacticsData = (await GM_getValue("ls_tactics")) || { tactics: [] };
  463. return tacticsData.tactics.some((tactic) => tactic.id === id);
  464. }
  465.  
  466. async function validateTacticName(name) {
  467. if (!name) {
  468. alert(strings.noTacticNameProvidedError);
  469. return false;
  470. }
  471.  
  472. const tacticsData = (await GM_getValue("ls_tactics")) || { tactics: [] };
  473. if (tacticsData.tactics.some((t) => t.name === name)) {
  474. alert(strings.alreadyExistingTacticNameError);
  475. return false;
  476. }
  477.  
  478. if (name.length > maxTacticNameLength) {
  479. alert(strings.tacticNameLengthError);
  480. return false;
  481. }
  482.  
  483. return true;
  484. }
  485.  
  486. async function saveTacticToStorage(tactic) {
  487. const tacticsData = (await GM_getValue("ls_tactics")) || { tactics: [] };
  488. tacticsData.tactics.push(tactic);
  489. await GM_setValue("ls_tactics", tacticsData);
  490. }
  491.  
  492. // _____Delete tactic_____
  493.  
  494. function createDeleteTacticButton() {
  495. const button = document.createElement("button");
  496. setupButton(button, "delete_tactic_button", strings.deleteButton);
  497.  
  498. button.addEventListener("click", function () {
  499. deleteTactic().catch(console.error);
  500. });
  501.  
  502. return button;
  503. }
  504.  
  505. async function deleteTactic() {
  506. const tacticsDropdownMenu = document.getElementById(
  507. "tactics_dropdown_menu"
  508. );
  509.  
  510. const selectedTactic = dropdownTactics.find(
  511. (tactic) => tactic.name === tacticsDropdownMenu.value
  512. );
  513.  
  514. if (!selectedTactic) {
  515. alert(strings.noTacticSelectedError);
  516. return;
  517. }
  518.  
  519. const confirmed = confirm(
  520. strings.deleteConfirmation.replace("{}", selectedTactic.name)
  521. );
  522.  
  523. if (!confirmed) {
  524. return;
  525. }
  526.  
  527. const tacticsData = (await GM_getValue("ls_tactics")) || { tactics: [] };
  528. tacticsData.tactics = tacticsData.tactics.filter(
  529. (tactic) => tactic.id !== selectedTactic.id
  530. );
  531.  
  532. await GM_setValue("ls_tactics", tacticsData);
  533.  
  534. dropdownTactics = dropdownTactics.filter(
  535. (tactic) => tactic.id !== selectedTactic.id
  536. );
  537.  
  538. const selectedOption = Array.from(tacticsDropdownMenu.options).find(
  539. (option) => option.value === selectedTactic.name
  540. );
  541. tacticsDropdownMenu.remove(selectedOption.index);
  542.  
  543. if (tacticsDropdownMenu.options[0]?.disabled) {
  544. tacticsDropdownMenu.selectedIndex = 0;
  545. }
  546.  
  547. alert(strings.deleteAlert.replace("{}", selectedTactic.name));
  548. }
  549.  
  550. // _____Rename tactic_____
  551.  
  552. function createRenameTacticButton() {
  553. const button = document.createElement("button");
  554. setupButton(button, "rename_tactic_button", strings.renameButton);
  555.  
  556. button.addEventListener("click", function () {
  557. renameTactic().catch(console.error);
  558. });
  559.  
  560. return button;
  561. }
  562.  
  563. async function renameTactic() {
  564. const tacticsDropdownMenu = document.getElementById(
  565. "tactics_dropdown_menu"
  566. );
  567.  
  568. const selectedTactic = dropdownTactics.find(
  569. (tactic) => tactic.name === tacticsDropdownMenu.value
  570. );
  571.  
  572. if (!selectedTactic) {
  573. alert(strings.noTacticSelectedError);
  574. return;
  575. }
  576.  
  577. const oldName = selectedTactic.name;
  578.  
  579. const newName = prompt(strings.tacticNamePrompt);
  580. const isValidName = await validateTacticName(newName);
  581. if (!isValidName) {
  582. return;
  583. }
  584.  
  585. const selectedOption = Array.from(tacticsDropdownMenu.options).find(
  586. (option) => option.value === selectedTactic.name
  587. );
  588.  
  589. const tacticsData = (await GM_getValue("ls_tactics")) || { tactics: [] };
  590. tacticsData.tactics = tacticsData.tactics.map((tactic) => {
  591. if (tactic.id === selectedTactic.id) {
  592. tactic.name = newName;
  593. }
  594. return tactic;
  595. });
  596.  
  597. await GM_setValue("ls_tactics", tacticsData);
  598.  
  599. dropdownTactics = dropdownTactics.map((tactic) => {
  600. if (tactic.id === selectedTactic.id) {
  601. tactic.name = newName;
  602. }
  603. return tactic;
  604. });
  605.  
  606. selectedOption.value = newName;
  607. selectedOption.textContent = newName;
  608.  
  609. const replacements = [oldName, newName];
  610. alert(strings.renameAlert.replace(/\{\}/g, () => replacements.shift()));
  611. }
  612.  
  613. // _____Update tactic_____
  614.  
  615. function createUpdateTacticButton() {
  616. const button = document.createElement("button");
  617. setupButton(button, "update_tactic_button", strings.updateButton);
  618.  
  619. button.addEventListener("click", function () {
  620. updateTactic().catch(console.error);
  621. });
  622.  
  623. return button;
  624. }
  625.  
  626. async function updateTactic() {
  627. const outfieldPlayers = Array.from(
  628. document.querySelectorAll(outfieldPlayersSelector)
  629. );
  630.  
  631. const tacticsDropdownMenu = document.getElementById(
  632. "tactics_dropdown_menu"
  633. );
  634.  
  635. const selectedTactic = dropdownTactics.find(
  636. (tactic) => tactic.name === tacticsDropdownMenu.value
  637. );
  638.  
  639. if (!selectedTactic) {
  640. alert(strings.noTacticSelectedError);
  641. return;
  642. }
  643.  
  644. const updatedCoordinates = outfieldPlayers.map((player) => [
  645. parseInt(player.style.left),
  646. parseInt(player.style.top),
  647. ]);
  648.  
  649. const newId = generateUniqueId(updatedCoordinates);
  650.  
  651. const tacticsData = (await GM_getValue("ls_tactics")) || { tactics: [] };
  652. const validationOutcome = await validateDuplicateTacticWithUpdatedCoord(
  653. newId,
  654. selectedTactic,
  655. tacticsData
  656. );
  657.  
  658. switch (validationOutcome) {
  659. case "unchanged":
  660. return;
  661. case "duplicate":
  662. alert(strings.duplicateTacticError);
  663. return;
  664. case "unique":
  665. break;
  666. default:
  667. return;
  668. }
  669.  
  670. const confirmed = confirm(
  671. strings.updateConfirmation.replace("{}", selectedTactic.name)
  672. );
  673.  
  674. if (!confirmed) {
  675. return;
  676. }
  677.  
  678. for (const tactic of tacticsData.tactics) {
  679. if (tactic.id === selectedTactic.id) {
  680. tactic.coordinates = updatedCoordinates;
  681. tactic.id = newId;
  682. }
  683. }
  684.  
  685. for (const tactic of dropdownTactics) {
  686. if (tactic.id === selectedTactic.id) {
  687. tactic.coordinates = updatedCoordinates;
  688. tactic.id = newId;
  689. }
  690. }
  691.  
  692. await GM_setValue("ls_tactics", tacticsData);
  693.  
  694. alert(strings.updateAlert.replace("{}", selectedTactic.name));
  695. }
  696.  
  697. async function validateDuplicateTacticWithUpdatedCoord(
  698. newId,
  699. selectedTac,
  700. tacticsData
  701. ) {
  702. if (newId === selectedTac.id) {
  703. return "unchanged";
  704. } else if (tacticsData.tactics.some((tac) => tac.id === newId)) {
  705. return "duplicate";
  706. } else {
  707. return "unique";
  708. }
  709. }
  710.  
  711. // _____Clear tactics_____
  712.  
  713. function createClearTacticsButton() {
  714. const button = document.createElement("button");
  715. setupButton(button, "clear_tactics_button", strings.clearButton);
  716.  
  717. button.addEventListener("click", function () {
  718. clearTactics().catch(console.error);
  719. });
  720.  
  721. return button;
  722. }
  723.  
  724. async function clearTactics() {
  725. const confirmed = confirm(strings.clearConfirmation);
  726.  
  727. if (!confirmed) {
  728. return;
  729. }
  730.  
  731. await GM_setValue("ls_tactics", { tactics: [] });
  732. dropdownTactics = [];
  733.  
  734. const tacticsDropdownMenu = document.getElementById(
  735. "tactics_dropdown_menu"
  736. );
  737. tacticsDropdownMenu.innerHTML = "";
  738. tacticsDropdownMenu.disabled = true;
  739.  
  740. alert(strings.clearAlert);
  741. }
  742.  
  743. // _____Reset default settings_____
  744.  
  745. function createResetTacticsButton() {
  746. const button = document.createElement("button");
  747. setupButton(button, "reset_tactics_button", strings.resetButton);
  748.  
  749. button.addEventListener("click", function () {
  750. resetTactics().catch(console.error);
  751. });
  752.  
  753. return button;
  754. }
  755.  
  756. async function resetTactics() {
  757. const confirmed = confirm(strings.resetConfirmation);
  758.  
  759. if (!confirmed) {
  760. return;
  761. }
  762.  
  763. const response = await fetch(defaultTacticsDataUrl);
  764. const data = await response.json();
  765. const defaultTactics = data.tactics;
  766.  
  767. await GM_setValue("ls_tactics", { tactics: defaultTactics });
  768. dropdownTactics = defaultTactics;
  769.  
  770. const tacticsDropdownMenu = document.getElementById(
  771. "tactics_dropdown_menu"
  772. );
  773. tacticsDropdownMenu.innerHTML = "";
  774. tacticsDropdownMenu.appendChild(createPlaceholderOption());
  775. addTacticsToDropdown(tacticsDropdownMenu, dropdownTactics);
  776. tacticsDropdownMenu.disabled = false;
  777.  
  778. alert(strings.resetAlert);
  779. }
  780.  
  781. // _____Import/Export_____
  782.  
  783. function createImportTacticsButton() {
  784. const button = document.createElement("button");
  785. setupButton(button, "import_tactics_button", strings.importButton);
  786.  
  787. button.addEventListener("click", function () {
  788. importTactics().catch(console.error);
  789. });
  790.  
  791. return button;
  792. }
  793.  
  794. function createExportTacticsButton() {
  795. const button = document.createElement("button");
  796. setupButton(button, "export_tactics_button", strings.exportButton);
  797. button.addEventListener("click", exportTactics);
  798. return button;
  799. }
  800.  
  801. async function importTactics() {
  802. const input = document.createElement("input");
  803. input.type = "file";
  804. input.accept = ".json";
  805.  
  806. input.onchange = async function (event) {
  807. const file = event.target.files[0];
  808. const reader = new FileReader();
  809.  
  810. reader.onload = async function (event) {
  811. const importedTactics = JSON.parse(event.target.result).tactics;
  812.  
  813. let existingTactics = await GM_getValue("ls_tactics", { tactics: [] });
  814. existingTactics = existingTactics.tactics;
  815.  
  816. const mergedTactics = [...existingTactics];
  817. for (const importedTactic of importedTactics) {
  818. if (
  819. !existingTactics.some((tactic) => tactic.id === importedTactic.id)
  820. ) {
  821. mergedTactics.push(importedTactic);
  822. }
  823. }
  824.  
  825. await GM_setValue("ls_tactics", { tactics: mergedTactics });
  826.  
  827. mergedTactics.sort((a, b) => a.name.localeCompare(b.name));
  828. dropdownTactics = mergedTactics;
  829.  
  830. const tacticsDropdownMenu = document.getElementById(
  831. "tactics_dropdown_menu"
  832. );
  833. tacticsDropdownMenu.innerHTML = "";
  834. tacticsDropdownMenu.append(createPlaceholderOption());
  835. addTacticsToDropdown(tacticsDropdownMenu, dropdownTactics);
  836. tacticsDropdownMenu.disabled = false;
  837. };
  838.  
  839. reader.readAsText(file);
  840. };
  841.  
  842. input.click();
  843. }
  844.  
  845. function exportTactics() {
  846. const tactics = GM_getValue("ls_tactics", { tactics: [] });
  847. const tacticsJson = JSON.stringify(tactics);
  848. const blob = new Blob([tacticsJson], { type: "application/json" });
  849. const url = URL.createObjectURL(blob);
  850.  
  851. const link = document.createElement("a");
  852. link.href = url;
  853. link.download = "tactics.json";
  854. link.click();
  855.  
  856. URL.revokeObjectURL(url);
  857. }
  858.  
  859. // _____About button_____
  860.  
  861. function createAboutButton() {
  862. const button = document.createElement("button");
  863. setupButton(button, "about_button", strings.aboutButton);
  864.  
  865. button.addEventListener("click", function (event) {
  866. event.stopPropagation();
  867. if (
  868. infoModal.style.display === "none" ||
  869. infoModal.style.opacity === "0"
  870. ) {
  871. showInfo();
  872. }
  873. });
  874.  
  875. return button;
  876. }
  877.  
  878. function showInfo() {
  879. infoModal.style.display = "block";
  880. setTimeout(function () {
  881. infoModal.style.opacity = "1";
  882. }, 0);
  883. }
  884.  
  885. function hideInfo() {
  886. infoModal.style.opacity = "0";
  887. setTimeout(function () {
  888. infoModal.style.display = "none";
  889. }, 500);
  890. }
  891.  
  892. function createInfoModal() {
  893. const modal = document.createElement("div");
  894. setupModal(modal, "info_modal");
  895.  
  896. const modalContent = createModalContent();
  897. modal.appendChild(modalContent);
  898.  
  899. window.onclick = function (event) {
  900. if (event.target == modal) {
  901. hideInfo();
  902. }
  903. };
  904.  
  905. return modal;
  906. }
  907.  
  908. function setupModal(modal, id) {
  909. modal.id = id;
  910. modal.style.display = "none";
  911. modal.style.position = "fixed";
  912. modal.style.zIndex = "1";
  913. modal.style.left = "50%";
  914. modal.style.top = "50%";
  915. modal.style.transform = "translate(-50%, -50%)";
  916. modal.style.opacity = "0";
  917. modal.style.transition = "opacity 0.5s ease-in-out";
  918. }
  919.  
  920. function createModalContent() {
  921. const modalContent = document.createElement("div");
  922. styleModalContent(modalContent);
  923.  
  924. const title = createTitle();
  925. const infoText = createInfoText();
  926. const feedbackText = createFeedbackText();
  927.  
  928. modalContent.appendChild(title);
  929. modalContent.appendChild(infoText);
  930. modalContent.appendChild(feedbackText);
  931.  
  932. return modalContent;
  933. }
  934.  
  935. function styleModalContent(content) {
  936. content.style.backgroundColor = "#fefefe";
  937. content.style.margin = "auto";
  938. content.style.padding = "20px";
  939. content.style.border = "1px solid #888";
  940. content.style.width = "80%";
  941. content.style.maxWidth = "500px";
  942. content.style.borderRadius = "10px";
  943. content.style.fontFamily = "Montserrat, sans-serif";
  944. content.style.textAlign = "center";
  945. content.style.color = "#000";
  946. content.style.fontSize = "16px";
  947. content.style.lineHeight = "1.5";
  948. }
  949.  
  950. function createTitle() {
  951. const title = document.createElement("h2");
  952. title.id = "info_modal_title";
  953. title.textContent = "MZ Tactics Selector";
  954. title.style.fontSize = "24px";
  955. title.style.fontWeight = "bold";
  956. title.style.marginBottom = "20px";
  957. return title;
  958. }
  959.  
  960. function createInfoText() {
  961. const infoText = document.createElement("p");
  962. infoText.id = "info_modal_info_text";
  963. infoText.innerHTML = strings.modalContentInfoText;
  964. return infoText;
  965. }
  966.  
  967. function createFeedbackText() {
  968. const feedbackText = document.createElement("p");
  969. feedbackText.id = "info_modal_feedback_text";
  970. feedbackText.innerHTML = strings.modalContentFeedbackText;
  971. return feedbackText;
  972. }
  973.  
  974. function setInfoModal() {
  975. infoModal = createInfoModal();
  976. document.body.appendChild(infoModal);
  977. document.addEventListener("click", function (event) {
  978. if (
  979. infoModal.style.display === "block" &&
  980. !infoModal.contains(event.target)
  981. ) {
  982. infoModal.style.display = "none";
  983. }
  984. });
  985. }
  986.  
  987. // _____Language Dropdown Menu_____
  988.  
  989. function createLanguageDropdownMenu() {
  990. const dropdown = document.createElement("select");
  991. setupDropdownMenu(dropdown, "language_dropdown_menu");
  992.  
  993. for (const lang of languages) {
  994. const option = document.createElement("option");
  995. option.value = lang.code;
  996. option.textContent = lang.name;
  997. if (lang.code === activeLanguage) {
  998. option.selected = true;
  999. }
  1000. dropdown.appendChild(option);
  1001. }
  1002.  
  1003. dropdown.addEventListener("change", function () {
  1004. changeLanguage(this.value).catch(console.error);
  1005. });
  1006.  
  1007. return dropdown;
  1008. }
  1009.  
  1010. function createLanguageDropdownMenuLabel() {
  1011. const label = document.createElement("span");
  1012. setupDropdownMenuLabel(
  1013. label,
  1014. "language_dropdown_menu_label",
  1015. strings.languageDropdownMenuLabel
  1016. );
  1017. return label;
  1018. }
  1019.  
  1020. async function changeLanguage(languageCode) {
  1021. try {
  1022. const translationDataUrl = `https://raw.githubusercontent.com/douglasdotv/tactics-selector/main/json/lang/${languageCode}.json?callback=?`;
  1023. const translations = await (await fetch(translationDataUrl)).json();
  1024.  
  1025. i18next.changeLanguage(languageCode);
  1026. i18next.addResourceBundle(languageCode, "translation", translations);
  1027.  
  1028. GM_setValue("language", languageCode);
  1029.  
  1030. updateTranslation();
  1031.  
  1032. const language = languages.find((lang) => lang.code === languageCode);
  1033. if (language) {
  1034. const flagImage = document.getElementById("language_flag");
  1035. flagImage.src = language.flag;
  1036. }
  1037. } catch (err) {
  1038. console.error(err);
  1039. }
  1040. }
  1041.  
  1042. function updateTranslation() {
  1043. for (let key in strings) {
  1044. strings[key] = i18next.t(key);
  1045. }
  1046.  
  1047. document.getElementById("add_tactic_button").textContent =
  1048. strings.addButton;
  1049. document.getElementById("delete_tactic_button").textContent =
  1050. strings.deleteButton;
  1051. document.getElementById("rename_tactic_button").textContent =
  1052. strings.renameButton;
  1053. document.getElementById("update_tactic_button").textContent =
  1054. strings.updateButton;
  1055. document.getElementById("clear_tactics_button").textContent =
  1056. strings.clearButton;
  1057. document.getElementById("reset_tactics_button").textContent =
  1058. strings.resetButton;
  1059. document.getElementById("import_tactics_button").textContent =
  1060. strings.importButton;
  1061. document.getElementById("export_tactics_button").textContent =
  1062. strings.exportButton;
  1063. document.getElementById("tactics_dropdown_menu_label").textContent =
  1064. strings.tacticsDropdownMenuLabel;
  1065. document.getElementById("language_dropdown_menu_label").textContent =
  1066. strings.languageDropdownMenuLabel;
  1067. document.getElementById("info_modal_info_text").innerHTML =
  1068. strings.modalContentInfoText;
  1069. document.getElementById("info_modal_feedback_text").innerHTML =
  1070. strings.modalContentFeedbackText;
  1071. document.getElementById("about_button").textContent = strings.aboutButton;
  1072. }
  1073.  
  1074. function getActiveLanguage() {
  1075. let language = GM_getValue("language");
  1076. if (!language) {
  1077. let browserLanguage = navigator.language || "en";
  1078. browserLanguage = browserLanguage.split("-")[0];
  1079. const languageExists = languages.some(
  1080. (lang) => lang.code === browserLanguage
  1081. );
  1082. language = languageExists ? browserLanguage : "en";
  1083. }
  1084. return language;
  1085. }
  1086.  
  1087. // _____Other_____
  1088.  
  1089. function applyUxxFilter() {
  1090. const minAge = 16;
  1091. const maxAge = 21;
  1092. let links = "";
  1093.  
  1094. for (let i = minAge; i <= maxAge; ++i) {
  1095. if (i !== minAge) {
  1096. links += " ";
  1097. }
  1098. links += '<a href="#">' + i + "</a>";
  1099. }
  1100.  
  1101. $(".age-wrapper label").append(" " + links);
  1102.  
  1103. let last = null;
  1104. $(".age-wrapper label a").click(function () {
  1105. const current = $(this).text().trim();
  1106.  
  1107. if (last === current) {
  1108. $("#age_from").val(current);
  1109. $("#age_from").change();
  1110. }
  1111.  
  1112. $("#age_to").val(current);
  1113. $("#age_to").change();
  1114.  
  1115. if (parseInt($("#age_from").val()) > parseInt($("#age_to").val())) {
  1116. $("#age_from").val($("#age_to").val());
  1117. $("#age_from").change();
  1118. }
  1119.  
  1120. $("#filterSubmit").click();
  1121. last = current;
  1122. });
  1123. }
  1124.  
  1125. function appendChildren(parent, children) {
  1126. children.forEach((ch) => {
  1127. parent.appendChild(ch);
  1128. });
  1129. }
  1130.  
  1131. function insertAfterElement(something, element) {
  1132. element.parentNode.insertBefore(something, element.nextSibling);
  1133. }
  1134.  
  1135. function createMainDiv() {
  1136. const div = document.createElement("div");
  1137. setupMainDiv(div);
  1138. return div;
  1139. }
  1140.  
  1141. function setupMainDiv(div) {
  1142. div.id = "tactics_selector_div";
  1143. div.style.width = "100%";
  1144. div.style.display = "flex";
  1145. div.style.flexDirection = "column";
  1146. div.style.alignItems = "stretch";
  1147. div.style.marginTop = "6px";
  1148. div.style.marginLeft = "6px";
  1149. }
  1150.  
  1151. function createMainDivFirstRow() {
  1152. const row = document.createElement("div");
  1153. row.id = "tactics_selector_div_first_row";
  1154. row.style.display = "flex";
  1155. row.style.justifyContent = "space-between";
  1156. row.style.flexWrap = "wrap";
  1157. row.style.width = "75%";
  1158. return row;
  1159. }
  1160.  
  1161. function createMainDivSecondRow() {
  1162. const row = document.createElement("div");
  1163. row.id = "tactics_selector_div_second_row";
  1164. row.style.display = "flex";
  1165. row.style.justifyContent = "flex-start";
  1166. row.style.flexWrap = "wrap";
  1167. return row;
  1168. }
  1169.  
  1170. function createLabelDropdownMenuGroup(label, dropdown) {
  1171. const group = document.createElement("div");
  1172. group.style.display = "flex";
  1173. group.appendChild(label);
  1174. group.appendChild(dropdown);
  1175. return group;
  1176. }
  1177.  
  1178. function setupDropdownMenu(dropdown, id) {
  1179. dropdown.id = id;
  1180. dropdown.style.fontSize = "12px";
  1181. dropdown.style.fontFamily = "Montserrat, sans-serif";
  1182. dropdown.style.border = "2px solid #000";
  1183. dropdown.style.borderRadius = "2px";
  1184. dropdown.style.background = "linear-gradient(to right, #add8e6, #e6f7ff)";
  1185. dropdown.style.color = "#000";
  1186. dropdown.style.boxShadow = "3px 3px 5px rgba(0, 0, 0, 0.2)";
  1187. dropdown.style.cursor = "pointer";
  1188. dropdown.style.outline = "none";
  1189. dropdown.style.margin = "6px 0 6px 6px";
  1190. }
  1191.  
  1192. function setupDropdownMenuLabel(description, id, textContent) {
  1193. description.id = id;
  1194. description.textContent = textContent;
  1195. description.style.fontFamily = "Montserrat, sans-serif";
  1196. description.style.fontSize = "12px";
  1197. description.style.color = "#000";
  1198. description.style.margin = "6px 0 6px 6px";
  1199. }
  1200.  
  1201. function setupButton(button, id, textContent) {
  1202. button.id = id;
  1203. button.textContent = textContent;
  1204. button.style.fontFamily = "Montserrat, sans-serif";
  1205. button.style.fontSize = "12px";
  1206. button.style.color = "#000";
  1207. button.style.marginLeft = "6px";
  1208. button.style.marginTop = "6px";
  1209. button.style.cursor = "pointer";
  1210. button.style.boxShadow = "3px 3px 5px rgba(0, 0, 0, 0.2)";
  1211. }
  1212.  
  1213. function createPlaceholderOption() {
  1214. const placeholderOption = document.createElement("option");
  1215. placeholderOption.value = "";
  1216. placeholderOption.text = "";
  1217. placeholderOption.disabled = true;
  1218. placeholderOption.selected = true;
  1219. return placeholderOption;
  1220. }
  1221.  
  1222. function createFlagImage() {
  1223. const img = document.createElement("img");
  1224. img.id = "language_flag";
  1225. img.style.height = "15px";
  1226. img.style.width = "25px";
  1227. img.style.margin = "9px 0 6px 6px";
  1228. const activeLang = languages.find((lang) => lang.code === activeLanguage);
  1229. if (activeLang) {
  1230. img.src = activeLang.flag;
  1231. }
  1232. return img;
  1233. }
  1234.  
  1235. function generateUniqueId(coordinates) {
  1236. const sortedCoordinates = coordinates.sort(
  1237. (a, b) => a[1] - b[1] || a[0] - b[0]
  1238. );
  1239.  
  1240. const coordString = sortedCoordinates
  1241. .map((coord) => `${coord[1]}_${coord[0]}`)
  1242. .join("_");
  1243.  
  1244. return sha256Hash(coordString);
  1245. }
  1246.  
  1247. function sha256Hash(str) {
  1248. const shaObj = new jsSHA("SHA-256", "TEXT");
  1249. shaObj.update(str);
  1250. const hash = shaObj.getHash("HEX");
  1251. return hash;
  1252. }
  1253. })();