Goatlings Stat Viewer & Quick Swap Add-On

View stats of active Goatling and swap them from anywhere.

// ==UserScript==
// @name     Goatlings Stat Viewer & Quick Swap Add-On
// @namespace goatlings.statandswap
// @description View stats of active Goatling and swap them from anywhere.
// @version  2.0.0
// @license GPL
// @grant           GM.getValue
// @grant           GM.setValue
// @grant           GM.xmlHttpRequest
// @match https://www.goatlings.com/*
// ==/UserScript==


// CHANGE ME (if you want):
let showStats = true // default true
let showList = true // default true
let showScrollbar = false // default false, change to true if you to see the scrollbar on the Goatling list
// (most) CSS can be changed at the bottom


//  GOATLING STAT VIEWER (updated for the tabs update!!!):
GM.xmlHttpRequest( {
    method:     "GET",
    url:        "https://www.goatlings.com/MyGoatlings/",
    onload:     parseResponse
} );

// adding this for greasemonkey compatibility (plus gm_addstyle is deprecated)
function addGlobalStyle(css) {
    var head, style;
    head = document.getElementsByTagName('head')[0];
    if (!head) { return; }
    style = document.createElement('style');
    style.type = 'text/css';
    style.innerHTML = css;
    head.appendChild(style);
} // credit arserbin3 on stackoverflow

async function parseResponse(response) {
    const myGoatlingsHTML = response.responseText;
    const parser = new DOMParser();
    const doc = parser.parseFromString(myGoatlingsHTML, "text/html");

    // 1. Figure out what the active Goatlings name is:
    let activeGoatling = doc.querySelector("a[href='https://www.goatlings.com/mypets/']").innerHTML;
    let activeImage = doc.getElementById("active_pet_image").getElementsByTagName('img')[0].src;
    let activeHP = null, activeMood = null, activeLevel = null, activeHunger = null, activeEXP = null

    // 2. We have pain in the ass tabs now, so we gotta grab a list of em all along with their urls:
    let tabDivs = doc.querySelectorAll('.pv-cat');
    let tabMap = {};

    tabDivs.forEach(function(div) {
        let tabLink = div.querySelectorAll('a[href^="https://www.goatlings.com/MyGoatlings/manage/"]')[1];
        if (tabLink) {
            let tabName = tabLink.textContent.trim();
            let link = tabLink.getAttribute('href');
            tabMap[tabName] = link;
        }
    });

    // So that I can re-use as much of the existing code as possible, I'm just going to combine all of the tabs into one page
    async function fetchAndParse(url) {
        let response = await fetch(url);
        let text = await response.text();
        return parser.parseFromString(text, "text/html");
    }

    async function combineTheTabs(tabMap) {
        let allGoats = document.implementation.createHTMLDocument("All Goats");
        for (let [tabName, url] of Object.entries(tabMap)) {
            let doc = await fetchAndParse(url);

            // Create a wrapper div so there can be some separation between the tabs even though they're all being combined onto one page
            let wrapperDiv = allGoats.createElement('div');
            wrapperDiv.className = 'tab-content-wrapper';
            wrapperDiv.setAttribute('data-tab-name', tabName); // Tab name is stored here in case it's needed in the future
            wrapperDiv.append(...doc.body.children);
            allGoats.body.appendChild(wrapperDiv);
        }
        return allGoats
    }

    let myGoatsDoc = await combineTheTabs(tabMap);
    let myGoats = Array.from(myGoatsDoc.querySelectorAll ("div.mystuff"));
    let myGoatNames = [], goatData = null, splitData = null
    //iterate through goatling stats and find which belongs to active
    for (let goat of myGoats){
        let currentGoat = goat.innerText;
        if (currentGoat.includes("Goatlings:")) continue; // Filtering out the tabs amongst the goats
        myGoatNames.push(currentGoat.split("\n")[2].trim());
        if (currentGoat.includes(activeGoatling)){
            goatData = currentGoat;
        }
    }
    splitData = goatData.split("\n");
    for (let data of splitData){
        if (data.includes("Level:")){
            activeLevel = data.trim();
        } else if (data.includes("HP:")){
            activeHP = data.trim();
        } else if (data.includes("Hunger:")){
            activeHunger = data.trim();
        } else if (data.includes("Mood:")){
            activeMood = data.trim();
        } else if (data.includes("EXP:")){
            activeEXP = data.trim();
        }
    }


    // Doing some basic scaling:
    let scaleVal = 1;
    if (window.innerWidth < 1400){
        scaleVal = 0.75
    } if (window.innerWidth < 1300) {
        scaleVal = 0.6
    }


    // STAT VIEWER CODE:
    let stats = document.createElement("div");
    stats.id = 'stats'
    if (activeGoatling.length > 18){
        stats.innerHTML = "<div style='text-align:center;font-size:16px'>" + activeGoatling + "</div>";
    } else{
        stats.innerHTML = "<div style='text-align:center'>" + activeGoatling + "</div>";
    }
    stats.innerHTML += "<div style='border-radius:12px;background:white;width:80%;display:flex;justify-content:center;margin:auto'><div style='position:relative;left:2.5px'><img src=" + activeImage +"></div></div>";
    // making the font size for exp smaller when it's huge
    if (activeEXP.length >= 20){
        stats.innerHTML += activeLevel + "<br /><div style=font-size:14px>" + activeEXP + "</div>" + activeHP + "<br />" + activeHunger + "<br />" + activeMood;
    } else{
        stats.innerHTML += activeLevel + "<br />" + activeEXP + "<br />" + activeHP + "<br />" + activeHunger + "<br />" + activeMood;
    }
    stats.classList.add("based","basedCard");
    // grabbing the stored values for the element coordinates (default to 10)
    stats.style.left = await GM.getValue("startX", 10);
    stats.style.top = await GM.getValue("startY", 10);
    stats.style.transform = "scale(" + scaleVal + "," + scaleVal + ")";
    // add it into the page
    if (showStats === true){
        document.body.appendChild(stats);
    }


    // QUICK SWITCHER CODE:
    // get the links to post to for switching goatlings
    let makeActiveMap = {};
    myGoats.forEach(currentGoat => {
        if (currentGoat.innerText.includes("Goatlings:")) return; // Filtering out the tabs amongst the goats (again)
        let name = currentGoat.querySelector('p:nth-of-type(2)').textContent.trim();
        let makeActiveLink = Array.from(currentGoat.querySelectorAll('a')).find(a => a.textContent.trim() === 'Make Active');
        makeActiveMap[name] = makeActiveLink.href;
    });

    // start cooking up the div to display your goats
    let goatList = document.createElement("div");
    goatList.id = 'goatList'
    goatList.innerHTML = "<div style='text-align:center'> My Goatlings </div>";
    // add each goat name as a button
    for (let goat of myGoatNames){
        goatList.innerHTML += "<div class='basedButtonGroup' id='swapButton'><button class='basedButton'>" + goat + "</button></div>"
    }
    goatList.classList.add("based","basedList");
    goatList.style.left = await GM.getValue("listX", 10);
    goatList.style.top = await GM.getValue("listY", 10);
    goatList.style.transform = "scale("+scaleVal+","+scaleVal+")";
    // add it into the page
    if (showList === true){
        document.body.appendChild(goatList);
    }
    // function to send a post request to tell the game we want to swap to a specific goatling
    function swapGoatling(event){
        let targetGoat = event.currentTarget.innerText
        GM.xmlHttpRequest({
            method: "GET",
            url: makeActiveMap[targetGoat],
            onload: function(response) {
                // refresh after requesting
                window.location = document.URL;
            }
        });
    }

    let goatButtons = document.querySelectorAll("#swapButton");
    // add in a listener, so once the buttons are clicked, the function is called
    for (let goatButton of goatButtons){
        goatButton.addEventListener ("click", swapGoatling, false);
    }

    // adjust scale when resizing past some thresholds
    window.addEventListener("resize", reSize);
    function reSize(){
        let scaleVal = 1;
        if (window.innerWidth < 1400){
            scaleVal = 0.75
        } if (window.innerWidth < 1300) {
            scaleVal = 0.6
        }
        stats.style.transform = "scale("+scaleVal+","+scaleVal+")";
        goatList.style.transform = "scale("+scaleVal+","+scaleVal+")";
    }

    // following is shamelessly lifted from w3schools
    if (showStats === true){
        dragElement(document.getElementById("stats"));
    }
    if (showList === true){
        dragElement(document.getElementById("goatList"));
    }
    function dragElement(elmnt) {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        elmnt.onmousedown = dragMouseDown;

        function dragMouseDown(e) {
            e = e || window.event;
            e.preventDefault();
            pos3 = e.clientX;
            pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        }

        function elementDrag(e) {
            e = e || window.event;
            e.preventDefault();
            // calculating new cursor position:
            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;
            elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
            elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
        }

        async function closeDragElement(e) {
            document.onmouseup = null;
            document.onmousemove = null;

            e = e || window.event;
            e.preventDefault();
            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;

            // saving element co-ordinates for some level of location persistence
            let setValY = "startY", setValX = "startX";
            // need to keep 2 separate values for both elements
            if (elmnt.id === "goatList"){
                setValY = "listY";
                setValX = "listX";
            }
            GM.setValue(setValY, (elmnt.offsetTop - pos2));
            GM.setValue(setValX, (elmnt.offsetLeft - pos1));
        }
    }
}


// EDIT HERE FOR STYLE!
addGlobalStyle (`
    .basedButtonGroup .basedButton {
        background-color: #956e43;
        border: 4px solid #ad8a60;
        color: white;
        padding: 5px 10px;
        text-align: center;
        text-decoration: none;
        font-size: 15px;
        cursor: pointer;
        min-width: 122px;
        display: block;
        margin: auto;
        border-radius:8px;
        text-shadow:-2px -2px 0 #ae8c5c, 2px -2px 0 #ae8c5c, -2px 2px 0 #ae8c5c, 2px 2px 0 #ae8c5c;
        font-weight:bold;
        font-family:Trebuchet MS;
        margin-bottom: 2px;

    }

    .basedButtonGroup .basedButton:not(:last-child) {
        border-bottom: none;
    }

    .basedButtonGroup .basedButton:hover {
        background-color: #755839;
    }

    .based {
        font-family:Trebuchet MS;
        text-shadow:-1px -1px 0 #a2bfa1, 1px -1px 0 #a2bfa1, -1px 1px 0 #a2bfa1, 1px 1px 0 #a2bfa1;
        cursor:move;
        position:absolute;
        border-radius:12px;
        font-size:21px;
        color:white;
        font-weight:bold;
        background-image:url('https://www.goatlings.com/images/layout/greenbg.gif');
        border:3px rgb(220,228,220) solid;
        }

    .basedCard {
        z-index:10000;
        padding:3px;
    }

    .basedList {
        z-index:10001;
        min-height:100px;
        max-height:284px;
        overflow-y:auto;
        overflow-x:hidden;
        min-width:135px;
    }
`);

if (showScrollbar === false){
    addGlobalStyle (`
    .basedList {
        -ms-overflow-style: none;
        scrollbar-width: none;
    }
    .basedList::-webkit-scrollbar {
        display: none;
    }
    `);
}