IQRPG Enhanced

Enhanced features for IQRPG including notifications and alerts

当前为 2025-12-08 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         IQRPG Enhanced
// @namespace    https://iqrpg.com/
// @version      1.0.2
// @description  Enhanced features for IQRPG including notifications and alerts
// @author       Sanjin
// @license      MIT
// @match        https://iqrpg.com/game.html
// @match        https://www.iqrpg.com/game.html
// @match        http://iqrpg.com/game.html
// @match        http://www.iqrpg.com/game.html
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // ============================================
    // Configuration & State Management
    // ============================================
    const DEFAULT_CONFIG = {
        notifications: {
            globalEvents: {
                sound: true,
                desktop: true
            },
            clan: {
                sound: true,
                desktop: true,
                watchtower: true,
                clanChatGlobals: true
            },
            actionBonus: {
                sound: true,
                desktop: true
            },
            bossSpawn: {
                sound: true,
                desktop: true
            },
            tradeAlert: {
                sound: true,
                desktop: true,
                sellingKeywords: [],
                buyingKeywords: []
            },
            gatheringEvents: {
                woodcutting: {
                    sound: true,
                    desktop: true
                },
                quarrying: {
                    sound: true,
                    desktop: true
                },
                mining: {
                    sound: true,
                    desktop: true
                }
            },
            message: {
                sound: true,
                desktop: true
            },
            autos: {
                sound: true,
                desktop: true,
                threshold: 100,  // Alert when autos reach this number
                repeatCount: 1,  // Number of times to repeat alert while under threshold (1 = no repeat)
                repeatInterval: 1  // Seconds between repeat alerts (0 = immediate repeats, no delay)
            },
            potions: {
                sound: true,
                desktop: true,
                threshold: 100,  // Alert when potions reach this number
                repeatCount: 1,  // Number of times to repeat alert while under threshold (1 = no repeat)
                repeatInterval: 1  // Seconds between repeat alerts (0 = immediate repeats, no delay)
            },
            dungeon: {
                sound: true,
                desktop: true,
                onlyWhenAllKeysComplete: false  // Only notify when all dungeon keys are completed
            },
            mastery: {
                sound: true,
                desktop: true
            },
            land: {
                sound: true,
                desktop: true
            },
            skills: {
                sound: true,
                desktop: true
            },
            itemDrop: {
                sound: true,
                desktop: true,
                itemKeywords: []
            },
            abyssBattles: {
                sound: true,
                desktop: true
            }
        },
        sounds: {
            globalEvent: 'https://audio.jukehost.co.uk/qyoNau6faKvNTt2NVyZ3Mcr8WDw1ueiv',
            actionBonus: 'https://audio.jukehost.co.uk/wHRlgKNZdfDnXfLsoTqDjcluHENngS4b',
            bossSpawn: 'https://audio.jukehost.co.uk/zL9Qk16xdxOKyJfMDUPwliTfsKAVJW6n',
            rareBoss: 'https://audio.jukehost.co.uk/eFJ1jyr4Hf1tujq4E64lPXIGI0y3dYeY',
            tradeAlert: 'https://audio.jukehost.co.uk/kbWqZVtOxyOB3Whq0o5Em6LLOGJjP2CY',
            gatheringEvent: 'https://audio.jukehost.co.uk/yuTJytEhB55P1iFzAVwgX7wU4sr1h7cp',
            message: 'https://audio.jukehost.co.uk/s3Nil94O25qt8bKUZp3CrL3z5YzIS1OE',
            autos: 'https://audio.jukehost.co.uk/WKlTn6GvA0e3UGCAvW3IwaJj7vT7VBmL',
            dungeon: 'https://audio.jukehost.co.uk/ccIvfx6WghmSymNDuZEeuGFFpMS84CY5',
            mastery: 'https://audio.jukehost.co.uk/9DW0A6lxLQtstNHZwuiNoGcJciEJ5rdh',
            land: 'https://audio.jukehost.co.uk/ccIvfx6WghmSymNDuZEeuGFFpMS84CY5',
            skills: 'https://audio.jukehost.co.uk/9DW0A6lxLQtstNHZwuiNoGcJciEJ5rdh',
            clanWatchtower: 'https://audio.jukehost.co.uk/wf7tdKTnzx1Kb0wQHqLDab9pfhEHk130',
            clanGlobals: 'https://audio.jukehost.co.uk/qyoNau6faKvNTt2NVyZ3Mcr8WDw1ueiv',
            itemDrop: 'https://audio.jukehost.co.uk/mlm4l3BkKXDT54NaogHDmt9buhGO4Sa3',
            abyssBattles: 'https://audio.jukehost.co.uk/8WLa35FnrtIYNbBVhoiVdHKeFyLjwp0n',
            potions: 'https://audio.jukehost.co.uk/DsgIJBKLrrZHdx7R3XE1kirUcMJKaDjo',
            volume: 1.0  // Volume level (0.0 to 1.0)
        },
        gui: {
            enabled: true
        }
    };

    const CONFIG = JSON.parse(JSON.stringify(DEFAULT_CONFIG));

    // Constants
    const CONSTANTS = {
        DELAYS: {
            BUTTON_CREATION: 100,
            RETRY_SHORT: 500,
            NOTIFICATION_AUTO_CLOSE: 10000,
            SAVE_FEEDBACK: 2000
        },
        STRINGS: {
            PREMIUM_STORE: 'premium store',
            SOUND_GLOBAL: 'globalEvent',
            SOUND_ACTION_BONUS: 'actionBonus',
            SOUND_BOSS_SPAWN: 'bossSpawn',
            SOUND_RARE_BOSS: 'rareBoss',
            SOUND_TRADE_ALERT: 'tradeAlert',
            SOUND_GATHERING_EVENT: 'gatheringEvent',
            SOUND_MESSAGE: 'message',
            SOUND_AUTOS: 'autos',
            SOUND_DUNGEON: 'dungeon',
            SOUND_MASTERY: 'mastery',
            SOUND_LAND: 'land',
            SOUND_SKILLS: 'skills',
            SOUND_CLAN_WATCHTOWER: 'clanWatchtower',
            SOUND_CLAN_GLOBALS: 'clanGlobals',
            SOUND_ITEM_DROP: 'itemDrop',
            SOUND_ABYSS_BATTLES: 'abyssBattles',
            SOUND_POTIONS: 'potions',
            MESSAGE_TYPE_MSG: 'msg',
            MESSAGE_TYPE_ACTION_BONUS: 'actionBonus',
            DATA_TYPE_GLOBAL: 'global',
            DATA_TYPE_EVENT_GLOBAL: 'eventGlobal',
            DATA_TYPE_CLAN_GLOBAL: 'clanGlobal',
            DATA_TYPE_PM_FROM: 'pm-from',
            CHANNEL_TRADE: 'trade',
            CHANNEL_CLAN_PREFIX: 'clan-'
        },
        // Patterns to detect selling/buying intent in trade messages (case-insensitive)
        TRADE_PATTERNS: {
            SELLING: ['selling', 'wts', 'sell', 's>', '{s}'],
            BUYING: ['buying', 'wtb', 'buy', 'b>', '{b}']
        },
        BOSS_SPAWN_PATTERNS: {
            DEMON_HORN: ['demon horn', 'bosses start appearing']
        },
        // Mapping of item keys to their proper display names
        ITEM_NAME_MAP: {
            // Only dungeon keys need special names - all other items will be auto-converted
            'dungeon_key_1': 'Goblin Cave Key',
            'dungeon_key_2': 'Mountain Pass Key',
            'dungeon_key_3': 'Desolate Tombs Key',
            'dungeon_key_4': 'Dragonkin Lair Key',
            'dungeon_key_5': 'Sunken Ruins Key',
            'dungeon_key_6': 'Abandoned Tower Key',
            'dungeon_key_7': 'Haunted Cells Key',
            'dungeon_key_8': 'Hall of Dragons Key',
            'dungeon_key_9': 'The Vault Key',
            'dungeon_key_10': 'The Treasury Key'
        },
        SELECTORS: {
            FIXED_TOP: '.fixed-top',
            SECTION_3: '.section-3'
        }
    };

    // Helper function to preload all sounds
    function preloadAllSounds() {
        AudioManager.preloadSound(CONFIG.sounds.globalEvent, CONSTANTS.STRINGS.SOUND_GLOBAL);
        AudioManager.preloadSound(CONFIG.sounds.actionBonus, CONSTANTS.STRINGS.SOUND_ACTION_BONUS);
        AudioManager.preloadSound(CONFIG.sounds.bossSpawn, CONSTANTS.STRINGS.SOUND_BOSS_SPAWN);
        AudioManager.preloadSound(CONFIG.sounds.rareBoss, CONSTANTS.STRINGS.SOUND_RARE_BOSS);
        AudioManager.preloadSound(CONFIG.sounds.tradeAlert, CONSTANTS.STRINGS.SOUND_TRADE_ALERT);
        AudioManager.preloadSound(CONFIG.sounds.gatheringEvent, CONSTANTS.STRINGS.SOUND_GATHERING_EVENT);
        AudioManager.preloadSound(CONFIG.sounds.message, CONSTANTS.STRINGS.SOUND_MESSAGE);
        AudioManager.preloadSound(CONFIG.sounds.autos, CONSTANTS.STRINGS.SOUND_AUTOS);
        AudioManager.preloadSound(CONFIG.sounds.dungeon, CONSTANTS.STRINGS.SOUND_DUNGEON);
        AudioManager.preloadSound(CONFIG.sounds.mastery, CONSTANTS.STRINGS.SOUND_MASTERY);
        AudioManager.preloadSound(CONFIG.sounds.land, CONSTANTS.STRINGS.SOUND_LAND);
        AudioManager.preloadSound(CONFIG.sounds.skills, CONSTANTS.STRINGS.SOUND_SKILLS);
        AudioManager.preloadSound(CONFIG.sounds.clanWatchtower, CONSTANTS.STRINGS.SOUND_CLAN_WATCHTOWER);
        AudioManager.preloadSound(CONFIG.sounds.clanGlobals, CONSTANTS.STRINGS.SOUND_CLAN_GLOBALS);
        AudioManager.preloadSound(CONFIG.sounds.itemDrop, CONSTANTS.STRINGS.SOUND_ITEM_DROP);
        AudioManager.preloadSound(CONFIG.sounds.abyssBattles, CONSTANTS.STRINGS.SOUND_ABYSS_BATTLES);
        AudioManager.preloadSound(CONFIG.sounds.potions, CONSTANTS.STRINGS.SOUND_POTIONS);
    }

    // Load saved configuration
    function loadConfig() {
        let saved = null;
        try {
            const savedStr = localStorage.getItem('iqrpg_enhanced_config');
            if (savedStr) {
                saved = JSON.parse(savedStr);
            }
        } catch (e) {
            // Silently fail - use default config
        }
        if (saved) {
            // Deep merge instead of shallow assign
            CONFIG.notifications.globalEvents = {
                ...CONFIG.notifications.globalEvents,
                ...(saved.notifications?.globalEvents || {})
            };
            CONFIG.notifications.clan = {
                ...CONFIG.notifications.clan,
                ...(saved.notifications?.clan || {})
            };
            CONFIG.notifications.actionBonus = {
                ...CONFIG.notifications.actionBonus,
                ...(saved.notifications?.actionBonus || {})
            };
            CONFIG.notifications.bossSpawn = {
                ...CONFIG.notifications.bossSpawn,
                ...(saved.notifications?.bossSpawn || {})
            };
            CONFIG.notifications.tradeAlert = {
                ...CONFIG.notifications.tradeAlert,
                ...(saved.notifications?.tradeAlert || {})
            };
            // Handle gathering events with backward compatibility
            if (saved.notifications?.gatheringEvents) {
                // Check if old format exists (top-level sound/desktop)
                if (saved.notifications.gatheringEvents.sound !== undefined && 
                    !saved.notifications.gatheringEvents.woodcutting) {
                    // Old format detected - migrate to new structure
                    const oldConfig = saved.notifications.gatheringEvents;
                    CONFIG.notifications.gatheringEvents.woodcutting = {
                        sound: oldConfig.sound,
                        desktop: oldConfig.desktop
                    };
                    CONFIG.notifications.gatheringEvents.quarrying = {
                        sound: oldConfig.sound,
                        desktop: oldConfig.desktop
                    };
                    CONFIG.notifications.gatheringEvents.mining = {
                        sound: oldConfig.sound,
                        desktop: oldConfig.desktop
                    };
                } else {
                    // New format - merge each event type
                    CONFIG.notifications.gatheringEvents.woodcutting = {
                        ...CONFIG.notifications.gatheringEvents.woodcutting,
                        ...(saved.notifications.gatheringEvents.woodcutting || {})
                    };
                    CONFIG.notifications.gatheringEvents.quarrying = {
                        ...CONFIG.notifications.gatheringEvents.quarrying,
                        ...(saved.notifications.gatheringEvents.quarrying || {})
                    };
                    CONFIG.notifications.gatheringEvents.mining = {
                        ...CONFIG.notifications.gatheringEvents.mining,
                        ...(saved.notifications.gatheringEvents.mining || {})
                    };
                }
            }
            CONFIG.notifications.message = {
                ...CONFIG.notifications.message,
                ...(saved.notifications?.message || {})
            };
            CONFIG.notifications.autos = {
                ...CONFIG.notifications.autos,
                ...(saved.notifications?.autos || {})
            };
            CONFIG.notifications.dungeon = {
                ...CONFIG.notifications.dungeon,
                ...(saved.notifications?.dungeon || {})
            };
            CONFIG.notifications.mastery = {
                ...CONFIG.notifications.mastery,
                ...(saved.notifications?.mastery || {})
            };
            CONFIG.notifications.land = {
                ...CONFIG.notifications.land,
                ...(saved.notifications?.land || {})
            };
            CONFIG.notifications.skills = {
                ...CONFIG.notifications.skills,
                ...(saved.notifications?.skills || {})
            };
            CONFIG.notifications.itemDrop = {
                ...CONFIG.notifications.itemDrop,
                ...(saved.notifications?.itemDrop || {})
            };
            CONFIG.notifications.abyssBattles = {
                ...CONFIG.notifications.abyssBattles,
                ...(saved.notifications?.abyssBattles || {})
            };
            CONFIG.notifications.potions = {
                ...CONFIG.notifications.potions,
                ...(saved.notifications?.potions || {})
            };
            CONFIG.sounds = {
                ...CONFIG.sounds,
                ...(saved.sounds || {})
            };
            CONFIG.gui = {
                ...CONFIG.gui,
                ...(saved.gui || {})
            };
        }
    }

    // Save configuration
    function saveConfig() {
        try {
            localStorage.setItem('iqrpg_enhanced_config', JSON.stringify(CONFIG));
        } catch (e) {
            // Silently fail
        }
    }

    // Initialize config
    loadConfig();

    // ============================================
    // Audio Management
    // ============================================
    const AudioManager = {
        audioCache: new Map(),

        // Preload and cache audio files
        preloadSound(url, name) {
            if (!url || url === 'default') return null;
            
            if (this.audioCache.has(name)) {
                return this.audioCache.get(name);
            }

            try {
                const audio = new Audio(url);
                audio.preload = 'auto';
                this.audioCache.set(name, audio);
                return audio;
            } catch (e) {
                return null;
            }
        },

        // Play a sound
        playSound(name, url = null) {
            const volume = CONFIG.sounds.volume !== undefined ? CONFIG.sounds.volume : 1.0;
            
            if (url) {
                const audio = this.preloadSound(url, name);
                if (audio) {
                    audio.volume = Math.max(0, Math.min(1, volume)); // Clamp between 0 and 1
                    audio.currentTime = 0;
                    audio.play().catch(() => {
                        // Silently fail
                    });
                }
            } else if (this.audioCache.has(name)) {
                const audio = this.audioCache.get(name);
                audio.volume = Math.max(0, Math.min(1, volume)); // Clamp between 0 and 1
                audio.currentTime = 0;
                audio.play().catch(() => {
                    // Silently fail
                });
            }
        }
    };

    // Preload default sounds
    preloadAllSounds();

    // ============================================
    // Notification System
    // ============================================
    // Helper function to convert item key to display name
    // e.g., "undead_heart" -> "Undead Heart", "vial_of_orc_blood" -> "Vial Of Orc Blood"
    function formatItemName(itemKey) {
        if (!itemKey) return itemKey;
        
        // Split by underscore, capitalize first letter of each word, join with spaces
        return itemKey
            .split('_')
            .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
            .join(' ');
    }

    // Helper function to clean game messages (remove HTML tags and game item formatting codes)
    function cleanGameMessage(msg) {
        if (!msg) return '';
        
        // Remove HTML tags
        let cleaned = msg.replace(/<[^>]*>/g, '');
        
        // Remove game item formatting codes: [item:name] -> name
        cleaned = cleaned.replace(/\[item:([^\]]+)\]/g, (match, itemKey) => {
            // First check if this item key has a special mapping (only dungeon keys)
            const properName = CONSTANTS.ITEM_NAME_MAP[itemKey];
            if (properName) {
                return properName;
            }
            // Auto-convert the item key to a display name
            // e.g., "undead_heart" -> "Undead Heart", "runic_leather" -> "Runic Leather"
            return formatItemName(itemKey);
        });
        
        // Remove game item formatting codes: [--type: {...}--]
        // Parse JSON to extract item details, especially for trinkets
        cleaned = cleaned.replace(/\[--([^:]+):([\s\S]*?)--\]/g, (match, itemType, jsonData) => {
            // Try to parse the JSON data
            try {
                const data = JSON.parse(jsonData.trim());
                
                // Handle trinkets specifically
                if (itemType === 'trinket' && data.type !== undefined && data.tier !== undefined) {
                    // type: 1 = Battling Trinket, 2 = Gathering Trinket
                    const trinketType = data.type === 1 ? 'Battling' : data.type === 2 ? 'Gathering' : 'Trinket';
                    return `${trinketType} Trinket (T${data.tier})`;
                }
                
                // Handle jewels specifically
                if (itemType === 'jewel' && data.gemType !== undefined && data.rarity !== undefined) {
                    // gemType: 1 = Sapphire, 2 = Ruby, 3 = Emerald, 4 = Diamond
                    const gemTypeMap = {
                        1: 'Sapphire',
                        2: 'Ruby',
                        3: 'Emerald',
                        4: 'Diamond'
                    };
                    const gemName = gemTypeMap[data.gemType] || 'Jewel';
                    return `${gemName} Jewel (T${data.rarity})`;
                }
                
                // For other item types, try to extract name from JSON if available
                if (data.name) {
                    return data.name;
                } else if (data.itemName) {
                    return data.itemName;
                } else if (data.title) {
                    return data.title;
                }
            } catch (e) {
                // JSON parsing failed, fall through to default handling
            }
            
            // Fallback: return the item type if we can't parse or construct a name
            return itemType || '';
        });
        
        // Remove game item formatting codes: [name] -> name
        // This catches any remaining bracket patterns that weren't handled above
        cleaned = cleaned.replace(/\[([^\]]+)\]/g, (match, itemKey) => {
            // First check if this is a direct key in the map (dungeon keys)
            let properName = CONSTANTS.ITEM_NAME_MAP[itemKey];
            if (properName) {
                return properName;
            }
            
            // If it looks like an item key (contains underscores), auto-convert it
            if (itemKey.includes('_')) {
                return formatItemName(itemKey);
            }
            
            // If it's already a display name (no underscores), return as-is
            return itemKey;
        });
        
        // Clean up any extra whitespace
        cleaned = cleaned.replace(/\s+/g, ' ').trim();
        
        return cleaned;
    }

    // Helper function to convert emoji to data URL icon
    function emojiToIcon(emoji) {
        try {
            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');
            const size = 128; // Icon size
            canvas.width = size;
            canvas.height = size;
            
            // Set font size to render emoji large
            ctx.font = `${size * 0.8}px Arial`;
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            
            // Draw emoji on canvas
            ctx.fillText(emoji, size / 2, size / 2);
            
            // Convert to data URL
            return canvas.toDataURL('image/png');
        } catch (e) {
            return null;
        }
    }

    const NotificationManager = {
        // Request notification permission
        async requestPermission() {
            if ('Notification' in window && Notification.permission === 'default') {
                await Notification.requestPermission();
            }
        },

        // Show desktop notification
        showDesktopNotification(title, message, icon = null) {
            if (!('Notification' in window)) {
                return;
            }

            if (Notification.permission === 'granted') {
                const options = {
                    body: message,
                    icon: icon || null,
                    badge: icon || null,
                    tag: 'iqrpg-enhanced',
                    requireInteraction: false
                };

                const notification = new Notification(title, options);
                
                // Auto-close after delay
                setTimeout(() => {
                    notification.close();
                }, CONSTANTS.DELAYS.NOTIFICATION_AUTO_CLOSE);

                // Handle click to focus window
                notification.onclick = () => {
                    window.focus();
                    notification.close();
                };
            } else if (Notification.permission === 'default') {
                this.requestPermission();
            }
        },

        // Unified notification method
        notify(title, message, options = {}) {
            const { sound = false, desktop = false, soundName = null, soundUrl = null, emoji = null } = options;

            // Play sound if enabled
            if (sound && soundName) {
                AudioManager.playSound(soundName, soundUrl);
            }

            // Show desktop notification if enabled
            if (desktop) {
                // Convert emoji to icon if provided
                const icon = emoji ? emojiToIcon(emoji) : null;
                this.showDesktopNotification(title, message, icon);
            }
        }
    };

    // Request notification permission on load
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            NotificationManager.requestPermission();
        });
    } else {
        NotificationManager.requestPermission();
    }

    // ============================================
    // WebSocket Interception
    // ============================================
    const WebSocketInterceptor = {
        originalWebSocket: null,
        interceptedSockets: new Set(),

        init() {
            try {
                // Intercept WebSocket constructor
                this.originalWebSocket = window.WebSocket;
                const self = this;

                window.WebSocket = function(url, protocols) {
                    const ws = new self.originalWebSocket(url, protocols);
                    self.interceptSocket(ws);
                    return ws;
                };

                // Copy static properties
                Object.setPrototypeOf(window.WebSocket, self.originalWebSocket);
                Object.defineProperty(window.WebSocket, 'CONNECTING', {
                    value: self.originalWebSocket.CONNECTING,
                    writable: false
                });
                Object.defineProperty(window.WebSocket, 'OPEN', {
                    value: self.originalWebSocket.OPEN,
                    writable: false
                });
                Object.defineProperty(window.WebSocket, 'CLOSING', {
                    value: self.originalWebSocket.CLOSING,
                    writable: false
                });
                Object.defineProperty(window.WebSocket, 'CLOSED', {
                    value: self.originalWebSocket.CLOSED,
                    writable: false
                });
            } catch (e) {
                // Silently fail
            }
        },

        interceptSocket(ws) {
            if (this.interceptedSockets.has(ws)) return;
            this.interceptedSockets.add(ws);

            // Intercept messages
            const originalAddEventListener = ws.addEventListener.bind(ws);
            ws.addEventListener = function(type, listener, options) {
                if (type === 'message') {
                    const wrappedListener = function(event) {
                        // Call original listener first
                        listener(event);

                        // Capture and log message
                        let messageData;
                        try {
                            messageData = JSON.parse(event.data);
                        } catch (e) {
                            messageData = typeof event.data === 'object' && event.data !== null && !Array.isArray(event.data) 
                                ? event.data 
                                : { raw: event.data, _parseError: true };
                        }

                        // Process message for notifications
                        if (messageData && typeof messageData === 'object') {
                            MessageProcessor.process(messageData);
                        }
                    };
                    return originalAddEventListener(type, wrappedListener, options);
                }
                return originalAddEventListener(type, listener, options);
            };

            // Also intercept onmessage property
            Object.defineProperty(ws, 'onmessage', {
                get: function() {
                    return this._onmessage;
                },
                set: function(listener) {
                    this._onmessage = listener;
                    if (listener) {
                        ws.addEventListener('message', listener);
                    }
                },
                configurable: true
            });
        }
    };

    // ============================================
    // Message Processing
    // ============================================
    // Track last action bonus to avoid duplicate notifications
    let lastActionBonus = null;

    const MessageProcessor = {
        process(message) {
            if (!message || typeof message !== 'object') return;

            // Handle different message types
            switch (message.type) {
                case CONSTANTS.STRINGS.MESSAGE_TYPE_MSG:
                    this.handleMessageType(message);
                    break;
                case CONSTANTS.STRINGS.MESSAGE_TYPE_ACTION_BONUS:
                    this.handleActionBonus(message);
                    break;
                default:
                    // Unhandled message types - silently ignore
                    break;
            }
        },

        handleMessageType(message) {
            if (!message.data || !message.data.type) {
                return;
            }

            // Handle global events (crafting notifications)
            if (message.data.type === CONSTANTS.STRINGS.DATA_TYPE_GLOBAL) {
                const config = CONFIG.notifications.globalEvents;
                if (config.sound || config.desktop) {
                    const rawMsg = message.data.msg || 'A global event occurred';
                    const cleanMsg = cleanGameMessage(rawMsg);

                    NotificationManager.notify(
                        'Global',
                        cleanMsg,
                        {
                            sound: config.sound,
                            desktop: config.desktop,
                            soundName: CONSTANTS.STRINGS.SOUND_GLOBAL,
                            soundUrl: CONFIG.sounds.globalEvent,
                            emoji: '🎉'
                        }
                    );
                }
            }

            // Handle eventGlobal messages (gathering events and boss spawns)
            if (message.data.type === CONSTANTS.STRINGS.DATA_TYPE_EVENT_GLOBAL) {
                const rawMsg = message.data.msg || '';
                
                // Clean message (remove HTML tags, game formatting codes, normalize whitespace)
                const cleanMsg = cleanGameMessage(rawMsg);
                const msgLower = cleanMsg.toLowerCase();
                
                // Check if this is a boss spawn message (demon horn indicates multiple bosses spawning)
                const isDemonHorn = CONSTANTS.BOSS_SPAWN_PATTERNS.DEMON_HORN.some(pattern => 
                    msgLower.includes(pattern)
                );
                
                if (isDemonHorn) {
                    // Check if this is a rare boss (rarity-4 or higher)
                    // Check rawMsg for text-rarity-4, text-rarity-5, etc.
                    const isRareBoss = /text-rarity-[4-9]|text-rarity-\d{2,}/.test(rawMsg);
                    
                    // Always use bossSpawn config for toggles, but different sound URLs based on rarity
                    const config = CONFIG.notifications.bossSpawn;
                    const soundName = isRareBoss ? CONSTANTS.STRINGS.SOUND_RARE_BOSS : CONSTANTS.STRINGS.SOUND_BOSS_SPAWN;
                    const soundUrl = isRareBoss ? CONFIG.sounds.rareBoss : CONFIG.sounds.bossSpawn;
                    
                    if (config.sound || config.desktop) {
                        NotificationManager.notify(
                            'Boss Event',
                            cleanMsg,
                            {
                                sound: config.sound,
                                desktop: config.desktop,
                                soundName: soundName,
                                soundUrl: soundUrl,
                                emoji: '👹'
                            }
                        );
                    }
                } else {
                    // Gathering event (woodcutting, quarrying, mining)
                    // Determine specific event type and emoji
                    let eventType;
                    let eventEmoji;
                    let eventConfigKey = null;
                    
                    if (msgLower.includes('spirit tree') || msgLower.includes('forest')) {
                        eventType = 'Woodcutting Event';
                        eventEmoji = '🌲';
                        eventConfigKey = 'woodcutting';
                    } else if (msgLower.includes('sinkhole') || msgLower.includes('ground shakes')) {
                        eventType = 'Quarrying Event';
                        eventEmoji = '🪨';
                        eventConfigKey = 'quarrying';
                    } else if (msgLower.includes('meteorite')) {
                        eventType = 'Mining Event';
                        eventEmoji = '☄️';
                        eventConfigKey = 'mining';
                    }
                    
                    // Only notify if this specific event type is enabled
                    if (eventConfigKey) {
                        const config = CONFIG.notifications.gatheringEvents[eventConfigKey];
                        if (config && (config.sound || config.desktop)) {
                            NotificationManager.notify(
                                eventType,
                                cleanMsg,
                                {
                                    sound: config.sound,
                                    desktop: config.desktop,
                                    soundName: CONSTANTS.STRINGS.SOUND_GATHERING_EVENT,
                                    soundUrl: CONFIG.sounds.gatheringEvent,
                                    emoji: eventEmoji
                                }
                            );
                        }
                    }
                }
            }

            // Handle private messages
            if (message.data.type === CONSTANTS.STRINGS.DATA_TYPE_PM_FROM) {
                const config = CONFIG.notifications.message;
                if (config.sound || config.desktop) {
                    const rawMsg = message.data.msg || '';
                    const cleanMsg = cleanGameMessage(rawMsg);
                    const username = message.data.username || 'Unknown';

                    NotificationManager.notify(
                        `Message from ${username}`,
                        cleanMsg,
                        {
                            sound: config.sound,
                            desktop: config.desktop,
                            soundName: CONSTANTS.STRINGS.SOUND_MESSAGE,
                            soundUrl: CONFIG.sounds.message,
                            emoji: '💬'
                        }
                    );
                }
            }

            // Handle trade channel messages
            if (message.channel === CONSTANTS.STRINGS.CHANNEL_TRADE && message.data.type === CONSTANTS.STRINGS.MESSAGE_TYPE_MSG) {
                this.handleTradeMessage(message);
            }

            // Handle clan chat global messages
            if (message.channel && message.channel.startsWith(CONSTANTS.STRINGS.CHANNEL_CLAN_PREFIX) && 
                message.data.type === CONSTANTS.STRINGS.DATA_TYPE_CLAN_GLOBAL) {
                const config = CONFIG.notifications.clan;
                const rawMsg = message.data.msg || '';
                const cleanMsg = cleanGameMessage(rawMsg);
                
                // Check if this is a watchtower event
                const isWatchtower = rawMsg.toLowerCase().includes('watchtower');
                
                if (isWatchtower) {
                    // Handle watchtower events
                    if (config.watchtower && (config.sound || config.desktop)) {
                        NotificationManager.notify(
                            'Clan Watchtower',
                            cleanMsg,
                            {
                                sound: config.sound,
                                desktop: config.desktop,
                                soundName: CONSTANTS.STRINGS.SOUND_CLAN_WATCHTOWER,
                                soundUrl: CONFIG.sounds.clanWatchtower,
                                emoji: '🐉'
                            }
                        );
                    }
                } else {
                    // Handle other clan global events
                    if (config.clanChatGlobals && (config.sound || config.desktop)) {
                        NotificationManager.notify(
                            'Clan Global',
                            cleanMsg,
                            {
                                sound: config.sound,
                                desktop: config.desktop,
                                soundName: CONSTANTS.STRINGS.SOUND_CLAN_GLOBALS,
                                soundUrl: CONFIG.sounds.clanGlobals,
                                emoji: '🎉'
                            }
                        );
                    }
                }
            }
        },

        handleTradeMessage(message) {
            const config = CONFIG.notifications.tradeAlert;
            if (!config.sound && !config.desktop) return;

            const msgText = (message.data.msg || '').toLowerCase();
            const username = message.data.username || 'Unknown';

            // Normalize patterns to lowercase for case-insensitive matching
            const sellingPatterns = CONSTANTS.TRADE_PATTERNS.SELLING.map(p => p.toLowerCase());
            const buyingPatterns = CONSTANTS.TRADE_PATTERNS.BUYING.map(p => p.toLowerCase());

            // Check if message contains selling indicators (case-insensitive)
            const isSelling = sellingPatterns.some(pattern => msgText.includes(pattern));
            // Check if message contains buying indicators (case-insensitive)
            const isBuying = buyingPatterns.some(pattern => msgText.includes(pattern));

            // Get keywords to check based on message type
            let keywordsToCheck = [];
            let tradeType = '';

            if (isSelling && config.sellingKeywords && config.sellingKeywords.length > 0) {
                keywordsToCheck = config.sellingKeywords;
                tradeType = 'Selling';
            } else if (isBuying && config.buyingKeywords && config.buyingKeywords.length > 0) {
                keywordsToCheck = config.buyingKeywords;
                tradeType = 'Buying';
            }

            if (keywordsToCheck.length === 0) return;

            // Check if any of the keywords match (case-insensitive)
            const matchedKeywords = keywordsToCheck.filter(keyword => {
                // Normalize keyword to lowercase for case-insensitive matching
                const kw = (keyword || '').toLowerCase().trim();
                return kw && msgText.includes(kw);
            });

            if (matchedKeywords.length > 0) {
                const rawMsg = message.data.msg || '';
                const cleanMsg = cleanGameMessage(rawMsg);
                const notificationMsg = `${username}: ${cleanMsg}`;

                NotificationManager.notify(
                    'Trade Alert',
                    notificationMsg,
                    {
                        sound: config.sound,
                        desktop: config.desktop,
                        soundName: CONSTANTS.STRINGS.SOUND_TRADE_ALERT,
                        soundUrl: CONFIG.sounds.tradeAlert,
                        emoji: '💱'
                    }
                );

            }
        },

        handleActionBonus(message) {
            const config = CONFIG.notifications.actionBonus;
            if ((config.sound || config.desktop) && message.data && message.data.actionBonus) {
                const actionBonus = message.data.actionBonus;
                
                // Only notify if the action bonus has actually changed (not on initial load)
                if (lastActionBonus !== null && actionBonus !== lastActionBonus) {
                    const msg = `The action bonus is now active whilst ${actionBonus}`;

                    NotificationManager.notify(
                        'Action Bonus Changed',
                        msg,
                        {
                            sound: config.sound,
                            desktop: config.desktop,
                            soundName: CONSTANTS.STRINGS.SOUND_ACTION_BONUS,
                            soundUrl: CONFIG.sounds.actionBonus,
                            emoji: '✨'
                        }
                    );
                }
                
                // Always update the tracked value
                lastActionBonus = actionBonus;
            }
        }
    };

    // ============================================
    // DOM Observation for UI-Based Events
    // ============================================
    const DOMMonitor = {
        checkInterval: null,
        lastAutosCount: null,
        autosRepeatCount: 0,
        isUnderThreshold: false,
        lastAutosNotificationTime: null,
        lastDungeonText: null,
        pendingDungeonText: null,
        lastMasteryLevel: null,
        lastMasteryName: null,
        lastRaidTimer: null,
        skillLevels: new Map(),
        processedItemDrops: new Set(),
        lastAbyssBattlesCount: null,
        lastPotionCounts: new Map(),
        potionRepeatCounts: new Map(),
        potionUnderThreshold: new Map(),
        lastPotionNotificationTimes: new Map(),
        
        init() {
            // Wait for DOM to be ready
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', () => this.startObserving());
            } else {
                this.startObserving();
            }
        },
        
        startObserving() {
            // Check periodically for autos changes, dungeon completion, mastery level increases, land/raid completion, skill level increases, and item drops
            this.checkInterval = setInterval(() => {
                this.checkForAutos();
                this.checkForDungeonCompletion();
                this.checkForMasteryLevelIncrease();
                this.checkForLandCompletion();
                this.checkForSkillLevelIncrease();
                this.checkForItemDrops();
                this.checkForAbyssBattlesCompletion();
                this.checkForPotionThreshold();
            }, 1000); // Check every second
        },
        
        checkForAutos() {
            const config = CONFIG.notifications.autos;
            if (!config.sound && !config.desktop) return;
            
            const autoElement = document.querySelector('.action-timer__text');
            if (!autoElement) return;
            
            const text = autoElement.textContent || '';
            const match = text.match(/Autos Remaining:\s*(\d+)/i);
            
            if (match) {
                const currentAutos = parseInt(match[1], 10);
                const threshold = config.threshold || 0;
                const repeatCount = config.repeatCount || 1;
                const wasUnderThreshold = this.isUnderThreshold;
                const isCurrentlyUnder = currentAutos <= threshold;
                
                // Check if we've crossed the threshold (went from above to at/below threshold)
                if (this.lastAutosCount !== null && 
                    this.lastAutosCount > threshold && 
                    currentAutos <= threshold) {
                    
                    // Just crossed threshold - trigger first notification
                    this.autosRepeatCount = 1;
                    this.isUnderThreshold = true;
                    this.lastAutosNotificationTime = Date.now();
                    
                    NotificationManager.notify(
                        'Autos Alert',
                        text.trim(),
                        {
                            sound: config.sound,
                            desktop: config.desktop,
                            soundName: CONSTANTS.STRINGS.SOUND_AUTOS,
                            soundUrl: CONFIG.sounds.autos,
                            emoji: '🪫'
                        }
                    );
                } 
                // Check if we're still under threshold and need to repeat
                else if (isCurrentlyUnder && wasUnderThreshold && this.autosRepeatCount < repeatCount) {
                    const repeatInterval = config.repeatInterval !== undefined ? config.repeatInterval : 0; // Default to 0 (immediate) if not set
                    const now = Date.now();
                    const timeSinceLastNotification = this.lastAutosNotificationTime 
                        ? (now - this.lastAutosNotificationTime) / 1000 
                        : Infinity; // If no previous notification, allow it
                    
                    // Check if enough time has passed (or interval is 0 for immediate repeats)
                    if (repeatInterval === 0 || timeSinceLastNotification >= repeatInterval) {
                        // Still under threshold and haven't reached repeat limit
                        this.autosRepeatCount++;
                        this.lastAutosNotificationTime = now;
                        
                        NotificationManager.notify(
                            'Autos Alert',
                            text.trim(),
                            {
                                sound: config.sound,
                                desktop: config.desktop,
                                soundName: CONSTANTS.STRINGS.SOUND_AUTOS,
                                soundUrl: CONFIG.sounds.autos,
                                emoji: '🪫'
                            }
                        );
                    }
                }
                // Check if we've gone back above threshold
                else if (wasUnderThreshold && !isCurrentlyUnder) {
                    // Reset repeat counter and notification time when we go back above threshold
                    this.autosRepeatCount = 0;
                    this.isUnderThreshold = false;
                    this.lastAutosNotificationTime = null;
                }
                // Update state if we're currently under threshold
                else if (isCurrentlyUnder) {
                    this.isUnderThreshold = true;
                }
                
                this.lastAutosCount = currentAutos;
            }
        },
        
        checkForDungeonCompletion() {
            const config = CONFIG.notifications.dungeon;
            if (!config.sound && !config.desktop) return;
            
            const dungeonChest = document.querySelector('.d_chest');
            
            // If chest exists, track it but don't notify yet if we need to check progress
            if (dungeonChest) {
                // Find the green text paragraph with the completion message
                const greenText = dungeonChest.querySelector('.green-text');
                if (!greenText) return;
                
                const text = greenText.textContent || '';
                
                // Only proceed if this is a new dungeon completion (different text)
                if (text && text !== this.lastDungeonText) {
                    // If we need to check for all keys complete, wait for progress text to update
                    if (config.onlyWhenAllKeysComplete) {
                        // Store the text and check progress when chest disappears
                        this.pendingDungeonText = text;
                        return;
                    }
                    
                    // Otherwise, notify immediately
                    this.sendDungeonNotification(text, config);
                }
            } 
            // When chest disappears, check progress text if we have a pending notification
            else if (this.pendingDungeonText && config.onlyWhenAllKeysComplete) {
                // Query .progress__text directly
                const progressText = document.querySelector('.progress__text');
                if (progressText) {
                    const progressTextContent = (progressText.textContent || '').trim().toLowerCase();
                    // If it contains "dungeoneering", user has more keys - don't notify
                    if (progressTextContent.includes('dungeoneering')) {
                        // Still has keys left, skip notification
                        this.pendingDungeonText = null;
                        return;
                    }
                    // Otherwise, all keys are completed - send notification
                    this.sendDungeonNotification(this.pendingDungeonText, config);
                    this.pendingDungeonText = null;
                } else {
                    // Progress text not found, clear pending
                    this.pendingDungeonText = null;
                }
            } else {
                // Reset tracking when chest disappears and no pending notification
                this.lastDungeonText = null;
                this.pendingDungeonText = null;
            }
        },
        
        sendDungeonNotification(text, config) {
            this.lastDungeonText = text;
            
            NotificationManager.notify(
                'Dungeon Complete',
                text.trim(),
                {
                    sound: config.sound,
                    desktop: config.desktop,
                    soundName: CONSTANTS.STRINGS.SOUND_DUNGEON,
                    soundUrl: CONFIG.sounds.dungeon,
                    emoji: '🗝️'
                }
            );
        },
        
        checkForMasteryLevelIncrease() {
            const config = CONFIG.notifications.mastery;
            if (!config.sound && !config.desktop) return;
            
            try {
                // Navigate to masteries panel: .game-grid > first div > second .main-section
                const gameGrid = document.querySelector('.game-grid');
                if (!gameGrid) return;
                
                const leftSidebar = gameGrid.children[0]; // First div (left sidebar)
                if (!leftSidebar) return;
                
                // Find all .main-section divs in left sidebar
                const mainSections = leftSidebar.querySelectorAll('.main-section');
                if (mainSections.length < 2) return; // Need at least 2 sections
                
                const masteriesPanel = mainSections[1]; // Second .main-section (masteries panel)
                if (!masteriesPanel) return;
                
                // Find .main-section__body
                const body = masteriesPanel.querySelector('.main-section__body');
                if (!body) return;
                
                // Find the unnamed div (first child of body)
                const unnamedDiv = body.children[0];
                if (!unnamedDiv) return;
                
                // Find all .relative.clickable divs (all masteries)
                const masteryDivs = unnamedDiv.querySelectorAll('.relative.clickable');
                if (!masteryDivs || masteryDivs.length === 0) return;
                
                // Find the active mastery (the one that's green/selected)
                // The active one should have a .flex.space-between with span.activeText and span.green-text
                let activeMastery = null;
                let masteryName = null;
                let masteryLevel = null;
                
                for (const masteryDiv of masteryDivs) {
                    const flexDiv = masteryDiv.querySelector('.flex.space-between');
                    if (!flexDiv) continue;
                    
                    const activeText = flexDiv.querySelector('span.activeText');
                    const greenText = flexDiv.querySelector('span.green-text');
                    
                    // If both exist, this is likely the active mastery
                    if (activeText && greenText) {
                        activeMastery = masteryDiv;
                        masteryName = (activeText.textContent || '').trim();
                        const levelText = (greenText.textContent || '').trim();
                        
                        // Parse level number directly (green-text is just a number)
                        masteryLevel = parseInt(levelText, 10);
                        
                        // Validate that we got a valid number
                        if (isNaN(masteryLevel)) {
                            continue; // Skip this mastery if level is not a valid number
                        }
                        break;
                    }
                }
                
                // If we found an active mastery with valid data
                if (activeMastery && masteryName && masteryLevel !== null && !isNaN(masteryLevel)) {
                    // Check if this is a new mastery or level increase
                    if (this.lastMasteryName !== masteryName) {
                        // User switched to a different mastery - reset tracking
                        this.lastMasteryName = masteryName;
                        this.lastMasteryLevel = masteryLevel;
                        return;
                    }
                    
                    // Same mastery - check if level increased
                    if (this.lastMasteryLevel !== null && masteryLevel > this.lastMasteryLevel) {
                        // Level increased!
                        NotificationManager.notify(
                            'Mastery Level Up!',
                            `${masteryName} reached level ${masteryLevel}`,
                            {
                                sound: config.sound,
                                desktop: config.desktop,
                                soundName: CONSTANTS.STRINGS.SOUND_MASTERY,
                                soundUrl: CONFIG.sounds.mastery,
                                emoji: '⭐'
                            }
                        );
                    }
                    
                    // Update tracking
                    this.lastMasteryName = masteryName;
                    this.lastMasteryLevel = masteryLevel;
                }
            } catch (error) {
                // Silently fail if DOM structure changes or element not found
            }
        },
        
        checkForLandCompletion() {
            const config = CONFIG.notifications.land;
            if (!config.sound && !config.desktop) return;
            
            try {
                // Use direct selector approach - find the flex container containing "Raid:" text
                const flexContainers = document.querySelectorAll('.flex.space-between');
                let raidContainer = null;
                
                // Find the container that contains "Raid:" text
                for (const container of flexContainers) {
                    const text = container.textContent || '';
                    if (text.includes('Raid:')) {
                        raidContainer = container;
                        break;
                    }
                }
                
                if (!raidContainer) return;
                
                // Find the div containing "Raid:" text (first child div)
                const raidTextDiv = Array.from(raidContainer.children).find(child => {
                    return child.tagName === 'DIV' && (child.textContent || '').includes('Raid:');
                });
                
                if (!raidTextDiv) return;
                
                // Extract raid name from the span inside the raid text div
                const raidNameSpan = raidTextDiv.querySelector('span');
                const raidName = raidNameSpan ? (raidNameSpan.textContent || '').trim() : '';
                
                if (!raidName) return;
                
                // Find the status div - it should be the second child div (not the raid text div)
                const statusDiv = Array.from(raidContainer.children).find(child => {
                    return child.tagName === 'DIV' && child !== raidTextDiv && 
                           child.textContent && child.textContent.trim();
                });
                
                if (!statusDiv) return;
                
                // Get the text from the <a> tag inside the status div, or fallback to div text
                const statusLink = statusDiv.querySelector('a');
                const statusText = (statusLink ? statusLink.textContent : statusDiv.textContent || '').trim();
                
                // Check if status changed to "Returned"
                if (statusText.toLowerCase() === 'returned') {
                    // Only notify when transitioning from NOT "returned" to "returned"
                    if (this.lastRaidTimer !== 'returned') {
                        NotificationManager.notify(
                            'Land',
                            `Raid: ${raidName} Returned!`,
                            {
                                sound: config.sound,
                                desktop: config.desktop,
                                soundName: CONSTANTS.STRINGS.SOUND_LAND,
                                soundUrl: CONFIG.sounds.land,
                                emoji: '🏰'
                            }
                        );
                    }
                    
                    // Update tracking
                    this.lastRaidTimer = 'returned';
                } else {
                    // Status is not "Returned" - update tracking
                    this.lastRaidTimer = statusText;
                }
            } catch (error) {
                // Silently fail if DOM structure changes or element not found
            }
        },
        
        checkForSkillLevelIncrease() {
            const config = CONFIG.notifications.skills;
            if (!config.sound && !config.desktop) return;
            
            try {
                // Use direct selector approach - find all skill-bar divs
                const skillBars = document.querySelectorAll('.skill-bar');
                
                if (!skillBars || skillBars.length === 0) return;
                
                // Process each skill-bar
                for (const skillBar of skillBars) {
                    // Find the progress__text element (try both variations)
                    const progressText = skillBar.querySelector('.progress__text') || 
                                       skillBar.querySelector('.progress_text');
                    
                    if (!progressText) continue;
                    
                    const text = (progressText.textContent || '').trim();
                    
                    // Parse text like "Battling (120)" to extract skill name and level
                    // Match pattern: "Skill Name (level)"
                    const match = text.match(/^(.+?)\s*\((\d+)\)$/);
                    
                    if (!match || match.length < 3) continue;
                    
                    const skillName = match[1].trim();
                    const currentLevel = parseInt(match[2], 10);
                    
                    if (!skillName || isNaN(currentLevel)) continue;
                    
                    // Check if we've seen this skill before
                    const previousLevel = this.skillLevels.get(skillName);
                    
                    if (previousLevel !== undefined) {
                        // Skill exists in tracking - check if level increased
                        if (currentLevel > previousLevel) {
                            // Level increased!
                            NotificationManager.notify(
                                'Skills',
                                `Skill leveled up! ${skillName} (${currentLevel})`,
                                {
                                    sound: config.sound,
                                    desktop: config.desktop,
                                    soundName: CONSTANTS.STRINGS.SOUND_SKILLS,
                                    soundUrl: CONFIG.sounds.skills,
                                    emoji: '⏫'
                                }
                            );
                        }
                    }
                    
                    // Update tracking (always update to current level)
                    this.skillLevels.set(skillName, currentLevel);
                }
            } catch (error) {
                // Silently fail if DOM structure changes or element not found
            }
        },
        
        checkForItemDrops() {
            const config = CONFIG.notifications.itemDrop;
            if (!config.sound && !config.desktop) return;
            if (!config.itemKeywords || config.itemKeywords.length === 0) return;
            
            // Filter out any empty or whitespace-only keywords
            const validKeywords = config.itemKeywords.filter(kw => kw && typeof kw === 'string' && kw.trim().length > 0);
            if (validKeywords.length === 0) return;
            
            try {
                // Find the log div in the right sidebar
                const logDiv = document.querySelector('#log-div') || document.querySelector('.log-div');
                if (!logDiv) return;
                
                // Find all item clickable divs within the log
                const itemDivs = logDiv.querySelectorAll('.item.clickable');
                if (!itemDivs || itemDivs.length === 0) return;
                
                // Process each item drop entry
                for (const itemDiv of itemDivs) {
                    // Find the paragraph containing the item name
                    const itemNamePara = itemDiv.querySelector('p');
                    if (!itemNamePara) continue;
                    
                    // Extract item name (remove brackets if present)
                    let itemName = (itemNamePara.textContent || '').trim();
                    itemName = itemName.replace(/^\[|\]$/g, '').trim();
                    
                    if (!itemName) continue;
                    
                    // Check if this item matches any tracked keyword (case-insensitive)
                    const itemNameLower = itemName.toLowerCase();
                    const matchedKeyword = validKeywords.find(keyword => {
                        const kw = (keyword || '').toLowerCase().trim();
                        return kw && itemNameLower.includes(kw);
                    });
                    
                    if (!matchedKeyword) continue;
                    
                    // Find the amount and timestamp - look for it in the log entry container
                    // The structure is: log entry container -> timestamp span -> amount span -> item div
                    let amount = '1'; // Default to 1 if not found
                    let timestamp = ''; // Track timestamp for unique identification
                    
                    // Try to find the log entry container (parent of item div, or parent's parent)
                    let container = itemDiv.parentElement;
                    while (container && container !== logDiv) {
                        // Look for spans that might contain the amount or timestamp
                        const spans = container.querySelectorAll('span');
                        for (const span of spans) {
                            const spanText = (span.textContent || '').trim();
                            
                            // Check if this span contains a number pattern like "+1 " or "+5 "
                            if (spanText.match(/\+?\d+/)) {
                                const amountMatch = spanText.match(/\+?(\d+)/);
                                if (amountMatch && amountMatch[1]) {
                                    amount = amountMatch[1];
                                }
                            }
                            
                            // Try to capture timestamp (usually first span in log entry, not a number)
                            if (!timestamp && spanText && spanText.length > 0 && !spanText.match(/^\+?\d+$/)) {
                                timestamp = spanText;
                            }
                        }
                        if (amount !== '1' && timestamp) break; // Found both, exit loop
                        
                        // Move up to parent container
                        container = container.parentElement;
                    }
                    
                    // Create unique identifier: item name + amount + timestamp
                    // This prevents duplicate notifications even if DOM elements are recreated
                    const uniqueId = `${itemNameLower}|${amount}|${timestamp}`;
                    
                    // Skip if already processed
                    if (this.processedItemDrops.has(uniqueId)) continue;
                    
                    // Mark as processed
                    this.processedItemDrops.add(uniqueId);
                    
                    // Send notification
                    NotificationManager.notify(
                        'Item Log',
                        `Found ${amount}x ${itemName}`,
                        {
                            sound: config.sound,
                            desktop: config.desktop,
                            soundName: CONSTANTS.STRINGS.SOUND_ITEM_DROP,
                            soundUrl: CONFIG.sounds.itemDrop,
                            emoji: '💰'
                        }
                    );
                }
            } catch (error) {
                // Silently fail if DOM structure changes or element not found
            }
        },
        
        checkForAbyssBattlesCompletion() {
            const config = CONFIG.notifications.abyssBattles;
            if (!config.sound && !config.desktop) return;
            
            try {
                // Find the div with classes "margin-top-small grey-text" containing " Battles Remaining"
                const mainGameSection = document.querySelector('.main-game-section');
                if (!mainGameSection) return;
                
                // Find all divs with the specific classes
                const battleDivs = mainGameSection.querySelectorAll('.margin-top-small.grey-text');
                let battlesDiv = null;
                
                // Find the one containing " Battles Remaining"
                for (const div of battleDivs) {
                    const text = div.textContent || '';
                    if (text.includes('Battles Remaining')) {
                        battlesDiv = div;
                        break;
                    }
                }
                
                // If battlesDiv doesn't exist, we're not in abyss battles section
                // Reset tracking and don't assume completion
                if (!battlesDiv) {
                    this.lastAbyssBattlesCount = null;
                    return;
                }
                
                // Find the span with class "green-text" inside it
                const battlesSpan = battlesDiv.querySelector('span.green-text');
                if (!battlesSpan) {
                    // If span doesn't exist but div does, reset tracking
                    this.lastAbyssBattlesCount = null;
                    return;
                }
                
                const battlesText = (battlesSpan.textContent || '').trim();
                const battlesCount = parseInt(battlesText, 10);
                
                // Check if count is valid
                if (isNaN(battlesCount)) {
                    // If text is not a number, reset tracking
                    this.lastAbyssBattlesCount = null;
                    return;
                }
                
                // Detect when battles reach 0 (completed)
                // Only notify when transitioning from > 0 to 0
                if (battlesCount === 0) {
                    // Only notify if we haven't already notified for this completion
                    // and we were previously tracking a count > 0
                    if (this.lastAbyssBattlesCount !== null && this.lastAbyssBattlesCount > 0) {
                        NotificationManager.notify(
                            'Abyss Battles',
                            'All Abyss Battles Completed!',
                            {
                                sound: config.sound,
                                desktop: config.desktop,
                                soundName: CONSTANTS.STRINGS.SOUND_ABYSS_BATTLES,
                                soundUrl: CONFIG.sounds.abyssBattles,
                                emoji: '🌀'
                            }
                        );
                    }
                }
                
                // Update tracking (always update, even if 0, to prevent duplicate notifications)
                this.lastAbyssBattlesCount = battlesCount;
            } catch (error) {
                // Silently fail if DOM structure changes or element not found
                this.lastAbyssBattlesCount = null;
            }
        },
        
        checkForPotionThreshold() {
            const config = CONFIG.notifications.potions;
            if (!config.sound && !config.desktop) return;
            
            try {
                // Find the Effects panel - look for divs with class "flex space-between" that contain potions
                // The structure is: .flex.space-between > span (potion name) > span.green-text (count)
                const allFlexDivs = document.querySelectorAll('.flex.space-between');
                if (!allFlexDivs || allFlexDivs.length === 0) return;
                
                const threshold = config.threshold || 0;
                const repeatCount = config.repeatCount || 1;
                
                // Process each flex div to find potion entries
                for (const flexDiv of allFlexDivs) {
                    // Check if this div contains a potion (has an item clickable with a potion name and a green-text span)
                    const itemDiv = flexDiv.querySelector('.item.clickable');
                    if (!itemDiv) continue;
                    
                    const potionNamePara = itemDiv.querySelector('p');
                    if (!potionNamePara) continue;
                    
                    // Extract potion name (remove brackets if present)
                    let potionName = (potionNamePara.textContent || '').trim();
                    potionName = potionName.replace(/^\[|\]$/g, '').trim();
                    if (!potionName) continue;
                    
                    // Check if this is actually a potion (contains "Potion" in the name)
                    if (!potionName.toLowerCase().includes('potion')) continue;
                    
                    // Find the green-text span with the count
                    const countSpan = flexDiv.querySelector('span.green-text');
                    if (!countSpan) continue;
                    
                    // Parse the count (remove commas)
                    const countText = (countSpan.textContent || '').trim().replace(/,/g, '');
                    const currentCount = parseInt(countText, 10);
                    
                    if (isNaN(currentCount)) continue;
                    
                    // Get previous count for this potion
                    const previousCount = this.lastPotionCounts.get(potionName);
                    const wasUnderThreshold = this.potionUnderThreshold.get(potionName) || false;
                    const isCurrentlyUnder = currentCount <= threshold;
                    
                    // Check if we've crossed the threshold (went from above to at/below threshold)
                    if (previousCount !== undefined && 
                        previousCount > threshold && 
                        currentCount <= threshold) {
                        
                        // Just crossed threshold - trigger first notification
                        this.potionRepeatCounts.set(potionName, 1);
                        this.potionUnderThreshold.set(potionName, true);
                        this.lastPotionNotificationTimes.set(potionName, Date.now());
                        
                        NotificationManager.notify(
                            'Potion Alert',
                            `${potionName}: ${countText.toLocaleString()} remaining`,
                            {
                                sound: config.sound,
                                desktop: config.desktop,
                                soundName: CONSTANTS.STRINGS.SOUND_POTIONS,
                                soundUrl: CONFIG.sounds.potions,
                                emoji: '🧪'
                            }
                        );
                    } 
                    // Check if we're still under threshold and need to repeat
                    else if (isCurrentlyUnder && wasUnderThreshold) {
                        const currentRepeatCount = this.potionRepeatCounts.get(potionName) || 0;
                        if (currentRepeatCount < repeatCount) {
                            const repeatInterval = config.repeatInterval !== undefined ? config.repeatInterval : 0; // Default to 0 (immediate) if not set
                            const now = Date.now();
                            const lastNotificationTime = this.lastPotionNotificationTimes.get(potionName);
                            const timeSinceLastNotification = lastNotificationTime 
                                ? (now - lastNotificationTime) / 1000 
                                : Infinity; // If no previous notification, allow it
                            
                            // Check if enough time has passed (or interval is 0 for immediate repeats)
                            if (repeatInterval === 0 || timeSinceLastNotification >= repeatInterval) {
                                // Still under threshold and haven't reached repeat limit
                                this.potionRepeatCounts.set(potionName, currentRepeatCount + 1);
                                this.lastPotionNotificationTimes.set(potionName, now);
                                
                                NotificationManager.notify(
                                    'Potion Alert',
                                    `${potionName}: ${countText.toLocaleString()} remaining`,
                                    {
                                        sound: config.sound,
                                        desktop: config.desktop,
                                        soundName: CONSTANTS.STRINGS.SOUND_POTIONS,
                                        soundUrl: CONFIG.sounds.potions,
                                        emoji: '🧪'
                                    }
                                );
                            }
                        }
                    }
                    // Check if we've gone back above threshold
                    else if (wasUnderThreshold && !isCurrentlyUnder) {
                        // Reset repeat counter and notification time when we go back above threshold
                        this.potionRepeatCounts.set(potionName, 0);
                        this.potionUnderThreshold.set(potionName, false);
                        this.lastPotionNotificationTimes.delete(potionName);
                    }
                    // Update state if we're currently under threshold
                    else if (isCurrentlyUnder) {
                        this.potionUnderThreshold.set(potionName, true);
                    }
                    
                    // Update tracking
                    this.lastPotionCounts.set(potionName, currentCount);
                }
                
                // Clean up tracking for potions that no longer exist
                const currentPotionNames = new Set();
                for (const flexDiv of allFlexDivs) {
                    const itemDiv = flexDiv.querySelector('.item.clickable');
                    if (!itemDiv) continue;
                    const potionNamePara = itemDiv.querySelector('p');
                    if (!potionNamePara) continue;
                    let potionName = (potionNamePara.textContent || '').trim();
                    potionName = potionName.replace(/^\[|\]$/g, '').trim();
                    if (potionName && potionName.toLowerCase().includes('potion')) {
                        currentPotionNames.add(potionName);
                    }
                }
                
                // Remove tracking for potions that are no longer active
                for (const [potionName] of this.lastPotionCounts) {
                    if (!currentPotionNames.has(potionName)) {
                        this.lastPotionCounts.delete(potionName);
                        this.potionRepeatCounts.delete(potionName);
                        this.potionUnderThreshold.delete(potionName);
                        this.lastPotionNotificationTimes.delete(potionName);
                    }
                }
            } catch (error) {
                // Silently fail if DOM structure changes or element not found
            }
        },
        
        cleanup() {
            if (this.checkInterval) {
                clearInterval(this.checkInterval);
                this.checkInterval = null;
            }
        }
    };

    // ============================================
    // Image Modal Manager
    // ============================================
    const ImageModalManager = {
        modalOverlay: null,
        modal: null,
        image: null,
        videoFrame: null,
        initialized: false,
        escHandler: null,
        observer: null,
        processedNodes: new WeakSet(), // Track processed nodes to avoid reprocessing
        
        init() {
            if (this.initialized) return;
            
            // Wait for DOM to be ready
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', () => this.startObserving());
            } else {
                this.startObserving();
            }
            
            this.initialized = true;
        },
        
        startObserving() {
            // Use MutationObserver with debouncing to avoid interfering with chat
            let timeout;
            this.observer = new MutationObserver((mutations) => {
                // Debounce processing to avoid interfering with rapid DOM changes
                clearTimeout(timeout);
                timeout = setTimeout(() => {
                    mutations.forEach((mutation) => {
                        mutation.addedNodes.forEach((node) => {
                            if (node.nodeType === 1) { // Element node
                                // Process new nodes for plain text image and video URLs
                                this.processNodeForImageUrls(node);
                            }
                        });
                    });
                }, 200); // Small delay to let chat finish its DOM manipulation
            });
            
            // Observe the chat container
            const chatContainer = document.querySelector('.chat-content');
            if (chatContainer) {
                this.observer.observe(chatContainer, {
                    childList: true,
                    subtree: true
                });
                
                // Process existing messages
                this.processNodeForImageUrls(chatContainer);
            } else {
                // Retry if chat container not found yet
                setTimeout(() => this.startObserving(), 1000);
            }
        },
        
        processNodeForImageUrls(node) {
            // Skip if already processed
            if (this.processedNodes.has(node)) return;
            
            // Skip if this is an input, form, or interactive element
            if (node.tagName === 'INPUT' || 
                node.tagName === 'TEXTAREA' || 
                node.tagName === 'FORM' ||
                node.tagName === 'BUTTON' ||
                node.isContentEditable ||
                node.contentEditable === 'true') {
                return;
            }
            
            // Skip if inside a form or input
            if (node.closest('form') || 
                node.closest('input') || 
                node.closest('textarea') ||
                node.closest('[contenteditable="true"]')) {
                return;
            }
            
            // Skip if this is the chat container itself (we want to process its children)
            if (node.classList && node.classList.contains('chat-content')) {
                // Process all child nodes instead
                if (node.children) {
                    Array.from(node.children).forEach(child => {
                        this.processNodeForImageUrls(child);
                    });
                }
                return;
            }
            
            // Process any node that has text content and is likely a message
            // Be more flexible - process any element with text that contains URLs
            if (node.textContent && node.nodeType === 1) {
                const text = node.textContent;
                const urlPattern = /(https?:\/\/[^\s<>"']+)/gi;
                const urls = [...new Set(text.match(urlPattern) || [])];
                
                if (urls && urls.length > 0) {
                    // Filter for image and video URLs
                    const imageUrls = urls.filter(url => {
                        return this.isImageUrl(url);
                    });
                    const videoUrls = urls.filter(url => {
                        return this.isVideoUrl(url);
                    });
                    
                    if (imageUrls.length > 0 || videoUrls.length > 0) {
                        // Mark node as processed
                        this.processedNodes.add(node);
                        
                        // Convert text URLs to clickable links
                        this.convertTextUrlsToClickable(node, imageUrls, videoUrls);
                    }
                }
            }
        },
        
        convertTextUrlsToClickable(node, imageUrls, videoUrls = []) {
            // Use TreeWalker to find and replace text nodes containing URLs
            const allMediaUrls = [...imageUrls, ...videoUrls];
            const walker = document.createTreeWalker(
                node,
                NodeFilter.SHOW_TEXT,
                {
                    acceptNode: (textNode) => {
                        // Only process text nodes that contain our image or video URLs
                        const text = textNode.textContent;
                        const hasMediaUrl = allMediaUrls.some(url => text.includes(url));
                        return hasMediaUrl ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
                    }
                },
                false
            );
            
            const textNodesToReplace = [];
            let textNode;
            while (textNode = walker.nextNode()) {
                textNodesToReplace.push(textNode);
            }
            
            // Process text nodes in reverse order to maintain indices
            textNodesToReplace.reverse().forEach((textNode) => {
                const text = textNode.textContent;
                const urlPattern = /(https?:\/\/[^\s<>"']+)/gi;
                const parts = [];
                let lastIndex = 0;
                let match;
                
                // Split text by URLs
                while ((match = urlPattern.exec(text)) !== null) {
                    const url = match[0];
                    const index = match.index;
                    
                    // Add text before URL
                    if (index > lastIndex) {
                        const beforeText = text.substring(lastIndex, index);
                        if (beforeText) {
                            parts.push({ type: 'text', content: beforeText });
                        }
                    }
                    
                    // Add URL (check if it's an image or video)
                    if (imageUrls.includes(url)) {
                        parts.push({ type: 'imageUrl', content: url });
                    } else if (videoUrls.includes(url)) {
                        parts.push({ type: 'videoUrl', content: url });
                    } else {
                        parts.push({ type: 'text', content: url });
                    }
                    
                    lastIndex = index + url.length;
                }
                
                // Add remaining text
                if (lastIndex < text.length) {
                    const remainingText = text.substring(lastIndex);
                    if (remainingText) {
                        parts.push({ type: 'text', content: remainingText });
                    }
                }
                
                // If we found URLs to replace, create new elements
                if (parts.some(p => p.type === 'imageUrl' || p.type === 'videoUrl')) {
                    const fragment = document.createDocumentFragment();
                    
                    parts.forEach((part) => {
                        if (part.type === 'imageUrl' || part.type === 'videoUrl') {
                            // Create clickable span for image or video URL
                            const linkSpan = document.createElement('span');
                            linkSpan.textContent = part.content;
                            linkSpan.className = part.type === 'imageUrl' ? 'iqrpg-image-link' : 'iqrpg-video-link';
                            linkSpan.style.cssText = `
                                color: #4CAF50;
                                text-decoration: underline;
                                cursor: pointer;
                                user-select: text;
                            `;
                            linkSpan.setAttribute('data-url', part.content);
                            linkSpan.setAttribute('data-type', part.type === 'imageUrl' ? 'image' : 'video');
                            
                            // Add click handler
                            linkSpan.addEventListener('click', (e) => {
                                e.preventDefault();
                                e.stopPropagation();
                                this.openModal(part.content, part.type === 'imageUrl' ? 'image' : 'video');
                            }, true);
                            
                            // Add hover effect
                            linkSpan.addEventListener('mouseenter', () => {
                                linkSpan.style.color = '#66BB6A';
                            });
                            linkSpan.addEventListener('mouseleave', () => {
                                linkSpan.style.color = '#4CAF50';
                            });
                            
                            fragment.appendChild(linkSpan);
                        } else {
                            // Add plain text
                            fragment.appendChild(document.createTextNode(part.content));
                        }
                    });
                    
                    // Replace the text node with the fragment
                    const parent = textNode.parentNode;
                    if (parent) {
                        parent.replaceChild(fragment, textNode);
                    }
                }
            });
        },
        
        isImageUrl(url) {
            if (!url) return false;
            
            // Check for common image file extensions
            const imageExtensions = /\.(jpg|jpeg|png|gif|webp|bmp|svg)(\?|$)/i;
            if (imageExtensions.test(url)) return true;
            
            // Check for common image hosting services
            const imageHosts = [
                /imgur\.com/i,
                /giphy\.com/i,
                /tenor\.com/i,
                /gfycat\.com/i,
                /i\.redd\.it/i,
                /i\.imgur\.com/i,
                /media\.giphy\.com/i,
                /cdn\.discordapp\.com/i,
                /discord\.com\/attachments/i
            ];
            
            return imageHosts.some(pattern => pattern.test(url));
        },
        
        isVideoUrl(url) {
            if (!url) return false;
            
            // Check for YouTube URL patterns (including Shorts)
            const youtubePatterns = [
                /youtube\.com\/watch\?v=([\w-]+)/i,
                /youtu\.be\/([\w-]+)/i,
                /youtube\.com\/embed\/([\w-]+)/i,
                /youtube\.com\/v\/([\w-]+)/i,
                /youtube\.com\/shorts\/([\w-]+)/i  // YouTube Shorts
            ];
            
            return youtubePatterns.some(pattern => pattern.test(url));
        },
        
        getYouTubeEmbedUrl(url) {
            if (!url) return null;
            
            // Check if it's a Short
            const isShort = /youtube\.com\/shorts\/([\w-]+)/i.test(url);
            
            // Extract video ID from various YouTube URL formats
            let videoId = null;
            const patterns = [
                { regex: /youtube\.com\/watch\?v=([\w-]+)/i, index: 1 },
                { regex: /youtu\.be\/([\w-]+)/i, index: 1 },
                { regex: /youtube\.com\/embed\/([\w-]+)/i, index: 1 },
                { regex: /youtube\.com\/v\/([\w-]+)/i, index: 1 },
                { regex: /youtube\.com\/shorts\/([\w-]+)/i, index: 1 }  // Shorts pattern
            ];
            
            for (const { regex, index } of patterns) {
                const match = url.match(regex);
                if (match) {
                    videoId = match[index];
                    break;
                }
            }
            
            if (videoId) {
                return {
                    embedUrl: `https://www.youtube.com/embed/${videoId}`,
                    isShort: isShort || /youtube\.com\/shorts\//i.test(url)  // Double-check
                };
            }
            return null;
        },
        
        openModal(url, type = 'image') {
            // Create overlay if it doesn't exist
            if (!this.modalOverlay) {
                this.createModal();
            }
            
            if (type === 'video') {
                // Handle video (YouTube)
                const videoInfo = this.getYouTubeEmbedUrl(url);
                if (videoInfo && this.videoFrame) {
                    this.videoFrame.src = videoInfo.embedUrl;
                    
                    // Set aspect ratio based on video type
                    if (videoInfo.isShort) {
                        // YouTube Shorts: 9:16 (portrait)
                        this.videoFrame.style.aspectRatio = '9 / 16';
                        this.videoFrame.style.width = 'min(85vw, 405px)';  // 85% viewport width, capped at 405px
                        this.videoFrame.style.maxWidth = '405px';
                        this.videoFrame.style.maxHeight = '720px';
                    } else {
                        // Regular YouTube videos: 16:9 (landscape)
                        this.videoFrame.style.aspectRatio = '16 / 9';
                        this.videoFrame.style.width = 'min(85vw, 1280px)';  // 85% viewport width, capped at 1280px
                        this.videoFrame.style.maxWidth = '1280px';
                        this.videoFrame.style.maxHeight = '720px';
                    }
                    
                    this.videoFrame.style.display = 'block';
                    if (this.image) this.image.style.display = 'none';
                }
            } else {
                // Handle image
                if (this.image) {
                    this.image.src = url;
                    this.image.alt = 'Image';
                    this.image.style.display = 'block';
                }
                if (this.videoFrame) this.videoFrame.style.display = 'none';
            }
            
            // Show modal
            if (this.modalOverlay) {
                this.modalOverlay.classList.add('active');
                document.body.style.overflow = 'hidden';
            }
        },
        
        createModal() {
            // Create overlay
            this.modalOverlay = document.createElement('div');
            this.modalOverlay.className = 'iqrpg-image-modal-overlay';
            this.modalOverlay.style.cssText = `
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background: rgba(0, 0, 0, 0.9);
                z-index: 10000;
                display: flex;
                align-items: center;
                justify-content: center;
                opacity: 0;
                transition: opacity 0.3s ease;
                cursor: pointer;
                pointer-events: none;
            `;
            
            // Create modal container
            this.modal = document.createElement('div');
            this.modal.className = 'iqrpg-image-modal';
            this.modal.style.cssText = `
                position: relative;
                max-width: 90%;
                max-height: 90%;
                cursor: default;
                pointer-events: auto;
            `;
            
            // Create close button
            const closeBtn = document.createElement('button');
            closeBtn.innerHTML = '×';
            closeBtn.className = 'iqrpg-image-modal-close';
            closeBtn.style.cssText = `
                position: absolute;
                top: -40px;
                right: 0;
                background: rgba(255, 255, 255, 0.2);
                border: none;
                color: white;
                font-size: 32px;
                width: 40px;
                height: 40px;
                border-radius: 50%;
                cursor: pointer;
                display: flex;
                align-items: center;
                justify-content: center;
                transition: background 0.2s ease;
                pointer-events: auto;
            `;
            closeBtn.onmouseover = () => closeBtn.style.background = 'rgba(255, 255, 255, 0.3)';
            closeBtn.onmouseout = () => closeBtn.style.background = 'rgba(255, 255, 255, 0.2)';
            
            // Create image element
            this.image = document.createElement('img');
            this.image.style.cssText = `
                max-width: 100%;
                max-height: 90vh;
                object-fit: contain;
                border-radius: 8px;
                box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
                display: block;
            `;
            
            // Create video iframe element
            this.videoFrame = document.createElement('iframe');
            this.videoFrame.style.cssText = `
                aspect-ratio: 16 / 9;  /* Default to regular video ratio */
                border: none;
                border-radius: 8px;
                box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
                display: none;
            `;
            this.videoFrame.setAttribute('allowfullscreen', 'true');
            this.videoFrame.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture');
            
            // Assemble modal
            this.modal.appendChild(closeBtn);
            this.modal.appendChild(this.image);
            this.modal.appendChild(this.videoFrame);
            this.modalOverlay.appendChild(this.modal);
            document.body.appendChild(this.modalOverlay);
            
            // Add CSS for active state
            if (!document.getElementById('iqrpg-image-modal-styles')) {
                const style = document.createElement('style');
                style.id = 'iqrpg-image-modal-styles';
                style.textContent = `
                    .iqrpg-image-modal-overlay.active {
                        opacity: 1 !important;
                        pointer-events: auto !important;
                    }
                    .iqrpg-image-link:hover,
                    .iqrpg-video-link:hover {
                        color: #66BB6A !important;
                    }
                `;
                document.head.appendChild(style);
            }
            
            // Event listeners
            closeBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                this.closeModal();
            });
            
            this.modalOverlay.addEventListener('click', (e) => {
                if (e.target === this.modalOverlay) {
                    this.closeModal();
                }
            });
            
            // ESC key to close
            this.escHandler = (e) => {
                if (e.key === 'Escape' && this.modalOverlay.classList.contains('active')) {
                    this.closeModal();
                }
            };
            document.addEventListener('keydown', this.escHandler);
        },
        
        closeModal() {
            if (this.modalOverlay) {
                this.modalOverlay.classList.remove('active');
                document.body.style.overflow = '';
                
                // Remove image source to free memory
                if (this.image) {
                    this.image.src = '';
                    this.image.style.display = 'block';
                }
                
                // Remove video iframe source to stop playback and free memory
                if (this.videoFrame) {
                    this.videoFrame.src = '';
                    this.videoFrame.style.display = 'none';
                }
            }
        },
        
        cleanup() {
            if (this.observer) {
                this.observer.disconnect();
                this.observer = null;
            }
            if (this.escHandler) {
                document.removeEventListener('keydown', this.escHandler);
                this.escHandler = null;
            }
            if (this.modalOverlay) {
                this.modalOverlay.remove();
                this.modalOverlay = null;
                this.modal = null;
                this.image = null;
                this.videoFrame = null;
            }
            document.body.style.overflow = '';
        }
    };

    // ============================================
    // GUI Manager - Settings Button & Modal
    // ============================================
    const GUIManager = {
        initialized: false,
        
        // Section name mapping for navigation
        sectionNames: {
            volume: 'Volume',
            autos: 'Autos',
            globalEvents: 'Globals',
            bossSpawn: 'Bosses',
            gatheringEvents: 'Gathering',
            actionBonus: 'Action Bonus',
            clan: 'Clan',
            dungeon: 'Dungeon',
            land: 'Raid',
            mastery: 'Mastery',
            skills: 'Skills',
            tradeAlert: 'Trade',
            itemDrop: 'Log',
            message: 'Message',
            abyssBattles: 'Abyss',
            potions: 'Potions'
        },
        settingsButton: null,
        modal: null,
        modalOverlay: null,
        escKeyHandler: null,

        init() {
            if (this.initialized || !CONFIG.gui.enabled) return;
            this.initialized = true;

            // Wait for DOM to be ready
            const initGUI = () => {
                this.injectStyles();
                // Delay button creation slightly to ensure header content is loaded
                setTimeout(() => {
                    this.createSettingsButton();
                }, CONSTANTS.DELAYS.BUTTON_CREATION);
                this.createModal();
            };

            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', initGUI);
            } else {
                initGUI();
            }
        },

        injectStyles() {
            if (document.getElementById('iqrpg-enhanced-styles')) return;

            const style = document.createElement('style');
            style.id = 'iqrpg-enhanced-styles';
            style.textContent = `
                /* Settings Button */
                .iqrpg-settings-btn {
                    width: 32px;
                    height: 32px;
                    background: transparent;
                    border: none;
                    border-radius: 6px;
                    cursor: pointer;
                    box-shadow: none;
                    display: inline-flex;
                    align-items: center;
                    justify-content: center;
                    transition: all 0.3s ease;
                    font-size: 16px;
                    color: white;
                    padding: 0;
                    margin-right: 8px;
                    vertical-align: middle;
                }
                .iqrpg-settings-btn:hover {
                    transform: scale(1.05);
                    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
                }
                .iqrpg-settings-btn:active {
                    transform: scale(0.98);
                }

                /* Modal Overlay */
                .iqrpg-modal-overlay {
                    position: fixed;
                    top: 0;
                    left: 0;
                    width: 100%;
                    height: 100%;
                    background: rgba(0, 0, 0, 0.7);
                    z-index: 2000;
                    display: none;
                    align-items: center;
                    justify-content: center;
                    backdrop-filter: blur(2px);
                }
                .iqrpg-modal-overlay.active {
                    display: flex;
                }

                /* Modal */
                .iqrpg-modal {
                    background: #1e1e1e;
                    border-radius: 12px;
                    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
                    width: 90%;
                    max-width: 600px;
                    max-height: 90vh;
                    display: flex;
                    flex-direction: column;
                    z-index: 2001;
                    position: relative;
                }
                .iqrpg-modal-body {
                    overflow-y: auto;
                    flex: 1;
                    min-height: 0;
                }

                /* Modal Header */
                .iqrpg-modal-header {
                    padding: 20px;
                    border-bottom: 2px solid #333;
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    background: linear-gradient(135deg, #2a9d2a 0%, #227E22 100%);
                    border-radius: 10px 10px 0 0;
                    flex-shrink: 0;
                }
                .iqrpg-modal-title {
                    margin: 0;
                    color: white;
                    font-size: 24px;
                    font-weight: bold;
                }
                .iqrpg-modal-close {
                    background: rgba(255, 255, 255, 0.2);
                    border: none;
                    color: white;
                    width: 32px;
                    height: 32px;
                    border-radius: 50%;
                    cursor: pointer;
                    font-size: 20px;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    transition: all 0.2s;
                }
                .iqrpg-modal-close:hover {
                    background: rgba(255, 255, 255, 0.3);
                    transform: rotate(90deg);
                }

                /* Modal Navigation Bar */
                .iqrpg-modal-nav {
                    position: sticky;
                    top: 0;
                    z-index: 10;
                    background: #252525;
                    border-bottom: 2px solid #444;
                    padding: 6px 8px;
                    min-height: 60px;
                    flex-shrink: 0;
                }
                .iqrpg-nav-buttons {
                    display: flex;
                    flex-wrap: wrap;
                    gap: 3px;
                    align-items: center;
                }
                .iqrpg-nav-btn {
                    padding: 5px 10px;
                    background: #2a2a2a;
                    border: 1px solid #444;
                    border-radius: 4px;
                    color: #fff;
                    cursor: pointer;
                    font-size: 11.5px;
                    font-weight: 500;
                    transition: all 0.2s;
                    white-space: nowrap;
                    flex: 0 0 auto;
                    min-height: 26px;
                    display: inline-block;
                }
                .iqrpg-nav-btn:hover {
                    background: #333;
                    border-color: #227E22;
                    color: #227E22;
                }
                .iqrpg-nav-btn:active {
                    transform: scale(0.95);
                }

                /* Modal Body */
                .iqrpg-modal-body {
                    padding: 20px;
                    padding-bottom: 80px; /* Space for sticky footer */
                }
                
                /* Modal Footer */
                .iqrpg-modal-footer {
                    position: sticky;
                    bottom: 0;
                    z-index: 10;
                    background: #1e1e1e;
                    border-top: 2px solid #333;
                    padding: 15px 20px;
                    border-radius: 0 0 10px 10px;
                }

                /* Section */
                .iqrpg-section {
                    margin-bottom: 30px;
                    padding: 20px;
                    background: #2a2a2a;
                    border-radius: 8px;
                }
                .iqrpg-section-header {
                    display: flex;
                    align-items: center;
                    margin-bottom: 15px;
                }
                .iqrpg-section-title {
                    margin: 0;
                    color: #fff;
                    font-size: 18px;
                    font-weight: bold;
                }
                .iqrpg-section-content {
                    overflow: hidden;
                }

                /* Toggle Switch */
                .iqrpg-toggle-group {
                    display: flex;
                    align-items: center;
                    justify-content: space-between;
                    margin-bottom: 15px;
                    padding: 10px;
                    background: #1e1e1e;
                    border-radius: 6px;
                }
                .iqrpg-toggle-label {
                    color: #ccc;
                    font-size: 14px;
                    flex: 1;
                }
                .iqrpg-toggle-switch {
                    position: relative;
                    width: 50px;
                    height: 26px;
                    background: #555;
                    border-radius: 13px;
                    cursor: pointer;
                    transition: background 0.3s;
                }
                .iqrpg-toggle-switch.active {
                    background: #227E22;
                }
                .iqrpg-toggle-switch::after {
                    content: '';
                    position: absolute;
                    width: 20px;
                    height: 20px;
                    background: white;
                    border-radius: 50%;
                    top: 3px;
                    left: 3px;
                    transition: left 0.3s;
                }
                .iqrpg-toggle-switch.active::after {
                    left: 27px;
                }

                /* Input Group */
                .iqrpg-input-group {
                    margin-bottom: 15px;
                }
                .iqrpg-input-label {
                    display: block;
                    color: #ccc;
                    font-size: 14px;
                    margin-bottom: 5px;
                }
                .iqrpg-input {
                    width: 100%;
                    padding: 10px;
                    background: #1e1e1e;
                    border: 1px solid #444;
                    border-radius: 6px;
                    color: #fff;
                    font-size: 14px;
                    box-sizing: border-box;
                }
                .iqrpg-input:focus {
                    outline: none;
                    border-color: #227E22;
                }

                /* Volume Slider */
                .iqrpg-volume-group {
                    display: flex;
                    align-items: center;
                    justify-content: space-between;
                    margin-bottom: 15px;
                    padding: 10px;
                    background: #1e1e1e;
                    border-radius: 6px;
                }
                .iqrpg-volume-label {
                    color: #ccc;
                    font-size: 14px;
                    flex: 1;
                    margin-right: 15px;
                }
                .iqrpg-volume-slider {
                    flex: 2;
                    height: 6px;
                    background: #333;
                    border-radius: 3px;
                    outline: none;
                    -webkit-appearance: none;
                    appearance: none;
                    margin: 0;
                    padding: 0;
                }
                .iqrpg-volume-slider::-webkit-slider-runnable-track {
                    width: 100%;
                    height: 6px;
                    background: linear-gradient(to right, #227E22 0%, #227E22 var(--volume-percent, 0%), #333 var(--volume-percent, 0%), #333 100%);
                    border-radius: 3px;
                }
                .iqrpg-volume-slider::-webkit-slider-thumb {
                    -webkit-appearance: none;
                    appearance: none;
                    width: 18px;
                    height: 18px;
                    background: #227E22;
                    border-radius: 50%;
                    cursor: pointer;
                    transition: background 0.2s;
                    margin-top: -6px;
                    position: relative;
                    z-index: 1;
                }
                .iqrpg-volume-slider::-webkit-slider-thumb:hover {
                    background: #2a9d2a;
                }
                .iqrpg-volume-slider::-moz-range-track {
                    width: 100%;
                    height: 6px;
                    background: #333;
                    border-radius: 3px;
                    border: none;
                }
                .iqrpg-volume-slider::-moz-range-progress {
                    background: #227E22;
                    height: 6px;
                    border-radius: 3px 0 0 3px;
                }
                .iqrpg-volume-slider::-moz-range-thumb {
                    width: 18px;
                    height: 18px;
                    background: #227E22;
                    border-radius: 50%;
                    cursor: pointer;
                    border: none;
                    transition: background 0.2s;
                }
                .iqrpg-volume-slider::-moz-range-thumb:hover {
                    background: #2a9d2a;
                }
                .iqrpg-volume-value {
                    color: #227E22;
                    font-size: 14px;
                    font-weight: bold;
                    min-width: 45px;
                    text-align: right;
                    margin-left: 15px;
                }

                /* Buttons */
                .iqrpg-button-group {
                    display: flex;
                    gap: 10px;
                    margin-top: 20px;
                }
                .iqrpg-button {
                    flex: 1;
                    padding: 8px 16px;
                    border: none;
                    border-radius: 6px;
                    cursor: pointer;
                    font-size: 14px;
                    font-weight: bold;
                    transition: all 0.3s;
                }
                .iqrpg-button-primary {
                    background: linear-gradient(135deg, #2a9d2a 0%, #227E22 100%);
                    color: white;
                }
                .iqrpg-button-primary:hover {
                    transform: translateY(-2px);
                    box-shadow: 0 4px 12px rgba(34, 126, 34, 0.4);
                }
                .iqrpg-button-secondary {
                    background: #444;
                    color: white;
                }
                .iqrpg-button-secondary:hover {
                    background: #555;
                }
            `;
            document.head.appendChild(style);
        },

        findButtonContainer() {
            // Find .fixed-top first (this is the actual header bar)
            const fixedTop = document.querySelector(CONSTANTS.SELECTORS.FIXED_TOP);
            
            if (!fixedTop) {
                // DOM not ready yet - return null to trigger retry
                return null;
            }

            // Find .section-3 inside .fixed-top (this is where Premium Store is)
            const section3 = fixedTop.querySelector(CONSTANTS.SELECTORS.SECTION_3);
            
            if (!section3) {
                // section-3 not ready yet - return null to trigger retry
                return null;
            }

            // Find the "Premium Store" element - it's in a <p> with an <a> tag
            // We want to insert before the <p> element that contains "Premium Store"
            let premiumStore = null;
            
            // First, try to find the <p> element containing "Premium Store"
            const paragraphs = section3.querySelectorAll('p');
            for (const p of paragraphs) {
                const text = (p.textContent || p.innerText || '').trim().toLowerCase();
                if (text.includes(CONSTANTS.STRINGS.PREMIUM_STORE)) {
                    premiumStore = p;
                    break;
                }
            }
            
            // If not found in <p>, try <a> tags and get their parent <p>
            if (!premiumStore) {
                const links = section3.querySelectorAll('a');
                for (const a of links) {
                    const text = (a.textContent || a.innerText || '').trim().toLowerCase();
                    if (text.includes(CONSTANTS.STRINGS.PREMIUM_STORE)) {
                        // Find the parent <p> element
                        let parent = a.parentElement;
                        while (parent && parent !== section3 && parent.tagName !== 'P') {
                            parent = parent.parentElement;
                        }
                        if (parent && parent.tagName === 'P') {
                            premiumStore = parent;
                        } else {
                            premiumStore = a;
                        }
                        break;
                    }
                }
            }
            
            // If Premium Store not found yet, return null to trigger retry
            if (!premiumStore) {
                return null;
            }

            return { 
                premiumStore: premiumStore, 
                insertParent: section3 
            };
        },

        createSettingsButton() {
            if (this.settingsButton) return;

            const button = document.createElement('button');
            button.className = 'iqrpg-settings-btn';
            button.innerHTML = '⚙️';
            button.title = 'IQRPG Enhanced Settings';
            button.setAttribute('aria-label', 'Open IQRPG Enhanced Settings');

            button.addEventListener('click', (e) => {
                e.stopPropagation();
                this.openModal();
            });

            // Try to create button with retry logic (DOM might not be fully loaded)
            let retryCount = 0;
            const maxRetries = 10;
            
            const tryCreateButton = () => {
                // Skip if already successfully placed
                if (this.settingsButton) return true;
                
                const container = this.findButtonContainer();
                
                // Container not ready yet - retry
                if (!container) {
                    retryCount++;
                    if (retryCount < maxRetries) {
                        setTimeout(tryCreateButton, CONSTANTS.DELAYS.RETRY_SHORT);
                    }
                    return false;
                }
                
                try {
                    // Insert before Premium Store element in section-3
                    container.insertParent.insertBefore(button, container.premiumStore);
                    this.settingsButton = button;
                    return true;
                } catch (e) {
                    retryCount++;
                    if (retryCount < maxRetries) {
                        setTimeout(tryCreateButton, CONSTANTS.DELAYS.RETRY_SHORT);
                    }
                    return false;
                }
            };

            tryCreateButton();
        },

        createModal() {
            if (this.modal) return;

            // Overlay
            const overlay = document.createElement('div');
            overlay.className = 'iqrpg-modal-overlay';
            overlay.addEventListener('click', (e) => {
                if (e.target === overlay) {
                    this.closeModal();
                }
            });

            // Modal
            const modal = document.createElement('div');
            modal.className = 'iqrpg-modal';

            // Header
            const header = document.createElement('div');
            header.className = 'iqrpg-modal-header';
            header.innerHTML = `
                <h2 class="iqrpg-modal-title">IQRPG Enhanced Settings</h2>
                <button class="iqrpg-modal-close" aria-label="Close">×</button>
            `;
            const closeBtn = header.querySelector('.iqrpg-modal-close');
            if (closeBtn) {
                closeBtn.addEventListener('click', () => this.closeModal());
            }

            // Navigation Bar
            const navBar = document.createElement('div');
            navBar.className = 'iqrpg-modal-nav';
            navBar.innerHTML = `
                <div class="iqrpg-nav-buttons">
                    ${Object.entries(this.sectionNames).map(([type, name]) => 
                        `<button class="iqrpg-nav-btn" data-section="${type}" title="${name}">${name}</button>`
                    ).join('')}
                </div>
            `;

            // Add ESC key listener once in createModal
            this.escKeyHandler = (e) => {
                if (e.key === 'Escape' && this.modalOverlay && this.modalOverlay.classList.contains('active')) {
                    this.closeModal();
                }
            };
            document.addEventListener('keydown', this.escKeyHandler);

            // Body
            const body = document.createElement('div');
            body.className = 'iqrpg-modal-body';
            body.innerHTML = this.generateSettingsHTML();

            // Footer (sticky action buttons)
            const footer = document.createElement('div');
            footer.className = 'iqrpg-modal-footer';
            footer.innerHTML = `
                <div class="iqrpg-button-group">
                    <button class="iqrpg-button iqrpg-button-primary" id="iqrpg-save-btn">Save Settings</button>
                    <button class="iqrpg-button iqrpg-button-secondary" id="iqrpg-reset-btn">Reset to Defaults</button>
                    <button class="iqrpg-button iqrpg-button-secondary" id="iqrpg-toggle-sound-btn">Toggle All Sound Alerts</button>
                    <button class="iqrpg-button iqrpg-button-secondary" id="iqrpg-toggle-notifications-btn">Toggle All Notifications</button>
                </div>
            `;

            modal.appendChild(header);
            modal.appendChild(navBar);
            modal.appendChild(body);
            modal.appendChild(footer);
            overlay.appendChild(modal);

            document.body.appendChild(overlay);

            this.modalOverlay = overlay;
            this.modal = modal;

            // Attach event listeners
            this.attachEventListeners();
        },

        generateNotificationSection(type, label) {
            const config = CONFIG.notifications[type];
            // Map notification type to sound key
            const soundKeyMap = {
                'globalEvents': CONSTANTS.STRINGS.SOUND_GLOBAL,
                'actionBonus': CONSTANTS.STRINGS.SOUND_ACTION_BONUS,
                'tradeAlert': CONSTANTS.STRINGS.SOUND_TRADE_ALERT,
                'gatheringEvents': CONSTANTS.STRINGS.SOUND_GATHERING_EVENT,
                'itemDrop': CONSTANTS.STRINGS.SOUND_ITEM_DROP,
                'message': CONSTANTS.STRINGS.SOUND_MESSAGE,
                'abyssBattles': CONSTANTS.STRINGS.SOUND_ABYSS_BATTLES,
                'potions': CONSTANTS.STRINGS.SOUND_POTIONS
            };
            const soundKey = soundKeyMap[type] || type;
            
            return `
                <div class="iqrpg-section" id="section-${type}" data-section-type="${type}">
                    <div class="iqrpg-section-header">
                        <h3 class="iqrpg-section-title">${label} Notifications</h3>
                    </div>
                    <div class="iqrpg-section-content">
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Sound Alerts</label>
                            <div class="iqrpg-toggle-switch ${config.sound ? 'active' : ''}" 
                                 data-setting="notifications.${type}.sound"></div>
                        </div>
                        
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Desktop Notifications</label>
                            <div class="iqrpg-toggle-switch ${config.desktop ? 'active' : ''}" 
                                 data-setting="notifications.${type}.desktop"></div>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Sound URL (optional)</label>
                            <input type="text" class="iqrpg-input" 
                                   data-setting="sounds.${soundKey}" 
                                   value="${CONFIG.sounds[soundKey] || ''}" 
                                   placeholder="Leave empty for default">
                        </div>
                    </div>
                </div>
            `;
        },

        generateBossSection() {
            const config = CONFIG.notifications.bossSpawn;
            const bossSpawnSoundKey = CONSTANTS.STRINGS.SOUND_BOSS_SPAWN;
            const rareBossSoundKey = CONSTANTS.STRINGS.SOUND_RARE_BOSS;
            
            return `
                <div class="iqrpg-section" id="section-bossSpawn" data-section-type="bossSpawn">
                    <div class="iqrpg-section-header">
                        <h3 class="iqrpg-section-title">Bosses Notifications</h3>
                    </div>
                    <div class="iqrpg-section-content">
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Sound Alerts</label>
                            <div class="iqrpg-toggle-switch ${config.sound ? 'active' : ''}" 
                                 data-setting="notifications.bossSpawn.sound"></div>
                        </div>
                        
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Desktop Notifications</label>
                            <div class="iqrpg-toggle-switch ${config.desktop ? 'active' : ''}" 
                                 data-setting="notifications.bossSpawn.desktop"></div>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Boss Sound URL (optional)</label>
                            <input type="text" class="iqrpg-input" 
                                   data-setting="sounds.${bossSpawnSoundKey}" 
                                   value="${CONFIG.sounds[bossSpawnSoundKey] || ''}" 
                                   placeholder="Leave empty for default">
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Rare Boss Sound URL (optional)</label>
                            <input type="text" class="iqrpg-input" 
                                   data-setting="sounds.${rareBossSoundKey}" 
                                   value="${CONFIG.sounds[rareBossSoundKey] || ''}" 
                                   placeholder="Leave empty for default">
                            <small style="color: #888; font-size: 12px; display: block; margin-top: 5px;">
                                Sound for rarity-4+ bosses
                            </small>
                        </div>
                    </div>
                </div>
            `;
        },

        generateClanSection() {
            const config = CONFIG.notifications.clan;
            const watchtowerSoundKey = CONSTANTS.STRINGS.SOUND_CLAN_WATCHTOWER;
            const globalsSoundKey = CONSTANTS.STRINGS.SOUND_CLAN_GLOBALS;
            
            return `
                <div class="iqrpg-section" id="section-clan" data-section-type="clan">
                    <div class="iqrpg-section-header">
                        <h3 class="iqrpg-section-title">Clan</h3>
                    </div>
                    <div class="iqrpg-section-content">
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Sound Alerts</label>
                            <div class="iqrpg-toggle-switch ${config.sound ? 'active' : ''}" 
                                 data-setting="notifications.clan.sound"></div>
                        </div>
                        
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Desktop Notifications</label>
                            <div class="iqrpg-toggle-switch ${config.desktop ? 'active' : ''}" 
                                 data-setting="notifications.clan.desktop"></div>
                        </div>
                        
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Watchtower</label>
                            <div class="iqrpg-toggle-switch ${config.watchtower ? 'active' : ''}" 
                                 data-setting="notifications.clan.watchtower"></div>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Watchtower Sound URL (optional)</label>
                            <input type="text" class="iqrpg-input" 
                                   data-setting="sounds.${watchtowerSoundKey}" 
                                   value="${CONFIG.sounds[watchtowerSoundKey] || ''}" 
                                   placeholder="Leave empty for default">
                        </div>
                        
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Clan Chat Globals</label>
                            <div class="iqrpg-toggle-switch ${config.clanChatGlobals ? 'active' : ''}" 
                                 data-setting="notifications.clan.clanChatGlobals"></div>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Clan Globals Sound URL (optional)</label>
                            <input type="text" class="iqrpg-input" 
                                   data-setting="sounds.${globalsSoundKey}" 
                                   value="${CONFIG.sounds[globalsSoundKey] || ''}" 
                                   placeholder="Leave empty for default">
                        </div>
                    </div>
                </div>
            `;
        },

        generateItemDropSection() {
            const config = CONFIG.notifications.itemDrop;
            const soundKey = CONSTANTS.STRINGS.SOUND_ITEM_DROP;
            const itemKeywords = (config.itemKeywords || []).join(', ');
            
            return `
                <div class="iqrpg-section" id="section-itemDrop" data-section-type="itemDrop">
                    <div class="iqrpg-section-header">
                        <h3 class="iqrpg-section-title">Log (Item Drops)</h3>
                    </div>
                    <div class="iqrpg-section-content">
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Sound Alerts</label>
                            <div class="iqrpg-toggle-switch ${config.sound ? 'active' : ''}" 
                                 data-setting="notifications.itemDrop.sound"></div>
                        </div>
                        
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Desktop Notifications</label>
                            <div class="iqrpg-toggle-switch ${config.desktop ? 'active' : ''}" 
                                 data-setting="notifications.itemDrop.desktop"></div>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Item Keywords (comma-separated)</label>
                            <input type="text" class="iqrpg-input" 
                                   data-setting="notifications.itemDrop.itemKeywords" 
                                   data-type="array"
                                   value="${itemKeywords}" 
                                   placeholder="e.g. golden egg, diamond, malachite">
                            <small style="color: #888; font-size: 12px; display: block; margin-top: 5px;">
                                Alert when these items are found in the log panel (case-insensitive)
                            </small>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Sound URL (optional)</label>
                            <input type="text" class="iqrpg-input" 
                                   data-setting="sounds.${soundKey}" 
                                   value="${CONFIG.sounds[soundKey] || ''}" 
                                   placeholder="Leave empty for default">
                        </div>
                    </div>
                </div>
            `;
        },

        generateGatheringEventsSection() {
            const woodcutting = CONFIG.notifications.gatheringEvents.woodcutting;
            const quarrying = CONFIG.notifications.gatheringEvents.quarrying;
            const mining = CONFIG.notifications.gatheringEvents.mining;
            const soundKey = CONSTANTS.STRINGS.SOUND_GATHERING_EVENT;
            
            return `
                <div class="iqrpg-section" id="section-gatheringEvents" data-section-type="gatheringEvents">
                    <div class="iqrpg-section-header">
                        <h3 class="iqrpg-section-title">Gathering</h3>
                    </div>
                    <div class="iqrpg-section-content">
                        <!-- Woodcutting Event -->
                        <div style="margin-bottom: 20px; padding: 15px; background: #1e1e1e; border-radius: 6px;">
                            <h4 style="margin: 0 0 10px 0; color: #fff; font-size: 16px;">🌲 Woodcutting Event</h4>
                            <div class="iqrpg-toggle-group">
                                <label class="iqrpg-toggle-label">Sound Alerts</label>
                                <div class="iqrpg-toggle-switch ${woodcutting.sound ? 'active' : ''}" 
                                     data-setting="notifications.gatheringEvents.woodcutting.sound"></div>
                            </div>
                            <div class="iqrpg-toggle-group">
                                <label class="iqrpg-toggle-label">Desktop Notifications</label>
                                <div class="iqrpg-toggle-switch ${woodcutting.desktop ? 'active' : ''}" 
                                     data-setting="notifications.gatheringEvents.woodcutting.desktop"></div>
                            </div>
                        </div>
                        
                        <!-- Quarrying Event -->
                        <div style="margin-bottom: 20px; padding: 15px; background: #1e1e1e; border-radius: 6px;">
                            <h4 style="margin: 0 0 10px 0; color: #fff; font-size: 16px;">🪨 Quarrying Event</h4>
                            <div class="iqrpg-toggle-group">
                                <label class="iqrpg-toggle-label">Sound Alerts</label>
                                <div class="iqrpg-toggle-switch ${quarrying.sound ? 'active' : ''}" 
                                     data-setting="notifications.gatheringEvents.quarrying.sound"></div>
                            </div>
                            <div class="iqrpg-toggle-group">
                                <label class="iqrpg-toggle-label">Desktop Notifications</label>
                                <div class="iqrpg-toggle-switch ${quarrying.desktop ? 'active' : ''}" 
                                     data-setting="notifications.gatheringEvents.quarrying.desktop"></div>
                            </div>
                        </div>
                        
                        <!-- Mining Event -->
                        <div style="margin-bottom: 20px; padding: 15px; background: #1e1e1e; border-radius: 6px;">
                            <h4 style="margin: 0 0 10px 0; color: #fff; font-size: 16px;">☄️ Mining Event</h4>
                            <div class="iqrpg-toggle-group">
                                <label class="iqrpg-toggle-label">Sound Alerts</label>
                                <div class="iqrpg-toggle-switch ${mining.sound ? 'active' : ''}" 
                                     data-setting="notifications.gatheringEvents.mining.sound"></div>
                            </div>
                            <div class="iqrpg-toggle-group">
                                <label class="iqrpg-toggle-label">Desktop Notifications</label>
                                <div class="iqrpg-toggle-switch ${mining.desktop ? 'active' : ''}" 
                                     data-setting="notifications.gatheringEvents.mining.desktop"></div>
                            </div>
                        </div>
                        
                        <!-- Sound URL (shared for all gathering events) -->
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Sound URL (optional)</label>
                            <input type="text" class="iqrpg-input" 
                                   data-setting="sounds.${soundKey}" 
                                   value="${CONFIG.sounds[soundKey] || ''}" 
                                   placeholder="Leave empty for default">
                        </div>
                    </div>
                </div>
            `;
        },

        generateTradeAlertSection() {
            const config = CONFIG.notifications.tradeAlert;
            const soundKey = CONSTANTS.STRINGS.SOUND_TRADE_ALERT;
            const sellingKeywords = (config.sellingKeywords || []).join(', ');
            const buyingKeywords = (config.buyingKeywords || []).join(', ');
            
            return `
                <div class="iqrpg-section" id="section-tradeAlert" data-section-type="tradeAlert">
                    <div class="iqrpg-section-header">
                        <h3 class="iqrpg-section-title">Trade</h3>
                    </div>
                    <div class="iqrpg-section-content">
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Sound Alerts</label>
                            <div class="iqrpg-toggle-switch ${config.sound ? 'active' : ''}" 
                                 data-setting="notifications.tradeAlert.sound"></div>
                        </div>
                        
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Desktop Notifications</label>
                            <div class="iqrpg-toggle-switch ${config.desktop ? 'active' : ''}" 
                                 data-setting="notifications.tradeAlert.desktop"></div>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Selling Keywords (comma-separated)</label>
                            <input type="text" class="iqrpg-input" 
                                   data-setting="notifications.tradeAlert.sellingKeywords" 
                                   data-type="array"
                                   value="${sellingKeywords}" 
                                   placeholder="e.g. iq, mana, qs2">
                            <small style="color: #888; font-size: 12px; display: block; margin-top: 5px;">
                                Alert when someone is selling these items (detects: selling, wts, sell, s>)
                            </small>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Buying Keywords (comma-separated)</label>
                            <input type="text" class="iqrpg-input" 
                                   data-setting="notifications.tradeAlert.buyingKeywords" 
                                   data-type="array"
                                   value="${buyingKeywords}" 
                                   placeholder="e.g. iq, mana, qs2">
                            <small style="color: #888; font-size: 12px; display: block; margin-top: 5px;">
                                Alert when someone is buying these items (detects: buying, wtb, buy, b>)
                            </small>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Sound URL (optional)</label>
                            <input type="text" class="iqrpg-input" 
                                   data-setting="sounds.${soundKey}" 
                                   value="${CONFIG.sounds[soundKey] || ''}" 
                                   placeholder="Leave empty for default">
                        </div>
                    </div>
                </div>
            `;
        },

        generateAutosSection() {
            const config = CONFIG.notifications.autos;
            const soundKey = CONSTANTS.STRINGS.SOUND_AUTOS;
            const threshold = config.threshold || 100;
            const repeatCount = config.repeatCount || 1;
            
            return `
                <div class="iqrpg-section" id="section-autos" data-section-type="autos">
                    <div class="iqrpg-section-header">
                        <h3 class="iqrpg-section-title">Autos Alert</h3>
                    </div>
                    <div class="iqrpg-section-content">
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Sound Alerts</label>
                            <div class="iqrpg-toggle-switch ${config.sound ? 'active' : ''}" 
                                 data-setting="notifications.autos.sound"></div>
                        </div>
                        
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Desktop Notifications</label>
                            <div class="iqrpg-toggle-switch ${config.desktop ? 'active' : ''}" 
                                 data-setting="notifications.autos.desktop"></div>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Alert Threshold</label>
                            <input type="number" class="iqrpg-input" 
                                   data-setting="notifications.autos.threshold" 
                                   value="${threshold}" 
                                   min="0"
                                   placeholder="100">
                            <small style="color: #888; font-size: 12px; display: block; margin-top: 5px;">
                                Alert when autos remaining reaches this number or below
                            </small>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Repeat Count</label>
                            <input type="number" class="iqrpg-input" 
                                   data-setting="notifications.autos.repeatCount" 
                                   value="${repeatCount}" 
                                   min="1"
                                   placeholder="1">
                            <small style="color: #888; font-size: 12px; display: block; margin-top: 5px;">
                                Number of times to repeat alert while autos stay under threshold (1 = no repeat)
                            </small>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Repeat Interval (seconds)</label>
                            <input type="number" class="iqrpg-input" 
                                   data-setting="notifications.autos.repeatInterval" 
                                   value="${config.repeatInterval !== undefined ? config.repeatInterval : 1}" 
                                   min="0"
                                   placeholder="1">
                            <small style="color: #888; font-size: 12px; display: block; margin-top: 5px;">
                                Seconds between repeat alerts (0 = immediate repeats, no delay)
                            </small>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Sound URL (optional)</label>
                            <input type="text" class="iqrpg-input" 
                                   data-setting="sounds.${soundKey}" 
                                   value="${CONFIG.sounds[soundKey] || ''}" 
                                   placeholder="Leave empty for default">
                        </div>
                    </div>
                </div>
            `;
        },

        generatePotionSection() {
            const config = CONFIG.notifications.potions;
            const soundKey = CONSTANTS.STRINGS.SOUND_POTIONS;
            const threshold = config.threshold || 100;
            const repeatCount = config.repeatCount || 1;
            
            return `
                <div class="iqrpg-section" id="section-potions" data-section-type="potions">
                    <div class="iqrpg-section-header">
                        <h3 class="iqrpg-section-title">Potion Alert</h3>
                    </div>
                    <div class="iqrpg-section-content">
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Sound Alerts</label>
                            <div class="iqrpg-toggle-switch ${config.sound ? 'active' : ''}" 
                                 data-setting="notifications.potions.sound"></div>
                        </div>
                        
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Desktop Notifications</label>
                            <div class="iqrpg-toggle-switch ${config.desktop ? 'active' : ''}" 
                                 data-setting="notifications.potions.desktop"></div>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Alert Threshold</label>
                            <input type="number" class="iqrpg-input" 
                                   data-setting="notifications.potions.threshold" 
                                   value="${threshold}" 
                                   min="0"
                                   placeholder="100">
                            <small style="color: #888; font-size: 12px; display: block; margin-top: 5px;">
                                Alert when potion remaining reaches this number or below
                            </small>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Repeat Count</label>
                            <input type="number" class="iqrpg-input" 
                                   data-setting="notifications.potions.repeatCount" 
                                   value="${repeatCount}" 
                                   min="1"
                                   placeholder="1">
                            <small style="color: #888; font-size: 12px; display: block; margin-top: 5px;">
                                Number of times to repeat alert while potion stays under threshold (1 = no repeat)
                            </small>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Repeat Interval (seconds)</label>
                            <input type="number" class="iqrpg-input" 
                                   data-setting="notifications.potions.repeatInterval" 
                                   value="${config.repeatInterval !== undefined ? config.repeatInterval : 1}" 
                                   min="0"
                                   placeholder="1">
                            <small style="color: #888; font-size: 12px; display: block; margin-top: 5px;">
                                Seconds between repeat alerts (0 = immediate repeats, no delay)
                            </small>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Sound URL (optional)</label>
                            <input type="text" class="iqrpg-input" 
                                   data-setting="sounds.${soundKey}" 
                                   value="${CONFIG.sounds[soundKey] || ''}" 
                                   placeholder="Leave empty for default">
                        </div>
                    </div>
                </div>
            `;
        },

        generateDungeonSection() {
            const config = CONFIG.notifications.dungeon;
            const soundKey = CONSTANTS.STRINGS.SOUND_DUNGEON;
            
            return `
                <div class="iqrpg-section" id="section-dungeon" data-section-type="dungeon">
                    <div class="iqrpg-section-header">
                        <h3 class="iqrpg-section-title">Dungeon Completion</h3>
                    </div>
                    <div class="iqrpg-section-content">
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Sound Alerts</label>
                            <div class="iqrpg-toggle-switch ${config.sound ? 'active' : ''}" 
                                 data-setting="notifications.dungeon.sound"></div>
                        </div>
                        
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Desktop Notifications</label>
                            <div class="iqrpg-toggle-switch ${config.desktop ? 'active' : ''}" 
                                 data-setting="notifications.dungeon.desktop"></div>
                        </div>
                        
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Only When All Keys Complete</label>
                            <div class="iqrpg-toggle-switch ${config.onlyWhenAllKeysComplete ? 'active' : ''}" 
                                 data-setting="notifications.dungeon.onlyWhenAllKeysComplete"></div>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Sound URL (optional)</label>
                            <input type="text" class="iqrpg-input" 
                                   data-setting="sounds.${soundKey}" 
                                   value="${CONFIG.sounds[soundKey] || ''}" 
                                   placeholder="Leave empty for default">
                        </div>
                    </div>
                </div>
            `;
        },

        generateMasterySection() {
            const config = CONFIG.notifications.mastery;
            const soundKey = CONSTANTS.STRINGS.SOUND_MASTERY;
            
            return `
                <div class="iqrpg-section" id="section-mastery" data-section-type="mastery">
                    <div class="iqrpg-section-header">
                        <h3 class="iqrpg-section-title">Mastery Level Increases</h3>
                    </div>
                    <div class="iqrpg-section-content">
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Sound Alerts</label>
                            <div class="iqrpg-toggle-switch ${config.sound ? 'active' : ''}" 
                                 data-setting="notifications.mastery.sound"></div>
                        </div>
                        
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Desktop Notifications</label>
                            <div class="iqrpg-toggle-switch ${config.desktop ? 'active' : ''}" 
                                 data-setting="notifications.mastery.desktop"></div>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Sound URL (optional)</label>
                            <input type="text" class="iqrpg-input" 
                                   data-setting="sounds.${soundKey}" 
                                   value="${CONFIG.sounds[soundKey] || ''}" 
                                   placeholder="Leave empty for default">
                        </div>
                    </div>
                </div>
            `;
        },

        generateLandSection() {
            const config = CONFIG.notifications.land;
            const soundKey = CONSTANTS.STRINGS.SOUND_LAND;
            
            return `
                <div class="iqrpg-section" id="section-land" data-section-type="land">
                    <div class="iqrpg-section-header">
                        <h3 class="iqrpg-section-title">Raid Completion</h3>
                    </div>
                    <div class="iqrpg-section-content">
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Sound Alerts</label>
                            <div class="iqrpg-toggle-switch ${config.sound ? 'active' : ''}" 
                                 data-setting="notifications.land.sound"></div>
                        </div>
                        
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Desktop Notifications</label>
                            <div class="iqrpg-toggle-switch ${config.desktop ? 'active' : ''}" 
                                 data-setting="notifications.land.desktop"></div>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Sound URL (optional)</label>
                            <input type="text" class="iqrpg-input" 
                                   data-setting="sounds.${soundKey}" 
                                   value="${CONFIG.sounds[soundKey] || ''}" 
                                   placeholder="Leave empty for default">
                        </div>
                    </div>
                </div>
            `;
        },

        generateSkillsSection() {
            const config = CONFIG.notifications.skills;
            const soundKey = CONSTANTS.STRINGS.SOUND_SKILLS;
            
            return `
                <div class="iqrpg-section" id="section-skills" data-section-type="skills">
                    <div class="iqrpg-section-header">
                        <h3 class="iqrpg-section-title">Skill Level Increases</h3>
                    </div>
                    <div class="iqrpg-section-content">
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Sound Alerts</label>
                            <div class="iqrpg-toggle-switch ${config.sound ? 'active' : ''}" 
                                 data-setting="notifications.skills.sound"></div>
                        </div>
                        
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Desktop Notifications</label>
                            <div class="iqrpg-toggle-switch ${config.desktop ? 'active' : ''}" 
                                 data-setting="notifications.skills.desktop"></div>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Sound URL (optional)</label>
                            <input type="text" class="iqrpg-input" 
                                   data-setting="sounds.${soundKey}" 
                                   value="${CONFIG.sounds[soundKey] || ''}" 
                                   placeholder="Leave empty for default">
                        </div>
                    </div>
                </div>
            `;
        },

        generateAbyssBattlesSection() {
            const config = CONFIG.notifications.abyssBattles;
            const soundKey = CONSTANTS.STRINGS.SOUND_ABYSS_BATTLES;
            
            return `
                <div class="iqrpg-section" id="section-abyssBattles" data-section-type="abyssBattles">
                    <div class="iqrpg-section-header">
                        <h3 class="iqrpg-section-title">Abyss Battles Completion</h3>
                    </div>
                    <div class="iqrpg-section-content">
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Sound Alerts</label>
                            <div class="iqrpg-toggle-switch ${config.sound ? 'active' : ''}" 
                                 data-setting="notifications.abyssBattles.sound"></div>
                        </div>
                        
                        <div class="iqrpg-toggle-group">
                            <label class="iqrpg-toggle-label">Desktop Notifications</label>
                            <div class="iqrpg-toggle-switch ${config.desktop ? 'active' : ''}" 
                                 data-setting="notifications.abyssBattles.desktop"></div>
                        </div>
                        
                        <div class="iqrpg-input-group">
                            <label class="iqrpg-input-label">Sound URL (optional)</label>
                            <input type="text" class="iqrpg-input" 
                                   data-setting="sounds.${soundKey}" 
                                   value="${CONFIG.sounds[soundKey] || ''}" 
                                   placeholder="Leave empty for default">
                        </div>
                    </div>
                </div>
            `;
        },

        generateSettingsHTML() {
            const volume = (CONFIG.sounds.volume !== undefined ? CONFIG.sounds.volume : 1.0) * 100;
            return `
                <div class="iqrpg-section" id="section-volume" data-section-type="volume">
                    <div class="iqrpg-section-header">
                        <h3 class="iqrpg-section-title">Volume</h3>
                    </div>
                    <div class="iqrpg-section-content">
                        <div class="iqrpg-volume-group">
                            <label class="iqrpg-volume-label">Sound Volume</label>
                            <input type="range" 
                                   class="iqrpg-volume-slider" 
                                   id="iqrpg-volume-slider"
                                   data-setting="sounds.volume"
                                   min="0" 
                                   max="100" 
                                   value="${volume}" 
                                   step="1">
                            <span class="iqrpg-volume-value" id="iqrpg-volume-value">${Math.round(volume)}%</span>
                        </div>
                    </div>
                </div>
                
                ${this.generateAutosSection()}
                ${this.generateNotificationSection('globalEvents', 'Globals')}
                ${this.generateBossSection()}
                ${this.generateGatheringEventsSection()}
                ${this.generateNotificationSection('actionBonus', 'Action Bonus')}
                ${this.generateClanSection()}
                ${this.generateDungeonSection()}
                ${this.generateLandSection()}
                ${this.generateMasterySection()}
                ${this.generateSkillsSection()}
                ${this.generateTradeAlertSection()}
                ${this.generateItemDropSection()}
                ${this.generateNotificationSection('message', 'Message')}
                ${this.generateAbyssBattlesSection()}
                ${this.generatePotionSection()}
            `;
        },

        attachEventListeners() {
            if (!this.modal) return;

            // Use event delegation - attach listeners once to modal container
            // This works with dynamically regenerated HTML and prevents duplicate listeners
            
            // Navigation button click handlers
            this.modal.addEventListener('click', (e) => {
                if (e.target.classList.contains('iqrpg-nav-btn')) {
                    const sectionType = e.target.getAttribute('data-section');
                    const targetSection = this.modal.querySelector(`#section-${sectionType}`);
                    if (targetSection) {
                        targetSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
                        e.target.style.background = '#227E22';
                        setTimeout(() => {
                            e.target.style.background = '';
                        }, 300);
                    }
                }
            });

            // Toggle switches
            this.modal.addEventListener('click', (e) => {
                if (e.target.classList.contains('iqrpg-toggle-switch')) {
                    e.target.classList.toggle('active');
                    const setting = e.target.getAttribute('data-setting');
                    this.updateSetting(setting, e.target.classList.contains('active'));
                }
            });

            // Volume slider
            this.modal.addEventListener('input', (e) => {
                if (e.target.id === 'iqrpg-volume-slider') {
                    const volumeValue = this.modal.querySelector('#iqrpg-volume-value');
                    if (volumeValue) {
                        const value = parseFloat(e.target.value);
                        const volumePercent = Math.round(value);
                        volumeValue.textContent = `${volumePercent}%`;
                        const volumeDecimal = value / 100;
                        this.updateSetting('sounds.volume', volumeDecimal);
                        
                        // Update the filled portion of the slider
                        e.target.style.setProperty('--volume-percent', `${value}%`);
                    }
                }
            });

            // Input fields
            this.modal.addEventListener('change', (e) => {
                if (e.target.classList.contains('iqrpg-input')) {
                    const setting = e.target.getAttribute('data-setting');
                    const dataType = e.target.getAttribute('data-type');
                    let value = e.target.value;
                    
                    if (e.target.type === 'number') {
                        value = parseInt(value, 10) || 0;
                    }
                    
                    if (dataType === 'array') {
                        value = value.split(',')
                            .map(v => v.trim())
                            .filter(v => v.length > 0);
                    }
                    
                    this.updateSetting(setting, value);
                }
            });

            // Save button
            this.modal.addEventListener('click', (e) => {
                if (e.target.id === 'iqrpg-save-btn') {
                    this.saveSettings();
                }
            });

            // Reset button
            this.modal.addEventListener('click', (e) => {
                if (e.target.id === 'iqrpg-reset-btn') {
                    if (confirm('Reset all settings to defaults? This cannot be undone.')) {
                        this.resetSettings();
                    }
                }
            });

            // Toggle all sound alerts button
            this.modal.addEventListener('click', (e) => {
                if (e.target.id === 'iqrpg-toggle-sound-btn') {
                    this.toggleAllSoundAlerts();
                }
            });

            // Toggle all notifications button
            this.modal.addEventListener('click', (e) => {
                if (e.target.id === 'iqrpg-toggle-notifications-btn') {
                    this.toggleAllDesktopNotifications();
                }
            });
        },

        updateSetting(path, value) {
            const keys = path.split('.');
            let obj = CONFIG;
            for (let i = 0; i < keys.length - 1; i++) {
                if (!obj[keys[i]]) obj[keys[i]] = {};
                obj = obj[keys[i]];
            }
            obj[keys[keys.length - 1]] = value;
        },

        reloadAudio() {
            AudioManager.audioCache.clear();
            preloadAllSounds();
        },

        saveSettings() {
            saveConfig();
            
            // If item keywords exist, mark all current items in log as processed
            // This prevents alerts for items that were already in the log before keywords were set
            const itemKeywords = CONFIG.notifications.itemDrop?.itemKeywords || [];
            if (itemKeywords.length > 0) {
                const validKeywords = itemKeywords.filter(kw => kw && typeof kw === 'string' && kw.trim().length > 0);
                if (validKeywords.length > 0) {
                    try {
                        const logDiv = document.querySelector('#log-div') || document.querySelector('.log-div');
                        if (logDiv) {
                            const itemDivs = logDiv.querySelectorAll('.item.clickable');
                            for (const itemDiv of itemDivs) {
                                const itemNamePara = itemDiv.querySelector('p');
                                if (!itemNamePara) continue;
                                
                                let itemName = (itemNamePara.textContent || '').trim();
                                itemName = itemName.replace(/^\[|\]$/g, '').trim();
                                if (!itemName) continue;
                                
                                const itemNameLower = itemName.toLowerCase();
                                const matchedKeyword = validKeywords.find(keyword => {
                                    const kw = (keyword || '').toLowerCase().trim();
                                    return kw && itemNameLower.includes(kw);
                                });
                                
                                if (!matchedKeyword) continue;
                                
                                // Extract amount and timestamp to create unique ID
                                let amount = '1';
                                let timestamp = '';
                                let container = itemDiv.parentElement;
                                while (container && container !== logDiv) {
                                    const spans = container.querySelectorAll('span');
                                    for (const span of spans) {
                                        const spanText = (span.textContent || '').trim();
                                        if (spanText.match(/\+?\d+/)) {
                                            const amountMatch = spanText.match(/\+?(\d+)/);
                                            if (amountMatch && amountMatch[1]) {
                                                amount = amountMatch[1];
                                            }
                                        }
                                        if (!timestamp && spanText && spanText.length > 0 && !spanText.match(/^\+?\d+$/)) {
                                            timestamp = spanText;
                                        }
                                    }
                                    if (amount !== '1' && timestamp) break;
                                    container = container.parentElement;
                                }
                                
                                // Mark current items as processed using unique ID
                                const uniqueId = `${itemNameLower}|${amount}|${timestamp}`;
                                DOMMonitor.processedItemDrops.add(uniqueId);
                            }
                        }
                    } catch (e) {
                        // Silently fail if DOM structure changes
                    }
                }
            }
            
            this.reloadAudio();
            
            // Show feedback
            const saveBtn = this.modal?.querySelector('#iqrpg-save-btn');
            if (saveBtn) {
                const originalText = saveBtn.textContent;
                saveBtn.textContent = 'Saved!';
                saveBtn.style.background = '#4caf50';
                setTimeout(() => {
                    saveBtn.textContent = originalText;
                    saveBtn.style.background = '';
                }, CONSTANTS.DELAYS.SAVE_FEEDBACK);
            }
        },


        resetSettings() {
            // Reset to defaults
            Object.assign(CONFIG, JSON.parse(JSON.stringify(DEFAULT_CONFIG)));
            saveConfig();
            this.reloadAudio();
            
            // Recreate modal to reflect new settings
            if (this.modalOverlay) {
                this.modalOverlay.remove();
            }
            this.modal = null;
            this.modalOverlay = null;
            this.createModal();
        },

        toggleAllSoundAlerts() {
            // Get current state - check if all are enabled or disabled
            const allNotificationTypes = [
                'globalEvents',
                'actionBonus',
                'bossSpawn',
                'tradeAlert',
                'message',
                'autos',
                'potions',
                'dungeon',
                'mastery',
                'land',
                'skills',
                'itemDrop',
                'abyssBattles',
                'clan'
            ];
            
            // Check gathering events
            const gatheringTypes = ['woodcutting', 'quarrying', 'mining'];
            
            // Determine if we should enable or disable (if any are enabled, disable all; otherwise enable all)
            let anyEnabled = false;
            for (const type of allNotificationTypes) {
                if (CONFIG.notifications[type]?.sound) {
                    anyEnabled = true;
                    break;
                }
            }
            if (!anyEnabled) {
                for (const type of gatheringTypes) {
                    if (CONFIG.notifications.gatheringEvents[type]?.sound) {
                        anyEnabled = true;
                        break;
                    }
                }
            }
            
            const newValue = !anyEnabled;
            
            // Toggle all notification types
            for (const type of allNotificationTypes) {
                if (CONFIG.notifications[type]) {
                    CONFIG.notifications[type].sound = newValue;
                }
            }
            
            // Toggle gathering events
            for (const type of gatheringTypes) {
                if (CONFIG.notifications.gatheringEvents[type]) {
                    CONFIG.notifications.gatheringEvents[type].sound = newValue;
                }
            }
            
            // Update UI
            this.refreshSettingsUI();
        },
        
        toggleAllDesktopNotifications() {
            // Get current state - check if all are enabled or disabled
            const allNotificationTypes = [
                'globalEvents',
                'actionBonus',
                'bossSpawn',
                'tradeAlert',
                'message',
                'autos',
                'potions',
                'dungeon',
                'mastery',
                'land',
                'skills',
                'itemDrop',
                'abyssBattles',
                'clan'
            ];
            
            // Check gathering events
            const gatheringTypes = ['woodcutting', 'quarrying', 'mining'];
            
            // Determine if we should enable or disable (if any are enabled, disable all; otherwise enable all)
            let anyEnabled = false;
            for (const type of allNotificationTypes) {
                if (CONFIG.notifications[type]?.desktop) {
                    anyEnabled = true;
                    break;
                }
            }
            if (!anyEnabled) {
                for (const type of gatheringTypes) {
                    if (CONFIG.notifications.gatheringEvents[type]?.desktop) {
                        anyEnabled = true;
                        break;
                    }
                }
            }
            
            const newValue = !anyEnabled;
            
            // Toggle all notification types
            for (const type of allNotificationTypes) {
                if (CONFIG.notifications[type]) {
                    CONFIG.notifications[type].desktop = newValue;
                }
            }
            
            // Toggle gathering events
            for (const type of gatheringTypes) {
                if (CONFIG.notifications.gatheringEvents[type]) {
                    CONFIG.notifications.gatheringEvents[type].desktop = newValue;
                }
            }
            
            // Update UI
            this.refreshSettingsUI();
        },
        
        refreshSettingsUI() {
            // Regenerate settings HTML to reflect current config
            const body = this.modal.querySelector('.iqrpg-modal-body');
            if (body) {
                body.innerHTML = this.generateSettingsHTML();
                
                // Re-initialize volume slider CSS variable
                const volumeSlider = this.modal.querySelector('#iqrpg-volume-slider');
                if (volumeSlider) {
                    const currentVolume = parseFloat(volumeSlider.value);
                    volumeSlider.style.setProperty('--volume-percent', `${currentVolume}%`);
                }
            }
        },

        openModal() {
            if (this.modalOverlay) {
                this.modalOverlay.classList.add('active');
                // Update modal content with current config
                const body = this.modal.querySelector('.iqrpg-modal-body');
                body.innerHTML = this.generateSettingsHTML();
                
                // Initialize volume slider CSS variable
                const volumeSlider = this.modal.querySelector('#iqrpg-volume-slider');
                if (volumeSlider) {
                    const currentVolume = parseFloat(volumeSlider.value);
                    volumeSlider.style.setProperty('--volume-percent', `${currentVolume}%`);
                }
                
                // No need to re-attach listeners - event delegation handles dynamic content
            }
        },

        closeModal() {
            if (this.modalOverlay) {
                this.modalOverlay.classList.remove('active');
            }
        },

        cleanup() {
            if (this.escKeyHandler) {
                document.removeEventListener('keydown', this.escKeyHandler);
                this.escKeyHandler = null;
            }
        }
    };

    // ============================================
    // Initialization
    // ============================================
    function init() {
        // Initialize WebSocket interception
        WebSocketInterceptor.init();

        // Initialize GUI
        GUIManager.init();

        // Initialize DOM monitoring
        DOMMonitor.init();

        // Initialize image modal manager
        ImageModalManager.init();
    }

    // Start initialization
    init();

})();