Bonk Mod Settings Core

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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);
})();