FlatMMO UI Tweaks

UI Tweaks, support for portrait mode and more. For FlatMMO

目前為 2025-07-30 提交的版本,檢視 最新版本

// ==UserScript==
// @name         FlatMMO UI Tweaks
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  UI Tweaks, support for portrait mode and more. For FlatMMO
// @author       Pizza1337
// @match        *://flatmmo.com/play.php
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // --- Global State ---
    const skillUI = { elements: {}, total: {} };
    let userscriptMusicTrack = null;
    let isGridCreated = false;
    let hasVolumeSlidersBeenAdded = false;
    let hasTableSettingsBeenAdded = false;
    let areAudioHooksApplied = false;
    const isPortraitMode = localStorage.getItem('flatmmo_ui_position') === 'below';

    // --- New Theme Engine ---

    function injectThemeStyles() {
        const styles = `
            /* --- General UI Fixes --- */
            .settings-ui td:first-child {
                padding-left: 10px;
            }
            .tm-settings-row td { /* Ensures consistent height for custom settings */
                padding-top: 5px;
                padding-bottom: 5px;
            }

            /* --- Shared readability rules for dark themes --- */
            body.theme-dark a, body.theme-pumpkin a, body.theme-sea a, body.theme-omboko a, body.theme-mystic a { color: #8ab4f8 !important; }
            body.theme-dark a:hover, body.theme-pumpkin a:hover, body.theme-sea a:hover, body.theme-omboko a:hover, body.theme-mystic a:hover { color: #c3dafa !important; }

            /* --- Dark Mode Theme --- */
            body.theme-dark .ui-panel, body.theme-dark .modal-content, body.theme-dark #volume-controls-wrapper div,
            body.theme-dark .ach-sub-menu-btn-td, body.theme-dark .donor-shop-entry, body.theme-dark .ui-donor-chat-tags-info,
            body.theme-dark .npc-chat-options-modal-title, body.theme-dark .npc-chat-options-modal-options div {
                background-color: #2c2f33; color: #f1f1f1; box-shadow: 1px 1px 5px #121212;
            }
            body.theme-dark table.settings-ui tr:nth-child(odd), body.theme-dark .quests-ui tr:nth-child(odd),
            body.theme-dark .monster-log-ui-table tr:nth-child(odd) { background-color: #3c4045; }
            body.theme-dark table.settings-ui tr:nth-child(even), body.theme-dark .quests-ui tr:nth-child(even),
            body.theme-dark .monster-log-ui-table tr:nth-child(even) { background-color: #32353b; }
            body.theme-dark .achievements-ui tr { background-color: #323b33; border-color: #455a46; }
            body.theme-dark .total-level-div { background-color: #23272a !important; }
            body.theme-dark .hint, body.theme-dark .color-grey { color: #b0b0b0 !important; }
            body.theme-dark .equipement-stats-ui-table, body.theme-dark .right-click-item-modal-table-stats,
            body.theme-dark .monster-log-modal-drops, body.theme-dark .npc-chat-message-modal-message,
            body.theme-dark .player-sell-booth-entry td { background-color: rgba(0, 0, 0, 0.2); }
            body.theme-dark div[style*="color:green"], body.theme-dark .color-green { color: #6fbf73 !important; }
            body.theme-dark .color-red { color: #ff8a80 !important; }
            body.theme-dark .ach-sub-menu-btn-td:hover, body.theme-dark .npc-chat-options-modal-options div:hover { background-color: #4a4e53; }
            body.theme-dark .hover-continue-npc-chat-message-modal:hover { color: #ff8a80 !important; }

            /* --- Pumpkin Spice Theme --- */
            body.theme-pumpkin .ui-panel, body.theme-pumpkin .modal-content, body.theme-pumpkin #volume-controls-wrapper div,
            body.theme-pumpkin .ach-sub-menu-btn-td, body.theme-pumpkin .donor-shop-entry, body.theme-pumpkin .ui-donor-chat-tags-info,
            body.theme-pumpkin .npc-chat-options-modal-title, body.theme-pumpkin .npc-chat-options-modal-options div {
                background-color: #2a2421; color: #f5e4d9; box-shadow: 1px 1px 5px #1a1411; border: 1px solid #4a3421;
            }
            body.theme-pumpkin .ui-panel-title { color: #e67e22; }
            body.theme-pumpkin table.settings-ui tr:nth-child(odd), body.theme-pumpkin .quests-ui tr:nth-child(odd),
            body.theme-pumpkin .monster-log-ui-table tr:nth-child(odd) { background-color: #3b312c; }
            body.theme-pumpkin table.settings-ui tr:nth-child(even), body.theme-pumpkin .quests-ui tr:nth-child(even),
            body.theme-pumpkin .monster-log-ui-table tr:nth-child(even) { background-color: #312925; }
            body.theme-pumpkin .achievements-ui tr { background-color: #3b2e25; border-color: #5a4431; }
            body.theme-pumpkin .skill-cell-bar-fill { background-color: #d35400 !important; }
            body.theme-pumpkin .total-level-div { background-color: #1c1815 !important; }
            body.theme-pumpkin .hint, body.theme-pumpkin .color-grey { color: #c9bca2 !important; }
            body.theme-pumpkin .equipement-stats-ui-table, body.theme-pumpkin .right-click-item-modal-table-stats,
            body.theme-pumpkin .monster-log-modal-drops, body.theme-pumpkin .npc-chat-message-modal-message,
            body.theme-pumpkin .player-sell-booth-entry td { background-color: rgba(40, 26, 13, 0.2); }
            body.theme-pumpkin div[style*="color:green"], body.theme-pumpkin .color-green { color: #e67e22 !important; }
            body.theme-pumpkin .color-red { color: #e84c3d !important; }
            body.theme-pumpkin a { color: #f39c12 !important; }
            body.theme-pumpkin .ach-sub-menu-btn-td:hover, body.theme-pumpkin .npc-chat-options-modal-options div:hover { background-color: #4a3c35; }
            body.theme-pumpkin .hover-continue-npc-chat-message-modal:hover { color: #ffab70 !important; }

            /* --- Deep Sea Theme --- */
            body.theme-sea .ui-panel, body.theme-sea .modal-content, body.theme-sea #volume-controls-wrapper div,
            body.theme-sea .ach-sub-menu-btn-td, body.theme-sea .donor-shop-entry, body.theme-sea .ui-donor-chat-tags-info,
            body.theme-sea .npc-chat-options-modal-title, body.theme-sea .npc-chat-options-modal-options div {
                background-color: #0d253f; color: #e1f5fe; box-shadow: 1px 1px 5px #051525; border: 1px solid #01b4e4;
            }
            body.theme-sea .ui-panel-title { color: #90cea1; }
            body.theme-sea table.settings-ui tr:nth-child(odd), body.theme-sea .quests-ui tr:nth-child(odd),
            body.theme-sea .monster-log-ui-table tr:nth-child(odd) { background-color: #0a1d31; }
            body.theme-sea table.settings-ui tr:nth-child(even), body.theme-sea .quests-ui tr:nth-child(even),
            body.theme-sea .monster-log-ui-table tr:nth-child(even) { background-color: #102a45; }
            body.theme-sea .achievements-ui tr { background-color: #103a45; border-color: #1a5a65; }
            body.theme-sea .skill-cell-bar-fill { background-color: #01b4e4 !important; }
            body.theme-sea .total-level-div { background-color: #071726 !important; }
            body.theme-sea .hint, body.theme-sea .color-grey { color: #a4c8d1 !important; }
            body.theme-sea .equipement-stats-ui-table, body.theme-sea .right-click-item-modal-table-stats,
            body.theme-sea .monster-log-modal-drops, body.theme-sea .npc-chat-message-modal-message,
            body.theme-sea .player-sell-booth-entry td { background-color: rgba(1, 180, 228, 0.1); }
            body.theme-sea div[style*="color:green"], body.theme-sea .color-green { color: #90cea1 !important; }
            body.theme-sea .color-red { color: #5eb5b9 !important; }
            body.theme-sea a { color: #01b4e4 !important; }
            body.theme-sea .ach-sub-menu-btn-td:hover, body.theme-sea .npc-chat-options-modal-options div:hover { background-color: #133a5f; }
            body.theme-sea .hover-continue-npc-chat-message-modal:hover { color: #66d9ff !important; }

            /* --- Mystic Vale Theme (Darker) --- */
            body.theme-mystic .ui-panel, body.theme-mystic .modal-content, body.theme-mystic #volume-controls-wrapper div,
            body.theme-mystic .ach-sub-menu-btn-td, body.theme-mystic .donor-shop-entry, body.theme-mystic .ui-donor-chat-tags-info,
            body.theme-mystic .npc-chat-options-modal-title, body.theme-mystic .npc-chat-options-modal-options div {
                background-color: #6A6E94; color: #f0f0f0; box-shadow: 1px 1px 5px #4a4c6a;
            }
            body.theme-mystic .ui-panel-title { color: #d1d3e0; }
            body.theme-mystic table.settings-ui tr:nth-child(odd), body.theme-mystic .quests-ui tr:nth-child(odd),
            body.theme-mystic .monster-log-ui-table tr:nth-child(odd) { background-color: #7A7EA1; }
            body.theme-mystic table.settings-ui tr:nth-child(even), body.theme-mystic .quests-ui tr:nth-child(even),
            body.theme-mystic .monster-log-ui-table tr:nth-child(even) { background-color: #868AAD; }
            body.theme-mystic .achievements-ui tr { background-color: #7a8aa1; border-color: #9598b9; }
            body.theme-mystic .skill-cell-bar-fill { background-color: #9598B9 !important; }
            body.theme-mystic .skill-cell { background-color: #5f6284 !important; }
            body.theme-mystic .total-level-div { background-color: #5f6284 !important; color: #f0f0f0 !important; }
            body.theme-mystic .hint, body.theme-mystic .color-grey { color: #d1d3e0 !important; }
            body.theme-mystic .equipement-stats-ui-table, body.theme-mystic .right-click-item-modal-table-stats,
            body.theme-mystic .monster-log-modal-drops, body.theme-mystic .npc-chat-message-modal-message,
            body.theme-mystic .player-sell-booth-entry td { background-color: rgba(60, 62, 84, 0.2); }
            body.theme-mystic div[style*="color:green"], body.theme-mystic .green-hover:hover, body.theme-mystic .color-green { color: #b2fab4 !important; }
            body.theme-mystic .color-red { color: #f5c0c0 !important; }
            body.theme-mystic .color-blue { color: #a6cfff !important; }
            body.theme-mystic a { color: #d1d3e0 !important; }
            body.theme-mystic .ach-sub-menu-btn-td:hover, body.theme-mystic .npc-chat-options-modal-options div:hover { background-color: #7A7EA1; }
            body.theme-mystic .hover-continue-npc-chat-message-modal:hover { color: #f5c0c0 !important; }

            /* --- Omboko Theme --- */
            body.theme-omboko .ui-panel, body.theme-omboko .modal-content, body.theme-omboko #volume-controls-wrapper div,
            body.theme-omboko .ach-sub-menu-btn-td, body.theme-omboko .donor-shop-entry, body.theme-omboko .ui-donor-chat-tags-info,
            body.theme-omboko .npc-chat-options-modal-title, body.theme-omboko .npc-chat-options-modal-options div {
                background-color: #1D2319; color: #d1dcc6; box-shadow: 1px 1px 5px #000; border: 1px solid #4B682E;
            }
            body.theme-omboko .ui-panel-title { color: #9eb883; }
            body.theme-omboko table.settings-ui tr:nth-child(odd), body.theme-omboko .quests-ui tr:nth-child(odd),
            body.theme-omboko .monster-log-ui-table tr:nth-child(odd) { background-color: #25311B; }
            body.theme-omboko table.settings-ui tr:nth-child(even), body.theme-omboko .quests-ui tr:nth-child(even),
            body.theme-omboko .monster-log-ui-table tr:nth-child(even) { background-color: #30411F; }
            body.theme-omboko .achievements-ui tr { background-color: #30411F; border-color: #4B682E; }
            body.theme-omboko .skill-cell-bar-fill { background-color: #4B682E !important; }
            body.theme-omboko .skill-cell { background-color: #30411F !important; }
            body.theme-omboko .total-level-div { background-color: #11150f !important; }
            body.theme-omboko .hint, body.theme-omboko .color-grey { color: #8f9984 !important; }
            body.theme-omboko .equipement-stats-ui-table, body.theme-omboko .right-click-item-modal-table-stats,
            body.theme-omboko .monster-log-modal-drops, body.theme-omboko .npc-chat-message-modal-message,
            body.theme-omboko .player-sell-booth-entry td { background-color: rgba(75, 104, 46, 0.1); }
            body.theme-omboko div[style*="color:green"], body.theme-omboko .color-green { color: #9eb883 !important; }
            body.theme-omboko .color-red { color: #e57373 !important; }
            body.theme-omboko a { color: #9eb883 !important; }
            body.theme-omboko .ach-sub-menu-btn-td:hover, body.theme-omboko .npc-chat-options-modal-options div:hover { background-color: #3D5426; }
            body.theme-omboko .hover-continue-npc-chat-message-modal:hover { color: #c4d6b1 !important; }
        `;
        const styleSheet = document.createElement("style");
        styleSheet.type = "text/css";
        styleSheet.innerText = styles;
        document.head.appendChild(styleSheet);
    }

    /**
     * Applies a theme by adding a class to the body element.
     */
    function applyTheme(themeName) {
        document.body.className = document.body.className.replace(/theme-\w+/g, '');
        if (themeName && themeName !== 'default') {
            document.body.classList.add(`theme-${themeName}`);
        }
    }

    // --- Core Functions (from previous versions) ---

    function get_xp_for_level(level) {
        if (level <= 1) return 0;
        return parseInt(Math.pow(level, 3 + (level / 200)));
    }

    function recalculateAndDisplayTotal() {
        if (!skillUI.total.totalDiv) return;
        let totalLevel = 0;
        let totalXp = 0;
        for (const skillName in skillUI.elements) {
            totalLevel += skillUI.elements[skillName].level || 0;
            totalXp += skillUI.elements[skillName].xp || 0;
        }
        skillUI.total.totalDiv.innerText = `Total Level: ${totalLevel}`;
        skillUI.total.totalDiv.setAttribute("title", `${totalXp.toLocaleString('en-US')} XP`);
    }

    function updateSkillCell(skillName) {
        const component = skillUI.elements[skillName];
        if (!component || !component.originalLevelEl) return;
        const levelText = component.originalLevelEl.innerText;
        const xpText = component.originalXpEl.innerText;
        const currentLevel = parseInt(levelText.replace("Level ", ""));
        const [currentStr, maxStr] = xpText.replace(" XP", "").split("/");
        const currentXP = parseInt(currentStr.replace(/,/g, "")) || 0;
        const nextLevelXP = parseInt(maxStr.replace(/,/g, "")) || 0;
        component.level = currentLevel;
        component.xp = currentXP;
        const previousLevelXP = get_xp_for_level(currentLevel);
        const xpGainedThisLevel = Math.max(0, currentXP - previousLevelXP);
        const xpNeededForThisLevel = nextLevelXP - previousLevelXP;
        let progress = 0;
        if (xpNeededForThisLevel > 0) {
            progress = Math.min((xpGainedThisLevel / xpNeededForThisLevel) * 100, 100);
        }
        component.label.innerText = currentLevel;
        component.tooltip.textContent = xpText;
        component.barFill.style.width = `${progress}%`;
        recalculateAndDisplayTotal();
    }

    function applyAudioHooks() {
        areAudioHooksApplied = true;

        const original_play_sound = window.play_sound;
        window.play_sound = function(path, vol = 1) {
            const savedVolume = parseFloat(localStorage.getItem('flatmmo_sound_volume') ?? 1.0);
            original_play_sound.call(this, path, savedVolume * vol);
        };

        window.pause_track = function() {
            if (userscriptMusicTrack) {
                userscriptMusicTrack.pause();
                userscriptMusicTrack.currentTime = 0;
            }
            userscriptMusicTrack = null;
        };

        window.play_track = function(f) {
            window.pause_track();
            if (localStorage.getItem('music_off') == 1) {
                return;
            }
            const savedVolume = parseFloat(localStorage.getItem('flatmmo_music_volume') ?? 0.08);
            userscriptMusicTrack = new Audio("sounds/tracks/" + f);
            userscriptMusicTrack.volume = savedVolume;
            userscriptMusicTrack.play();
            window.track = userscriptMusicTrack;
        };
    }

    function moveUiBelowCanvas() {
        const table = document.querySelector('#game table');
        if (!table) return;

        const canvas = table.querySelector('canvas');
        const canvasTd = canvas?.closest('td');
        const uiTd = table.querySelector('.td-ui');

        if (canvas && canvasTd && uiTd) {
            const newTbody = document.createElement('tbody');
            const canvasRow = document.createElement('tr');
            const uiRow = document.createElement('tr');

            canvasRow.appendChild(canvasTd);
            uiRow.appendChild(uiTd);
            newTbody.append(canvasRow, uiRow);
            table.innerHTML = '';
            table.appendChild(newTbody);

            uiTd.style.cssText = 'padding: 0; margin: 0; overflow: hidden; position: relative;';
            const uiWrapper = document.createElement('div');
            uiWrapper.style.transformOrigin = 'top left';
            uiWrapper.style.display = 'inline-block';

            while (uiTd.firstChild) {
                uiWrapper.appendChild(uiTd.firstChild);
            }
            uiTd.appendChild(uiWrapper);

            const applyZoomAndWidthFix = () => {
                const canvasWidth = canvas.offsetWidth;
                const ratio = window.devicePixelRatio || 1;
                const scale = 1 / ratio;

                uiWrapper.style.transform = `scale(${scale})`;
                uiWrapper.style.width = `${canvasWidth * ratio}px`;

                requestAnimationFrame(() => {
                    const realHeight = uiWrapper.getBoundingClientRect().height;
                    uiTd.style.height = `${realHeight}px`;
                });
            };

            applyZoomAndWidthFix();
            window.addEventListener('resize', applyZoomAndWidthFix);
        }
    }

    // --- Initial Setup ---
    injectThemeStyles();
    const savedTheme = localStorage.getItem('flatmmo_ui_theme') || 'default';
    applyTheme(savedTheme);

    if (isPortraitMode) {
        const layoutObserver = new MutationObserver((mutations, observer) => {
            if (document.querySelector('#game table canvas')) {
                moveUiBelowCanvas();
                observer.disconnect();
            }
        });
        layoutObserver.observe(document.body, { childList: true, subtree: true });
    }

    // --- Main Observer for UI building and modifications ---
    const mainObserver = new MutationObserver(() => {
        if (!areAudioHooksApplied && typeof window.play_sound === 'function' && typeof window.play_track === 'function') {
            applyAudioHooks();
        }

        if (!isGridCreated) {
            const skillPanel = document.querySelector("#ui-panel-skills");
            const skillTable = skillPanel?.querySelector("table.skills-ui");

            if (skillPanel && skillTable && skillPanel.style.display !== "none") {
                const rows = Array.from(skillTable.querySelectorAll("tr"));
                if (rows.length > 0) {
                    isGridCreated = true;

                    const grid = document.createElement("div");
                    grid.style.cssText = "display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin: 20px 0;";

                    rows.slice(0, -1).forEach(row => {
                        const levelEl = row.querySelector("span[id$='-level']");
                        if (!levelEl) return;
                        const skillName = levelEl.id.replace('-level', '');
                        const iconEl = row.querySelector("img.icon");
                        const xpEl = row.querySelector("span[id$='-xp']");
                        const onclickAttr = row.getAttribute("onclick");

                        const cell = document.createElement("div");
                        cell.style.cssText = `display: flex; flex-direction: column; align-items: center; justify-content: space-between; background: #222; color: #fff; padding: 10px; border-radius: 8px; font-size: 14px; cursor: pointer;`;
                        cell.classList.add('skill-cell');
                        if (onclickAttr) cell.onclick = () => eval(onclickAttr);

                        const img = document.createElement("img");
                        img.src = iconEl.src;
                        img.style.cssText = "width: 32px; height: 32px; margin-bottom: 5px;";

                        const label = document.createElement("div");
                        const spacer = document.createElement("div");
                        spacer.style.flexGrow = "1";

                        const barContainer = document.createElement("div");
                        barContainer.style.cssText = `width: 100%; height: 6px; background: #444; border-radius: 4px; margin-top: 6px; overflow: hidden;`;
                        const barFill = document.createElement("div");
                        barFill.style.cssText = "height: 100%; background: #00cc66; border-radius: 4px 0 0 4px;";
                        barFill.classList.add('skill-cell-bar-fill');
                        barContainer.appendChild(barFill);

                        const tooltip = document.createElement("div");
                        let tooltipStyle = `position: fixed; padding: 6px 10px; background: #333; color: #fff; border-radius: 6px; font-size: 14px; box-shadow: 0 2px 6px rgba(0,0,0,0.4); pointer-events: none; z-index: 9999; white-space: nowrap; display: none;`;

                        if (isPortraitMode) {
                            const ratio = window.devicePixelRatio || 1;
                            const scale = 1 / ratio;
                            tooltipStyle += `transform: scale(${scale}); transform-origin: left top;`;
                        }
                        tooltip.style.cssText = tooltipStyle;
                        document.body.appendChild(tooltip);

                        cell.addEventListener("mousemove", e => {
                            tooltip.style.left = `${e.pageX + 15}px`;
                            tooltip.style.top = `${e.pageY + 10}px`;
                        });
                        cell.addEventListener("mouseenter", () => { tooltip.style.display = "block"; });
                        cell.addEventListener("mouseleave", () => { tooltip.style.display = "none"; });

                        cell.append(img, label, spacer, barContainer);
                        grid.appendChild(cell);

                        skillUI.elements[skillName] = { label, barFill, tooltip, originalLevelEl: levelEl, originalXpEl: xpEl };
                        updateSkillCell(skillName);
                    });

                    const totalDiv = document.createElement("div");
                    totalDiv.style.cssText = `text-align: center; margin-top: 10px; padding: 10px; background: #444; color: #fff; font-weight: bold; border-radius: 8px;`;
                    totalDiv.classList.add('total-level-div');
                    skillUI.total.totalDiv = totalDiv;

                    recalculateAndDisplayTotal();

                    skillTable.style.display = "none";
                    skillPanel.append(grid, totalDiv);

                    const skillObserver = new MutationObserver(() => {
                        Object.keys(skillUI.elements).forEach(updateSkillCell);
                    });
                    skillObserver.observe(skillTable, { childList: true, subtree: true });
                }
            }
        }

        // Add settings controls and rows, using separate flags for robustness
        const settingsPanel = document.querySelector("#ui-panel-settings");
        if (settingsPanel) {
            // Add Volume Sliders
            if (!hasVolumeSlidersBeenAdded) {
                const soundIcon = document.getElementById('settings-sound-icon');
                const musicIcon = document.getElementById('settings-music-icon');
                const settingsTable = settingsPanel.querySelector("table.settings-ui");
                if (soundIcon && musicIcon && settingsTable) {
                    hasVolumeSlidersBeenAdded = true;

                    const mainControlsContainer = document.createElement('div');
                    mainControlsContainer.id = 'volume-controls-wrapper';
                    mainControlsContainer.style.cssText = 'display: flex; justify-content: space-around; padding: 10px; align-items: start;';

                    const handleVolumeScroll = (event, slider) => {
                        event.preventDefault();
                        const step = parseInt(slider.step, 10);
                        const currentValue = parseInt(slider.value, 10);
                        let newValue;
                        if (event.deltaY < 0) {
                            newValue = Math.min(100, currentValue + step);
                        } else {
                            newValue = Math.max(0, currentValue - step);
                        }
                        if (newValue !== currentValue) {
                            slider.value = newValue;
                            slider.dispatchEvent(new Event('input'));
                        }
                    };

                    const createVolumeControl = (type, iconElement, defaultValue) => {
                        const container = document.createElement('div');
                        container.style.cssText = 'display: flex; flex-direction: column; align-items: center; gap: 8px;';

                        const slider = document.createElement('input');
                        slider.type = 'range';
                        slider.min = 0;
                        slider.max = 100;
                        slider.step = 10;
                        slider.value = (localStorage.getItem(`flatmmo_${type}_volume`) ?? defaultValue) * 100;
                        slider.style.cssText = 'width: 60px; margin: 0;';

                        const tooltip = document.createElement('div');
                        let tooltipStyle = `position: fixed; padding: 6px 10px; background: #333; color: #fff; border-radius: 6px; font-size: 14px; box-shadow: 0 2px 6px rgba(0,0,0,0.4); pointer-events: none; z-index: 9999; display: none;`;
                        if (isPortraitMode) {
                            const ratio = window.devicePixelRatio || 1;
                            tooltipStyle += `transform: scale(${1 / ratio}); transform-origin: left top;`;
                        }
                        tooltip.style.cssText = tooltipStyle;
                        document.body.appendChild(tooltip);

                        const showTooltip = (e) => {
                            tooltip.textContent = `${slider.value}%`;
                            tooltip.style.left = `${e.pageX + 15}px`;
                            tooltip.style.top = `${e.pageY + 10}px`;
                            tooltip.style.display = 'block';
                        };

                        [slider, iconElement].forEach(el => {
                            el.addEventListener('wheel', (e) => handleVolumeScroll(e, slider));
                            el.addEventListener('mouseenter', showTooltip);
                            el.addEventListener('mousemove', showTooltip);
                            el.addEventListener('mouseleave', () => { tooltip.style.display = 'none'; });
                        });

                        slider.addEventListener('input', () => {
                            const newVolume = slider.value / 100;
                            localStorage.setItem(`flatmmo_${type}_volume`, newVolume);
                            if (type === 'music' && userscriptMusicTrack) {
                                userscriptMusicTrack.volume = newVolume;
                            }
                            tooltip.textContent = `${slider.value}%`;
                        });

                        container.append(iconElement, slider);
                        return container;
                    };

                    const soundControl = createVolumeControl('sound', soundIcon, 1.0);
                    const musicControl = createVolumeControl('music', musicIcon, 0.08);

                    mainControlsContainer.append(soundControl, musicControl);
                    settingsPanel.insertBefore(mainControlsContainer, settingsTable);
                }
            }

            // Add Table Settings (Theme, Portrait Mode)
            if (!hasTableSettingsBeenAdded) {
                const settingsTable = settingsPanel.querySelector("table.settings-ui");
                const firstRow = settingsTable?.querySelector('tbody tr');
                if (settingsTable && firstRow) {
                    hasTableSettingsBeenAdded = true;
                    const settingsTBody = settingsTable.querySelector('tbody');

                    // Theme Selector Row
                    const themeRow = firstRow.cloneNode(true);
                    themeRow.classList.add('tm-settings-row');
                    let tds = themeRow.querySelectorAll('td');
                    tds[0].textContent = 'UI Theme';
                    tds[1].innerHTML = `
                        <select id="theme-selector" style="width: 100%; vertical-align: middle;">
                            <option value="default">Default</option>
                            <option value="dark">Dark Mode</option>
                            <option value="pumpkin">Pumpkin Spice</option>
                            <option value="sea">Deep Sea</option>
                            <option value="mystic">Mystic Vale</option>
                            <option value="omboko">Omboko</option>
                        </select>
                    `;
                    settingsTBody.appendChild(themeRow);

                    const themeSelector = document.getElementById('theme-selector');
                    themeSelector.value = savedTheme;
                    themeSelector.addEventListener('change', (e) => {
                        const newTheme = e.target.value;
                        localStorage.setItem('flatmmo_ui_theme', newTheme);
                        applyTheme(newTheme);
                    });

                    // Portrait Mode Row
                    const portraitRow = firstRow.cloneNode(true);
                    portraitRow.classList.add('tm-settings-row');
                    tds = portraitRow.querySelectorAll('td');
                    tds[0].textContent = 'Portrait Mode (Reload required)';
                    tds[1].innerHTML = `<input type="checkbox" id="ui-layout-checkbox" style="vertical-align: middle;">`;
                    settingsTBody.appendChild(portraitRow);

                    const checkbox = document.getElementById('ui-layout-checkbox');
                    checkbox.checked = isPortraitMode;
                    checkbox.addEventListener('change', () => {
                        localStorage.setItem('flatmmo_ui_position', checkbox.checked ? 'below' : 'side');
                        location.reload();
                    });
                }
            }
        }
    });

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

})();