FlatMMO Inventory Grouping Script

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);
})();