MZ Axis

Enhances the ManagerZone tactics interface: copy/paste formations, display rival formations with additional player data, and adjust player positions in real time (mouse + touchscreen), and now you can save your tactics.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         MZ Axis
// @namespace    http://tampermonkey.net/
// @version      0.4.1
// @description  Enhances the ManagerZone tactics interface: copy/paste formations, display rival formations with additional player data, and adjust player positions in real time (mouse + touchscreen), and now you can save your tactics.
// @author       jrcl
// @match        https://www.managerzone.com/?p=tactics*
// @grant        GM_xmlhttpRequest
// @grant        GM_log
// @grant        GM_addStyle
// @require      https://cdnjs.cloudflare.com/ajax/libs/pako/2.0.4/pako.min.js
// @license      MIT
// ==/UserScript==

GM_addStyle(`
.fieldpos::after {
    content: attr(data-x) " " attr(data-y);
    position: absolute;
    bottom: -14px;
    left: 50%;
    transform: translateX(-50%);
    font-size: 8px;
    color: #fff;
    pointer-events: none;
    white-space: nowrap;
    text-align: center;
    font-family: monospace;
}

/* error input */
.mz-error-input{
    background-color:#ffd6d6;
    transition: background-color 0.2s ease;
}
`);


(function() {
    'use strict';

    let _x = '--';
    let _y = '--';
    let inputX;
    let inputY;

    let formation = [];
    let rivalFormation = [];
    let rivalTeam = null;
    const conversionRatesToUSD = {USD: 1,R$: 2.827003,SEK: 7.4234,EUR: 0.80887,GBP: 0.555957,DKK: 6.00978,NOK: 6.921908,CHF: 1.265201,CAD: 1.3003,AUD: 1.309244,ILS: 4.378812,MXN: 10.82507,ARS: 2.807162,BOB: 7.905644,PYG: 5671.047,UYU: 28.88898,RUB: 28.21191,PLN: 3.801452,ISK: 71.15307,BGN: 1.576971,ZAR: 5.999531,THB: 43.46507,SIT: 190.539,SKK: 29.75788,JPY: 123.7233,INR: 43.66706,MZ: 7.4234,MM: 7.4234,点: 7.4234,};

    if(isSoccer()) {

        createRivalButton();
        createCoordinatesPanel();
        updateCoordinatesPanel();

        initPlayers();
        observePitchChanges();

        document.addEventListener("keydown", setKeys);
    }


    // Función para crear y configurar el botón de rival
    function createRivalButton() {
        // Obtener el primer span dentro de 'formation-container'
        const firstSpanOnFContainer = document.querySelector('#formation-container span');
        if (!firstSpanOnFContainer) return;

        // Crear y configurar el botón para la táctica del rival
        const toAddRivalBtn = document.createElement('button');
        toAddRivalBtn.id = "rivalBtn";
        toAddRivalBtn.textContent = "xmlRival";
        toAddRivalBtn.style.color = "blue";

        // Agregar el botón al contenedor
        firstSpanOnFContainer.appendChild(toAddRivalBtn);

        // Agregar eventos
        toAddRivalBtn.addEventListener('click', cfgRivalFormation);
        toAddRivalBtn.addEventListener('mouseover', showAdditionalInfoRivalPlayers);
        toAddRivalBtn.addEventListener('touchstart', e => { e.preventDefault(); showAdditionalInfoRivalPlayers(); });
    }

    function observePitchChanges() {

        const pitch = document.getElementById("pitch");
        if (!pitch) return;

        const observer = new MutationObserver((mutations) => {

            for (const m of mutations) {

                // Nuevos jugadores añadidos
                if (m.type === "childList") {

                    for (const node of m.addedNodes) {

                        if (!(node instanceof HTMLElement)) continue;

                        if (node.classList?.contains("fieldpos")) {

                            setupPlayers();
                            return;

                        }

                    }

                }

                // Cambios en style (movimiento)
                if (m.type === "attributes" && m.attributeName === "style") {

                    const el = m.target;

                    if (el.classList?.contains("fieldpos")) {

                        updatePlayerCoords(el);

                        const active = getActivePlayer();
                        if (active === el) {
                            setCoordsLabel({ currentTarget: active });
                        }

                    }

                }

                // Cambios en class (selección)
                if (m.type === "attributes" && m.attributeName === "class") {

                    const el = m.target;

                    if (el.classList?.contains("fieldpos")) {

                        const active = getActivePlayer();

                        if (!active) {
                            _x = '--';
                            _y = '--';
                        } else {
                            setCoordsLabel({ currentTarget: active });
                            attachArrowControlListeners();
                        }

                        updateCoordinatesPanel();

                    }

                }

            }

        });

        observer.observe(pitch, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ["style", "class"]
        });

    }

    function waitForElement(selector){

        return new Promise(resolve => {

            const el = document.querySelector(selector);
            if(el) return resolve(el);

            const observer = new MutationObserver(()=>{

                const el = document.querySelector(selector);

                if(el){
                    observer.disconnect();
                    resolve(el);
                }

            });

            observer.observe(document.body,{
                childList:true,
                subtree:true
            });

        });

    }

    function attachArrowControlListeners() {
        const arrowControls = document.querySelectorAll('.arrow-controls');

        arrowControls.forEach(control => {
            const buttons = control.querySelectorAll('.left-button, .right-button, .up-button, .down-button');
            buttons.forEach(button => {
                if (!button.dataset.listenerAttached) {
                    button.addEventListener('click', () => {
                        const activePlayer = getActivePlayer();
                        if (activePlayer) {
                            setCoordsLabel({ currentTarget: activePlayer });
                        }
                    });
                    button.dataset.listenerAttached = "true";
                }
            });
        });
    }

    function showAdditionalInfoRivalPlayers() {
        const addInfo = document.getElementsByClassName('additionalInfoRivalPlayer');
        for (let i = addInfo.length - 1; i >= 0; i--) {
            addInfo[i].classList.toggle('hidden');
        }
    }

    function initPlayers() {

        const container = document.getElementById('pitch');
        if (!container) return;

        if (container.dataset.mzAxisInit) return;
        container.dataset.mzAxisInit = "true";

        container.addEventListener('click', handlePlayerEvent);
        container.addEventListener('keydown', handlePlayerEvent);

        function handlePlayerEvent(e) {

            const player = e.target.closest('.fieldpos.fieldpos-ok.ui-draggable');

            if (!player) return;

            setCoordsLabel({ currentTarget: player });

            const validX = isInRange(inputX.value, 'x');
            const validY = isInRange(inputY.value, 'y');

            // manejar warnings
            validX ? removeWarning(inputX) : addWarning(inputX);
            validY ? removeWarning(inputY) : addWarning(inputY);
        }

        setupPlayers();
    }

    function getActivePlayer() {
        return document.querySelector('.fieldpos.ui-selected');
    }

    function getFieldPlayers() {
        return document.querySelectorAll('.fieldpos.fieldpos-ok.ui-draggable:not(.substitute):not(.goalkeeper)');
    }

    function setupPlayers() {

        const players = document.querySelectorAll('.fieldpos.ui-draggable');

        players.forEach(player => {

            // evitar reinicializar
            if (player.dataset.initialized) return;
            player.dataset.initialized = "true";
            updatePlayerCoords(player);



            $(player).draggable({
                drag: function(event, ui) {


                    const player = this;

                    const pos = pitchToCoords(ui.position.left, ui.position.top);

                    player.dataset.x = pos.x;
                    player.dataset.y = pos.y;

                    _x = pos.x;
                    _y = pos.y;

                    updateCoordinatesPanel();
                }
            });

            let originalStop = $(player).draggable("option", "stop");

            $(player).draggable("option", "stop", function(event, ui) {
                if (originalStop) originalStop.call(this, event, ui);
                updatePlayerCoords(player);
                _x = player.dataset.x;
                _y = player.dataset.y;
                updateCoordinatesPanel();
            });

        });
    }

    function setCoordsLabel(event) {

        const player = event.currentTarget;

        getOffset(player);

        updateCoordinatesPanel();
    }

    function getOffset( el ) {
        const pos = pitchToCoords(el.offsetLeft, el.offsetTop);
        _x = pos.x;
        _y = pos.y;
    }

    function updateCoordinatesPanel(){
        inputX.value = _x;
        inputY.value = _y;

        const disabled = (_x === '--');
        inputX.disabled = disabled;
        inputY.disabled = disabled;
    }

    function copyFormation() {

        const coords = getCurrentCoords();
        if(!coords) return;

        const txtCoord = coords.map(p => `${p.x},${p.y}`).join("\n");

        navigator.clipboard.writeText(txtCoord)
            .then(() => {
            console.log('Formation copied to the clipboard');
            alert('Formation copied to the clipboard');
        })
            .catch(err => {
            console.error('Error copying to the clipboard:', err);
        })
        //console.log(txtCoord);
    }

    function getCurrentCoords(){

        const players = [...getFieldPlayers()];

        if(players.length !== 10){
            alert("Not Valid Formation");
            return null;
        }

        const coords = players.map(p => ({
            x: Number(p.dataset.x),
            y: Number(p.dataset.y)
        }));

        // ordenar por Y (arriba→abajo) y luego por X (izquierda→derecha)
        coords.sort((a,b)=> a.y - b.y || a.x - b.x);

        return coords;

    }

    function saveTactic(){

        const coords = getCurrentCoords();
        if(!coords) return;

        const name = prompt("Tactic name:");
        if(!name) return;

        const text = name + "\n" + coords.map(p => `${p.x},${p.y}`).join("\n");

        showNote(2026,'01','01',1);

        setTimeout(()=>{

            const textarea = document.querySelector("textarea[name='cal_message']");
            if(!textarea) return;

            textarea.value = text;

            $("#calendar_add_not_btn").triggerHandler("click");

        },500);

    }

    async function loadTactics(){

        $("#calendar-new").datepicker("setDate", new Date(2026,0,1));

        setTimeout(async ()=>{

            [...document.querySelectorAll("#calendar-new td[data-handler='selectDay']")]
            .find(td => td.textContent.trim() === "1")
            .click();

            await waitForElement("textarea[id^='note']");

            const notes = [...document.querySelectorAll("textarea[id^='note']")];

            const tactics = [];

            notes.forEach(n => {

                const noteID = n.id.replace("note","");

                const lines = n.value
                    .split("\n")
                    .map(l=>l.trim())
                    .filter(Boolean);

                if(lines.length >= 11){

                    const name = lines[0];
                    const coords = lines.slice(1,11);

                    tactics.push({ name, coords, noteID });

                }

            });

            powerboxClose('calendar-administration');

            showTacticsModal(tactics);

        },300);

    }

    function showTacticsModal(tactics){

        let modal = document.getElementById("mzTacticModal");

        if(modal) modal.remove();

        modal = document.createElement("div");
        modal.id = "mzTacticModal";

        modal.style.position = "fixed";
        modal.style.top = "120px";
        modal.style.left = "50%";
        modal.style.transform = "translateX(-50%)";
        modal.style.background = "#f2f2f2";
        modal.style.border = "1px solid #666";
        modal.style.padding = "10px";
        modal.style.zIndex = "9999";
        modal.style.minWidth = "220px";

        modal.style.maxHeight = "70vh";
        modal.style.overflowY = "auto";

        let html = `<div style="font-weight:bold;margin-bottom:8px;">Tactics Library</div>`;

        tactics.forEach((t,i)=>{
            html += `
            <div style="display:flex;justify-content:space-between;
            padding:4px;border-bottom:1px solid #ddd;">

            <span class="mzTacticItem"
            data-index="${i}"
            style="cursor:pointer;">
            ${t.name}
            </span>

            <span class="mzDeleteTactic"
            data-index="${i}"
            style="cursor:pointer;color:red;">
            🗑
            </span>

            </div>`;
        });

        html += `
        <div style="text-align:center;margin-top:8px;">
            <button id="mzCloseTacticModal">Close</button>
        </div>
        `;

        modal.innerHTML = html;
        document.body.appendChild(modal);

        modal.querySelectorAll(".mzTacticItem").forEach(el=>{

            el.onclick = function(){

                const t = tactics[this.dataset.index];

                loadTactic(t);

                modal.remove();

            };

        });


        modal.querySelectorAll(".mzDeleteTactic").forEach(el=>{

            el.onclick = function(){

            const t = tactics[this.dataset.index];

            if(!confirm("Delete tactic: "+t.name+" ?")) return;

            confirm_delete_note(
                t.noteID,
                '2026-01-01 00:00:00',
                2026,
                '01',
                1
            );

            modal.remove();

            setTimeout(()=>{
                powerboxClose('calendar-administration');
                loadTactics(); // recargar lista
            },400);

            };

        });

        document.getElementById("mzCloseTacticModal")
        .onclick = ()=> modal.remove();
    }

    function loadTactic(tactic){

        formation = [];

        tactic.coords.forEach(c => {

            let [x,y] = c.split(",");

            const pos = coordsToPitch(Number(x), Number(y));
            formation.push([ pos.left, pos.top ]);
        });

        // Optional because it’s already saved in sorted order
        formation = sortFormation(formation);
        setFormation();
    }

    function sortFormation(arr){
        return arr.sort((a,b) => b[1] - a[1] || a[0] - b[0]);
    }

    function pasteFormation() {
        navigator.clipboard.readText()
          .then(text => {
            console.log('Clipboard text:', text)
            // is xml content?
            if (text.search("xml") != -1) {
                // parse clipboard to xml file
                console.log('Cartesian coordinate system from XML file');
                let parser = new DOMParser();
                let xmlDoc = parser.parseFromString(text,"text/xml");
                
                formation = [];
                let pos = xmlDoc.getElementsByTagName("Pos")
                for (let i = 1; i < pos.length; i++) {
                    formation.push([Number(pos[i].getAttribute("x")) - 7, Number(pos[i].getAttribute("y")) - 9]);
                }

                formation = sortFormation(formation); 
                // formation.sort(function(a,b) { if (b[1] == a[1]) return a[0] - b[0]; else return b[1] - a[1]; })
                setFormation();
            } else if (text.trim().split('\n').length == 10) { // is there 10 coord?
                console.log('Cartesian coordinate system from Clipboard');
                let coorXY = text.trim().split('\n')

                formation = [];
                for (let pair of coorXY) {
                    let [xAxis,yAxis] = pair.split(',');
                    const pos = coordsToPitch( Number(xAxis), Number(yAxis) );
                    formation.push([ pos.left, pos.top]);
                    // formation.push([Number(xAxis) + 97, 155 - Number(yAxis)]);
                }

                formation = sortFormation(formation); 
                //formation.sort(function(a,b) { if (b[1] == a[1]) return a[0] - b[0]; else return b[1] - a[1]; })
                setFormation();
            } else if (formation.length == 10) {
                console.log('Cartesian coordinate system from internal var');
                setFormation();
            } else {
                console.log("Nothing to do");
            }


          })
          .catch(err => {
            console.error('Error reading from the clipboard:', err)
          })
    }

    function setFormation() {
        let player = getFieldPlayers();

        if (formation.length == player.length) {
            // get an order
            let oldFormation = [];
            for (let i = 0; i < player.length; i++) {
                oldFormation.push([Number((player[i].style.left).slice(0,-2)), Number((player[i].style.top).slice(0,-2)), i]);
            }

            oldFormation = sortFormation(oldFormation); 
            //oldFormation.sort(function(a,b) { if (b[1] == a[1]) return a[0] - b[0]; else return b[1] - a[1]; })

            for (let i = 0; i < formation.length; i++) {
                player[oldFormation[i][2]].style.left = formation[i][0] + 'px';
                player[oldFormation[i][2]].style.top = formation[i][1] + 'px';
            }
            alert('Formation successfully copied');
            formation = []; // empty formation
        }
    }

    function cfgRivalFormation() {
        let elBoton = document.getElementById('rivalBtn');

        if(elBoton.innerHTML == "xmlRival") {
            pasteRivalFormation();
        } else {
            elBoton.style.color = "blue";
            elBoton.innerHTML = "xmlRival";

            let rivalPlayers = document.getElementById('rivalPlayers');
            if(rivalPlayers) {
                rivalPlayers.parentElement.removeChild(rivalPlayers);
            }

            navigator.clipboard.writeText('')
            .then(() => {
                console.log('Clipboard Clear');
            })
            .catch(err => {
                console.error('Error clearing the clipboard:', err);
            })
        }
    }

    function pasteRivalFormation(){
        navigator.clipboard.readText()
          .then(text => {
            // is xml content?
            if (text.search("xml") != -1) {
                console.log('Clipboard text:', text)
                // parse clipboard to xml file
                console.log('Cartesian coordinate system from XML file');
                let parser = new DOMParser();
                let xmlDoc = parser.parseFromString(text,"text/xml");
                
                rivalFormation = [];
                let pos = xmlDoc.getElementsByTagName("Pos")

                // x, y, shirtno, value, height, weight, age, countryShortname
                for (let i = 0; i < pos.length; i++) {
                    rivalFormation.push([Number(pos[i].getAttribute("x")) - 7, Number(pos[i].getAttribute("y")) - 9, '9', '', '', '', '', '' ]);
                }
                
                rivalFormation = sortFormation(rivalFormation); 
                // rivalFormation.sort(function(a,b) { if (b[1] == a[1]) return a[0] - b[0]; else return b[1] - a[1]; })

                rivalTeam = xmlDoc.querySelector("Team");
                rivalTeam.setAttribute("tactic", rivalTeam.getAttribute('tactics'));
                setRivalFormation();

            } else if (text != null && text != "") {
                // is valid id number
                
                let matchId = prompt("Please enter your Match ID [mid]:", (text.trim().length < 14 && !isNaN(text.trim()))? text.trim():"No IDea");

                if (matchId == null) {
                  console.log("User cancel this");  
                } else if(matchId == "" || matchId == "No IDea" || matchId.length < 5 || matchId.length > 14 || isNaN(matchId.trim()) ) {
                  console.log("User dont have any idea about this or not valid match id");
                } else {
                    matchId = matchId.trim();
                    console.log(`Your matchId is:${matchId}`);
                    getDataFromXML(matchId);
                }
            } else if (rivalFormation.length > 0) {
                console.log('Formation from system on internal var');
                setRivalFormation();
            } else {
                console.log("Nothing to do");
            }

          })
          .catch(err => {
            console.error('Error reading from the clipboard:', err)
          })
    }

    function getDataFromXML(matchId) {
        let statsUrl = `http://download06.managerzone.com/data/soccer/${matchId.slice(-1)}/${matchId.charAt(matchId.length-2)}/stats${matchId}.xml.gz`;
        GM_xmlhttpRequest({
            method: 'GET',
            // url: 'http://download06.managerzone.com/data/soccer/7/1/stats1486585817.xml.gz',
            url: statsUrl,
            responseType: 'arraybuffer',  // We want to handle the binary data (arraybuffer)
            onload: function(response) {
                if (response.status === 200) {
                    try {
                        // Decompress the .gz file using pako
                        const decompressed = pako.ungzip(new Uint8Array(response.response), { to: 'string' });

                        // Parse the XML data
                        const parser = new DOMParser();
                        const xmlDoc = parser.parseFromString(decompressed, 'text/xml');

                        let teams = xmlDoc.documentElement.getElementsByTagName("Team");

                        let ishome = 0;
                        if (confirm(`Press [ OK ] if your rival is:\t${teams[0].getAttribute('name')}\nPress [ Cancel ] if your rival is:\t${teams[1].getAttribute('name')}`)) {
                          ishome = 1;
                        } else {
                          ishome = 0;
                        }

                        rivalTeam = ishome ? teams[0] : teams[1];
                        console.log(rivalTeam.getAttribute('name'));

                        rivalFormation = [];
                        // x, y, shirtno, value, height, weight, age, countryShortname
                        let players = xmlDoc.querySelectorAll(`Player[teamId="${rivalTeam.getAttribute('id')}"][origin*=","]`);
                        for (let i = 0; i < players.length; i++) {
                            let origin = players[i].getAttribute('origin');
                            let xy = origin.split(",");
                            rivalFormation.push([StatsToPos_X(xy[0], ishome) - 7, StatsToPos_Y(xy[1], ishome) - 9, players[i].getAttribute('shirtno'), '', '', '', '', '' ]);
                        }

                        // Additional Data
                        let squadTeamUrl = `http://www.managerzone.com/xml/team_playerlist.php?sport_id=1&team_id=${rivalTeam.getAttribute('id')}`
                        GM_xmlhttpRequest({
                            method: 'GET',
                            url: squadTeamUrl,
                            onload: function(response) {
                                const parser = new DOMParser();
                                const xmlDoc = parser.parseFromString(response.responseText, "text/xml");
                                let teamPlayers = xmlDoc.querySelector('TeamPlayers');
                                for (let i = 0; i < players.length; i++) {
                                    let player = xmlDoc.querySelector(`Player[id="${players[i].getAttribute('id')}"]`);
                                    if (player) {
                                        let value = Number(player.getAttribute('value')) / (conversionRatesToUSD[teamPlayers.getAttribute('teamCurrency')] || 1)
                                        rivalFormation[i][3] = '' + value.toFixed(0);
                                        //player.getAttribute('value').replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
                                        rivalFormation[i][4] = player.getAttribute('height');
                                        rivalFormation[i][5] = player.getAttribute('weight');
                                        rivalFormation[i][6] = player.getAttribute('age');
                                        rivalFormation[i][7] = player.getAttribute('countryShortname');
                                    }
                                }
                                setRivalFormation();
                            },
                            onerror: function(error) {
                                console.log('Error:', error);
                            }
                        });

                        
                    } catch (error) {
                        console.error('Error decompressing or parsing XML:', error);
                    }
                } else {
                    console.error('Failed to fetch the file:', response.statusText);
                    alert('The match has probably not been viewed in 2D yet, therefore we cannot obtain the statistics, or try again later.');
                }
            },
            onerror: function(error) {
                console.error('Error with GM_xmlhttpRequest:', error);
            }
        });
    }

    function StatsToPos_X (i, isHome) {
        let ret = isHome ? Math.round(-.255800462 * i + 199.8228530689) : Math.round(.2555000556 * i + 8.3741302936);
        return ret;
    }

    function StatsToPos_Y (i, isHome) {
        let ret = isHome ? Math.round(-.3073207154 * i + 315.9278777381) : Math.round(.3070644902 * i + 9.2794889414);
        return ret;
    }


    function setRivalFormation () {
        let rivalPlayers = document.createElement('div');
        rivalPlayers.id = "rivalPlayers";
        let formationText = document.getElementById('formation_text');
        formationText.parentNode.insertBefore(rivalPlayers, formationText.nextSibling);
        console.log('players on the pitch');

        let contentRival = '';
        if (rivalTeam.hasAttribute("name")) {
            contentRival += `<div style="position:absolute; left: 1px; top: 3px; width: 215px;">`+
                            `<div style="display: flex; justify-content: space-between; font-size:0.7em; color:black"><div style="display: flex; flex-direction: column; justify-content: space-around; max-width: 90px; text-align: center;"><span>${rivalTeam.getAttribute('name')}</span><span><img src="https://www.managerzone.com/nocache-910/img/flags/64/${rivalTeam.getAttribute('country').toLowerCase()}.png" width="14" alt=""></span></div>` +
                            `<span><img src="https://www.managerzone.com/dynimg/badge.php?team_id=${rivalTeam.getAttribute('id')}" width="25" alt=""></span></div></div>`;
        }

        contentRival += `<div style="position:absolute; left: 1px; top: 44px; width: 215px">` +
                        `<div style="display: flex; justify-content: space-around; font-size:0.7em;"><span>${rivalTeam.getAttribute('tactic')}</span><span>${rivalTeam.getAttribute('playstyle')}</span><span>${rivalTeam.getAttribute('aggression')}</span></div></div>`;

        for (let i = 0; i < rivalFormation.length; i++) {
            contentRival += "<div style=\"position:absolute; left: " + (194 - rivalFormation[i][0]) + "px; top: " + (310 - rivalFormation[i][1]) + "px;\">" +
                    "<div style=\"border-radius: 50%; width: 18px; height: 18px; border: 2px solid #FFE42B; color: black; display: flex; align-items: center; justify-content: center; font-size:0.9em;  font-weight: bold;\">"+rivalFormation[i][2]+"</div>" +
                    "<div style=\"position:absolute; top: -6px; width:22px; display: flex; justify-content: space-between; font-size:0.45em; color:black\"><span>" + (rivalFormation[i][0] - 97) + "</span><span>" + (155 - rivalFormation[i][1]) + "</span></div>" +
                    `<div class="additionalInfoRivalPlayer hidden"><div style="position: absolute; white-space: nowrap; font-size:0.45em; writing-mode: vertical-rl; text-align: right; color:black; left: 22px; top: -6px; height:37px"><span>${rivalFormation[i][3]?rivalFormation[i][3].replace(/\B(?=(\d{3})+(?!\d))/g, ' ')+" USD":''}</span></div>` +
                    `<div style="display: flex; justify-content: space-between; font-size:0.3em; color:black"><span>${rivalFormation[i][4]?rivalFormation[i][4]+" cm":''}</span><span>${rivalFormation[i][6]?rivalFormation[i][6]+" y":''}</span></div>` +
                    `<div style="display: flex; justify-content: space-between; font-size:0.3em; color:black"><span>${rivalFormation[i][5]?rivalFormation[i][5]+" kg":''}</span><span>${rivalFormation[i][7]?`<img src="https://www.managerzone.com/nocache-910/img/flags/s_${rivalFormation[i][7]}.gif" width="7" alt="">`:''}</span></div></div></div>`;
        }

        rivalPlayers.innerHTML = contentRival;

        let elBoton = document.getElementById('rivalBtn');
        elBoton.style.color = '#FF4500';
        elBoton.innerHTML = "clearRival";
    }

    function createCoordinatesPanel(){

        if (document.getElementById("divCoords")) return;

        const coordsContainer = document.getElementById('formation-container');
        if (!coordsContainer) return;

        const div = document.createElement("div");
        div.id = "divCoords";

        div.innerHTML = `
            <span style="font-weight:600">
                Coords:
                <span style="color:green">X</span>
                <input id="inputX" type="text" style="width:24px">
                <span style="color:blue">Y</span>
                <input id="inputY" type="text" style="width:24px">
            </span>
            &nbsp;<button id="copyBtn">Copy</button>
            &nbsp;<button id="pasteBtn">Paste</button>
            &nbsp;&nbsp;<button id="saveBtn">Save</button>
            &nbsp;<button id="loadBtn">Load</button>
        `;

        coordsContainer.appendChild(div);

        const copyBtn = div.querySelector("#copyBtn");
        const pasteBtn = div.querySelector("#pasteBtn");
        const saveBtn = div.querySelector("#saveBtn");
        saveBtn.style.color = "white";
        saveBtn.style.background = "#4CAF50";
        const loadBtn = div.querySelector("#loadBtn");
        loadBtn.style.color = "white";
        loadBtn.style.background = "#007bff";

        inputX = div.querySelector("#inputX");
        inputY = div.querySelector("#inputY");

        inputX.inputMode = "numeric";
        inputY.inputMode = "numeric";
        inputX.addEventListener("input", setPlayerPosition);
        inputY.addEventListener("input", setPlayerPosition);
        const selectAll = e => e.target.select();
        inputX.addEventListener("focus", selectAll);
        inputY.addEventListener("focus", selectAll);

        copyBtn.addEventListener("click", copyFormation);
        pasteBtn.addEventListener("click", pasteFormation);
        saveBtn.addEventListener("click", saveTactic);
        loadBtn.addEventListener("click", loadTactics);

        [copyBtn, pasteBtn, saveBtn, loadBtn].forEach(btn => {
            btn.style.display = "inline-block";
            btn.style.margin = "0 0px";
            btn.style.padding = "2px 3px"; // más pequeño en móviles
            btn.style.boxSizing = "border-box";
        });

    }

    function setPlayerPosition() {

        const x = Number(inputX.value);
        const y = Number(inputY.value);

        const player = getActivePlayer();
        if(!player) return;

        const validX = isInRange(x, 'x');
        const validY = isInRange(y, 'y');

        validX ? removeWarning(inputX) : addWarning(inputX);
        validY ? removeWarning(inputY) : addWarning(inputY);

        if(validX && validY){
            const pos = coordsToPitch(x, y);
            player.style.left = pos.left + "px";
            player.style.top = pos.top + "px";
        }
    }

    function isInRange(number, coordinate) {

        const n = Number(number);

        if (Number.isNaN(n)) return false;

        if (coordinate === 'x') {
            return n >= -96 && n <= 96;
        }

        if (coordinate === 'y') {
            return n >= -157 && n <= 101;
        }

        return false;
    }

    function setKeys(key) {
        if((key.keyCode === 37 || key.keyCode === 38 || key.keyCode === 39 || key.keyCode === 40)
           && (key.currentTarget.activeElement.localName != 'input')) {

            const players = document.querySelectorAll('.fieldpos.fieldpos-ok.ui-draggable.ui-selected, .fieldpos.ui-selected.fieldpos-collision');

            if(players.length) {
                const pos = pitchToCoords(players[0].offsetLeft, players[0].offsetTop);
                _x = pos.x;
                _y = pos.y;
            } else {
                _y = '--';
                _x = '--';
            }
            updateCoordinatesPanel();
        }
    }

    function updatePlayerCoords(player){

        const pos = pitchToCoords(player.offsetLeft, player.offsetTop);

        player.dataset.x = pos.x;
        player.dataset.y = pos.y;

    }

    function coordsToPitch(x, y){
        return {
            left: x + 97,
            top: 155 - y
        };
    }

    function pitchToCoords(left, top){
        return {
            x: Math.round(left) - 97,
            y: 155 - Math.round(top)
        };
    }

    function addWarning(input) {
        input.classList.add("mz-error-input");
    }

    function removeWarning(input) {
        input.classList.remove("mz-error-input");
    }

    function isSoccer() {
        const sport = document.getElementById('tactics_box');
        return sport?.classList.contains('soccer') ?? false;
    }

})();