// ==UserScript== // @name ylOppTacticsPreview (Modified) // @namespace douglaskampl // @version 5.0.1 // @description Shows the latest tactics used by an opponent // @author kostrzak16 (feat. Douglas and xente) // @match https://www.managerzone.com/?p=match&sub=scheduled // @icon https://www.google.com/s2/favicons?sz=64&domain=managerzone.com // @grant GM_addStyle // @grant GM_getResourceText // @grant GM_getValue // @grant GM_setValue // @require https://cdnjs.cloudflare.com/ajax/libs/spin.js/2.3.2/spin.min.js // @resource oppTacticsPreviewStyles https://br18.org/mz/userscript/other/Slezsko.css // @run-at document-idle // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/489482/ylOppTacticsPreview%20%28Modified%29.user.js // @updateURL https://update.greasyfork.icu/scripts/489482/ylOppTacticsPreview%20%28Modified%29.meta.js // ==/UserScript== (function () { 'use strict'; class OpponentTacticsPreview { static CONSTANTS = { MATCH_TYPE_GROUPS: { 'All': [ { id: 'no_restriction', label: 'Senior' }, { id: 'u23', label: 'U23' }, { id: 'u21', label: 'U21' }, { id: 'u18', label: 'U18' } ], 'World League': [ { id: 'world_series', label: 'Senior WL' }, { id: 'u23_world_series', label: 'U23 WL' }, { id: 'u21_world_series', label: 'U21 WL' }, { id: 'u18_world_series', label: 'U18 WL' } ], 'Official League': [ { id: 'series', label: 'Senior League' }, { id: 'u23_series', label: 'U23 League' }, { id: 'u21_series', label: 'U21 League' }, { id: 'u18_series', label: 'U18 League' } ] }, URLS: { MATCH_STATS: (matchId) => `https://www.managerzone.com/matchviewer/getMatchFiles.php?type=stats&mid=${matchId}&sport=soccer`, MATCH_LIST: 'https://www.managerzone.com/ajax.php?p=matches&sub=list&sport=soccer', PITCH_IMAGE: (matchId) => `https://www.managerzone.com/dynimg/pitch.php?match_id=${matchId}`, MATCH_RESULT: (matchId) => `https://www.managerzone.com/?p=match&sub=result&mid=${matchId}`, CLUBHOUSE: 'https://www.managerzone.com/?p=clubhouse' }, STORAGE_KEYS: { MATCH_LIMIT: 'ylopp_match_limit', SAVED_TEAMS: 'ylopp_saved_teams', USER_TEAM_ID: 'ylopp_user_team_id' }, DEFAULTS: { MATCH_LIMIT: 10, MAX_SAVED_TEAMS: 15, MAX_MATCH_LIMIT: 100 }, SELECTORS: { FIXTURES_LIST: '#fixtures-results-list-wrapper', STATS_XENTE: '#legendDiv', ELO_SCHEDULED: '#eloScheduledSelect', HOME_TEAM: '.home-team-column.flex-grow-1', SELECT_WRAPPER: 'dd.set-default-wrapper' }, }; constructor() { this.ourTeamName = null; this.userTeamId = null; this.currentOpponentTid = ''; this.spinnerInstance = null; this.observer = new MutationObserver(() => { this.insertIconsAndListeners(); }); } getMatchLimit() { return GM_getValue(OpponentTacticsPreview.CONSTANTS.STORAGE_KEYS.MATCH_LIMIT, OpponentTacticsPreview.CONSTANTS.DEFAULTS.MATCH_LIMIT); } setMatchLimit(limit) { const numericLimit = parseInt(limit, 10); if (!isNaN(numericLimit) && numericLimit > 0 && numericLimit <= OpponentTacticsPreview.CONSTANTS.DEFAULTS.MAX_MATCH_LIMIT) { GM_setValue(OpponentTacticsPreview.CONSTANTS.STORAGE_KEYS.MATCH_LIMIT, numericLimit); } } getSavedTeams() { return GM_getValue(OpponentTacticsPreview.CONSTANTS.STORAGE_KEYS.SAVED_TEAMS, []); } saveTeam(teamId, teamName) { if (!teamId || !teamName || teamName.startsWith('Team ')) { return; } let teams = this.getSavedTeams(); const existingIndex = teams.findIndex(team => team.id === teamId); if (existingIndex > -1) { teams.splice(existingIndex, 1); } teams.unshift({ id: teamId, name: teamName }); const trimmedTeams = teams.slice(0, OpponentTacticsPreview.CONSTANTS.DEFAULTS.MAX_SAVED_TEAMS); GM_setValue(OpponentTacticsPreview.CONSTANTS.STORAGE_KEYS.SAVED_TEAMS, trimmedTeams); } startObserving() { const fixturesList = document.querySelector(OpponentTacticsPreview.CONSTANTS.SELECTORS.FIXTURES_LIST); if (fixturesList) { this.observer.observe(fixturesList, { childList: true, subtree: true }); } } showLoadingSpinner() { if (this.spinnerInstance) { return; } const spinnerContainer = document.createElement('div'); spinnerContainer.id = 'spinjs-overlay'; document.body.appendChild(spinnerContainer); this.spinnerInstance = new Spinner({ color: '#FFFFFF', lines: 12, top: '50%', left: '50%' }).spin(spinnerContainer); } hideLoadingSpinner() { if (this.spinnerInstance) { this.spinnerInstance.stop(); this.spinnerInstance = null; } const spinnerContainer = document.getElementById('spinjs-overlay'); if (spinnerContainer) { spinnerContainer.remove(); } } extractTeamNameFromHtml(htmlDocument, teamId) { const nameCounts = new Map(); const teamLinks = htmlDocument.querySelectorAll('.teams-wrapper a.clippable'); teamLinks.forEach(link => { const linkUrl = new URL(link.href, location.href); const linkTid = linkUrl.searchParams.get('tid'); if (linkTid === teamId) { const fullName = link.querySelector('.full-name')?.textContent.trim(); if (fullName) { nameCounts.set(fullName, (nameCounts.get(fullName) || 0) + 1); } } }); if (nameCounts.size > 0) { let mostCommonName = ''; let maxCount = 0; for (const [name, count] of nameCounts.entries()) { if (count > maxCount) { maxCount = count; mostCommonName = name; } } return mostCommonName; } const boldTeamNameElement = htmlDocument.querySelector('.teams-wrapper a.clippable > strong > .full-name'); if (boldTeamNameElement) { return boldTeamNameElement.textContent.trim(); } return null; } async fetchLatestTactics(teamId, matchType) { const modal = document.getElementById('interaction-modal'); if (modal) { this.fadeOutAndRemove(modal); } this.showLoadingSpinner(); try { const response = await fetch( OpponentTacticsPreview.CONSTANTS.URLS.MATCH_LIST, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, body: `type=played&hidescore=false&tid1=${teamId}&offset=&selectType=${matchType}&limit=max`, credentials: 'include' } ); if (!response.ok) { throw new Error(`Network response was not ok: ${response.statusText}`); } const data = await response.json(); const parser = new DOMParser(); const htmlDocument = parser.parseFromString(data.list, 'text/html'); const actualTeamName = this.extractTeamNameFromHtml(htmlDocument, teamId); const finalTeamName = actualTeamName || `Team ${teamId}`; this.saveTeam(teamId, finalTeamName); this.currentOpponentTid = teamId; this.processTacticsData(htmlDocument, matchType, finalTeamName); } catch (error) { console.error('Failed to fetch latest tactics:', error); } finally { this.hideLoadingSpinner(); } } isRelevantMatch(entry) { const wrapper = entry.querySelector('.responsive-hide.match-reference-text-wrapper'); if (!wrapper) { return true; } const hasLink = wrapper.querySelector('a'); return hasLink !== null; } processTacticsData(htmlDocument, matchType, opponentName) { const matchEntries = htmlDocument.querySelectorAll('dl > dd.odd'); const container = this.createTacticsContainer(matchType, opponentName); document.body.appendChild(container); const listWrapper = container.querySelector('.tactics-list'); let processedCount = 0; const matchLimit = this.getMatchLimit(); for (const entry of matchEntries) { if (processedCount >= matchLimit) { break; } if (!this.isRelevantMatch(entry)) { continue; } const link = entry.querySelector('a.score-shown'); if (!link) { continue; } const dl = link.closest('dl'); const theScore = link.textContent.trim(); const homeTeamName = dl.querySelector('.home-team-column .full-name')?.textContent.trim() || 'Home'; const awayTeamName = dl.querySelector('.away-team-column .full-name')?.textContent.trim() || 'Away'; const mid = new URLSearchParams(new URL(link.href, location.href).search).get('mid'); if (!mid) { continue; } let [homeGoals, awayGoals] = [0, 0]; if (theScore.includes('-')) { const parts = theScore.split('-').map(x => parseInt(x.trim(), 10)); if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) { [homeGoals, awayGoals] = parts; } } const opponentIsHome = (homeTeamName === opponentName); const tacticUrl = OpponentTacticsPreview.CONSTANTS.URLS.PITCH_IMAGE(mid); const resultUrl = OpponentTacticsPreview.CONSTANTS.URLS.MATCH_RESULT(mid); const canvas = this.createCanvasWithReplacedColors(tacticUrl, opponentIsHome); const item = document.createElement('div'); item.className = 'tactic-item'; const opponentGoals = opponentIsHome ? homeGoals : awayGoals; const otherGoals = opponentIsHome ? awayGoals : homeGoals; if (opponentGoals > otherGoals) { item.classList.add('tactic-win'); } else if (opponentGoals < otherGoals) { item.classList.add('tactic-loss'); } else { item.classList.add('tactic-draw'); } const linkA = document.createElement('a'); linkA.href = resultUrl; linkA.target = '_blank'; linkA.className = 'tactic-link'; linkA.appendChild(canvas); const scoreP = document.createElement('p'); scoreP.textContent = `${homeTeamName} ${theScore} ${awayTeamName}`; linkA.appendChild(scoreP); item.appendChild(linkA); this.addPlaystyleHover(mid, canvas, this.currentOpponentTid); listWrapper.appendChild(item); processedCount++; } if (processedCount === 0) { const message = document.createElement('div'); message.className = 'no-tactics-message'; message.textContent = 'No recent valid tactics found for this team and category.'; listWrapper.appendChild(message); } container.classList.add('fade-in'); } showInteractionModal(teamId, sourceElement) { const existingModal = document.getElementById('interaction-modal'); if (existingModal) { this.fadeOutAndRemove(existingModal); } const modal = document.createElement('div'); modal.id = 'interaction-modal'; modal.classList.add('fade-in'); const header = document.createElement('div'); header.className = 'interaction-modal-header'; const title = document.createElement('span'); title.textContent = ''; header.appendChild(title); const settingsIcon = document.createElement('span'); settingsIcon.className = 'settings-icon'; settingsIcon.innerHTML = '⚙'; header.appendChild(settingsIcon); modal.appendChild(header); const teamInputSection = this.createTeamInputSection(modal, teamId); this.createTabbedButtons(modal, teamInputSection.teamIdInput); const settingsPanel = this.createSettingsPanel(modal); settingsIcon.onclick = () => { settingsPanel.style.display = settingsPanel.style.display === 'block' ? 'none' : 'block'; }; document.body.appendChild(modal); const rect = sourceElement.getBoundingClientRect(); modal.style.position = 'absolute'; modal.style.top = `${window.scrollY + rect.bottom + 5}px`; modal.style.left = `${window.scrollX + rect.left}px`; } createTeamInputSection(container, initialTeamId) { const section = document.createElement('div'); section.className = 'interaction-section team-input-section'; const label = document.createElement('label'); label.textContent = 'Team ID:'; label.htmlFor = 'team-id-input'; section.appendChild(label); const teamIdInput = document.createElement('input'); teamIdInput.type = 'text'; teamIdInput.id = 'team-id-input'; teamIdInput.value = initialTeamId; section.appendChild(teamIdInput); const select = this.createRecentsDropdown(teamIdInput); section.appendChild(select); container.appendChild(section); return { teamIdInput, recentsSelect: select }; } createRecentsDropdown(teamIdInput) { const select = document.createElement('select'); select.className = 'recents-select'; const defaultOption = document.createElement('option'); defaultOption.textContent = 'Recent Teams'; defaultOption.value = ''; select.appendChild(defaultOption); this.getSavedTeams().forEach(team => { const option = document.createElement('option'); option.value = team.id; option.textContent = `${team.name} (${team.id})`; select.appendChild(option); }); select.onchange = () => { if (select.value) { teamIdInput.value = select.value; } }; return select; } createTabbedButtons(container, teamIdInput) { const tabContainer = document.createElement('div'); tabContainer.className = 'tab-container'; const tabHeaders = document.createElement('div'); tabHeaders.className = 'tab-headers'; const tabContents = document.createElement('div'); tabContents.className = 'tab-contents'; Object.entries(OpponentTacticsPreview.CONSTANTS.MATCH_TYPE_GROUPS).forEach(([groupName, types], index) => { const header = document.createElement('button'); header.className = 'tab-header'; header.textContent = groupName; const content = document.createElement('div'); content.className = 'tab-content'; types.forEach(type => { const button = document.createElement('button'); button.textContent = type.label; button.onclick = () => { const teamId = teamIdInput.value.trim(); if (teamId) { this.fetchLatestTactics(teamId, type.id); } }; content.appendChild(button); }); header.onclick = () => { tabContainer.querySelectorAll('.tab-header').forEach(h => h.classList.remove('active')); tabContainer.querySelectorAll('.tab-content').forEach(c => c.style.display = 'none'); header.classList.add('active'); content.style.display = 'flex'; }; tabHeaders.appendChild(header); tabContents.appendChild(content); if (index === 0) { header.classList.add('active'); content.style.display = 'flex'; } else { content.style.display = 'none'; } }); tabContainer.appendChild(tabHeaders); tabContainer.appendChild(tabContents); container.appendChild(tabContainer); } createSettingsPanel(modalContainer) { const panel = document.createElement('div'); panel.className = 'settings-panel'; panel.style.display = 'none'; const limitLabel = document.createElement('label'); limitLabel.textContent = `Match Limit (1-${OpponentTacticsPreview.CONSTANTS.DEFAULTS.MAX_MATCH_LIMIT}):`; panel.appendChild(limitLabel); const limitInput = document.createElement('input'); limitInput.type = 'text'; limitInput.inputMode = 'numeric'; limitInput.pattern = '[0-9]*'; limitInput.value = this.getMatchLimit(); limitInput.oninput = () => { limitInput.value = limitInput.value.replace(/\D/g, ''); }; limitInput.onchange = () => this.setMatchLimit(limitInput.value); panel.appendChild(limitInput); modalContainer.appendChild(panel); return panel; } createTacticsContainer(matchType, opponent) { const existingContainer = document.getElementById('tactics-container'); if (existingContainer) { this.fadeOutAndRemove(existingContainer); } const container = document.createElement('div'); container.id = 'tactics-container'; container.className = 'tactics-container'; const header = document.createElement('div'); header.className = 'tactics-header'; const title = document.createElement('div'); title.className = 'match-info-text'; let matchTypeLabel = matchType; for (const group in OpponentTacticsPreview.CONSTANTS.MATCH_TYPE_GROUPS) { const found = OpponentTacticsPreview.CONSTANTS.MATCH_TYPE_GROUPS[group].find(t => t.id === matchType); if (found) { matchTypeLabel = found.label; break; } } title.innerHTML = `