Greasy Fork is available in English.

Grundo's Cafe - Battledome Journal

Keeps track of battledome history (prizes, moves per battle...) and some UI improvements

// ==UserScript==
// @name         Grundo's Cafe - Battledome Journal
// @namespace    https://www.grundos.cafe/
// @version      0.14
// @description  Keeps track of battledome history (prizes, moves per battle...) and some UI improvements
// @author       yon
// @match        *://*.grundos.cafe/dome*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=grundos.cafe
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.deleteValue
// @require      https://unpkg.com/jquery/dist/jquery.min.js
// @require      https://unpkg.com/gridjs/dist/gridjs.umd.js
// ==/UserScript==

// Community data: https://lookerstudio.google.com/s/jTcYlUzx0zQ

// Please contact Yon#epyslone for constructive feedback O:-)
var version = 0.14;

// Please update according to your timezone (should be midnight NST for your timezone)
var seasonStarts = {
                    3: "2024-02-21 09:00:00",
                    2: "2023-10-02 09:00:00",
                    1: ""
};

(async function() {
    'use strict';

    try {
        if (document.URL.includes('grundos.cafe/dome/#journal')) {
            await showJournal();
            return;
        }
        showJournalLink();

        if (document.URL.includes('/dome/status')) {
            await GM.setValue('gc_bd_move_number', 0);
        }
        else if (document.URL.includes('/dome/1p/select')) {
            onSelectPage();
        }
        else if (document.URL.includes('/dome/1p/battle')) {
            await onBattlePage();
        }
        else if (document.URL.includes('/dome/1p/endbattle')) {
            await onBattleEndPage();
        }
    } catch (error) {
        let errorMessage = `
  <div class="error-message">
    <p>Oops! Something went wrong with the Battledome Journal script.</p>
    <p>Please check you have the latest option from <a href="https://greasyfork.org/en/scripts/477948-grundo-s-cafe-battledome-journal">here</a></p>
    <p>If it still does not work, please keep the tab open (or save the HTML) and contact Yon#epyslone, the script probably needs an update.</p>
  </div>
`;
        $('div[id="page_content"]').prepend(errorMessage);

        throw error;
    }
})();

function getDateString() {
  const now = new Date();
  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, '0'); // Months are zero-based
  const day = String(now.getDate()).padStart(2, '0');
  const hour = String(now.getHours()).padStart(2, '0');
  const minute = String(now.getMinutes()).padStart(2, '0');
  const second = String(now.getSeconds()).padStart(2, '0');

  return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}

function showJournalLink() {
    let pageContents = $('div[id="page_content"]');
    if (pageContents.length > 0) {
        const htmlString = `
        <div id="battledome_journal_header" style="display: flex; justify-content: space-between; margin-bottom: 12px;">
            <div id="battledome_journal_withdraw"></div>
            <div id="battledome_journal_heal_hp"></div>
            <div id="battledome_journal_redirection"">
                <a href="https://www.grundos.cafe/dome/#journal" target="_blank">Go to Journal</a>
            </div>
        </div>`;
        pageContents[0].insertAdjacentHTML("afterbegin", htmlString);
    }
}

async function showJournal() {
    let journalElement = getJournalElement();

    replacePageWithElement(journalElement);

    await addExportButtonListener(journalElement);
    await addResetButtonListener(journalElement);
    await addSettingsButtonListener(journalElement);

    await loadGrid();
}

function getJournalElement() {
    let journalElement = document.createElement('div');
    let html = `
              <center>
                <div id="battledome_journal">
                  <h1>Battledome Journal</h1>
                  <button id="battledome_export">Export</button>
                  <button id="battledome_reset">Reset</button>
                  <div id="battledome_history"></div>
                  <div id="battledome_settings"></div>
                </div>
              </center>`;
    journalElement.innerHTML = html;

    let cssLink = document.createElement("link");
    cssLink.rel = "stylesheet";
    cssLink.href = "https://unpkg.com/gridjs/dist/theme/mermaid.min.css";
    journalElement.appendChild(cssLink);

    var styleElement = document.createElement('style');
    styleElement.textContent = `
        #battledome_journal {
            h1 { margin-bottom: 24px; }
            #battledome_export { font-size: 16px; padding: 8px 16px; }
            #battledome_reset { font-size: 16px; padding: 8px 16px; }
            #battledome_history { margin: 24px; }
            #battledome_settings { margin: 24px; }
            .gridjs-search { float: initial; width: "100%" }
            .gridjs-search-input { width: 100% }
        }`;
    journalElement.appendChild(styleElement);

    return journalElement;
}

async function loadGrid() {
    let rowsPerPage = 15;

    let rows = await getJournalHistoryRows();
    // start loading grid data
    let grid = new gridjs.Grid({
        columns: getJournalHistoryColumns(),
        data: rows,
        pagination: {
            limit: rowsPerPage,
            summary: true
        },
        resizable: true,
        search: {
            debounceTimeout: 0
        },
        sort: {
            multiColumn: true
        },
        autoWidth: true
    }).render(document.getElementById("battledome_history"));

    // show loading message
    let loadingElement = showLoadingMessage();

    // wait for grid to finish loading
    let expectedRows = Math.min(rows.length, rowsPerPage);
    await waitForGridCompleteLoad(expectedRows);

    // hide loading message
    hideLoadingMessage(loadingElement);

    fixGridJsTable();
}

function getJournalHistoryColumns() {
    return ["Season", "Date", "Opponent", "Result", "Difficulty (Win Count)", "Total Moves", "Pet HP", "Opponent HP", "Reward"];
}

async function getJournalHistoryRows() {
    let battlesData = await GM.getValue('gc_bd_battles_data', {'version': version});

    let tableData = [];

    let opponentNames = Object.keys(battlesData).filter(opponentName => battlesData.hasOwnProperty(opponentName) && opponentName !== 'version').sort();
    for (const opponentName of opponentNames) {
        let opponentData = battlesData[opponentName];
        for (let battleId in opponentData) {
            let battleData = opponentData[battleId];
            let date = battleData['date'];
            let season = getSeason(battleData['date']);
            let result = battleData['result'];
            let isDaily = battleData['is_daily'];
            let winCount = battleData['win_count'];
            let difficulty = isDaily ? 'Daily' : winCount ?? '?';
            let move_number = battleData['move_number'] ?? '?';
            let petHp = `${battleData['pet_info']['current_hp']} / ${battleData['pet_info']['max_hp']}`;
            let opponentHp = `${battleData['opponent_info']['current_hp']} / ${battleData['opponent_info']['max_hp']}`;
            let reward = battleData['prize_name'] ?? '';
            tableData.push([season, date, opponentName, result, difficulty, move_number, petHp, opponentHp, reward]);
        }
    }
    return tableData;
}

function getSeason(date) {
    let seasons = Object.keys(seasonStarts).sort().reverse();
    for (const season of seasons) {
        if (date >= seasonStarts[season]) {
            return season;
        }
    }
}

function showLoadingMessage() {
    let loadingElement = document.createElement('div');
    loadingElement.innerHTML = `
        <h2>Loading...</h2>
        <div class="loader"></div>
    `;
    var styleElement = document.createElement('style');
    styleElement.textContent = `
        h2 { color: #3498db; /* Blue */ }
        .loader {
                border: 16px solid #f3f3f3; /* Light grey */
                border-top: 16px solid #3498db; /* Blue */
                border-radius: 50%;
                width: 120px;
                height: 120px;
                animation: spin 2s linear infinite;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
    `;
    loadingElement.appendChild(styleElement);
    $('div[class="gridjs-head"]')[0].parentNode.appendChild(loadingElement);
    return loadingElement;
}

function hideLoadingMessage(loadingElement) {
    loadingElement.style.display = "none";
}

async function waitForGridCompleteLoad(expectedRows) {
    let done = false;
    while (!done) {
        // wait 100 ms
        await new Promise(r => setTimeout(r, 100));
        // check
        let currentRows = $('table[role="grid"] > tbody > tr');
        if (currentRows && currentRows.length === expectedRows) {
            done = true;
        }
    }
}

function fixGridJsTable() {
    // issue: the table does not load fully before interacting with it
    // small hack: interacting by sorting by date descending
    // todo: I need to either change library or find a better fix

    // sort asc
    $('table[role="grid"] > thead > tr > th[data-column-id="date"] > button')[0].click();
    // sort desc
    $('table[role="grid"] > thead > tr > th[data-column-id="date"] > button')[0].click();
}

function replacePageWithElement(element) {
    let page = document.querySelector('html');
    page.parentNode.replaceChild(element, page);
}

async function addExportButtonListener(journalElement) {
    let battlesData = await GM.getValue('gc_bd_battles_data', {'version': version});

    let exportElement = $('button[id="battledome_export"]');
    exportElement.click(exportFunction);
    async function exportFunction() {
        const filename = `${getDateString()}_battledome_journal.json`;
        const jsonString = JSON.stringify(battlesData, null, 2); // The third argument is for indentation
        const blob = new Blob([jsonString], { type: 'application/json' });
        const downloadLink = document.createElement('a');
        downloadLink.href = window.URL.createObjectURL(blob);
        downloadLink.download = filename;
        journalElement.appendChild(downloadLink);
        downloadLink.click();
        journalElement.removeChild(downloadLink);
    };
}

async function addResetButtonListener(journalElement) {
    let resetElement = $('button[id="battledome_reset"]');
    resetElement.click(resetFunction);
    async function resetFunction() {
        if (confirm("Do you really want to reset your Battledome Journal? This action cannot be undone (but you can export it first).") == true) {
            await GM.deleteValue('gc_bd_battles_data');
        }
    };
}

async function addSettingsButtonListener(journalElement) {
    let settingsElement = $('div[id="battledome_settings"]')[0];
    let html = `
        <details>
            <summary>Settings</summary>
            <div id="battledome_settings_parameters"></div>
        </details>`;
    settingsElement.innerHTML = html;

    var styleElement = document.createElement('style');
    styleElement.textContent = `
        summary { font-weight: bold; font-size: 28px; }
        label { font-size: 24px; }
        select { font-size: 20px; margin-left: 8px; }
        textarea { width: 80%; height: 160px; }
        #battledome_settings_save { font-size: 16px; padding: 8px 16px; }`;
    settingsElement.appendChild(styleElement);

    let settingsDetailsElement = $('div[id="battledome_settings_parameters"]')[0];

    let parameters = {
        "share_rewards": {
            "label": "ε(´。•᎑•`)っ 💕 Share your future prize rewards with the community 💕",
            "possibleValues": {
                "public": "Yes",
                "private": "Yes but keep my name hidden (only visible to admin)",
                "no": "No"
            },
            "defaultValue": "no"
        },
        "selection_sort_opponents": {
            "label": "(Opponent selection) Sort opponents",
            "possibleValues": {
                "difficulty": "By difficulty",
                "name": "By name"
            },
            "defaultValue": "no"
        },
        "display_withdraw": {
            "label": "(During battle) Display status withdraw page",
            "possibleValues": {
                "yes": "Yes",
                "no": "No"
            },
            "defaultValue": "yes"
        },
        "display_heal_hp": {
            "label": "(During battle) Display at how much HP the opponent will heal",
            "possibleValues": {
                "yes": "Yes",
                "no": "No"
            },
            "defaultValue": "yes"
        },
        "battle_sort_weapons_name": {
            "label": "(During battle) Order weapons by name",
            "possibleValues": {
                "yes": "Yes",
                "no": "No"
            },
            "defaultValue": "no"
        },
        "battle_limit_duplicate_weapons": {
            "label": "(During battle) Display a maximum of two similar weapons (useful for long RoDN fights)",
            "possibleValues": {
                "yes": "Yes",
                "no": "No"
            },
            "defaultValue": "no"
        },
        "display_previous_seasons_rewards": {
            "label": "(Battle result) Display previous seasons rewards",
            "possibleValues": {
                "yes": "Yes",
                "no": "No"
            },
            "defaultValue": "yes"
        },
        "rewards_sort_order": {
            "label": "(Battle result) Display opponent rewards in the following order",
            "possibleValues": {
                "desc": "Latest first",
                "asc": "Oldest first"
            },
            "defaultValue": "desc"
        },
        "aggregate_rewards": {
            "label": "(Battle result) Aggregate opponent rewards per item",
            "possibleValues": {
                "yes": "Yes",
                "no": "No"
            },
            "defaultValue": "no"
        },
        "battle_sort_weapons_list": {
            "label": "(During battle) Order weapons by the following list (case insensitive, use commas to separate weapons, specified will always be before the non-specified, non-specified will be ordered by the previous sort option)",
            "type": "textarea",
            "defaultValue": ""
        }
    };

    let currentSettings = await GM.getValue('gc_bd_settings', getDefaultSettings());
    console.log(currentSettings);

    for (const [parameter, values] of Object.entries(parameters)) {
        const label = document.createElement("label");
        label.setAttribute("for", parameter);
        label.textContent = values["label"] + ":";

        let parameterCurrentSetting = currentSettings[parameter] ?? values["defaultValue"];

        if (values["type"] == "textarea") {
            const textarea = document.createElement("textarea");
            textarea.id = parameter;
            textarea.textContent = parameterCurrentSetting;

            const paragraph = document.createElement("p");
            paragraph.appendChild(label);
            paragraph.appendChild(document.createElement("br"));
            paragraph.appendChild(textarea);

            settingsDetailsElement.appendChild(paragraph);
        }
        // dropdown
        else {
            const select = document.createElement("select");
            select.id = parameter;

            for (const [value, label] of Object.entries(values["possibleValues"])) {
                const option = document.createElement("option");
                option.value = value;
                option.textContent = label;
                if (value == parameterCurrentSetting) {
                    option.selected = true;
                }
                select.appendChild(option);
            }

            const paragraph = document.createElement("p");
            paragraph.appendChild(label);
            paragraph.appendChild(select);

            settingsDetailsElement.appendChild(paragraph);
        }
    }

    var saveSettingsElement = document.createElement('button');
    saveSettingsElement.id = 'battledome_settings_save';
    saveSettingsElement.textContent = 'Save settings';
    saveSettingsElement.addEventListener('click', saveSettingsFunction);
    async function saveSettingsFunction() {
        let newSettings = {};
        for (const [parameter, values] of Object.entries(parameters)) {
            let selectElement = document.getElementById(parameter);
            newSettings[parameter] = selectElement.value;
        }
        await GM.setValue('gc_bd_settings', newSettings);
        console.log('New settings:', newSettings);
        alert('Settings have been saved.')
    };
    settingsDetailsElement.appendChild(saveSettingsElement);
}

function getDefaultSettings() {
    let defaultSettings = {
        "share_rewards": "no",
        "selection_sort_opponents": "no",
        "display_withdraw": "yes",
        "display_heal_hp": "yes",
        "battle_sort_weapons_name": "no",
        "battle_limit_duplicate_weapons": "no",
        "display_previous_seasons_rewards": "yes",
        "rewards_sort_order": "desc",
        "aggregate_rewards": "no",
        "battle_sort_weapons_list": ""
    };
    return defaultSettings;
}

function onSelectPage() {
    displayOpponentOrderByNameIfNeeded();
}

async function displayOpponentOrderByNameIfNeeded() {
    let settings = await GM.getValue('gc_bd_settings', getDefaultSettings());
    let setting = settings["selection_sort_opponents"];
    if (setting === "name") {
        let tbodys = $('form[action="/dome/1p/select/"] > table[id="challengerlist"] > tbody');
        if (tbodys.length > 0) {
            let tbody = tbodys[0];
            let trs = {};
            tbody.querySelectorAll('tr:not(:first-child)').forEach(function (v) {
                let opponentName = v.querySelector('button').textContent.trim();
                trs[opponentName] = v;
                v.parentNode.removeChild(v);
            });

            let trsSorted = Object.entries(trs).sort((a, b) => {
                return a[0].localeCompare(b[0]);
            });
            for (const [opponentName, tr] of trsSorted) {
                tbody.appendChild(tr);
            }
        }
    }
}

async function onBattlePage() {
    // retrieve our pet's info
    let petInfo = {'name': null, 'current_hp': null, 'max_hp': null};
    let petElement = $('div[id="hpbars"] > table > tbody > tr:nth-child(3) > td:nth-child(1)')[0];
    petInfo['name'] = petElement.innerHTML.split('<br>')[0].trim();
    let petHps = $(petElement).find('strong').text();
    petInfo['current_hp'] = parseInt(petHps.split('/')[0].trim());
    petInfo['max_hp'] = parseInt(petHps.split('/')[1].trim());

    // retrieve our opponent's info
    let opponentInfo = {'name': null, 'current_hp': null, 'max_hp': null};
    let opponentElement = $('div[id="hpbars"] > table > tbody > tr:nth-child(3) > td:nth-child(3)')[0];
    opponentInfo['name'] = opponentElement.innerHTML.split('<br>')[0].trim();
    let opponentHps = $(opponentElement).find('strong').text();
    opponentInfo['current_hp'] = parseInt(opponentHps.split('/')[0].trim());
    opponentInfo['max_hp'] = parseInt(opponentHps.split('/')[1].trim());

    await GM.setValue('gc_bd_pet_info', petInfo);
    await GM.setValue('gc_bd_opponent_info', opponentInfo);

    // works with the Battledome Utility script too
    const buttons = document.querySelectorAll('input[type="submit"][value="Go!"]');
    buttons.forEach(button => {
        button.addEventListener('click', async function (event) {
            // event.preventDefault();

            let moveNumber = await GM.getValue('gc_bd_move_number', 0);
            moveNumber += 1;
            await GM.setValue('gc_bd_move_number', moveNumber);
            console.log("Move number:", moveNumber);
        });
    });

    await displayWithdrawPageIfNeeded();
    await displayHealHpIfNeeded(opponentInfo['max_hp']);
    await displayLimitDuplicateWeaponsIfNeeded();
    await displayWeaponUpdatedOrderIfNeeded();
}

async function displayWithdrawPageIfNeeded() {
    let settings = await GM.getValue('gc_bd_settings', getDefaultSettings());
    let setting = settings["display_withdraw"];
    if (setting === "yes") {
        $('div[id="battledome_journal_withdraw"]').append('<a href="https://www.grundos.cafe/dome/status/">Withdraw</a>');
    }
}

async function displayHealHpIfNeeded(opponentMaxHp) {
    let settings = await GM.getValue('gc_bd_settings', getDefaultSettings());
    let setting = settings["display_heal_hp"];
    if (setting === "yes") {
        let message = `Opponent will heal when reaching ${Math.floor(opponentMaxHp * 0.4)} HP`;
        $('div[id="battledome_journal_heal_hp"]').append(message);
    }
}

async function displayLimitDuplicateWeaponsIfNeeded() {
    // skip this setting if Battledome Utility (keyboard) script is active
    if ($('form[action="/dome/1p/battle/"] div[id="bd-table"]').length > 0) {
      return;
    }

    let settings = await GM.getValue('gc_bd_settings', getDefaultSettings());
    let setting = settings["battle_limit_duplicate_weapons"];
    if (setting === "yes") {
        let tbodys = $('form[id="bd-form"] > table > tbody');
        if (tbodys.length > 0) {
            let tbody = tbodys[0];
            let weaponNames = {};
            let trs = [];
            let tds = [];
            tbody.querySelectorAll('tr > td').forEach(function (v) {
                let weaponName = v.querySelector('label').textContent.trim();
                weaponNames[weaponName] = (weaponNames[weaponName] || 0) + 1;
                if (weaponNames[weaponName] <= 2) {
                    tds.push(v);
                }
                v.parentNode.removeChild(v);
            });
            tbody.querySelectorAll('tr').forEach(function (v) {
                trs.push(v);
                v.parentNode.removeChild(v);
            });
            const td_per_tr = 4;
            for (let i = 0; i < tds.length; i++) {
                let tr = trs[Math.floor(i / td_per_tr)];
                tr.appendChild(tds[i]);
                if (i % td_per_tr == 0) {
                    tbody.appendChild(tr);
                }
            }
        }
    }
}

async function displayWeaponUpdatedOrderIfNeeded() {
    // for now, skip this setting if Battledome Utility (keyboard) script is active
    // if you want to order weapons, you can put this script before the keyboard script by:
    // Tampermonkey: Settings -> General -> Position: set it lower than the keyboard script
    if ($('form[action="/dome/1p/battle/"] div[id="bd-table"]').length > 0) {
      return;
    }

    let settings = await GM.getValue('gc_bd_settings', getDefaultSettings());
    let byNameSetting = settings["battle_sort_weapons_name"];
    let byListSetting = settings["battle_sort_weapons_list"] ?? "";
    if (byNameSetting === "yes" || byListSetting !== "") {
        let tbodys = $('form[id="bd-form"] > table > tbody');
        if (tbodys.length > 0) {
            let tbody = tbodys[0];
            let weapons = {};
            let trs = [];
            let tds = [];
            tbody.querySelectorAll('tr > td').forEach(function (v) {
                let weaponName = v.querySelector('label').textContent.trim();
                if (weaponName in weapons) {
                    weapons[weaponName].push(v);
                }
                else
                {
                    weapons[weaponName] = [v];
                }
                v.parentNode.removeChild(v);
            });
            tbody.querySelectorAll('tr').forEach(function (v) {
                trs.push(v);
                v.parentNode.removeChild(v);
            });
            let weaponsSorted = getSortedWeapons(weapons, byNameSetting, byListSetting);
            for (const [weaponName, td] of weaponsSorted) {
                tds.push(td);
            }
            let tdsFlat = tds.flat();
            const td_per_tr = 4;
            for (let i = 0; i < tdsFlat.length; i++) {
                let tr = trs[Math.floor(i / td_per_tr)];
                tr.appendChild(tdsFlat[i]);
                if (i % td_per_tr == 0) {
                    tbody.appendChild(tr);
                }
            }
        }
    }
}

function getSortedWeapons(weapons, byNameSetting, byListSetting) {
    let weaponsSorted = Object.entries(weapons);
    if (byNameSetting === "yes") {
        weaponsSorted = Object.entries(weapons).sort((a, b) => {
            // a[0] is the weapon name
            return a[0].localeCompare(b[0]);
        });
    }
    if (byListSetting !== "") {
        let expectedOrder = byListSetting.split(',');
        expectedOrder = removeDuplicates(expectedOrder);

        let weaponsFoundInList = [];
        expectedOrder.forEach(expectedWeapon => {
            for (const weapon of weaponsSorted) {
                // weapon[0] is the weapon name
                if (weapon[0].toLowerCase() === expectedWeapon.toLowerCase().trim()) {
                    weaponsFoundInList.push(weapon);
                }
            }
        });

        let weaponsNotFoundInList = [];
        weaponsSorted.forEach(weapon => {
            if (!weaponsFoundInList.includes(weapon)) {
                weaponsNotFoundInList.push(weapon);
            }
        });

        // concatenate both lists
        weaponsSorted = [...weaponsFoundInList, ...weaponsNotFoundInList];
    }
    return weaponsSorted;
}

function removeDuplicates(weapons) {
    const uniqueSet = new Set();
    const result = [];
    for (const item of weapons) {
        if (!uniqueSet.has(item)) {
            uniqueSet.add(item);
            result.push(item);
        }
    }
    return result;
}

async function onBattleEndPage() {
    let resultElements = $('div[id="hpbars"] > table > tbody > tr > td:nth-child(1)');
    if (resultElements && resultElements.length > 0) {
        let result = resultElements[0].textContent.trim();

        let prizeName = null;
        let isDaily = null;
        let prizeImage = null;
        let prizeElements = $('div[id="prize_blurb"] > p');
        if (prizeElements && prizeElements.length > 0) {
            prizeName = $(prizeElements[0]).find('strong').text();

            isDaily = prizeElements[0].textContent.includes('daily challenge');

            prizeImage = $(prizeElements[1]).find('img').attr('src');
        }

        let winCount = null;
        if (result === 'Winner') {
            let winCountElements = $('div[id="record_blurb"] > p');
            if (winCountElements && winCountElements.length > 0) {
                let winCountText = winCountElements[0].textContent.trim();
                let winCountWords = winCountText.split(' ');
                winCount = winCountWords[winCountWords.length - 2];
            }
        }

        let petInfo = await GM.getValue('gc_bd_pet_info', {'name': null, 'current_hp': null, 'max_hp': null});
        let opponentInfo = await GM.getValue('gc_bd_opponent_info', {'name': null, 'current_hp': null, 'max_hp': null});

        let moveNumber = await GM.getValue('gc_bd_move_number', null);

        let battleData = {'date': getDateString(), 'pet_info': petInfo, 'opponent_info': opponentInfo, 'result': result, 'win_count': winCount, 'prize_name': prizeName,
                          'prize_image': prizeImage, 'move_number': moveNumber, 'is_daily': isDaily};
        console.log(battleData);

        let battlesData = await GM.getValue('gc_bd_battles_data', {'version': version});
        if (battlesData[opponentInfo['name']] === undefined) {
            battlesData[opponentInfo['name']] = [];
        }
        battlesData[opponentInfo['name']].push(battleData);

        battlesData = versionUpgrader(battlesData);

        await GM.setValue('gc_bd_battles_data', battlesData);

        await showPrizesFromOpponentData(battlesData[opponentInfo['name']]);

        await GM.setValue('gc_bd_move_number', 0);

        await sendRewardsDataIfNeeded(battleData);
    }
}

async function showPrizesFromOpponentData(opponentData) {
    const prizesElement = document.createElement('div');
    prizesElement.className = 'prizes';

    const prizesTextElement = document.createElement('p');
    prizesTextElement.className = 'prizes-text center';
    prizesTextElement.textContent = 'Here are all the rewards you have got so far:';
    prizesElement.appendChild(prizesTextElement);

    const prizesListElement = document.createElement('div');
    prizesListElement.className = 'itemList';

    let settings = await GM.getValue('gc_bd_settings', getDefaultSettings());

    let setting = settings["display_previous_seasons_rewards"];
    if (setting === "no") {
        let currentSeason = getSeason(getDateString());
        opponentData = opponentData.filter(battleData => getSeason(battleData['date']) === currentSeason);
    }

    setting = settings["rewards_sort_order"];
    if (setting === "desc") {
        opponentData = opponentData.reverse();
    }

    setting = settings["aggregate_rewards"];
    if (setting === "yes") {
        let aggregatedCounts = {};
        let opponentDataFiltered = [];
        for (let battleData of opponentData) {
            let prizeName = battleData['prize_name'];
            if (prizeName in aggregatedCounts) {
                aggregatedCounts[prizeName] += 1;
            }
            else
            {
                aggregatedCounts[prizeName] = 1;
                opponentDataFiltered.push(battleData);
            }
        }
        for (let battleData of opponentDataFiltered) {
            let prizeName = battleData['prize_name'];
            battleData['count'] = aggregatedCounts[prizeName];
        }
        opponentData = opponentDataFiltered;
    }

    opponentData.forEach(battleData => {
        if (!battleData['prize_image']) {
            return;
        }

        let prizeName = battleData['prize_name'];
        let prizeImage = battleData['prize_image'];
        let count = battleData['count'] ?? 0; // generated from aggregate_rewards setting, 0 means 1 but the count won't show
        let shownPrizeName = prizeName;
        if (count > 0) {
            shownPrizeName += ' x' + count;
        }

        const invItem = document.createElement('div');
        invItem.className = 'shop-item';

        const img = document.createElement('img');
        img.className = 'med-image border-1';
        img.src = prizeImage;

        const itemDiv = document.createElement('div');
        itemDiv.className = 'item-info';
        itemDiv.innerHTML = `<span>${shownPrizeName}</span>`;

        const linksDiv = document.createElement('div');
        linksDiv.id = prizeName + '-links';
        linksDiv.className = 'searchhelp';
        linksDiv.setAttribute('style', `display: flex; justify-content: center; align-items: center; gap: 4px;`);

        const formattedName = prizeName.replaceAll(' ', '%20');

        const swLink = document.createElement('a');
        swLink.href = `/market/wizard/?query=${formattedName}`;
        swLink.target = '_blank';
        const swImg = document.createElement('img');
        swImg.src = 'https://grundoscafe.b-cdn.net/misc/wiz.png';
        swLink.appendChild(swImg);
        linksDiv.appendChild(swLink);

        const sdbLink = document.createElement('a');
        sdbLink.href = `/safetydeposit/?page=1&query=${formattedName}&exact=1`;
        sdbLink.target = '_blank';
        const sdbImg = document.createElement('img');
        sdbImg.src = 'https://grundoscafe.b-cdn.net/misc/sdb.gif';
        sdbLink.appendChild(sdbImg);
        linksDiv.appendChild(sdbLink);

        const tpLink = document.createElement('a');
        tpLink.href = `/island/tradingpost/browse/?query=${formattedName}`;
        tpLink.target = '_blank';
        const tpImg = document.createElement('img');
        tpImg.src = 'https://grundoscafe.b-cdn.net/misc/tp.png';
        tpLink.appendChild(tpImg);
        linksDiv.appendChild(tpLink);

        const wlLink = document.createElement('a');
        wlLink.href = `/wishlist/search/?query=${formattedName}`;
        wlLink.target = '_blank';
        const wlImg = document.createElement('img');
        wlImg.src = 'https://grundoscafe.b-cdn.net/misc/wish_icon.png';
        wlLink.appendChild(wlImg);
        linksDiv.appendChild(wlLink);

        invItem.appendChild(img);
        invItem.appendChild(itemDiv);
        invItem.appendChild(linksDiv);

        prizesListElement.appendChild(invItem);
    });
    prizesElement.appendChild(prizesListElement);

    $('div[id="page_content"]').append(prizesElement);

    return prizesElement;
}

async function sendRewardsDataIfNeeded(battleData) {
    if (!battleData['prize_image']) {
        return;
    }

    // Based with permission on Twiggies's "GC - Quest Reward Stat Collector"
    // https://greasyfork.org/en/scripts/482138-gc-quest-reward-stat-collector/code
    let settings = await GM.getValue('gc_bd_settings', getDefaultSettings());
    let setting = settings["share_rewards"];
    if (setting === "public" || setting === "private") {
        let username = $('div[id="userinfo"] > a[href^="/userlookup/?user="]')[0].href.split('/?user=')[1];
        let privacy = setting;

        let formId = '1FAIpQLScaNFSPpvA81XbST7hxyA3bJQ0lDRQTn6deSjFFkQLsn_kAuQ';
        const query = {
            "1350867233": version,
            "1534307698": battleData['date'],
            "1053758418": battleData['opponent_info']['name'],
            "1344427827": battleData['is_daily'],
            "1007726898": battleData['prize_name'],
            "1375386844": battleData['prize_image'],
            "1128869702": battleData['win_count'],
            "348938961": battleData['move_number'],
            "202326325": battleData['pet_info']['current_hp'],
            "1821086970": battleData['pet_info']['max_hp'],
            "1775514780": battleData['opponent_info']['max_hp'],
            "1550065838": username,
            "815139160": privacy,
        };
        let formLink = `https://docs.google.com/forms/d/e/${formId}/formResponse?usp=pp_url`;
        for (const [key, value] of Object.entries(query)) {
            formLink += `&entry.${key}=${value}`;
        }

        let opts = {
            mode: 'no-cors',
            referrer: 'no-referrer',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
        }

        let response = fetch(formLink, opts)
        .then(response => {
            console.log("Battle data submitted");
        })
        .catch(error => {
            console.log("Error:", error);
            console.log(response)
        });
    }
}

// updates the previous battles data if needed during version change
function versionUpgrader(battlesData) {
  if (battlesData['version'] <= 0.2) {
      for (let opponentName in battlesData) {
          if (battlesData.hasOwnProperty(opponentName) && opponentName !== 'version') {
              let opponentData = battlesData[opponentName];
              for (let battleId in opponentData) {
                  if (opponentData[battleId]['result'] === 'won') {
                      opponentData[battleId]['result'] = 'Winner';
                  } else if (opponentData[battleId]['result'] === 'lost') {
                      opponentData[battleId]['result'] = 'Loser';
                  }
              }
          }
      }

      battlesData['version'] = version;
  }
  return battlesData;
}

// TODO:
// import past data
// better search/filters
// fix search bar speed
// filter the list of healing opponents
// move number does not always work when using utility script
// error when keyboard utility script is ordered before this script