Bonk Chat Macros (BonkMods)

F-key chat macros for Bonk.io with per-account storage.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Bonk Chat Macros (BonkMods)
// @namespace    https://greasyfork.org/en/users/1552147-ansonii-crypto
// @version      0.0.1
// @description  F-key chat macros for Bonk.io with per-account storage.
// @match        https://bonk.io/gameframe-release.html
// @run-at       document-start
// @grant        none
// @license      N/A
// ==/UserScript==

(() => {
    'use strict';

    const STORAGE_KEY_BASE = 'bonk_mod_chatmacros_';

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

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

    function normalizeName(name) {
        return (name || '').trim().toLowerCase();
    }

    function injectChatMacrosStyles() {
        if (document.getElementById('chatmacros_smart_ui_styles')) return;

        const style = document.createElement('style');
        style.id = 'chatmacros_smart_ui_styles';
        style.textContent = `
            .smartchat-row {
                margin-bottom: 4px;
            }

            .smartchat-toggle-label {
                display: flex;
                align-items: center;
                gap: 6px;
                cursor: pointer;
                font-size: 11px;
                user-select: none;
            }
            .smartchat-toggle-input {
                display: none;
            }
            .smartchat-toggle-switch {
                position: relative;
                width: 30px;
                height: 14px;
                border-radius: 999px;
                background: #0000001A;
                box-shadow: inset 0 0 0 1px rgba(0,0,0,0.6);
                transition: background 0.15s ease-out, box-shadow 0.15s ease-out;
            }
            .smartchat-toggle-knob {
                position: absolute;
                top: 2px;
                left: 2px;
                width: 10px;
                height: 10px;
                border-radius: 50%;
                background: #e5e5e5;
                box-shadow: 0 0 2px rgba(0,0,0,0.5);
                transition: transform 0.15s ease-out;
            }
            .smartchat-toggle-input:checked + .smartchat-toggle-switch {
                background: #009688;
                box-shadow: inset 0 0 0 1px rgba(0,0,0,0.4);
            }
            .smartchat-toggle-input:checked + .smartchat-toggle-switch .smartchat-toggle-knob {
                transform: translateX(14px);
            }

            .smartchat-tag-label {
                font-size: 11px;
                margin-bottom: 2px;
            }
            .smartchat-tag-input {
                display: flex;
                flex-wrap: wrap;
                gap: 4px;
                padding: 3px;
                min-height: 20px;
                background: #0000001A;
                border-radius: 4px;
                border: 1px solid #555;
            }
            .smartchat-tag-input-field {
                flex: 1 1 60px;
                min-width: 40px;
                border: none;
                outline: none;
                background: transparent;
                font-size: 11px;
                color: inherit;
            }
        `;
        (document.head || document.documentElement).appendChild(style);
    }

    injectChatMacrosStyles();

    let storageKey = STORAGE_KEY_BASE + 'default';
    let lastAccountName = null;
    let nameObserverInitialized = false;

    let chatConfig = null;

    function defaultConfig() {
        const keys = {};
        for (let i = 1; i <= 8; i++) {
            keys['F' + i] = '';
        }
        return {
            enabled: true,
            keys
        };
    }

    function loadConfig() {
        try {
            const raw = localStorage.getItem(storageKey);
            if (!raw) {
                chatConfig = defaultConfig();
                return;
            }
            const data = JSON.parse(raw);
            if (!data || typeof data !== 'object') {
                chatConfig = defaultConfig();
                return;
            }

            const merged = defaultConfig();
            if (typeof data.enabled === 'boolean') merged.enabled = data.enabled;
            if (data.keys && typeof data.keys === 'object') {
                for (let i = 1; i <= 8; i++) {
                    const k = 'F' + i;
                    if (typeof data.keys[k] === 'string') {
                        merged.keys[k] = data.keys[k];
                    }
                }
            }
            chatConfig = merged;
        } catch (e) {
            console.error('[ChatMacros] Failed to load config:', e);
            chatConfig = defaultConfig();
        }
    }

    function saveConfig() {
        if (!chatConfig) return;
        try {
            localStorage.setItem(storageKey, JSON.stringify(chatConfig));
        } catch (e) {
            console.error('[ChatMacros] Failed to save config:', e);
        }
    }

    function updateAccountFromName() {
        const el = $('pretty_top_name');
        const account = normalizeName(el ? el.textContent : '') || 'guest';
        if (account === lastAccountName) return;

        lastAccountName = account;
        storageKey = STORAGE_KEY_BASE + account;
        loadConfig();

        if (window.bonkMods) {
            renderSettingsBlockCached();
        }
    }

    function determineStorageKey() {
        updateAccountFromName();

        const nameEl = $('pretty_top_name');
        if (nameEl && !nameObserverInitialized) {
            const obs = new MutationObserver(() => {
                updateAccountFromName();
            });
            obs.observe(nameEl, {
                childList: true,
                characterData: true,
                subtree: true
            });
            nameObserverInitialized = true;
        }
    }

    waitForElement('pretty_top_name', determineStorageKey);

    loadConfig();

    function isInGame() {
        return !!( $('ingameui') || $('gamecanvas') || $('roomHolder') );
    }

    function findChatInput() {
        const active = document.activeElement;
        if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')) {
            const type = (active.type || '').toLowerCase();
            if (type === '' || type === 'text' || type === 'search') {
                return active;
            }
        }

        const candidates = Array.from(
            document.querySelectorAll('input[type="text"], input:not([type]), textarea')
        );
        for (const el of candidates) {
            const style = window.getComputedStyle(el);
            if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue;
            if (el.disabled || el.readOnly) continue;
            const rect = el.getBoundingClientRect();
            if (rect.height > 0 && rect.width > 0 && rect.height <= 40) {
                return el;
            }
        }
        return null;
    }

    function sendChatMessageDOM(msg) {
        const text = (msg || '').trim();
        if (!text) return false;

        const input = findChatInput();
        if (!input) {
            console.warn('[ChatMacros] No chat input found for DOM send.');
            return false;
        }

        input.focus();
        input.value = text;

        const inputEv = new Event('input', { bubbles: true });
        input.dispatchEvent(inputEv);

        const enterOpts = {
            key: 'Enter',
            code: 'Enter',
            keyCode: 13,
            which: 13,
            bubbles: true,
            cancelable: true
        };
        const kd = new KeyboardEvent('keydown', enterOpts);
        const ku = new KeyboardEvent('keyup', enterOpts);
        kd._chatMacroSynthetic = true;
        ku._chatMacroSynthetic = true;

        input.dispatchEvent(kd);
        input.dispatchEvent(ku);

        return true;
    }

    let keyHandlerAttached = false;

    function handleKeydown(e) {
        if (e._chatMacroSynthetic) return;

        const key = e.key;
        if (!/^F[1-8]$/.test(key)) return;

        if (!chatConfig) loadConfig();
        if (!chatConfig || !chatConfig.enabled) return;

        const msg = (chatConfig.keys && chatConfig.keys[key]) || '';
        if (!msg.trim()) {
            return;
        }

        if (isInGame() && !findChatInput()) {
            return;
        }

        const ok = sendChatMessageDOM(msg);
        if (!ok) {
            return;
        }

        e.preventDefault();
        e.stopPropagation();
        if (typeof e.stopImmediatePropagation === 'function') {
            e.stopImmediatePropagation();
        }
    }

    function attachKeyHandler() {
        if (keyHandlerAttached) return;
        keyHandlerAttached = true;
        // capture=true so we get a chance to act before Bonk
        window.addEventListener('keydown', handleKeydown, true);
    }

    attachKeyHandler();

    let settingsRendererInstance = null;

    function renderSettingsBlock(container) {
        settingsRendererInstance = container;
        container.innerHTML = '';

        if (!chatConfig) loadConfig();

        const root = document.createElement('div');
        root.style.fontSize = '12px';
        container.appendChild(root);

        const mkRow = () => {
            const div = document.createElement('div');
            div.className = 'smartchat-row';
            root.appendChild(div);
            return div;
        };

        const mkCheckbox = (labelText, initial, onChange) => {
            const row = mkRow();

            const label = document.createElement('label');
            label.className = 'smartchat-toggle-label';

            const input = document.createElement('input');
            input.type = 'checkbox';
            input.className = 'smartchat-toggle-input';
            input.checked = !!initial;

            const switchSpan = document.createElement('span');
            switchSpan.className = 'smartchat-toggle-switch';

            const knob = document.createElement('span');
            knob.className = 'smartchat-toggle-knob';
            switchSpan.appendChild(knob);

            const textSpan = document.createElement('span');
            textSpan.textContent = labelText;

            input.addEventListener('change', () => onChange(input.checked));

            label.appendChild(input);
            label.appendChild(switchSpan);
            label.appendChild(textSpan);

            row.appendChild(label);
            return input;
        };

        const mkMacroInput = (keyName, initial, onChange) => {
            const row = mkRow();

            const label = document.createElement('div');
            label.className = 'smartchat-tag-label';
            label.textContent = keyName + ':';
            row.appendChild(label);

            const wrapper = document.createElement('div');
            wrapper.className = 'smartchat-tag-input';

            const input = document.createElement('input');
            input.type = 'text';
            input.className = 'smartchat-tag-input-field';
            input.value = initial || '';
            input.placeholder = 'Message sent by ' + keyName;

            input.addEventListener('input', () => {
                onChange(input.value);
            });

            wrapper.appendChild(input);
            row.appendChild(wrapper);
        };

        mkCheckbox('Enable F-key chat macros', chatConfig.enabled, val => {
            chatConfig.enabled = val;
            saveConfig();
        });

        const infoRow = mkRow();
        const info = document.createElement('div');
        info.style.fontSize = '11px';
        info.style.opacity = '0.8';
        info.textContent =
            'Define quick chat messages for F1–F8. Works in lobby / text chat. In-game canvas chat will still use Bonk’s default F-keys if no text field exists.';
        infoRow.appendChild(info);

        for (let i = 1; i <= 8; i++) {
            const k = 'F' + i;
            const value = (chatConfig.keys && chatConfig.keys[k]) || '';
            mkMacroInput(k, value, newVal => {
                chatConfig.keys[k] = newVal;
                saveConfig();
            });
        }
    }

    function renderSettingsBlockCached() {
        if (settingsRendererInstance) {
            renderSettingsBlock(settingsRendererInstance);
        }
    }

    function initBonkModsIntegration() {
        const gm = window.bonkMods;
        if (!gm || !gm.addBlock || !gm.registerMod) return;

        // Register our mod
        gm.registerMod({
            id: 'chatmacros',
            name: 'Chat Macros',
            version: '1.1.0',
            author: 'You',
            description: 'Assign custom F1–F8 quick chat messages with per-account storage.'
        });

        gm.registerCategory({
            id: 'chat',
            label: 'Chat',
            order: 5
        });

        gm.addBlock({
            id: 'chatmacros_main',
            modId: 'chatmacros',
            categoryId: 'chat',
            title: 'Chat Macros',
            order: 1,
            render: renderSettingsBlock
        });
    }

    if (window.bonkMods) {
        initBonkModsIntegration();
    } else {
        window.addEventListener('bonkModsReady', initBonkModsIntegration);
    }
})();