IQRPG+ Enhanced

Audio signals for various aspects of IQRPG with enhanced GUI

// ==UserScript==
// @name         IQRPG+ Enhanced
// @namespace    https://www.iqrpg.com/
// @version      0.2.0
// @description  Audio signals for various aspects of IQRPG with enhanced GUI
// @Author       Bunjo & vifs, enhanced by Sanjin
// @match        http://iqrpg.com/game.html
// @match        https://iqrpg.com/game.html
// @match        http://www.iqrpg.com/game.html
// @match        https://www.iqrpg.com/game.html
// @require      http://code.jquery.com/jquery-latest.js
// @grant        none
// @license      MIT
// @homepageURL  https://github.com/yourusername/iqrpg-plus
// @supportURL   https://github.com/yourusername/iqrpg-plus/issues
// ==/UserScript==

/*
    Shout out to Xortrox & euphone & Karubo for their contributions to the script.

    Enhanced by: Sanjin
*/

// Global variables for script outside the namespace
let globalUserSettings = {};
const OldSocket = window.WebSocket;

// WebSocket interception at top level for immediate execution
window.WebSocket = function WebSocket(url, protocols) {
    const socket = new OldSocket(...arguments);
    window.socket = socket;

    socket.addEventListener('message', function(event) {
        try {
            const message = JSON.parse(event.data);

            switch(message.type){
                case 'playersOnline':
                case 'loadMessages':
                case 'addItemsToUser':
                case 'notification':
                case 'bonus':
                    break;
                case 'event':
                    // Handle resource events (woodcutting, mining, quarrying)
                    if (!message.data || !message.data.type) return;

                    const eventType = message.data.type;
                    const timeRemaining = message.data.timeRemaining || 0;

                    if (eventType === "woodcutting" && globalUserSettings.eventAlert_Woodcutting) {
                        // Handle woodcutting event
                        if (globalUserSettings.eventAudioAlert) {
                            window.IQRPG_Plus.playSound(globalUserSettings.eventAlertSoundURL);
                        }
                        if (globalUserSettings.eventDesktopAlert) {
                            window.IQRPG_Plus.showNotification('IQRPG Event!', 'Woodcutting event has started!');
                        }

                        // Set timeout for end of event
                        setTimeout(function() {
                            if (globalUserSettings.eventAudioAlertFinished) {
                                window.IQRPG_Plus.playSound(globalUserSettings.eventAlertSoundURL);
                            }
                            if (globalUserSettings.eventDesktopAlert) {
                                window.IQRPG_Plus.showNotification('IQRPG Event Finished!', 'Woodcutting event has ended!');
                            }
                        }, timeRemaining * 10);
                    }
                    else if (eventType === "mining" && globalUserSettings.eventAlert_Mining) {
                        // Handle mining event
                        if (globalUserSettings.eventAudioAlert) {
                            window.IQRPG_Plus.playSound(globalUserSettings.eventAlertSoundURL);
                        }
                        if (globalUserSettings.eventDesktopAlert) {
                            window.IQRPG_Plus.showNotification('IQRPG Event!', 'Mining event has started!');
                        }

                        // Set timeout for end of event
                        setTimeout(function() {
                            if (globalUserSettings.eventAudioAlertFinished) {
                                window.IQRPG_Plus.playSound(globalUserSettings.eventAlertSoundURL);
                            }
                            if (globalUserSettings.eventDesktopAlert) {
                                window.IQRPG_Plus.showNotification('IQRPG Event Finished!', 'Mining event has ended!');
                            }
                        }, timeRemaining * 10);
                    }
                    else if (eventType === "quarrying" && globalUserSettings.eventAlert_Quarrying) {
                        // Handle quarrying event
                        if (globalUserSettings.eventAudioAlert) {
                            window.IQRPG_Plus.playSound(globalUserSettings.eventAlertSoundURL);
                        }
                        if (globalUserSettings.eventDesktopAlert) {
                            window.IQRPG_Plus.showNotification('IQRPG Event!', 'Quarrying event has started!');
                        }

                        // Set timeout for end of event
                        setTimeout(function() {
                            if (globalUserSettings.eventAudioAlertFinished) {
                                window.IQRPG_Plus.playSound(globalUserSettings.eventAlertSoundURL);
                            }
                            if (globalUserSettings.eventDesktopAlert) {
                                window.IQRPG_Plus.showNotification('IQRPG Event Finished!', 'Quarrying event has ended!');
                            }
                        }, timeRemaining * 10);
                    }
                    break;
                case 'msg':
                    // Handle different types of chat messages
                    if (!message.data || !message.data.type) return;

                    switch(message.data.type) {
                        case 'clanGlobal':
                            if (message.data.msg && message.data.msg.startsWith('The watchtower')) {
                                if (globalUserSettings.watchtowerAudioAlert) {
                                    window.IQRPG_Plus.playSound(globalUserSettings.watchtowerAlertSoundURL);
                                }
                                if (globalUserSettings.watchtowerDesktopAlert) {
                                    window.IQRPG_Plus.showNotification('IQRPG Watchtower!', message.data.msg);
                                }
                            }
                            break;
                        case 'pm-from':
                            if (globalUserSettings.whisperAlertOnlyWhenTabIsInactive) {
                                if (document.hidden) {
                                    if (globalUserSettings.whisperAudioAlert) {
                                        window.IQRPG_Plus.playSound(globalUserSettings.whisperAlertSoundURL);
                                    }
                                    if (globalUserSettings.whisperDesktopAlert) {
                                        window.IQRPG_Plus.showNotification('IQRPG Whisper!', message.data.username + ': ' + message.data.msg);
                                    }
                                }
                            } else {
                                if (globalUserSettings.whisperAudioAlert) {
                                    window.IQRPG_Plus.playSound(globalUserSettings.whisperAlertSoundURL);
                                }
                                if (globalUserSettings.whisperDesktopAlert) {
                                    window.IQRPG_Plus.showNotification('IQRPG Whisper!', message.data.username + ': ' + message.data.msg);
                                }
                            }
                            break;
                        case 'eventGlobal':
                            // Boss event detection
                            if (message.data.msg && message.data.msg.includes('A rift to the dark realm has opened')) {
                                if (globalUserSettings.bossAudioAlert) {
                                    window.IQRPG_Plus.playSound(globalUserSettings.bossAlertSoundURL);
                                }
                                if (globalUserSettings.bossDesktopAlert) {
                                    window.IQRPG_Plus.showNotification('IQRPG Boss!', message.data.msg);
                                }
                            }
                            break;
                        case 'pm-to':
                        case 'msg':
                        case 'global':
                        case 'me':
                            break;
                    }
                    break;
                case 'boss':
                    break;
            }
        } catch (error) {
            // Silent fail
        }
    });

    return socket;
};

// Use a namespace to avoid global scope pollution
window.IQRPG_Plus = (function() {
    // Private variables
    let userSettings = {};
    let isAlerting = false;
    let alertTimer = null;
    let currAutoAudioPlays = 0;
    let desktopNotificationOnCooldown = false;
    let bonusExp = false;
    let isDragging = false;
    let dragOffsetX = 0;
    let dragOffsetY = 0;

    // Store references to UI elements
    const elements = {
        buttonPanel: null,
        buttonContainer: null,
        settingsPanel: null,
        settingsButton: null,
    };

    // Store references to observers
    const observers = {
        main: null,
        land: null,
        mastery: null
    };

    // Default settings
    const defaultSettings = {
        masterAudioLevel: 1,
        autoAudioAlert: true,
        autoAlertSoundURL: 'https://www.pacdv.com/sounds/mechanical_sound_effects/gun-reload-1.wav',
        autoAlertRepeatInSeconds: 2,
        autoAlertNumber: 10,
        autoMaxNumberOfAudioAlerts: 2,
        autoDesktopAlert: true,
        dungeonAudioAlert: true,
        dungeonDesktopAlert: true,
        bossAudioAlert: true,
        bossAlertSoundURL: 'https://www.pacdv.com/sounds/interface_sound_effects/sound8.mp3',
        bossDefeatedSoundURL: 'https://ia801306.us.archive.org/32/items/FF7ACVictoryFanfareRingtoneperfectedMp3/FF7%20AC%20Victory%20Fanfare%20Ringtone%20%28perfected%20mp3%29.mp3',
        bossDesktopAlert: true,
        eventDesktopAlert: true,
        eventAlertSoundURL: 'https://www.pacdv.com/sounds/interface_sound_effects/sound8.mp3',
        eventAlert_Woodcutting: true,
        eventAlert_Quarrying: true,
        eventAlert_Mining: true,
        eventAudioAlert: true,
        eventAudioAlertFinished: true,
        whisperAudioAlert: true,
        whisperAlertSoundURL: 'https://www.pacdv.com/sounds/mechanical_sound_effects/spring_1.wav',
        whisperAlertOnlyWhenTabIsInactive: false,
        whisperDesktopAlert: true,
        landAudioAlert: true,
        landAlertSoundURL: 'https://www.pacdv.com/sounds/mechanical_sound_effects/coins_4.wav',
        masteryAudioAlert: true,
        masteryEveryXLevels: 50,
        masteryAlertSoundURL: 'https://ia801306.us.archive.org/32/items/FF7ACVictoryFanfareRingtoneperfectedMp3/FF7%20AC%20Victory%20Fanfare%20Ringtone%20%28perfected%20mp3%29.mp3',
        effectAudioAlert: true,
        effectAutoLeft: 5,
        effectAlertSoundURL: 'https://www.pacdv.com/sounds/mechanical_sound_effects/hammer-1.mp3',
        watchtowerAudioAlert: true,
        watchtowerAlertSoundURL: 'https://www.pacdv.com/sounds/interface_sound_effects/sound8.mp3',
        watchtowerDesktopAlert: true,
        bonusExpAudioAlert: true,
        bonusExpAlertSoundURL: 'https://www.pacdv.com/sounds/miscellaneous_sounds/magic-wand-1.wav'
    };

    // Load settings from localStorage
    function loadSettings() {
        try {
            const savedSettings = JSON.parse(localStorage.getItem('iqrpgSettings'));
            const result = savedSettings ? Object.assign({}, defaultSettings, savedSettings) : defaultSettings;

            // Copy settings to global variable for WebSocket access
            globalUserSettings = Object.assign({}, result);

            return result;
        } catch (e) {
            // Copy settings to global variable for WebSocket access
            globalUserSettings = Object.assign({}, defaultSettings);
            return defaultSettings;
        }
    }

    // Save settings to localStorage
    function saveSettings() {
        try {
            localStorage.setItem('iqrpgSettings', JSON.stringify(userSettings));
            // Update global settings for WebSocket access
            globalUserSettings = Object.assign({}, userSettings);
        } catch (e) {
            // Silently fail if saving fails
        }
    }

    // Apply settings to checkboxes
    function applySettingsToUI() {
        // Apply checkbox states
        for (const setting in userSettings) {
            const checkbox = document.getElementById(setting);
            if (checkbox && checkbox.type === 'checkbox') {
                checkbox.checked = userSettings[setting];
            }
        }

        // Apply volume slider
        const volumeSlider = document.getElementById('volumeSlider');
        if (volumeSlider) {
            volumeSlider.value = userSettings.masterAudioLevel;
        }
    }

    // Initialize the userscript
    function init() {
        userSettings = loadSettings();
        // Ensure we have up-to-date settings
        saveSettings();
        createUI();
        setupObservers();
        setupEventListeners();

        // Check Notification permissions
        if (Notification.permission !== "denied") {
            Notification.requestPermission();
        }
    }

    // Create and setup UI elements
    function createUI() {
        // Create main panel
        elements.buttonPanel = document.createElement('div');
        elements.buttonPanel.id = 'buttonPanel';

        // Create settings panel
        elements.settingsPanel = document.createElement('div');
        elements.settingsPanel.id = 'settingsPanel';
        elements.settingsPanel.className = 'hidden';
        elements.settingsPanel.innerHTML = `
            <h2>Settings</h2>
            <label>Master Volume: <input type="range" id="volumeSlider" min="0" max="1" step="0.1" value="${userSettings.masterAudioLevel}"></label>
            <button id="resetSettingsButton">Reset Settings</button>
            <div style="margin-top: 10px;">
                <label style="display: block;"><input type="checkbox" id="autoAudioAlert" ${userSettings.autoAudioAlert ? 'checked' : ''}> Auto Audio Alert</label>
                <label style="display: block;"><input type="checkbox" id="autoDesktopAlert" ${userSettings.autoDesktopAlert ? 'checked' : ''}> Auto Desktop Alert</label>
                <label style="display: block;"><input type="checkbox" id="dungeonAudioAlert" ${userSettings.dungeonAudioAlert ? 'checked' : ''}> Dungeon Audio Alert</label>
                <label style="display: block;"><input type="checkbox" id="dungeonDesktopAlert" ${userSettings.dungeonDesktopAlert ? 'checked' : ''}> Dungeon Desktop Alert</label>
                <label style="display: block;"><input type="checkbox" id="bossAudioAlert" ${userSettings.bossAudioAlert ? 'checked' : ''}> Boss Audio Alert</label>
                <label style="display: block;"><input type="checkbox" id="bossDesktopAlert" ${userSettings.bossDesktopAlert ? 'checked' : ''}> Boss Desktop Alert</label>
                <label style="display: block;"><input type="checkbox" id="eventAudioAlert" ${userSettings.eventAudioAlert ? 'checked' : ''}> Event Audio Alert</label>
                <label style="display: block;"><input type="checkbox" id="eventAudioAlertFinished" ${userSettings.eventAudioAlertFinished ? 'checked' : ''}> Event Finished Audio Alert</label>
                <label style="display: block;"><input type="checkbox" id="eventDesktopAlert" ${userSettings.eventDesktopAlert ? 'checked' : ''}> Event Desktop Alert</label>
                <label style="display: block;"><input type="checkbox" id="whisperAudioAlert" ${userSettings.whisperAudioAlert ? 'checked' : ''}> Whisper Audio Alert</label>
                <label style="display: block;"><input type="checkbox" id="whisperDesktopAlert" ${userSettings.whisperDesktopAlert ? 'checked' : ''}> Whisper Desktop Alert</label>
                <label style="display: block;"><input type="checkbox" id="landAudioAlert" ${userSettings.landAudioAlert ? 'checked' : ''}> Land Audio Alert</label>
                <label style="display: block;"><input type="checkbox" id="masteryAudioAlert" ${userSettings.masteryAudioAlert ? 'checked' : ''}> Mastery Audio Alert</label>
                <label style="display: block;"><input type="checkbox" id="effectAudioAlert" ${userSettings.effectAudioAlert ? 'checked' : ''}> Effect Audio Alert</label>
                <label style="display: block;"><input type="checkbox" id="watchtowerAudioAlert" ${userSettings.watchtowerAudioAlert ? 'checked' : ''}> Watchtower Audio Alert</label>
                <label style="display: block;"><input type="checkbox" id="watchtowerDesktopAlert" ${userSettings.watchtowerDesktopAlert ? 'checked' : ''}> Watchtower Desktop Alert</label>
                <label style="display: block;"><input type="checkbox" id="bonusExpAudioAlert" ${userSettings.bonusExpAudioAlert ? 'checked' : ''}> Bonus Exp Audio Alert</label>
            </div>
        `;

        // Create buttons with helper function
        elements.settingsButton = createControlButton('🔔');

        // Create container for buttons
        elements.buttonContainer = document.createElement('div');
        elements.buttonContainer.id = 'buttonContainer';

        // Assemble the UI
        elements.buttonContainer.appendChild(elements.settingsButton);

        elements.buttonPanel.appendChild(elements.buttonContainer);
        elements.buttonPanel.appendChild(elements.settingsPanel);
        document.body.appendChild(elements.buttonPanel);

        // Add stylesheets
        addStyles();

        // Apply saved position
        loadSavedPosition();

        // Initialize settings based on saved values
        applySettingsToUI();
    }

    // Helper to create control buttons
    function createControlButton(icon) {
        const button = document.createElement('button');
        button.textContent = icon;
        button.className = 'control-button';
        return button;
    }

    // Add CSS styles
    function addStyles() {
        const style = document.createElement('style');
        style.textContent = `
            #buttonPanel {
                position: fixed;
                top: 50px;
                right: 10px;
                z-index: 1001;
                background-color: rgba(0, 0, 0, 0.8);
                border: 1px solid #333;
                padding: 0;
                display: flex;
                flex-direction: column;
                cursor: move;
                overflow: visible;
                width: 120px;
                transition: box-shadow 0.2s ease;
            }

            #buttonPanel.dragging {
                box-shadow: 0 0 8px rgba(34, 116, 34, 0.7);
                opacity: 0.9;
            }

            #buttonContainer {
                display: flex;
                flex-direction: row;
                padding: 6px;
                width: 100%;
                box-sizing: border-box;
                justify-content: center;
                cursor: move;
            }

            .control-button {
                background-color: rgba(0, 0, 0, 0.8);
                color: white;
                border: 1px solid #333;
                padding: 8px;
                width: 25px;
                height: 25px;
                line-height: 1;
                text-align: center;
                cursor: pointer;
                font-size: 14px;
                margin: 0 2px;
                display: flex;
                align-items: center;
                justify-content: center;
            }

            .control-button:hover {
                border-color: #227422;
                background-color: rgba(10, 10, 10, 0.9);
            }

            #settingsPanel {
                position: absolute;
                top: 100%;
                left: -1px;
                width: calc(100% + 2px);
                min-width: 250px;
                background-color: rgba(0, 0, 0, 0.8);
                color: white;
                border: 1px solid #333;
                border-top: none;
                padding: 15px;
                display: none;
                z-index: 1002;
                font-family: 'Arial', sans-serif;
                box-sizing: border-box;
                max-height: 80vh;
                overflow-y: auto;
            }

            #resetSettingsButton {
                background-color: rgba(50, 10, 10, 0.8);
                color: white;
                border: 1px solid #333;
                padding: 5px 10px;
                cursor: pointer;
                font-size: 12px;
                margin: 5px 0;
            }

            #resetSettingsButton:hover {
                border-color: #227422;
                background-color: rgba(10, 10, 10, 0.9);
            }

            #volumeSlider {
                margin: 10px 0;
                width: 100%;
                accent-color: #227422;
            }

            #settingsPanel h2 {
                margin-top: 0;
                padding-top: 0;
                font-size: 16px;
                margin-bottom: 10px;
                color: #227422;
            }

            #settingsPanel label {
                font-size: 14px;
            }

            #settingsPanel input[type="checkbox"] {
                accent-color: #227422;
            }
        `;
        document.head.appendChild(style);
    }

    // Load saved position from localStorage
    function loadSavedPosition() {
        try {
            const savedPosition = localStorage.getItem('iqrpgPanelPosition');
            if (savedPosition) {
                const position = JSON.parse(savedPosition);
                elements.buttonPanel.style.left = position.left;
                elements.buttonPanel.style.top = position.top;
                elements.buttonPanel.style.right = 'auto'; // Clear right positioning when using left
            } else {
                // Initial position if no saved position exists
                elements.buttonPanel.style.top = '50px';
                elements.buttonPanel.style.right = '10px';
            }
        } catch (e) {
            // Use default position on error
            elements.buttonPanel.style.top = '50px';
            elements.buttonPanel.style.right = '10px';
        }
    }

    // Setup event listeners for UI elements
    function setupEventListeners() {
        // Settings button
        elements.settingsButton.addEventListener('click', (e) => {
            e.stopPropagation(); // Prevent triggering drag when clicking button
            toggleSettingsPanel();
        });

        // Reset settings button
        const resetSettingsButton = document.getElementById('resetSettingsButton');
        if (resetSettingsButton) {
            resetSettingsButton.addEventListener('click', () => {
                if (confirm('Are you sure you want to reset all settings to default?')) {
                    localStorage.removeItem('iqrpgSettings');
                    userSettings = Object.assign({}, defaultSettings);
                    applySettingsToUI();
                    saveSettings();
                    alert('Settings have been reset to default.');
                }
            });
        }

        // Volume slider
        const volumeSlider = document.getElementById('volumeSlider');
        if (volumeSlider) {
            volumeSlider.addEventListener('input', (event) => {
                userSettings.masterAudioLevel = event.target.value;
                saveSettings();
            });
        }

        // Checkbox settings
        setupSettingsToggleListeners();

        // Make panel draggable
        setupPanelDragging();

        // Start/stop dungeon button
        $('body').on('click', "button:contains('Start Dungeon')", function() {
            stopAlert();
        });

        // Keyboard shortcuts
        setupKeyboardShortcuts();
    }

    // Show/hide settings panel
    function toggleSettingsPanel() {
        if (elements.settingsPanel.style.display === 'none' || elements.settingsPanel.style.display === '') {
            updateSettingsPanelPosition();
            elements.settingsPanel.style.display = 'block';
        } else {
            elements.settingsPanel.style.display = 'none';
        }
    }

    // Graph panel implementation placeholder
    function showGraphPanel() {
        // To be implemented
    }

    // Combat panel implementation placeholder
    function showCombatPanel() {
        // To be implemented
    }

    // Setup toggle listeners for settings checkboxes
    function setupSettingsToggleListeners() {
        const toggleSettings = [
            'autoAudioAlert', 'autoDesktopAlert', 'dungeonAudioAlert', 'dungeonDesktopAlert',
            'bossAudioAlert', 'bossDesktopAlert', 'eventAudioAlert', 'eventDesktopAlert',
            'whisperAudioAlert', 'whisperDesktopAlert', 'landAudioAlert', 'masteryAudioAlert',
            'effectAudioAlert', 'watchtowerAudioAlert', 'watchtowerDesktopAlert', 'bonusExpAudioAlert',
            'eventAudioAlertFinished'
        ];

        toggleSettings.forEach(setting => {
            const checkbox = document.getElementById(setting);
            if (checkbox) {
                checkbox.addEventListener('change', (event) => {
                    userSettings[setting] = event.target.checked;
                    saveSettings();
                });
            }
        });
    }

    // Setup panel dragging functionality
    function setupPanelDragging() {
        elements.buttonPanel.addEventListener('mousedown', (e) => {
            // Only start dragging if clicking on the panel background or button container
            if (e.target === elements.buttonPanel || e.target === elements.buttonContainer) {
                isDragging = true;
                dragOffsetX = e.clientX - elements.buttonPanel.getBoundingClientRect().left;
                dragOffsetY = e.clientY - elements.buttonPanel.getBoundingClientRect().top;

                elements.buttonPanel.classList.add('dragging');
            }
        });

        document.addEventListener('mousemove', (e) => {
            if (isDragging) {
                const newX = e.clientX - dragOffsetX;
                const newY = e.clientY - dragOffsetY;

                // Calculate limits to keep panel within screen bounds
                const maxX = window.innerWidth - elements.buttonPanel.offsetWidth;
                const maxY = window.innerHeight - elements.buttonPanel.offsetHeight;

                // Apply position with limits
                elements.buttonPanel.style.left = Math.max(0, Math.min(maxX, newX)) + 'px';
                elements.buttonPanel.style.top = Math.max(0, Math.min(maxY, newY)) + 'px';
                elements.buttonPanel.style.right = 'auto'; // Clear right positioning

                // If panel is near the right edge, reposition the settings panel
                updateSettingsPanelPosition();
            }
        });

        document.addEventListener('mouseup', () => {
            if (isDragging) {
                isDragging = false;
                elements.buttonPanel.classList.remove('dragging');

                // Save panel position to localStorage for persistence
                const position = {
                    left: elements.buttonPanel.style.left,
                    top: elements.buttonPanel.style.top
                };
                localStorage.setItem('iqrpgPanelPosition', JSON.stringify(position));
            }
        });

        // Stop dragging if mouse leaves the window
        document.addEventListener('mouseleave', () => {
            if (isDragging) {
                isDragging = false;
                elements.buttonPanel.classList.remove('dragging');
            }
        });
    }

    // Update settings panel position based on button panel position
    function updateSettingsPanelPosition() {
        const panelRect = elements.buttonPanel.getBoundingClientRect();
        const screenWidth = window.innerWidth;

        // If panel is close to the right edge of the screen, position settings to the left
        if (panelRect.right + 250 > screenWidth) { // 250px is min-width of settings panel
            elements.settingsPanel.style.left = 'auto';
            elements.settingsPanel.style.right = '-1px';
        } else {
            elements.settingsPanel.style.left = '-1px';
            elements.settingsPanel.style.right = 'auto';
        }

        // Match the width to the button panel
        elements.settingsPanel.style.width = (elements.buttonPanel.offsetWidth + 2) + 'px';
    }

    // Setup keyboard shortcuts
    function setupKeyboardShortcuts() {
        document.addEventListener('keyup', function(e) {
            if (e.altKey === true) {
                let index = -1;
                const channels = $('.chat-channels').children();

                if (isNaN(e.key)) {
                    let direction = null;
                    if (e.which === 38) { // Up arrow
                        direction = true;
                    } else if (e.which === 40) { // Down arrow
                        direction = false;
                    }

                    if (direction !== null) {
                        let chatChannels = channels;

                        if (e.shiftKey === true) {
                            chatChannels = $('.chat-channels').children('.new-message, .active-channel');
                        }

                        index = chatChannels.index($('.active-channel'));
                        if (direction) {
                            index--;
                        } else {
                            index++;
                        }
                    }
                } else {
                    if (e.ctrlKey === true) {
                        index = -1 + parseInt(e.key);
                    }
                }

                if (index >= 0 && index < channels.length) {
                    channels[index].click();
                    $('#chatInput').focus();
                }
            }
        });
    }

    // Setup observers for DOM changes
    function setupObservers() {
        // Main observer for title changes
        const titleTarget = document.querySelector('title');
        if (titleTarget) {
            let lastTitle = document.title; // Track last title to prevent duplicate processing

            observers.main = new MutationObserver(function(mutations) {
                const currentTitle = document.title;
                // Only process if title actually changed
                if (currentTitle !== lastTitle) {
                    lastTitle = currentTitle;
                    handleTitleChange(currentTitle);
                }
            });

            observers.main.observe(titleTarget, {
                characterData: true,
                childList: true,
                subtree: true
            });
        }

        // Set up observer for land timers with delay
        setTimeout(function() {
            // Try different selectors that might contain the timer
            const actionTimer = document.querySelector("div.action-timer__text") ||
                               document.querySelector(".timer") ||
                               document.querySelector("[class*='timer']");

            if (actionTimer) {
                observers.land = new MutationObserver(handleLandMutations);
                observers.land.observe(actionTimer, {
                    characterData: true,
                    childList: true,
                    subtree: true
                });
            }

            // Setup mastery observer
            const masteries = document.querySelectorAll(".clickable > .flex.space-between > .green-text") ||
                             document.querySelectorAll("[class*='mastery-level']") ||
                             document.querySelectorAll("[class*='level']");

            if (masteries && masteries.length > 0) {
                observers.mastery = new MutationObserver(handleMasteryMutations);

                masteries.forEach(function(mastery) {
                    observers.mastery.observe(mastery, {
                        characterData: true,
                        childList: true,
                        subtree: true
                    });
                });
            }

            // Set interval for checking effects and bonus exp
            setInterval(function() {
                checkEffects();
                checkBonusExp();
            }, 5000);

        }, 2000); // Increased delay to ensure elements are loaded
    }

    // Handle auto remaining changes
    function handleAutoRemaining(data) {
        if (!data) return;

        try {
            const autosRemaining = parseInt(data.replace('Autos Remaining: ', ''));
            if (isNaN(autosRemaining)) return;

            if (autosRemaining <= userSettings.autoAlertNumber && userSettings.autoAlertNumber) {
                if (autosRemaining == userSettings.autoAlertNumber && userSettings.autoDesktopAlert) {
                    showNotification('IQRPG Auto Alert!', 'You have ' + userSettings.autoAlertNumber + ' remaining!');
                }
                // Only start sound alerts if auto audio alerts are enabled
                if (!isAlerting && userSettings.autoAudioAlert && canPlayMoreAlerts()) {
                    startAlert();
                }
            } else {
                stopAlert();
                currAutoAudioPlays = 0;
            }
        } catch (error) {
            // Silently fail if there's an error
        }
    }

    // Handle title changes
    function handleTitleChange(title) {
        switch (title) {
            case 'Dungeon Complete Idle Quest RPG':
                if (userSettings.dungeonDesktopAlert) {
                    showNotification('IQRPG Dungeon Alert!', 'You have completed your dungeon!');
                }
                if (!isAlerting && userSettings.dungeonAudioAlert && canPlayMoreAlerts()) {
                    startAlert();
                }
                break;
            case 'Clan Boss Defeated Idle Quest RPG':
                if (userSettings.watchtowerDesktopAlert) {
                    showNotification('IQRPG Watchtower Alert!', 'Your clan has defeated the boss!');
                }
                if (userSettings.watchtowerAudioAlert && canPlayMoreAlerts()) {
                    playSound(userSettings.bossDefeatedSoundURL);
                }
                break;
            case 'All Mobs Defeated Idle Quest RPG':
                if (userSettings.watchtowerDesktopAlert) {
                    showNotification('IQRPG Watchtower Alert!', 'All mobs have been defeated!');
                }
                if (!isAlerting && userSettings.watchtowerAudioAlert && canPlayMoreAlerts()) {
                    startAlert();
                }
                break;
            case 'Boss Defeated Idle Quest RPG':
                if (userSettings.bossDesktopAlert) {
                    showNotification('IQRPG Boss Alert!', 'The boss has been defeated!');
                }
                if (userSettings.bossAudioAlert) {
                    playSound(userSettings.bossDefeatedSoundURL);
                }
                break;
            case 'ALERT':
                break;
            default:
                stopAlert();
                currAutoAudioPlays = 0;
        }
    }

    // Handle land mutations
    function handleLandMutations(mutationRecords) {
        mutationRecords.forEach(function(mutation) {
            if (mutation.type == 'characterData') {
                if (mutation.target.data == '00:00') {
                    if (userSettings.landAudioAlert) {
                        playSound(userSettings.landAlertSoundURL);
                    }
                }
            }
        });
    }

    // Handle mastery mutations
    function handleMasteryMutations(mutationRecords) {
        mutationRecords.forEach(function(mutation) {
            if (mutation.type == 'characterData') {
                const level = parseInt(mutation.target.data);
                if (!isNaN(level) && level % userSettings.masteryEveryXLevels == 0) {
                    if (userSettings.masteryAudioAlert) {
                        playSound(userSettings.masteryAlertSoundURL);
                    }
                }
            }
        });
    }

    // Check for effects
    function checkEffects() {
        try {
            const effects = $(".main-section__body > div > .flex.space-between > .green-text");
            if (!effects || !effects.length) return;

            effects.each(function(index) {
                const effectLeft = $(effects[index])[0].innerHTML;
                if (effectLeft == userSettings.effectAutoLeft) {
                    if (userSettings.effectAudioAlert) {
                        playSound(userSettings.effectAlertSoundURL);
                    }
                }
            });
        } catch (error) {
            // Silently fail if there's an error
        }
    }

    // Check for bonus exp
    function checkBonusExp() {
        try {
            const bonusExpSpan = $('.main-section__body> div > div > div > span.exp-text');
            const hasBonus = bonusExpSpan != null && bonusExpSpan.length != 0;

            if (hasBonus && !bonusExp) {
                bonusExp = true;
                if (userSettings.bonusExpAudioAlert) {
                    playSound(userSettings.bonusExpAlertSoundURL);
                }
            } else if (!hasBonus && bonusExp) {
                bonusExp = false;
            }
        } catch (error) {
            // Silently fail if there's an error
        }
    }

    // Check if more alerts can be played
    function canPlayMoreAlerts() {
        return currAutoAudioPlays <= userSettings.autoMaxNumberOfAudioAlerts || userSettings.autoMaxNumberOfAudioAlerts == 0;
    }

    // Start alert sounds
    function startAlert() {
        if (isAlerting || !canPlayMoreAlerts()) return;

        isAlerting = true;
        currAutoAudioPlays++;

        // Check if auto audio alert is enabled before playing sound
        if (userSettings.autoAudioAlert) {
            playSound(userSettings.autoAlertSoundURL);
        }

        const repeatInterval = userSettings.autoAlertRepeatInSeconds * 1000;
        alertTimer = setInterval(() => {
            if (canPlayMoreAlerts()) {
                currAutoAudioPlays++;
                // Check if auto audio alert is enabled before playing sound
                if (userSettings.autoAudioAlert) {
                    playSound(userSettings.autoAlertSoundURL);
                }
            } else {
                stopAlert();
            }
        }, repeatInterval);
    }

    // Stop alert sounds
    function stopAlert() {
        isAlerting = false;
        if (alertTimer) {
            clearInterval(alertTimer);
            alertTimer = null;
        }
    }

    // Show desktop notification
    function showNotification(title, text) {
        if (desktopNotificationOnCooldown) return;

        try {
            desktopNotificationOnCooldown = true;
            setTimeout(() => { desktopNotificationOnCooldown = false; }, 7000);

            if (!("Notification" in window)) {
                return;
            }

            if (Notification.permission === "granted") {
                const notification = new Notification(title, { body: text });
                setupNotificationBehavior(notification);
            } else if (Notification.permission !== "denied") {
                Notification.requestPermission().then(function(permission) {
                    if (permission === "granted") {
                        const notification = new Notification(title, { body: text });
                        setupNotificationBehavior(notification);
                    }
                });
            }
        } catch (error) {
            // Silently fail if there's an error
        }
    }

    // Setup notification behavior
    function setupNotificationBehavior(notification) {
        if (!notification) return;

        notification.onclick = function() {
            window.focus();
            this.close();
        };

        setTimeout(() => {
            if (notification) notification.close();
        }, 7000);
    }

    // Play sound with volume control
    function playSound(sound, volume = null) {
        try {
            if (!sound) return;

            const audio = new Audio();
            audio.src = sound;
            audio.volume = volume !== null ? volume : userSettings.masterAudioLevel;
            audio.play();
        } catch (error) {
            // Silently fail if there's an error
        }
    }


    // Return public methods and properties
    return {
        init: init,
        playSound: playSound,
        showNotification: showNotification,
        toggleSettingsPanel: toggleSettingsPanel,
        stopAlert: stopAlert
    };
})();

// Initialize the script when the document is ready
(function() {
    // Wait for the page to be fully loaded
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        setTimeout(function() {
            window.IQRPG_Plus.init();
        }, 1000); // Slight delay to ensure everything is loaded
    } else {
        document.addEventListener('DOMContentLoaded', function() {
            setTimeout(function() {
                window.IQRPG_Plus.init();
            }, 1000);
        });
    }
})();