Bonk Mod Settings Core

Shared Generic/Modded Settings UI + mod list + categories for Bonk.io mods.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         Bonk Mod Settings Core
// @namespace    https://greasyfork.org/users/1552147-ansonii-crypto
// @version      3.0.0
// @description  Shared Generic/Modded Settings UI + mod list + categories for Bonk.io mods.
// @match        https://bonk.io/gameframe-release.html
// @run-at       document-start
// @grant        none
// @license      N/A
// ==/UserScript==

(() => {
    'use strict';

    function $(id) {
        return document.getElementById(id);
    }

    function waitForElement(id, cb) {
        const int = setInterval(() => {
            const el = $(id);
            if (el) {
                clearInterval(int);
                cb(el);
            }
        }, 200);
    }

    const global = window;
    global.bonkMods = global.bonkMods || {};
    const bonkMods = global.bonkMods;

    // Registries
    bonkMods._categories = bonkMods._categories || {};
    bonkMods._blocks = bonkMods._blocks || [];
    bonkMods._mods = bonkMods._mods || {};

    const DEFAULT_CATEGORY_ID = 'general';
    const FALLBACK_MOD_ID = 'other';

    let currentCategoryId = null;
    let currentModId = 'all';

    bonkMods.registerMod = function(meta) {
        if (!meta || !meta.id) return;
        const id = meta.id;
        const existing = bonkMods._mods[id] || {};
        bonkMods._mods[id] = Object.assign({
            id,
            name: id,
            version: '',
            author: '',
            description: '',
            homepage: '',
            devHint: ''
        }, existing, meta);

        renderModDropdown();
        renderModInfo();
        renderCategories();
        renderBlocks();
    };

    bonkMods.registerCategory = function(def) {
        if (!def || !def.id) return;
        if (!def.label) def.label = def.id;
        if (typeof def.order !== 'number') def.order = 100;

        if (!bonkMods._categories[def.id]) {
            bonkMods._categories[def.id] = {
                id: def.id,
                label: def.label,
                order: def.order
            };
        } else {
            Object.assign(bonkMods._categories[def.id], def);
        }

        renderCategories();
        renderBlocks();
    };

    bonkMods.addBlock = function(def) {
        if (!def || !def.id || typeof def.render !== 'function') return;
        if (!def.categoryId) def.categoryId = DEFAULT_CATEGORY_ID;
        if (!def.title) def.title = '';
        if (typeof def.order !== 'number') def.order = 100;
        if (!def.modId) def.modId = FALLBACK_MOD_ID;

        bonkMods._blocks.push(def);

        if (!bonkMods._categories[def.categoryId]) {
            bonkMods.registerCategory({
                id: def.categoryId,
                label: def.categoryId.charAt(0).toUpperCase() + def.categoryId.slice(1),
                order: 100
            });
        }

        if (!bonkMods._mods[def.modId] && def.modId === FALLBACK_MOD_ID) {
            bonkMods.registerMod({
                id: FALLBACK_MOD_ID,
                name: 'Other Mods',
                description: 'Blocks from mods that did not register themselves.'
            });
        }

        renderModDropdown();
        renderCategories();
        renderBlocks();
    };

    bonkMods.addModdedBlock = function(def) {
        if (!def) return;
        bonkMods.addBlock(Object.assign({ categoryId: DEFAULT_CATEGORY_ID }, def));
    };

    function setupSettingsShell(settingsContainer) {
        const topBar = $('settings_topBar');
        const closeBtn = $('settings_close');
        if (!topBar || !closeBtn) return;
        if ($('mod_tabs')) {
            renderModDropdown();
            renderModInfo();
            renderCategories();
            renderBlocks();
            return;
        }

        topBar.style.display = 'flex';
        topBar.style.alignItems = 'center';
        topBar.style.padding = '0 10px';
        topBar.style.boxSizing = 'border-box';

        const title = document.createElement('div');
        title.textContent = 'Settings';
        title.style.fontWeight = 'bold';
        title.style.marginRight = '12px';

        const tabs = document.createElement('div');
        tabs.id = 'mod_tabs';
        tabs.style.display = 'flex';
        tabs.style.gap = '6px';
        tabs.style.margin = '0 auto';
        tabs.innerHTML = `
            <div class="brownButton brownButton_classic buttonShadow mod_tab active" data-tab="generic">Generic</div>
            <div class="brownButton brownButton_classic buttonShadow mod_tab mod_tab_modded" data-tab="modded">
                Modded <span id="mod_tab_modname" style="font-weight:normal;opacity:0.7;"></span> ▾
            </div>
        `;

        closeBtn.style.position = 'static';
        closeBtn.style.marginLeft = 'auto';

        topBar.textContent = '';
        topBar.appendChild(title);
        topBar.appendChild(tabs);
        topBar.appendChild(closeBtn);

        if (!$('#bonk_mod_core_css')) {
            const style = document.createElement('style');
            style.id = 'bonk_mod_core_css';
            style.textContent = `
                .mod_tab {
                    padding: 4px 10px !important;
                    font-size: 13px;
                    line-height: normal;
                    height: auto !important;
                    opacity: 0.75;
                    position: relative;
                }
                .mod_tab.active { opacity: 1; }

                #mod_dropdown {
                    position: absolute;
                    top: 100%;
                    left: 0;
                    margin-top: 4px;
                    background: rgba(16,27,38,0.98);
                    border-radius: 6px;
                    box-shadow: 0 8px 20px rgba(0,0,0,0.6);
                    padding: 6px;
                    min-width: 200px;
                    z-index: 100000;
                    display: none;
                }
                #mod_dropdown_title {
                    font-size:11px;
                    text-transform:uppercase;
                    opacity:.8;
                    margin-bottom:4px;
                }
                .mod_dropdown_item {
                    font-size:12px;
                    padding:4px 6px;
                    border-radius:4px;
                    cursor:pointer;
                    display:flex;
                    justify-content:space-between;
                    align-items:center;
                    gap:4px;
                }
                .mod_dropdown_item span {
                    pointer-events:none;
                }
                .mod_dropdown_item small {
                    opacity:.7;
                    font-size:10px;
                }
                .mod_dropdown_item:hover {
                    background:rgba(255,255,255,0.08);
                }
                .mod_dropdown_item.active {
                    background:rgba(121,85,248,0.4);
                }

                #mod_modded_settings {
                    display:flex;
                    flex-direction:column;
                    height:100%;
                    box-sizing:border-box;
                    margin-top:4px;
                }

                #mod_modinfo {
                    padding:4px 7px 6px 7px;
                    margin-bottom:6px;
                    border-radius:4px;
                    background:rgba(0,0,0,0.18);
                    font-size:11px;
                }
                #mod_modinfo_title {
                    font-weight:bold;
                    font-size:12px;
                }
                #mod_modinfo_meta {
                    opacity:.8;
                    margin:1px 0 3px 0;
                }
                #mod_modinfo_desc {
                    opacity:.9;
                }
                #mod_modinfo_link a {
                    color:#9fd4ff;
                    text-decoration:underline;
                }

                #mod_cat_tabs {
                    display:flex;
                    gap:6px;
                    margin:4px 0 6px 0;
                    flex-wrap:wrap;
                }
                .mod_cat_tab {
                    padding:3px 9px !important;
                    font-size:12px;
                    height:auto !important;
                    cursor:pointer;
                    opacity:.75;
                }
                .mod_cat_tab.active {
                    opacity:1;
                    outline:1px solid rgba(255,255,255,0.25);
                }

                #mod_blocks_scroll {
                    position:relative;
                    flex:1 1 auto;
                    overflow-y:auto;
                    overflow-x:hidden;
                    padding-right:6px;
                    border-radius:4px;
                    background:rgba(0,0,0,0.1);
                }

                #mod_blocks_scroll::-webkit-scrollbar {
                    width:8px;
                }
                #mod_blocks_scroll::-webkit-scrollbar-track {
                    background:rgba(0,0,0,0.25);
                    border-radius:4px;
                }
                #mod_blocks_scroll::-webkit-scrollbar-thumb {
                    background:linear-gradient(#32485d,#182430);
                    border-radius:4px;
                    border:1px solid rgba(255,255,255,0.15);
                }
                #mod_blocks_scroll::-webkit-scrollbar-thumb:hover {
                    background:linear-gradient(#3e566d,#1f3140);
                }
                #mod_blocks_scroll {
                    scrollbar-width:thin;
                    scrollbar-color:#32485d rgba(0,0,0,0.25);
                }

                .mod_block {
                    padding:8px 6px 10px 6px;
                    border-bottom:1px solid rgba(255,255,255,0.08);
                }
                .mod_block:first-child {
                    border-top:1px solid rgba(255,255,255,0.08);
                }
                .mod_block_title {
                    font-weight:bold;
                    font-size:13px;
                }
                .mod_block_sub {
                    font-size:11px;
                    opacity:.8;
                    margin-top:2px;
                    margin-bottom:6px;
                }
            `;
            document.head.appendChild(style);
        }

        const genericWrap = document.createElement('div');
        genericWrap.id = 'mod_generic_settings';

        const moddedWrap = document.createElement('div');
        moddedWrap.id = 'mod_modded_settings';
        moddedWrap.style.display = 'none';
        moddedWrap.style.padding = '10px 10px 6px 10px';

        const modInfo = document.createElement('div');
        modInfo.id = 'mod_modinfo';
        modInfo.innerHTML = `
            <div id="mod_modinfo_title"></div>
            <div id="mod_modinfo_meta"></div>
            <div id="mod_modinfo_desc"></div>
            <div id="mod_modinfo_link"></div>
        `;

        const catTabs = document.createElement('div');
        catTabs.id = 'mod_cat_tabs';

        const blocksScroll = document.createElement('div');
        blocksScroll.id = 'mod_blocks_scroll';

        moddedWrap.appendChild(modInfo);
        moddedWrap.appendChild(catTabs);
        moddedWrap.appendChild(blocksScroll);

        [...settingsContainer.children].forEach(el => {
            if (
                el !== topBar &&
                el !== closeBtn &&
                el.id !== 'settings_cancelButton' &&
                el.id !== 'settings_saveButton'
            ) {
                genericWrap.appendChild(el);
            }
        });

        settingsContainer.insertBefore(genericWrap, settingsContainer.children[1]);
        settingsContainer.insertBefore(moddedWrap, $('settings_cancelButton'));

        // Primary tab switching
        tabs.querySelectorAll('.mod_tab').forEach(tab => {
            tab.addEventListener('click', (e) => {
                const which = tab.dataset.tab;
                tabs.querySelectorAll('.mod_tab').forEach(t =>
                    t.classList.toggle('active', t === tab)
                );
                const isModded = which === 'modded';
                genericWrap.style.display = isModded ? 'none' : 'block';
                moddedWrap.style.display = isModded ? 'flex' : 'none';

                // toggle dropdown when clicking the Modded tab body
                if (which === 'modded') {
                    e.stopPropagation();
                    toggleModDropdown();
                } else {
                    hideModDropdown();
                }
            });
        });

        // Dropdown container under Modded tab
        const moddedTab = topBar.querySelector('.mod_tab_modded');
        const dropdown = document.createElement('div');
        dropdown.id = 'mod_dropdown';
        dropdown.innerHTML = `
            <div id="mod_dropdown_title">Select mod</div>
            <div id="mod_dropdown_list"></div>
        `;
        moddedTab.style.position = 'relative';
        moddedTab.appendChild(dropdown);

        document.addEventListener('click', (e) => {
            if (!dropdown.contains(e.target) && !moddedTab.contains(e.target)) {
                hideModDropdown();
            }
        });

        bonkMods.registerCategory({ id: DEFAULT_CATEGORY_ID, label: 'General', order: 0 });

        bonkMods._mods['all'] = {
            id: 'all',
            name: 'All Mods',
            description: 'Show settings for every registered mod.',
            version: '',
            author: '',
            homepage: ''
        };

        bonkMods.registerMod({
            id: '__dev',
            name: 'Developers',
            version: '',
            author: 'Bonk Mod Settings Core',
            description: 'Information for script developers on how to hook into this UI.',
            homepage: ''
        });

        bonkMods.registerCategory({
            id: '__dev_cat',
            label: 'Developer Docs',
            order: 999
        });

        bonkMods.addBlock({
            id: '__dev_block',
            modId: '__dev',
            categoryId: '__dev_cat',
            title: 'Using Bonk Mod Settings Core',
            order: 0,
            render(container) {
                container.innerHTML = `
                    <div class="mod_block_sub">
                        Your script can register itself as a mod and add settings blocks here.
                    </div>
<pre style="font-size:10px;line-height:1.3;background:rgba(0,0,0,0.25);padding:4px 6px;border-radius:4px;white-space:pre-wrap;">
// Register your mod
bonkMods.registerMod({
  id: 'recolor',
  name: 'Re:Color',
  version: '1.0.0',
  author: 'You',
  description: 'Colour groups for player names.'
});

// (optional) register extra categories
bonkMods.registerCategory({
  id: 'cosmetics',
  label: 'Cosmetics',
  order: 10
});

// Add a block of settings
bonkMods.addBlock({
  id: 'recolor_groups',
  modId: 'recolor',
  categoryId: 'cosmetics',
  title: 'Colour Groups',
  order: 5,
  render(container) {
    // build your UI into container
  }
});</pre>
                    <div style="font-size:10px;opacity:.8;margin-top:4px;">
                        The core fires a <code>bonkModsReady</code> event on <code>window</code> when
                        it has initialised, so you can safely wait for it if load order is uncertain.
                    </div>
                `;
            }
        });

        // signal ready
        global.dispatchEvent(new Event('bonkModsReady'));

        renderModDropdown();
        renderModInfo();
        renderCategories();
        renderBlocks();
    }

    function toggleModDropdown() {
        const dd = $('mod_dropdown');
        if (!dd) return;
        dd.style.display = (dd.style.display === 'none' || dd.style.display === '') ? 'block' : 'none';
    }
    function hideModDropdown() {
        const dd = $('mod_dropdown');
        if (!dd) return;
        dd.style.display = 'none';
    }

    function renderModDropdown() {
        const listEl = $('mod_dropdown_list');
        const modNameSpan = $('mod_tab_modname');
        if (!listEl || !bonkMods._mods) return;

        const modsArr = Object.values(bonkMods._mods);
        if (!modsArr.length) return;

        if (!bonkMods._mods['all']) {
            bonkMods._mods['all'] = {
                id: 'all',
                name: 'All Mods',
                description: '',
                version: '',
                author: ''
            };
        }

        if (!currentModId || !bonkMods._mods[currentModId]) {
            currentModId = 'all';
        }

        modsArr.sort((a, b) => {
            if (a.id === 'all') return -1;
            if (b.id === 'all') return 1;
            if (a.name === b.name) return a.id > b.id ? 1 : -1;
            return a.name.localeCompare(b.name);
        });

        listEl.textContent = '';
        modsArr.forEach(mod => {
            const item = document.createElement('div');
            item.className = 'mod_dropdown_item' + (mod.id === currentModId ? ' active' : '');
            item.dataset.modId = mod.id;
            item.innerHTML = `
                <span>${mod.name}</span>
                <small>${mod.version || ''}</small>
            `;
            item.addEventListener('click', (e) => {
                e.stopPropagation();
                currentModId = mod.id;
                renderModDropdown();
                renderModInfo();
                renderCategories();
                renderBlocks();
                hideModDropdown();
            });
            listEl.appendChild(item);
        });

        if (modNameSpan && bonkMods._mods[currentModId]) {
            const label = currentModId === 'all'
                ? ''
                : '(' + bonkMods._mods[currentModId].name + ')';
            modNameSpan.textContent = label;
        }
    }

    function renderModInfo() {
        const mod = bonkMods._mods[currentModId] || bonkMods._mods['all'];
        const t = $('mod_modinfo_title');
        const m = $('mod_modinfo_meta');
        const d = $('mod_modinfo_desc');
        const l = $('mod_modinfo_link');
        if (!t || !m || !d || !l || !mod) return;

        t.textContent = mod.name || 'All Mods';
        m.textContent = [
            mod.version ? `v${mod.version}` : '',
            mod.author ? `by ${mod.author}` : ''
        ].filter(Boolean).join('  ·  ');
        d.textContent = mod.description || '';

        if (mod.homepage) {
            l.innerHTML = `<a href="${mod.homepage}" target="_blank" rel="noopener">Open page</a>`;
        } else {
            l.textContent = '';
        }

        if (mod.id === '__dev' && mod.devHint) {
            const extra = document.createElement('div');
            extra.style.fontSize = '10px';
            extra.style.opacity = '0.8';
            extra.style.marginTop = '3px';
            extra.textContent = mod.devHint;
            d.appendChild(extra);
        }
    }

    function renderCategories() {
        const catTabs = $('mod_cat_tabs');
        if (!catTabs) return;

        const blocks = bonkMods._blocks || [];

        const usedCatIds = new Set();
        blocks.forEach(b => {
            if (currentModId === 'all' || b.modId === currentModId) {
                usedCatIds.add(b.categoryId);
            }
        });

        const allCats = Object.values(bonkMods._categories)
            .filter(c => usedCatIds.has(c.id));
        if (!allCats.length) {
            catTabs.textContent = '';
            currentCategoryId = null;
            $('mod_blocks_scroll') && ( $('mod_blocks_scroll').textContent = '' );
            return;
        }

        allCats.sort((a, b) => {
            if (a.order === b.order) return a.label > b.label ? 1 : -1;
            return a.order - b.order;
        });

        if (!currentCategoryId || !usedCatIds.has(currentCategoryId)) {
            currentCategoryId = allCats[0].id;
        }

        catTabs.textContent = '';
        allCats.forEach(cat => {
            const btn = document.createElement('div');
            btn.className = 'brownButton brownButton_classic buttonShadow mod_cat_tab' +
                (cat.id === currentCategoryId ? ' active' : '');
            btn.dataset.catId = cat.id;
            btn.textContent = cat.label;
            btn.addEventListener('click', () => {
                currentCategoryId = cat.id;
                renderCategories();
                renderBlocks();
            });
            catTabs.appendChild(btn);
        });
    }

    function renderBlocks() {
        const scroll = $('mod_blocks_scroll');
        if (!scroll) return;
        const blocks = bonkMods._blocks || [];

        scroll.textContent = '';
        scroll.scrollTop = 0;

        if (!blocks.length || !currentCategoryId) return;

        const arr = blocks.slice().sort((a, b) => {
            const ca = bonkMods._categories[a.categoryId] || { order: 999 };
            const cb = bonkMods._categories[b.categoryId] || { order: 999 };
            if (ca.order !== cb.order) return ca.order - cb.order;
            if (a.order === b.order) return a.id > b.id ? 1 : -1;
            return a.order - b.order;
        });

        arr.forEach(def => {
            if (def.categoryId !== currentCategoryId) return;
            if (currentModId !== 'all' && def.modId !== currentModId) return;

            const block = document.createElement('div');
            block.className = 'mod_block';
            block.dataset.blockId = def.id;

            if (def.title) {
                const titleEl = document.createElement('div');
                titleEl.className = 'mod_block_title';
                titleEl.textContent = def.title;
                block.appendChild(titleEl);
            }

            const content = document.createElement('div');
            block.appendChild(content);
            scroll.appendChild(block);

            try {
                def.render(content);
            } catch (e) {
                console.error('[BonkModSettingsCore] error rendering block', def.id, e);
                content.textContent = 'Error loading this mod block.';
            }
        });
    }

    waitForElement('settingsContainer', setupSettingsShell);
})();