IQRPG+ Combined

Audio signals for various aspects of IQRPG

目前为 2025-04-10 提交的版本。查看 最新版本

// ==UserScript==
// @name         IQRPG+ Combined
// @namespace    https://www.iqrpg.com/
// @version      0.1.8
// @description  Audio signals for various aspects of IQRPG
// @Author       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
// ==/UserScript==
 
/*
    
Original script by Bunjo and Vifs

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

*/
 
// Use a namespace to avoid global scope pollution
const IQRPG_Plus = (function() {
    // Private variables
    let userSettings = {};
    let isAlerting = false;
    let alertTimer = null;
    let currAutoAudioPlays = 0;
    let canSendDesktopAlert = true;
    let audioRepeatLock = false;
    let desktopNotificationOnCooldown = false;
    let bonusExp = false;
    let isDragging = false;
    let dragOffsetX = 0;
    let dragOffsetY = 0;
    let processedEvents = new Map(); // Store processed events with their unique signature
    let lastEventTime = 0;
    let lastEventType = '';
    
    // Store references to UI elements
    const elements = {
        buttonPanel: null,
        buttonContainer: null,
        settingsPanel: null,
        settingsButton: null,
        graphButton: null,
        swordsButton: null,
        testVolumeButton: null
    };
    
    // Store references to observers
    const observers = {
        main: null,
        land: null,
        mastery: null
    };
    
    // Cache DOM query results
    const domCache = {};
    
    // 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',
        showDebugInfo: true
    };

    // Load settings from localStorage
    function loadSettings() {
        try {
            const savedSettings = JSON.parse(localStorage.getItem('iqrpgSettings'));
            // Ensure all default settings exist in the loaded settings
            const result = savedSettings ? Object.assign({}, defaultSettings, savedSettings) : defaultSettings;
            console.log('IQRPG+ Settings loaded:', result);
            return result;
        } catch (e) {
            console.error('Error loading settings:', e);
            return defaultSettings;
        }
    }

    // Save settings to localStorage
    function saveSettings() {
        try {
            localStorage.setItem('iqrpgSettings', JSON.stringify(userSettings));
            console.log('IQRPG+ Settings saved:', userSettings);
        } catch (e) {
            console.error('Error saving settings:', e);
        }
    }
    
    // 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();
        setupWebSocketInterception();
        setupObservers();
        setupEventListeners();
        
        // Check Notification permissions
        if (Notification.permission !== "denied") {
            Notification.requestPermission();
        }
    }
    
    // Create and setup UI elements
    function createUI() {
        try {
            // 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="testVolumeButton">Test Volume</button>
                <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('🔔');
            elements.graphButton = createControlButton('📊');
            elements.swordsButton = createControlButton('⚔️');
            
            // Create container for buttons
            elements.buttonContainer = document.createElement('div');
            elements.buttonContainer.id = 'buttonContainer';
            
            // Assemble the UI
            elements.buttonContainer.appendChild(elements.settingsButton);
            elements.buttonContainer.appendChild(elements.graphButton);
            elements.buttonContainer.appendChild(elements.swordsButton);
            
            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();
        } catch (error) {
            console.error('Error creating UI:', error);
        }
    }
    
    // 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;
            }
            
            #testVolumeButton, #resetSettingsButton {
                background-color: rgba(0, 0, 0, 0.8);
                color: white;
                border: 1px solid #333;
                padding: 5px 10px;
                cursor: pointer;
                font-size: 12px;
                margin: 5px 0;
            }
            
            #testVolumeButton:hover, #resetSettingsButton:hover {
                border-color: #227422;
                background-color: rgba(10, 10, 10, 0.9);
            }
            
            #resetSettingsButton {
                background-color: rgba(50, 10, 10, 0.8);
                margin-left: 10px;
            }
            
            #volumeSlider {
                margin: 10px 0;
                width: 90%;
                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) {
            console.error('Error loading saved position:', 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();
        });
        
        // Graph button - to be implemented
        elements.graphButton.addEventListener('click', (e) => {
            e.stopPropagation();
            showGraphPanel();
        });
        
        // Swords button - to be implemented
        elements.swordsButton.addEventListener('click', (e) => {
            e.stopPropagation();
            showCombatPanel();
        });
        
        // Test volume button
        const testVolumeButton = document.getElementById('testVolumeButton');
        if (testVolumeButton) {
            testVolumeButton.addEventListener('click', () => {
                console.log('Test volume button clicked. Current master volume:', userSettings.masterAudioLevel);
                playSound(userSettings.autoAlertSoundURL, userSettings.masterAudioLevel);
            });
        }
        
        // 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() {
        console.log('Graph panel functionality not yet implemented');
        // To be implemented
    }
    
    // Combat panel implementation placeholder
    function showCombatPanel() {
        console.log('Combat panel functionality not yet implemented');
        // 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();
                    console.log(`${setting} set to ${event.target.checked}`);
                });
            } else {
                console.log(`Checkbox element not found for setting: ${setting}`);
            }
        });
    }
    
    // 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();
                }
            }
        });
    }
    
    // WebSocket interception
    function setupWebSocketInterception() {
        const OldSocket = window.WebSocket;
        
        window.WebSocket = function WebSocket(url, protocols) {
            console.log('IQRPG+ Socket Monitor Initilized...');
            const socket = new OldSocket(...arguments);
            socket.addEventListener('message', handleWebSocketMessage);
            return socket;
        };
    }
    
    // Handle WebSocket messages
    function handleWebSocketMessage(event) {
        try {
            const message = JSON.parse(event.data);
            switch(message.type){
                case 'playersOnline':
                case 'loadMessages':
                case 'addItemsToUser':
                case 'notification':
                case 'bonus':
                    break;
                case 'event':
                    debugInfo('Event Data:');
                    debugInfo(message);
                    handleGameEvent(message);
                    break;
                case 'msg':
                    handleChatMessage(message);
                    break;
                case 'boss':
                    break;
                default:
                    debugInfo(message);
            }
        } catch (error) {
            console.error('Error handling WebSocket message:', error);
        }
    }
    
    // Handle game events (woodcutting, mining, etc)
    function handleGameEvent(message) {
        if (!message.data || !message.data.type) return;
        
        const eventType = message.data.type;
        const timeRemaining = message.data.timeRemaining || 0;
        const currentTime = Date.now();
        
        // Create a unique signature for this event
        // Combine type, time remaining, and any other distinguishing factors
        const eventSignature = `${eventType}-${timeRemaining}`;
        
        // Check if we've seen this exact event recently (within 1 second)
        if (processedEvents.has(eventSignature)) {
            const lastProcessedTime = processedEvents.get(eventSignature);
            // Only ignore if the same exact event was processed very recently (500ms)
            // This prevents double notifications while still allowing rapid consecutive events
            if (currentTime - lastProcessedTime < 500) {
                debugInfo('Ignoring duplicate event: ' + eventSignature);
                return;
            }
        }
        
        // Update the processed events map with this event
        processedEvents.set(eventSignature, currentTime);
        
        // Clean up old events from the map (older than 10 seconds)
        processedEvents.forEach((timestamp, signature) => {
            if (currentTime - timestamp > 10000) {
                processedEvents.delete(signature);
            }
        });
        
        if (eventType === "woodcutting" && userSettings.eventAlert_Woodcutting) {
            handleResourceEvent('Woodcutting', timeRemaining);
        } else if (eventType === "mining" && userSettings.eventAlert_Mining) {
            handleResourceEvent('Mining', timeRemaining);
        } else if (eventType === "quarrying" && userSettings.eventAlert_Quarrying) {
            handleResourceEvent('Quarrying', timeRemaining);
        } else {
            debugInfo('Unsupported Event - ' + eventType);
        }
    }
    
    // Handle resource events (common code for woodcutting, mining, quarrying)
    function handleResourceEvent(eventName, timeRemaining) {
        if (userSettings.eventAudioAlert) {
            playSound(userSettings.eventAlertSoundURL);
        }
        if (userSettings.eventDesktopAlert) {
            showNotification(`IQRPG Event!`, `${eventName} event has started!`);
        }
        
        // Set timeout for end of event
        setTimeout(function() {
            if (userSettings.eventAudioAlertFinished) {
                playSound(userSettings.eventAlertSoundURL);
            }
            if (userSettings.eventDesktopAlert) {
                showNotification(`IQRPG Event Finished!`, `${eventName} event has ended!`);
            }
        }, timeRemaining * 10);
    }
    
    // Handle chat messages
    function handleChatMessage(message) {
        if (!message.data || !message.data.type) return;
        
        switch(message.data.type) {
            case 'clanGlobal':
                if (message.data.msg && message.data.msg.startsWith('The watchtower')) {
                    if (userSettings.watchtowerAudioAlert) {
                        playSound(userSettings.watchtowerAlertSoundURL);
                    }
                    if (userSettings.watchtowerDesktopAlert) {
                        showNotification('IQRPG Watchtower!', message.data.msg);
                    }
                }
                break;
            case 'pm-from':
                handleWhisper(message);
                break;
            case 'eventGlobal':
                if (message.data.msg && message.data.msg.startsWith('A rift to the dark realm has opened')) {
                    if (userSettings.eventAudioAlert) {
                        playSound(userSettings.bossAlertSoundURL, 0.1);
                    }
                    if (userSettings.eventDesktopAlert) {
                        showNotification('IQRPG Boss!', 'A rift to the dark realm has opened!');
                    }
                }
                break;
            case 'pm-to':
            case 'msg':
            case 'global':
            case 'me':
                break;
            default:
                debugInfo('Unsupported msg type:' + message.data.type);
                debugInfo(message);
                break;
        }
    }
    
    // Handle whispers/private messages
    function handleWhisper(message) {
        if (userSettings.whisperAlertOnlyWhenTabIsInactive) {
            if (document.hidden) {
                triggerWhisperAlert(message);
            }
        } else {
            triggerWhisperAlert(message);
        }
    }
    
    // Trigger whisper alerts (sound and notification)
    function triggerWhisperAlert(message) {
        if (userSettings.whisperAudioAlert) {
            playSound(userSettings.whisperAlertSoundURL);
        }
        if (userSettings.whisperDesktopAlert) {
            if (canSendDesktopAlert) {
                showNotification('IQRPG Whisper!', message.data.username + ': ' + message.data.msg);
                canSendDesktopAlert = false;
                setTimeout(() => { canSendDesktopAlert = true; }, 10000);
            }
        }
    }
    
    // Setup observers for DOM changes
    function setupObservers() {
        const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
        const obsConfig = { childList: true, characterData: true, attributes: true, subtree: true };
        
        // Main observer for title changes and auto remaining
        observers.main = new MutationObserver(handleMutations);
        
        // Land observer for land timer
        observers.land = new MutationObserver(handleLandMutations);
        
        // Mastery observer for mastery levels
        observers.mastery = new MutationObserver(handleMasteryMutations);
        
        // Initialize observers with delay to ensure elements are available
        setTimeout(function() {
            try {
                const actionTimer = $("div.action-timer__text")[0];
                const title = $("head title")[0];
                
                if (actionTimer) {
                    observers.main.observe(actionTimer, obsConfig);
                }
                
                if (title) {
                    observers.main.observe(title, obsConfig);
                }
                
                if (userSettings.landAudioAlert) {
                    const landSection = $(".main-section")[2];
                    if (landSection) {
                        observers.land.observe(landSection, obsConfig);
                    }
                }
                
                if (userSettings.masteryAudioAlert) {
                    const masteries = $(".clickable > .flex.space-between > .green-text");
                    if (masteries.length > 0) {
                        masteries.each(function(index) {
                            observers.mastery.observe(masteries[index], obsConfig);
                        });
                    }
                }
            } catch (error) {
                console.error("Error setting up observers:", error);
            }
        }, 500);
    }
    
    // Handle main mutations (title changes, auto remaining)
    function handleMutations(mutationRecords) {
        try {
            // Check for effects
            if (userSettings.effectAudioAlert) {
                checkEffects();
            }
            
            // Check for bonus exp
            if (userSettings.bonusExpAudioAlert) {
                checkBonusExp();
            }
            
            mutationRecords.forEach(function(mutation) {
                // Handle title changes
                if (mutation.type == "childList") {
                    if (mutation.target.nodeName == "TITLE") {
                        handleTitleChange(mutation.target.innerHTML);
                    }
                }
                
                // Handle auto remaining changes
                if (mutation.type == "characterData") {
                    handleAutoRemaining(mutation.target.data);
                }
            });
        } catch (error) {
            console.error("Error handling mutations:", error);
        }
    }
    
    // Handle title changes
    function handleTitleChange(title) {
        console.log('Title changed to:', title);
        console.log('Current settings status - dungeonAudioAlert:', userSettings.dungeonAudioAlert, 
                    'bossAudioAlert:', userSettings.bossAudioAlert, 
                    'watchtowerAudioAlert:', userSettings.watchtowerAudioAlert);
                    
        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()) {
                    console.log('Starting dungeon audio alert. dungeonAudioAlert setting:', userSettings.dungeonAudioAlert);
                    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 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()) {
                    console.log('Starting auto audio alert. autoAudioAlert setting:', userSettings.autoAudioAlert);
                    startAlert();
                }
            } else {
                stopAlert();
                currAutoAudioPlays = 0;
            }
        } catch (error) {
            console.error("Error handling auto remaining:", error);
        }
    }
    
    // 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) {
            console.error("Error checking effects:", 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) {
            console.error("Error checking bonus exp:", 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)) {
                console.error("This browser does not support desktop notifications");
                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) {
            console.error("Error showing notification:", 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;
            
            // Added debug log for sound playback
            console.log('Playing sound:', sound, 'with volume:', 
                       (volume !== null ? volume : userSettings.masterAudioLevel),
                       'Master audio level:', userSettings.masterAudioLevel);
            
            const audio = new Audio(sound);
            audio.volume = volume !== null ? volume : userSettings.masterAudioLevel;
            
            const playPromise = audio.play();
            if (playPromise) {
                playPromise.catch(error => {
                    console.error("Error playing sound:", error);
                });
            }
        } catch (error) {
            console.error("Error playing sound:", error);
        }
    }
    
    // Debug logging
    function debugInfo(msg) {
        if (userSettings.showDebugInfo) {
            console.log(msg);
        }
    }

    return {
        init
    };
})();

// Initialize the script when the document is ready
$(document).ready(function() {
    IQRPG_Plus.init();
});