FlatMMO Inventory Grouping Script

This userscript automatically sorts your Flat MMO inventory by grouping similar items together according to customizable categories

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         FlatMMO Inventory Grouping Script
// @namespace    http://tampermonkey.net/
// @version      0.2.0
// @author       miniadri
// @description  This userscript automatically sorts your Flat MMO inventory by grouping similar items together according to customizable categories
// @match        https://flatmmo.com/play.php*
// @grant        none
// @license      GNU GPLv3 
// ==/UserScript==

(function() {
    'use strict';
    const SCRIPT_VERSION = "0.2.0";
    const AUTHOR = "miniadri";
    const DEFAULT_GROUPS = [
        {key: "00_food", patterns: ["cooked","cake","sushi","bread"], name: "Food", color: "#eaf7df"},
        {key: "01_weapon", patterns: ["knife","sword","bow","mace","staff"], name: "Weapons", color: "#fde0e0"},
        {key: "02_orb", patterns: ["orb"], name: "Orbs", color: "#ececff"},
        {key: "03_potion", patterns: ["potion", "vial", "large vial", "huge vial"], name: "Potions", color: "#fcf5df"},
        {key: "04_tool", patterns: ["axe","pickaxe","shovel","hell_shovel","fishing_net","thick_fishing_net","fishing_rod","harpoon"], name: "Tools", color: "#f7f7ea"},
        {key: "05_armor", patterns: ["armor","helmet","mask","hat","robe","body","chest","pants","legs","skirt","gloves","boots","shield","necklace","ring"], name: "Armor & Apparel", color: "#edeaff"},
        {key: "99_others", patterns: [], name: "Other", color: "#f3f3f3"}
    ];

    // Panel visual descriptions in plural and <40 characters:
    const GROUP_DESCRIPTIONS = [
        "cooked foods, cakes, sushi, breads",
        "knives, swords, bows, maces, staffs",
        "orbs, damages and elementals orbs",
        "potions, vials, leaves, mushrooms",
        "axes, pickaxes, shovels, fish related",
        "melee, archery and magic defense",
        "other: logs, seeds, ores, bars, etc."
    ];

    function getGroupsConfig() {
        let store = localStorage.getItem("customInventoryGroups");
        if(!store) return DEFAULT_GROUPS.slice();
        try {
            let parsed = JSON.parse(store);
            if (parsed.length && (parsed[0].patrones || parsed[0].nombre)) {
                localStorage.removeItem("customInventoryGroups");
                return DEFAULT_GROUPS.slice();
            }
            if (!parsed[0] || !("patterns" in parsed[0]) || !("name" in parsed[0])) {
                localStorage.removeItem("customInventoryGroups");
                return DEFAULT_GROUPS.slice();
            }
            return parsed;
        } catch (e) { return DEFAULT_GROUPS.slice(); }
    }
    function saveGroupsConfig(arr) {
        localStorage.setItem("customInventoryGroups", JSON.stringify(arr));
    }
    let groups = getGroupsConfig();

    function detectGroup(originalName) {
        let name = originalName.toLowerCase();
        if (
            name.includes("potion") ||
            name.includes("vial") ||
            name.includes("large vial") ||
            name.includes("huge vial") ||
            ((name.includes("leaf") && !name.includes("seed")) ||
             (name.includes("mushroom") && !name.includes("seed"))
            )
        ) return {key:"03_potion", idx:3};
        for (let i=0; i<groups.length; i++) {
            let group = groups[i];
            if(group.key==="03_potion") continue;
            for (let pat of group.patterns) {
                let exp = pat.replace(/_/g, "[ _]");
                if (name.match(exp) || name.includes(pat)) return {key: group.key, idx: i};
            }
        }
        return {key: "99_others", idx: groups.length-1};
    }
    function groupIndex(key) {
        for (let i=0; i<groups.length; i++) if (groups[i].key === key) return i;
        return groups.length - 1;
    }
    function darken(hex, px = 40) {
        if(!hex || !/^#[0-9A-Fa-f]{6}$/.test(hex)) return "#bbb";
        let c = hex.substring(1);
        let r = Math.max(parseInt(c.substring(0,2),16) - px, 0);
        let g = Math.max(parseInt(c.substring(2,4),16) - px, 0);
        let b = Math.max(parseInt(c.substring(4,6),16) - px, 0);
        return `#${r.toString(16).padStart(2,'0')}${g.toString(16).padStart(2,'0')}${b.toString(16).padStart(2,'0')}`;
    }
    function totalQuantity(content, name) {
        let imgs = content.querySelectorAll(`img[data-item-name="${name}"]`);
        let total = 0;
        imgs.forEach(img => {
            let span = img.parentElement.querySelector('.item-amount');
            let count = span ? parseInt(span.textContent.trim(), 10) : (() => {
                let m = img.getAttribute('onclick')?.match(/,\s*"(\d+)"\s*,/);
                return (m && m[1]) ? parseInt(m[1], 10) : 1;
            })();
            total += isNaN(count) ? 1 : count;
        });
        return total;
    }
    function updateSlotStyles() {
        let styleId = 'inv-group-styles';
        let styleNode = document.getElementById(styleId);
        if (styleNode) styleNode.remove();
        let css = '';
        groups.forEach(g => {
            css += `.item.invgrp_${g.key} { background: ${g.color} !important; border: 2px solid ${darken(g.color)} !important; border-radius:8px !important; transition:background 0.2s,border 0.2s; }\n`;
        });
        css += `.item.invgrp_99_others { background:#f3f3f3 !important; border:2px solid #bbb !important; border-radius:8px !important; }`;
        let st = document.createElement('style');
        st.id = styleId;
        st.textContent = css;
        document.head.appendChild(st);
    }
    let observer = null;
    function groupInventory() {
        let content = document.getElementById('ui-panel-inventory-content');
        if (!content) return;
        let items = Array.from(content.querySelectorAll('.item'));
        let filled = [], empty = [];
        items.forEach(div => {
            let img = div.querySelector('img[data-item-name]');
            if (!img || img.getAttribute('data-item-name') === 'none') {
                div.classList.remove(...Array.from(div.classList).filter(c => c.startsWith('invgrp_')));
                div.classList.add('invgrp_99_others');
                empty.push(div);
            } else {
                let group = detectGroup(img.getAttribute('data-item-name'));
                div.classList.remove(...Array.from(div.classList).filter(c => c.startsWith('invgrp_')));
                div.classList.add('invgrp_' + group.key);
                div.style.background = "";
                filled.push({
                    div: div,
                    groupIdx: group.idx,
                    groupKey: group.key,
                    name: img.getAttribute('data-item-name'),
                    quantity: totalQuantity(content, img.getAttribute('data-item-name'))
                });
            }
        });
        empty.forEach(div => {
            div.classList.remove(...Array.from(div.classList).filter(c => c.startsWith('invgrp_')));
            div.style.background = "#f8f8f888";
            div.style.border = "2px solid #ddd";
            div.style.borderRadius = "8px";
        });

        filled.sort((a, b) => {
            if (a.groupIdx !== b.groupIdx) return a.groupIdx - b.groupIdx;
            if (b.quantity !== a.quantity) return b.quantity - a.quantity;
            return a.name.localeCompare(b.name);
        });
        let final = filled.map(e=>e.div).concat(empty);
        final.forEach(div => { if(div.parentNode) div.parentNode.removeChild(div); });
        final.forEach(div => content.appendChild(div));
        updateSlotStyles();
        if (observer) observer.disconnect();
        observer = new MutationObserver(() => {
            setTimeout(groupInventory, 50);
        });
        observer.observe(content, {childList:true, subtree:true});
    }

    function openPanelConfig() {
        let win = document.getElementById("custom-inv-config-panel");
        if (win) win.remove();
        win = document.createElement('div');
        win.id = "custom-inv-config-panel";
        win.style.position = 'fixed';
        win.style.top = '70px';
        win.style.right = '60px';
        win.style.background = "#fafafc";
        win.style.color = "#222";
        win.style.zIndex = 99999;
        win.style.border = "2px solid #333";
        win.style.borderRadius = "12px";
        win.style.boxShadow = "0 6px 28px #2227";
        win.style.padding = "20px";
        win.style.minWidth = "480px";
        win.style.maxWidth = "700px";
        win.style.overflowX = "auto";
        win.style.fontFamily = "inherit";
        win.innerHTML = `
<div style="font-weight:bold;font-size:20px;margin-bottom:12px">
Inventory Grouping Options
<span style="float:right;font-size:13px;font-weight:normal;color:#666;opacity:.85">
Author: <b>${AUTHOR}</b> &nbsp;|&nbsp; Script version: <b>${SCRIPT_VERSION}</b>
</span>
</div>
<form id="groupsForm"></form>
<button id="saveNewOrder" style="margin: 18px 4px 0 0;padding:7px 26px;font-weight:bold;font-size:19px">Save</button>
<button id="restoreOrder" style="margin:18px 7px 0 0;padding:7px 18px;font-size:19px;">Restore defaults</button>
<button id="closeCustomInvPanel" style="float:right;padding:9px 18px;font-size:18px">Close</button>`;

        let form = win.querySelector("#groupsForm");
        form.style.display = "flex"; form.style.flexDirection = "column";
        form.style.gap = "14px";
        groups.forEach((g, i) => {
            let row = document.createElement("div");
            row.style.display = "flex";
            row.style.alignItems = "center";
            row.style.marginBottom = "0";
            let desc = GROUP_DESCRIPTIONS[i] || "other";
            row.innerHTML = `
                <input type="text" value="${g.name}" style="width:135px; min-width:96px; max-width:200px; margin-right:5px; font-size:18px" data-name-idx="${i}">
                <input type="color" value="${g.color}" style="margin:0 10px 0 0; flex:none; min-width:36px;max-width:36px;height:33px" data-color-idx="${i}">
                <span style="
                    flex:1;
                    font-family:monospace;
                    color:#888;
                    font-size:15px;
                    line-height:1.15;
                    padding:2px 0 2px 2px;
                    max-width:192px;
                    overflow-wrap:break-word;
                    word-break:break-word;
                    white-space:pre-line;
                    display:block;
                    text-align:left
                    " title="Items grouped here">${desc}</span>
                <div style="display:flex;flex-direction:column;gap:2px;min-width:42px">
                    <button type="button" style="font-weight:bold;margin:2px 0 2px 2px;min-width:32px" data-up-idx="${i}" ${i==0?"disabled":""}>↑</button>
                    <button type="button" style="font-weight:bold;margin:2px 0 2px 2px;min-width:32px" data-down-idx="${i}" ${i==groups.length-1?"disabled":""}>↓</button>
                </div>
            `;
            form.appendChild(row);
        });
        document.body.appendChild(win);
        form.addEventListener('input', function(ev){
            let idx = ev.target.getAttribute('data-name-idx');
            if (idx!==null) groups[idx].name = ev.target.value;
            idx = ev.target.getAttribute('data-color-idx');
            if (idx!==null) groups[idx].color = ev.target.value;
        });
        form.addEventListener('click', function(ev){
            if(ev.target.getAttribute('data-up-idx')!==null) {
                let idx = +ev.target.getAttribute('data-up-idx');
                if(idx>0) {
                    [groups[idx-1], groups[idx]] = [groups[idx], groups[idx-1]];
                    win.remove(); openPanelConfig();
                }
            }
            if(ev.target.getAttribute('data-down-idx')!==null) {
                let idx = +ev.target.getAttribute('data-down-idx');
                if(idx<groups.length-1) {
                    [groups[idx+1], groups[idx]] = [groups[idx], groups[idx+1]];
                    win.remove(); openPanelConfig();
                }
            }
        });
        win.querySelector('#saveNewOrder').onclick = function(){
            saveGroupsConfig(groups);
            win.remove();
            groups = getGroupsConfig();
            groupInventory();
        };
        win.querySelector('#restoreOrder').onclick = function(){
            localStorage.removeItem("customInventoryGroups");
            groups = getGroupsConfig();
            win.remove();
            groupInventory();
        };
        win.querySelector('#closeCustomInvPanel').onclick = function(){
            win.remove();
        };
    }
    function addSettingsBtn() {
        if(document.getElementById("btnCustomInvConfig")) return;
        let b = document.createElement('button');
        b.id = "btnCustomInvConfig";
        b.title = "Edit inventory grouping options";
        b.style.position = "fixed";
        b.style.top = "34px";
        b.style.right = "34px";
        b.style.zIndex = 99111;
        b.style.background = "#222";
        b.style.color = "#fff";
        b.style.padding = "8px 16px";
        b.style.border = "2px solid #77d";
        b.style.borderRadius = "8px";
        b.style.fontWeight = "bold";
        b.style.fontSize = "15px";
        b.textContent = "Inventory options";
        b.onclick = openPanelConfig;
        document.body.appendChild(b);
    }
    function activateGrouper() {
        let invRoot = document.getElementById('ui-panel-inventory-content');
        if (!invRoot) return;
        observer = new MutationObserver(() => {
            setTimeout(groupInventory, 60);
        });
        observer.observe(invRoot, {childList: true, subtree: true});
        setTimeout(groupInventory, 600);
        addSettingsBtn();
        updateSlotStyles();
    }
    window.addEventListener('load', activateGrouper);
})();