ylOppTacticsPreview (Modified)

Shows the latest tactics used by an opponent

  1. // ==UserScript==
  2. // @name ylOppTacticsPreview (Modified)
  3. // @namespace douglaskampl
  4. // @version 5.0.1
  5. // @description Shows the latest tactics used by an opponent
  6. // @author kostrzak16 (feat. Douglas and xente)
  7. // @match https://www.managerzone.com/?p=match&sub=scheduled
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=managerzone.com
  9. // @grant GM_addStyle
  10. // @grant GM_getResourceText
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @require https://cdnjs.cloudflare.com/ajax/libs/spin.js/2.3.2/spin.min.js
  14. // @resource oppTacticsPreviewStyles https://br18.org/mz/userscript/other/Slezsko.css
  15. // @run-at document-idle
  16. // @license MIT
  17. // ==/UserScript==
  18.  
  19. (function () {
  20. 'use strict';
  21.  
  22. class OpponentTacticsPreview {
  23. static CONSTANTS = {
  24. MATCH_TYPE_GROUPS: {
  25. 'All': [
  26. { id: 'no_restriction', label: 'Senior' },
  27. { id: 'u23', label: 'U23' },
  28. { id: 'u21', label: 'U21' },
  29. { id: 'u18', label: 'U18' }
  30. ],
  31. 'World League': [
  32. { id: 'world_series', label: 'Senior WL' },
  33. { id: 'u23_world_series', label: 'U23 WL' },
  34. { id: 'u21_world_series', label: 'U21 WL' },
  35. { id: 'u18_world_series', label: 'U18 WL' }
  36. ],
  37. 'Official League': [
  38. { id: 'series', label: 'Senior League' },
  39. { id: 'u23_series', label: 'U23 League' },
  40. { id: 'u21_series', label: 'U21 League' },
  41. { id: 'u18_series', label: 'U18 League' }
  42. ]
  43. },
  44. URLS: {
  45. MATCH_STATS: (matchId) => `https://www.managerzone.com/matchviewer/getMatchFiles.php?type=stats&mid=${matchId}&sport=soccer`,
  46. MATCH_LIST: 'https://www.managerzone.com/ajax.php?p=matches&sub=list&sport=soccer',
  47. PITCH_IMAGE: (matchId) => `https://www.managerzone.com/dynimg/pitch.php?match_id=${matchId}`,
  48. MATCH_RESULT: (matchId) => `https://www.managerzone.com/?p=match&sub=result&mid=${matchId}`,
  49. CLUBHOUSE: 'https://www.managerzone.com/?p=clubhouse'
  50. },
  51. STORAGE_KEYS: {
  52. MATCH_LIMIT: 'ylopp_match_limit',
  53. SAVED_TEAMS: 'ylopp_saved_teams',
  54. USER_TEAM_ID: 'ylopp_user_team_id'
  55. },
  56. DEFAULTS: {
  57. MATCH_LIMIT: 10,
  58. MAX_SAVED_TEAMS: 15,
  59. MAX_MATCH_LIMIT: 100
  60. },
  61. SELECTORS: {
  62. FIXTURES_LIST: '#fixtures-results-list-wrapper',
  63. STATS_XENTE: '#legendDiv',
  64. ELO_SCHEDULED: '#eloScheduledSelect',
  65. HOME_TEAM: '.home-team-column.flex-grow-1',
  66. SELECT_WRAPPER: 'dd.set-default-wrapper'
  67. },
  68. };
  69.  
  70. constructor() {
  71. this.ourTeamName = null;
  72. this.userTeamId = null;
  73. this.currentOpponentTid = '';
  74. this.spinnerInstance = null;
  75. this.observer = new MutationObserver(() => {
  76. this.insertIconsAndListeners();
  77. });
  78. }
  79.  
  80. getMatchLimit() {
  81. return GM_getValue(OpponentTacticsPreview.CONSTANTS.STORAGE_KEYS.MATCH_LIMIT, OpponentTacticsPreview.CONSTANTS.DEFAULTS.MATCH_LIMIT);
  82. }
  83.  
  84. setMatchLimit(limit) {
  85. const numericLimit = parseInt(limit, 10);
  86. if (!isNaN(numericLimit) && numericLimit > 0 && numericLimit <= OpponentTacticsPreview.CONSTANTS.DEFAULTS.MAX_MATCH_LIMIT) {
  87. GM_setValue(OpponentTacticsPreview.CONSTANTS.STORAGE_KEYS.MATCH_LIMIT, numericLimit);
  88. }
  89. }
  90.  
  91. getSavedTeams() {
  92. return GM_getValue(OpponentTacticsPreview.CONSTANTS.STORAGE_KEYS.SAVED_TEAMS, []);
  93. }
  94.  
  95. saveTeam(teamId, teamName) {
  96. if (!teamId || !teamName || teamName.startsWith('Team ')) {
  97. return;
  98. }
  99. let teams = this.getSavedTeams();
  100. const existingIndex = teams.findIndex(team => team.id === teamId);
  101. if (existingIndex > -1) {
  102. teams.splice(existingIndex, 1);
  103. }
  104. teams.unshift({ id: teamId, name: teamName });
  105. const trimmedTeams = teams.slice(0, OpponentTacticsPreview.CONSTANTS.DEFAULTS.MAX_SAVED_TEAMS);
  106. GM_setValue(OpponentTacticsPreview.CONSTANTS.STORAGE_KEYS.SAVED_TEAMS, trimmedTeams);
  107. }
  108.  
  109. startObserving() {
  110. const fixturesList = document.querySelector(OpponentTacticsPreview.CONSTANTS.SELECTORS.FIXTURES_LIST);
  111. if (fixturesList) {
  112. this.observer.observe(fixturesList, { childList: true, subtree: true });
  113. }
  114. }
  115.  
  116. showLoadingSpinner() {
  117. if (this.spinnerInstance) {
  118. return;
  119. }
  120. const spinnerContainer = document.createElement('div');
  121. spinnerContainer.id = 'spinjs-overlay';
  122. document.body.appendChild(spinnerContainer);
  123. this.spinnerInstance = new Spinner({ color: '#FFFFFF', lines: 12, top: '50%', left: '50%' }).spin(spinnerContainer);
  124. }
  125.  
  126. hideLoadingSpinner() {
  127. if (this.spinnerInstance) {
  128. this.spinnerInstance.stop();
  129. this.spinnerInstance = null;
  130. }
  131. const spinnerContainer = document.getElementById('spinjs-overlay');
  132. if (spinnerContainer) {
  133. spinnerContainer.remove();
  134. }
  135. }
  136.  
  137. extractTeamNameFromHtml(htmlDocument, teamId) {
  138. const nameCounts = new Map();
  139. const teamLinks = htmlDocument.querySelectorAll('.teams-wrapper a.clippable');
  140. teamLinks.forEach(link => {
  141. const linkUrl = new URL(link.href, location.href);
  142. const linkTid = linkUrl.searchParams.get('tid');
  143. if (linkTid === teamId) {
  144. const fullName = link.querySelector('.full-name')?.textContent.trim();
  145. if (fullName) {
  146. nameCounts.set(fullName, (nameCounts.get(fullName) || 0) + 1);
  147. }
  148. }
  149. });
  150.  
  151. if (nameCounts.size > 0) {
  152. let mostCommonName = '';
  153. let maxCount = 0;
  154. for (const [name, count] of nameCounts.entries()) {
  155. if (count > maxCount) {
  156. maxCount = count;
  157. mostCommonName = name;
  158. }
  159. }
  160. return mostCommonName;
  161. }
  162.  
  163. const boldTeamNameElement = htmlDocument.querySelector('.teams-wrapper a.clippable > strong > .full-name');
  164. if (boldTeamNameElement) {
  165. return boldTeamNameElement.textContent.trim();
  166. }
  167.  
  168. return null;
  169. }
  170.  
  171. async fetchLatestTactics(teamId, matchType) {
  172. const modal = document.getElementById('interaction-modal');
  173. if (modal) {
  174. this.fadeOutAndRemove(modal);
  175. }
  176. this.showLoadingSpinner();
  177. try {
  178. const response = await fetch(
  179. OpponentTacticsPreview.CONSTANTS.URLS.MATCH_LIST, {
  180. method: 'POST',
  181. headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },
  182. body: `type=played&hidescore=false&tid1=${teamId}&offset=&selectType=${matchType}&limit=max`,
  183. credentials: 'include'
  184. }
  185. );
  186. if (!response.ok) {
  187. throw new Error(`Network response was not ok: ${response.statusText}`);
  188. }
  189.  
  190. const data = await response.json();
  191. const parser = new DOMParser();
  192. const htmlDocument = parser.parseFromString(data.list, 'text/html');
  193. const actualTeamName = this.extractTeamNameFromHtml(htmlDocument, teamId);
  194. const finalTeamName = actualTeamName || `Team ${teamId}`;
  195.  
  196. this.saveTeam(teamId, finalTeamName);
  197. this.currentOpponentTid = teamId;
  198. this.processTacticsData(htmlDocument, matchType, finalTeamName);
  199. } catch (error) {
  200. console.error('Failed to fetch latest tactics:', error);
  201. } finally {
  202. this.hideLoadingSpinner();
  203. }
  204. }
  205.  
  206. isRelevantMatch(entry) {
  207. const wrapper = entry.querySelector('.responsive-hide.match-reference-text-wrapper');
  208. if (!wrapper) {
  209. return true;
  210. }
  211. const hasLink = wrapper.querySelector('a');
  212. return hasLink !== null;
  213. }
  214.  
  215. processTacticsData(htmlDocument, matchType, opponentName) {
  216. const matchEntries = htmlDocument.querySelectorAll('dl > dd.odd');
  217. const container = this.createTacticsContainer(matchType, opponentName);
  218. document.body.appendChild(container);
  219. const listWrapper = container.querySelector('.tactics-list');
  220. let processedCount = 0;
  221. const matchLimit = this.getMatchLimit();
  222.  
  223. for (const entry of matchEntries) {
  224. if (processedCount >= matchLimit) {
  225. break;
  226. }
  227.  
  228. if (!this.isRelevantMatch(entry)) {
  229. continue;
  230. }
  231.  
  232. const link = entry.querySelector('a.score-shown');
  233. if (!link) {
  234. continue;
  235. }
  236.  
  237. const dl = link.closest('dl');
  238. const theScore = link.textContent.trim();
  239. const homeTeamName = dl.querySelector('.home-team-column .full-name')?.textContent.trim() || 'Home';
  240. const awayTeamName = dl.querySelector('.away-team-column .full-name')?.textContent.trim() || 'Away';
  241. const mid = new URLSearchParams(new URL(link.href, location.href).search).get('mid');
  242. if (!mid) {
  243. continue;
  244. }
  245.  
  246. let [homeGoals, awayGoals] = [0, 0];
  247. if (theScore.includes('-')) {
  248. const parts = theScore.split('-').map(x => parseInt(x.trim(), 10));
  249. if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) {
  250. [homeGoals, awayGoals] = parts;
  251. }
  252. }
  253.  
  254. const opponentIsHome = (homeTeamName === opponentName);
  255. const tacticUrl = OpponentTacticsPreview.CONSTANTS.URLS.PITCH_IMAGE(mid);
  256. const resultUrl = OpponentTacticsPreview.CONSTANTS.URLS.MATCH_RESULT(mid);
  257. const canvas = this.createCanvasWithReplacedColors(tacticUrl, opponentIsHome);
  258. const item = document.createElement('div');
  259. item.className = 'tactic-item';
  260.  
  261. const opponentGoals = opponentIsHome ? homeGoals : awayGoals;
  262. const otherGoals = opponentIsHome ? awayGoals : homeGoals;
  263.  
  264. if (opponentGoals > otherGoals) {
  265. item.classList.add('tactic-win');
  266. } else if (opponentGoals < otherGoals) {
  267. item.classList.add('tactic-loss');
  268. } else {
  269. item.classList.add('tactic-draw');
  270. }
  271.  
  272. const linkA = document.createElement('a');
  273. linkA.href = resultUrl;
  274. linkA.target = '_blank';
  275. linkA.className = 'tactic-link';
  276. linkA.appendChild(canvas);
  277.  
  278. const scoreP = document.createElement('p');
  279. scoreP.textContent = `${homeTeamName} ${theScore} ${awayTeamName}`;
  280. linkA.appendChild(scoreP);
  281. item.appendChild(linkA);
  282.  
  283. this.addPlaystyleHover(mid, canvas, this.currentOpponentTid);
  284. listWrapper.appendChild(item);
  285. processedCount++;
  286. }
  287.  
  288. if (processedCount === 0) {
  289. const message = document.createElement('div');
  290. message.className = 'no-tactics-message';
  291. message.textContent = 'No recent valid tactics found for this team and category.';
  292. listWrapper.appendChild(message);
  293. }
  294. container.classList.add('fade-in');
  295. }
  296.  
  297. showInteractionModal(teamId, sourceElement) {
  298. const existingModal = document.getElementById('interaction-modal');
  299. if (existingModal) {
  300. this.fadeOutAndRemove(existingModal);
  301. }
  302. const modal = document.createElement('div');
  303. modal.id = 'interaction-modal';
  304. modal.classList.add('fade-in');
  305. const header = document.createElement('div');
  306. header.className = 'interaction-modal-header';
  307. const title = document.createElement('span');
  308. title.textContent = '';
  309. header.appendChild(title);
  310. const settingsIcon = document.createElement('span');
  311. settingsIcon.className = 'settings-icon';
  312. settingsIcon.innerHTML = '⚙';
  313. header.appendChild(settingsIcon);
  314. modal.appendChild(header);
  315. const teamInputSection = this.createTeamInputSection(modal, teamId);
  316. this.createTabbedButtons(modal, teamInputSection.teamIdInput);
  317. const settingsPanel = this.createSettingsPanel(modal);
  318. settingsIcon.onclick = () => {
  319. settingsPanel.style.display = settingsPanel.style.display === 'block' ? 'none' : 'block';
  320. };
  321. document.body.appendChild(modal);
  322. const rect = sourceElement.getBoundingClientRect();
  323. modal.style.position = 'absolute';
  324. modal.style.top = `${window.scrollY + rect.bottom + 5}px`;
  325. modal.style.left = `${window.scrollX + rect.left}px`;
  326. }
  327.  
  328. createTeamInputSection(container, initialTeamId) {
  329. const section = document.createElement('div');
  330. section.className = 'interaction-section team-input-section';
  331. const label = document.createElement('label');
  332. label.textContent = 'Team ID:';
  333. label.htmlFor = 'team-id-input';
  334. section.appendChild(label);
  335. const teamIdInput = document.createElement('input');
  336. teamIdInput.type = 'text';
  337. teamIdInput.id = 'team-id-input';
  338. teamIdInput.value = initialTeamId;
  339. section.appendChild(teamIdInput);
  340. const select = this.createRecentsDropdown(teamIdInput);
  341. section.appendChild(select);
  342. container.appendChild(section);
  343. return { teamIdInput, recentsSelect: select };
  344. }
  345.  
  346. createRecentsDropdown(teamIdInput) {
  347. const select = document.createElement('select');
  348. select.className = 'recents-select';
  349. const defaultOption = document.createElement('option');
  350. defaultOption.textContent = 'Recent Teams';
  351. defaultOption.value = '';
  352. select.appendChild(defaultOption);
  353. this.getSavedTeams().forEach(team => {
  354. const option = document.createElement('option');
  355. option.value = team.id;
  356. option.textContent = `${team.name} (${team.id})`;
  357. select.appendChild(option);
  358. });
  359. select.onchange = () => {
  360. if (select.value) {
  361. teamIdInput.value = select.value;
  362. }
  363. };
  364. return select;
  365. }
  366.  
  367. createTabbedButtons(container, teamIdInput) {
  368. const tabContainer = document.createElement('div');
  369. tabContainer.className = 'tab-container';
  370. const tabHeaders = document.createElement('div');
  371. tabHeaders.className = 'tab-headers';
  372. const tabContents = document.createElement('div');
  373. tabContents.className = 'tab-contents';
  374. Object.entries(OpponentTacticsPreview.CONSTANTS.MATCH_TYPE_GROUPS).forEach(([groupName, types], index) => {
  375. const header = document.createElement('button');
  376. header.className = 'tab-header';
  377. header.textContent = groupName;
  378. const content = document.createElement('div');
  379. content.className = 'tab-content';
  380. types.forEach(type => {
  381. const button = document.createElement('button');
  382. button.textContent = type.label;
  383. button.onclick = () => {
  384. const teamId = teamIdInput.value.trim();
  385. if (teamId) {
  386. this.fetchLatestTactics(teamId, type.id);
  387. }
  388. };
  389. content.appendChild(button);
  390. });
  391. header.onclick = () => {
  392. tabContainer.querySelectorAll('.tab-header').forEach(h => h.classList.remove('active'));
  393. tabContainer.querySelectorAll('.tab-content').forEach(c => c.style.display = 'none');
  394. header.classList.add('active');
  395. content.style.display = 'flex';
  396. };
  397. tabHeaders.appendChild(header);
  398. tabContents.appendChild(content);
  399. if (index === 0) {
  400. header.classList.add('active');
  401. content.style.display = 'flex';
  402. } else {
  403. content.style.display = 'none';
  404. }
  405. });
  406. tabContainer.appendChild(tabHeaders);
  407. tabContainer.appendChild(tabContents);
  408. container.appendChild(tabContainer);
  409. }
  410.  
  411. createSettingsPanel(modalContainer) {
  412. const panel = document.createElement('div');
  413. panel.className = 'settings-panel';
  414. panel.style.display = 'none';
  415. const limitLabel = document.createElement('label');
  416. limitLabel.textContent = `Match Limit (1-${OpponentTacticsPreview.CONSTANTS.DEFAULTS.MAX_MATCH_LIMIT}):`;
  417. panel.appendChild(limitLabel);
  418. const limitInput = document.createElement('input');
  419. limitInput.type = 'text';
  420. limitInput.inputMode = 'numeric';
  421. limitInput.pattern = '[0-9]*';
  422. limitInput.value = this.getMatchLimit();
  423. limitInput.oninput = () => {
  424. limitInput.value = limitInput.value.replace(/\D/g, '');
  425. };
  426. limitInput.onchange = () => this.setMatchLimit(limitInput.value);
  427. panel.appendChild(limitInput);
  428. modalContainer.appendChild(panel);
  429. return panel;
  430. }
  431.  
  432. createTacticsContainer(matchType, opponent) {
  433. const existingContainer = document.getElementById('tactics-container');
  434. if (existingContainer) {
  435. this.fadeOutAndRemove(existingContainer);
  436. }
  437. const container = document.createElement('div');
  438. container.id = 'tactics-container';
  439. container.className = 'tactics-container';
  440. const header = document.createElement('div');
  441. header.className = 'tactics-header';
  442. const title = document.createElement('div');
  443. title.className = 'match-info-text';
  444. let matchTypeLabel = matchType;
  445. for (const group in OpponentTacticsPreview.CONSTANTS.MATCH_TYPE_GROUPS) {
  446. const found = OpponentTacticsPreview.CONSTANTS.MATCH_TYPE_GROUPS[group].find(t => t.id === matchType);
  447. if (found) {
  448. matchTypeLabel = found.label;
  449. break;
  450. }
  451. }
  452. title.innerHTML = `<div class="title-main">${opponent} (${matchTypeLabel})</div>`;
  453. header.appendChild(title);
  454. const closeButton = document.createElement('button');
  455. closeButton.className = 'close-button';
  456. closeButton.textContent = '×';
  457. closeButton.onclick = () => this.fadeOutAndRemove(container);
  458. header.appendChild(closeButton);
  459. container.appendChild(header);
  460. const listWrapper = document.createElement('div');
  461. listWrapper.className = 'tactics-list';
  462. container.appendChild(listWrapper);
  463. return container;
  464. }
  465.  
  466. fadeOutAndRemove(el) {
  467. if (!el) {
  468. return;
  469. }
  470. el.classList.remove('fade-in');
  471. el.classList.add('fade-out');
  472. setTimeout(() => el.remove(), 200);
  473. }
  474.  
  475. identifyUserTeamName() {
  476. const ddRows = document.querySelectorAll('#fixtures-results-list > dd.odd');
  477. if (ddRows.length === 0) {
  478. return null;
  479. }
  480. const countMap = new Map();
  481. ddRows.forEach(dd => {
  482. const homeName = dd.querySelector('.home-team-column .full-name')?.textContent.trim();
  483. const awayName = dd.querySelector('.away-team-column .full-name')?.textContent.trim();
  484. if (homeName) {
  485. countMap.set(homeName, (countMap.get(homeName) || 0) + 1);
  486. }
  487. if (awayName) {
  488. countMap.set(awayName, (countMap.get(awayName) || 0) + 1);
  489. }
  490. });
  491. for (const [name, count] of countMap.entries()) {
  492. if (count === ddRows.length) {
  493. return name;
  494. }
  495. }
  496. return null;
  497. }
  498.  
  499. insertIconsAndListeners() {
  500. if (!this.ourTeamName) {
  501. this.ourTeamName = this.identifyUserTeamName();
  502. }
  503. if (!this.ourTeamName) {
  504. return;
  505. }
  506.  
  507. document.querySelectorAll('dd.odd').forEach(dd => {
  508. const selectWrapper = dd.querySelector(OpponentTacticsPreview.CONSTANTS.SELECTORS.SELECT_WRAPPER);
  509. if (selectWrapper && !selectWrapper.querySelector('.magnifier-icon')) {
  510. const homeTeamName = dd.querySelector('.home-team-column .full-name')?.textContent.trim();
  511. const awayTeamName = dd.querySelector('.away-team-column .full-name')?.textContent.trim();
  512. const homeTeamLink = dd.querySelector('.home-team-column a.clippable');
  513. const awayTeamLink = dd.querySelector('.away-team-column a.clippable');
  514. let opponentName = null,
  515. opponentTid = null;
  516. if (homeTeamName === this.ourTeamName && awayTeamName && awayTeamLink) {
  517. opponentName = awayTeamName;
  518. const awayHref = awayTeamLink.href;
  519. if (awayHref) {
  520. opponentTid = new URLSearchParams(new URL(awayHref, location.href).search).get('tid');
  521. }
  522. } else if (awayTeamName === this.ourTeamName && homeTeamName && homeTeamLink) {
  523. opponentName = homeTeamName;
  524. const homeHref = homeTeamLink.href;
  525. if (homeHref) {
  526. opponentTid = new URLSearchParams(new URL(homeHref, location.href).search).get('tid');
  527. }
  528. }
  529. if (opponentName && opponentTid) {
  530. const iconWrapper = document.createElement('span');
  531. iconWrapper.className = 'magnifier-icon';
  532. iconWrapper.dataset.tid = opponentTid;
  533. iconWrapper.dataset.opponent = opponentName;
  534. iconWrapper.title = 'Check opponent latest tactics';
  535. iconWrapper.textContent = '🔍';
  536. const select = selectWrapper.querySelector('select');
  537. if (select) {
  538. select.insertAdjacentElement('afterend', iconWrapper);
  539. }
  540. }
  541. }
  542. });
  543. }
  544.  
  545. createCanvasWithReplacedColors(imageUrl, opponentIsHome) {
  546. const canvas = document.createElement('canvas');
  547. canvas.width = 150;
  548. canvas.height = 200;
  549. const context = canvas.getContext('2d');
  550. const image = new Image();
  551. image.crossOrigin = 'Anonymous';
  552. image.onload = () => {
  553. if (opponentIsHome) {
  554. context.translate(canvas.width / 2, canvas.height / 2);
  555. context.rotate(Math.PI);
  556. context.translate(-canvas.width / 2, -canvas.height / 2);
  557. }
  558. context.drawImage(image, 0, 0, canvas.width, canvas.height);
  559. const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
  560. const data = imageData.data;
  561. const darkGreen = { r: 0, g: 100, b: 0 };
  562. for (let i = 0; i < data.length; i += 4) {
  563. const r = data[i],
  564. g = data[i + 1],
  565. b = data[i + 2];
  566. const isBlack = r < 30 && g < 30 && b < 30;
  567. const isYellow = r > 200 && g > 200 && b < 100;
  568. if (opponentIsHome) {
  569. if (isYellow) {
  570. data[i] = 0;
  571. data[i + 1] = 0;
  572. data[i + 2] = 0;
  573. } else if (isBlack) {
  574. data[i] = darkGreen.r;
  575. data[i + 1] = darkGreen.g;
  576. data[i + 2] = darkGreen.b;
  577. }
  578. } else {
  579. if (isBlack) {
  580. data[i] = 0;
  581. data[i + 1] = 0;
  582. data[i + 2] = 0;
  583. } else if (isYellow) {
  584. data[i] = darkGreen.r;
  585. data[i + 1] = darkGreen.g;
  586. data[i + 2] = darkGreen.b;
  587. }
  588. }
  589. }
  590. const tempData = new Uint8ClampedArray(data);
  591. for (let y = 1; y < canvas.height - 1; y++) {
  592. for (let x = 1; x < canvas.width - 1; x++) {
  593. const i = (y * canvas.width + x) * 4;
  594. if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0) {
  595. for (let dy = -1; dy <= 1; dy++) {
  596. for (let dx = -1; dx <= 1; dx++) {
  597. if (dx === 0 && dy === 0) {
  598. continue;
  599. }
  600. const ni = ((y + dy) * canvas.width + (x + dx)) * 4;
  601. if (!(data[ni] === 0 && data[ni + 1] === 0 && data[ni + 2] === 0)) {
  602. tempData[i] = 255;
  603. tempData[i + 1] = 255;
  604. tempData[i + 2] = 255;
  605. }
  606. }
  607. }
  608. }
  609. }
  610. }
  611. context.putImageData(new ImageData(tempData, canvas.width, canvas.height), 0, 0);
  612. };
  613. image.src = imageUrl;
  614. return canvas;
  615. }
  616.  
  617. async fetchPlaystyleChanges(mid, opponentTid) {
  618. try {
  619. const res = await fetch(OpponentTacticsPreview.CONSTANTS.URLS.MATCH_STATS(mid));
  620. const txt = await res.text();
  621. const xml = new DOMParser().parseFromString(txt, 'text/xml');
  622. const changes = Array.from(xml.querySelectorAll('Events Tactic'))
  623. .filter(n => n.getAttribute('teamId') === opponentTid)
  624. .map(n => {
  625. const tType = n.getAttribute('type');
  626. if (['playstyle', 'aggression', 'tactic'].includes(tType)) {
  627. return `Min ${n.getAttribute('time')}: ${tType} ${n.getAttribute('new_setting')}`;
  628. }
  629. return null;
  630. }).filter(Boolean);
  631. return changes.length ? changes.join('<br>') : 'No relevant changes detected';
  632. } catch (e) {
  633. return 'Could not fetch playstyle data.';
  634. }
  635. }
  636.  
  637. addPlaystyleHover(mid, canvas, opponentTid) {
  638. const tooltip = document.createElement('div');
  639. tooltip.className = 'playstyle-tooltip';
  640. document.body.appendChild(tooltip);
  641. canvas.addEventListener('mouseover', async (ev) => {
  642. tooltip.style.display = 'block';
  643. tooltip.innerHTML = 'Loading...';
  644. const info = await this.fetchPlaystyleChanges(mid, opponentTid);
  645. tooltip.innerHTML = info;
  646. tooltip.style.top = `${ev.pageY + 15}px`;
  647. tooltip.style.left = `${ev.pageX + 5}px`;
  648. });
  649. canvas.addEventListener('mousemove', (ev) => {
  650. tooltip.style.top = `${ev.pageY + 15}px`;
  651. tooltip.style.left = `${ev.pageX + 5}px`;
  652. });
  653. canvas.addEventListener('mouseout', () => {
  654. tooltip.style.display = 'none';
  655. });
  656. }
  657.  
  658. waitForEloValues() {
  659. const interval = setInterval(() => {
  660. const elements = document.querySelectorAll(OpponentTacticsPreview.CONSTANTS.SELECTORS.HOME_TEAM);
  661. if (elements.length > 0 && elements[elements.length - 1]?.innerHTML.includes('br')) {
  662. clearInterval(interval);
  663. this.insertIconsAndListeners();
  664. }
  665. }, 100);
  666. setTimeout(() => clearInterval(interval), 1500);
  667. }
  668.  
  669. handleClickEvents(e) {
  670. const clickedMagnifier = e.target.closest('.magnifier-icon');
  671. if (clickedMagnifier) {
  672. e.preventDefault();
  673. e.stopPropagation();
  674. const tid = clickedMagnifier.dataset.tid;
  675. const name = clickedMagnifier.dataset.opponent;
  676. if (!tid) {
  677. return;
  678. }
  679. this.saveTeam(tid, name);
  680. this.showInteractionModal(tid, clickedMagnifier);
  681. return;
  682. }
  683. const interactionModal = document.getElementById('interaction-modal');
  684. const tacticsContainer = document.getElementById('tactics-container');
  685. if (interactionModal && !interactionModal.contains(e.target)) {
  686. this.fadeOutAndRemove(interactionModal);
  687. }
  688. if (tacticsContainer && !tacticsContainer.contains(e.target)) {
  689. this.fadeOutAndRemove(tacticsContainer);
  690. }
  691. }
  692.  
  693. async initializeUserTeamId() {
  694. let storedId = GM_getValue(OpponentTacticsPreview.CONSTANTS.STORAGE_KEYS.USER_TEAM_ID, null);
  695. if (storedId) {
  696. this.userTeamId = storedId;
  697. return;
  698. }
  699.  
  700. try {
  701. const response = await fetch(OpponentTacticsPreview.CONSTANTS.URLS.CLUBHOUSE);
  702. const text = await response.text();
  703. const match = text.match(/dynimg\/badge\.php\?team_id=(\d+)/);
  704. if (match && match[1]) {
  705. this.userTeamId = match[1];
  706. GM_setValue(OpponentTacticsPreview.CONSTANTS.STORAGE_KEYS.USER_TEAM_ID, this.userTeamId);
  707. }
  708. } catch (error) {
  709. console.error('Could not fetch user team ID.', error);
  710. }
  711. }
  712.  
  713.  
  714. async init() {
  715. GM_addStyle(GM_getResourceText('oppTacticsPreviewStyles'));
  716. await this.initializeUserTeamId();
  717. const statsXenteRunning = document.querySelector(OpponentTacticsPreview.CONSTANTS.SELECTORS.STATS_XENTE);
  718. const eloScheduledSelected = document.querySelector(OpponentTacticsPreview.CONSTANTS.SELECTORS.ELO_SCHEDULED)?.checked;
  719. if (statsXenteRunning && eloScheduledSelected) {
  720. this.waitForEloValues();
  721. } else {
  722. this.insertIconsAndListeners();
  723. }
  724. this.startObserving();
  725. document.body.addEventListener('click', this.handleClickEvents.bind(this), true);
  726. }
  727. }
  728.  
  729. const プレビュー = new OpponentTacticsPreview();
  730. プレビュー.init();
  731. })();