// ==UserScript==
// @name IQRPG+ Combined
// @namespace https://www.iqrpg.com/
// @version 0.1.8
// @description Audio signals for various aspects of IQRPG
// @Author Sanjin
// @match http://iqrpg.com/game.html
// @match https://iqrpg.com/game.html
// @match http://www.iqrpg.com/game.html
// @match https://www.iqrpg.com/game.html
// @require http://code.jquery.com/jquery-latest.js
// @grant none
// ==/UserScript==
/*
Original script by Bunjo and Vifs
Shout out to Xortrox & euphone & Karubo for their contributions to the script.
*/
// Use a namespace to avoid global scope pollution
const IQRPG_Plus = (function() {
// Private variables
let userSettings = {};
let isAlerting = false;
let alertTimer = null;
let currAutoAudioPlays = 0;
let canSendDesktopAlert = true;
let audioRepeatLock = false;
let desktopNotificationOnCooldown = false;
let bonusExp = false;
let isDragging = false;
let dragOffsetX = 0;
let dragOffsetY = 0;
let processedEvents = new Map(); // Store processed events with their unique signature
let lastEventTime = 0;
let lastEventType = '';
// Store references to UI elements
const elements = {
buttonPanel: null,
buttonContainer: null,
settingsPanel: null,
settingsButton: null,
graphButton: null,
swordsButton: null,
testVolumeButton: null
};
// Store references to observers
const observers = {
main: null,
land: null,
mastery: null
};
// Cache DOM query results
const domCache = {};
// Default settings
const defaultSettings = {
masterAudioLevel: 1,
autoAudioAlert: true,
autoAlertSoundURL: 'https://www.pacdv.com/sounds/mechanical_sound_effects/gun-reload-1.wav',
autoAlertRepeatInSeconds: 2,
autoAlertNumber: 10,
autoMaxNumberOfAudioAlerts: 2,
autoDesktopAlert: true,
dungeonAudioAlert: true,
dungeonDesktopAlert: true,
bossAudioAlert: true,
bossAlertSoundURL: 'https://www.pacdv.com/sounds/interface_sound_effects/sound8.mp3',
bossDefeatedSoundURL: 'https://ia801306.us.archive.org/32/items/FF7ACVictoryFanfareRingtoneperfectedMp3/FF7%20AC%20Victory%20Fanfare%20Ringtone%20%28perfected%20mp3%29.mp3',
bossDesktopAlert: true,
eventDesktopAlert: true,
eventAlertSoundURL: 'https://www.pacdv.com/sounds/interface_sound_effects/sound8.mp3',
eventAlert_Woodcutting: true,
eventAlert_Quarrying: true,
eventAlert_Mining: true,
eventAudioAlert: true,
eventAudioAlertFinished: true,
whisperAudioAlert: true,
whisperAlertSoundURL: 'https://www.pacdv.com/sounds/mechanical_sound_effects/spring_1.wav',
whisperAlertOnlyWhenTabIsInactive: false,
whisperDesktopAlert: true,
landAudioAlert: true,
landAlertSoundURL: 'https://www.pacdv.com/sounds/mechanical_sound_effects/coins_4.wav',
masteryAudioAlert: true,
masteryEveryXLevels: 50,
masteryAlertSoundURL: 'https://ia801306.us.archive.org/32/items/FF7ACVictoryFanfareRingtoneperfectedMp3/FF7%20AC%20Victory%20Fanfare%20Ringtone%20%28perfected%20mp3%29.mp3',
effectAudioAlert: true,
effectAutoLeft: 5,
effectAlertSoundURL: 'https://www.pacdv.com/sounds/mechanical_sound_effects/hammer-1.mp3',
watchtowerAudioAlert: true,
watchtowerAlertSoundURL: 'https://www.pacdv.com/sounds/interface_sound_effects/sound8.mp3',
watchtowerDesktopAlert: true,
bonusExpAudioAlert: true,
bonusExpAlertSoundURL: 'https://www.pacdv.com/sounds/miscellaneous_sounds/magic-wand-1.wav',
showDebugInfo: true
};
// Load settings from localStorage
function loadSettings() {
try {
const savedSettings = JSON.parse(localStorage.getItem('iqrpgSettings'));
// Ensure all default settings exist in the loaded settings
const result = savedSettings ? Object.assign({}, defaultSettings, savedSettings) : defaultSettings;
console.log('IQRPG+ Settings loaded:', result);
return result;
} catch (e) {
console.error('Error loading settings:', e);
return defaultSettings;
}
}
// Save settings to localStorage
function saveSettings() {
try {
localStorage.setItem('iqrpgSettings', JSON.stringify(userSettings));
console.log('IQRPG+ Settings saved:', userSettings);
} catch (e) {
console.error('Error saving settings:', e);
}
}
// Apply settings to checkboxes
function applySettingsToUI() {
// Apply checkbox states
for (const setting in userSettings) {
const checkbox = document.getElementById(setting);
if (checkbox && checkbox.type === 'checkbox') {
checkbox.checked = userSettings[setting];
}
}
// Apply volume slider
const volumeSlider = document.getElementById('volumeSlider');
if (volumeSlider) {
volumeSlider.value = userSettings.masterAudioLevel;
}
}
// Initialize the userscript
function init() {
userSettings = loadSettings();
// Ensure we have up-to-date settings
saveSettings();
createUI();
setupWebSocketInterception();
setupObservers();
setupEventListeners();
// Check Notification permissions
if (Notification.permission !== "denied") {
Notification.requestPermission();
}
}
// Create and setup UI elements
function createUI() {
try {
// Create main panel
elements.buttonPanel = document.createElement('div');
elements.buttonPanel.id = 'buttonPanel';
// Create settings panel
elements.settingsPanel = document.createElement('div');
elements.settingsPanel.id = 'settingsPanel';
elements.settingsPanel.className = 'hidden';
elements.settingsPanel.innerHTML = `
<h2>Settings</h2>
<label>Master Volume: <input type="range" id="volumeSlider" min="0" max="1" step="0.1" value="${userSettings.masterAudioLevel}"></label>
<button id="testVolumeButton">Test Volume</button>
<button id="resetSettingsButton">Reset Settings</button>
<div style="margin-top: 10px;">
<label style="display: block;"><input type="checkbox" id="autoAudioAlert" ${userSettings.autoAudioAlert ? 'checked' : ''}> Auto Audio Alert</label>
<label style="display: block;"><input type="checkbox" id="autoDesktopAlert" ${userSettings.autoDesktopAlert ? 'checked' : ''}> Auto Desktop Alert</label>
<label style="display: block;"><input type="checkbox" id="dungeonAudioAlert" ${userSettings.dungeonAudioAlert ? 'checked' : ''}> Dungeon Audio Alert</label>
<label style="display: block;"><input type="checkbox" id="dungeonDesktopAlert" ${userSettings.dungeonDesktopAlert ? 'checked' : ''}> Dungeon Desktop Alert</label>
<label style="display: block;"><input type="checkbox" id="bossAudioAlert" ${userSettings.bossAudioAlert ? 'checked' : ''}> Boss Audio Alert</label>
<label style="display: block;"><input type="checkbox" id="bossDesktopAlert" ${userSettings.bossDesktopAlert ? 'checked' : ''}> Boss Desktop Alert</label>
<label style="display: block;"><input type="checkbox" id="eventAudioAlert" ${userSettings.eventAudioAlert ? 'checked' : ''}> Event Audio Alert</label>
<label style="display: block;"><input type="checkbox" id="eventAudioAlertFinished" ${userSettings.eventAudioAlertFinished ? 'checked' : ''}> Event Finished Audio Alert</label>
<label style="display: block;"><input type="checkbox" id="eventDesktopAlert" ${userSettings.eventDesktopAlert ? 'checked' : ''}> Event Desktop Alert</label>
<label style="display: block;"><input type="checkbox" id="whisperAudioAlert" ${userSettings.whisperAudioAlert ? 'checked' : ''}> Whisper Audio Alert</label>
<label style="display: block;"><input type="checkbox" id="whisperDesktopAlert" ${userSettings.whisperDesktopAlert ? 'checked' : ''}> Whisper Desktop Alert</label>
<label style="display: block;"><input type="checkbox" id="landAudioAlert" ${userSettings.landAudioAlert ? 'checked' : ''}> Land Audio Alert</label>
<label style="display: block;"><input type="checkbox" id="masteryAudioAlert" ${userSettings.masteryAudioAlert ? 'checked' : ''}> Mastery Audio Alert</label>
<label style="display: block;"><input type="checkbox" id="effectAudioAlert" ${userSettings.effectAudioAlert ? 'checked' : ''}> Effect Audio Alert</label>
<label style="display: block;"><input type="checkbox" id="watchtowerAudioAlert" ${userSettings.watchtowerAudioAlert ? 'checked' : ''}> Watchtower Audio Alert</label>
<label style="display: block;"><input type="checkbox" id="watchtowerDesktopAlert" ${userSettings.watchtowerDesktopAlert ? 'checked' : ''}> Watchtower Desktop Alert</label>
<label style="display: block;"><input type="checkbox" id="bonusExpAudioAlert" ${userSettings.bonusExpAudioAlert ? 'checked' : ''}> Bonus Exp Audio Alert</label>
</div>
`;
// Create buttons with helper function
elements.settingsButton = createControlButton('🔔');
elements.graphButton = createControlButton('📊');
elements.swordsButton = createControlButton('⚔️');
// Create container for buttons
elements.buttonContainer = document.createElement('div');
elements.buttonContainer.id = 'buttonContainer';
// Assemble the UI
elements.buttonContainer.appendChild(elements.settingsButton);
elements.buttonContainer.appendChild(elements.graphButton);
elements.buttonContainer.appendChild(elements.swordsButton);
elements.buttonPanel.appendChild(elements.buttonContainer);
elements.buttonPanel.appendChild(elements.settingsPanel);
document.body.appendChild(elements.buttonPanel);
// Add stylesheets
addStyles();
// Apply saved position
loadSavedPosition();
// Initialize settings based on saved values
applySettingsToUI();
} catch (error) {
console.error('Error creating UI:', error);
}
}
// Helper to create control buttons
function createControlButton(icon) {
const button = document.createElement('button');
button.textContent = icon;
button.className = 'control-button';
return button;
}
// Add CSS styles
function addStyles() {
const style = document.createElement('style');
style.textContent = `
#buttonPanel {
position: fixed;
top: 50px;
right: 10px;
z-index: 1001;
background-color: rgba(0, 0, 0, 0.8);
border: 1px solid #333;
padding: 0;
display: flex;
flex-direction: column;
cursor: move;
overflow: visible;
width: 120px;
transition: box-shadow 0.2s ease;
}
#buttonPanel.dragging {
box-shadow: 0 0 8px rgba(34, 116, 34, 0.7);
opacity: 0.9;
}
#buttonContainer {
display: flex;
flex-direction: row;
padding: 6px;
width: 100%;
box-sizing: border-box;
justify-content: center;
cursor: move;
}
.control-button {
background-color: rgba(0, 0, 0, 0.8);
color: white;
border: 1px solid #333;
padding: 8px;
width: 25px;
height: 25px;
line-height: 1;
text-align: center;
cursor: pointer;
font-size: 14px;
margin: 0 2px;
display: flex;
align-items: center;
justify-content: center;
}
.control-button:hover {
border-color: #227422;
background-color: rgba(10, 10, 10, 0.9);
}
#settingsPanel {
position: absolute;
top: 100%;
left: -1px;
width: calc(100% + 2px);
min-width: 250px;
background-color: rgba(0, 0, 0, 0.8);
color: white;
border: 1px solid #333;
border-top: none;
padding: 15px;
display: none;
z-index: 1002;
font-family: 'Arial', sans-serif;
box-sizing: border-box;
max-height: 80vh;
overflow-y: auto;
}
#testVolumeButton, #resetSettingsButton {
background-color: rgba(0, 0, 0, 0.8);
color: white;
border: 1px solid #333;
padding: 5px 10px;
cursor: pointer;
font-size: 12px;
margin: 5px 0;
}
#testVolumeButton:hover, #resetSettingsButton:hover {
border-color: #227422;
background-color: rgba(10, 10, 10, 0.9);
}
#resetSettingsButton {
background-color: rgba(50, 10, 10, 0.8);
margin-left: 10px;
}
#volumeSlider {
margin: 10px 0;
width: 90%;
accent-color: #227422;
}
#settingsPanel h2 {
margin-top: 0;
padding-top: 0;
font-size: 16px;
margin-bottom: 10px;
color: #227422;
}
#settingsPanel label {
font-size: 14px;
}
#settingsPanel input[type="checkbox"] {
accent-color: #227422;
}
`;
document.head.appendChild(style);
}
// Load saved position from localStorage
function loadSavedPosition() {
try {
const savedPosition = localStorage.getItem('iqrpgPanelPosition');
if (savedPosition) {
const position = JSON.parse(savedPosition);
elements.buttonPanel.style.left = position.left;
elements.buttonPanel.style.top = position.top;
elements.buttonPanel.style.right = 'auto'; // Clear right positioning when using left
} else {
// Initial position if no saved position exists
elements.buttonPanel.style.top = '50px';
elements.buttonPanel.style.right = '10px';
}
} catch (e) {
console.error('Error loading saved position:', e);
// Use default position on error
elements.buttonPanel.style.top = '50px';
elements.buttonPanel.style.right = '10px';
}
}
// Setup event listeners for UI elements
function setupEventListeners() {
// Settings button
elements.settingsButton.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent triggering drag when clicking button
toggleSettingsPanel();
});
// Graph button - to be implemented
elements.graphButton.addEventListener('click', (e) => {
e.stopPropagation();
showGraphPanel();
});
// Swords button - to be implemented
elements.swordsButton.addEventListener('click', (e) => {
e.stopPropagation();
showCombatPanel();
});
// Test volume button
const testVolumeButton = document.getElementById('testVolumeButton');
if (testVolumeButton) {
testVolumeButton.addEventListener('click', () => {
console.log('Test volume button clicked. Current master volume:', userSettings.masterAudioLevel);
playSound(userSettings.autoAlertSoundURL, userSettings.masterAudioLevel);
});
}
// Reset settings button
const resetSettingsButton = document.getElementById('resetSettingsButton');
if (resetSettingsButton) {
resetSettingsButton.addEventListener('click', () => {
if (confirm('Are you sure you want to reset all settings to default?')) {
localStorage.removeItem('iqrpgSettings');
userSettings = Object.assign({}, defaultSettings);
applySettingsToUI();
saveSettings();
alert('Settings have been reset to default.');
}
});
}
// Volume slider
const volumeSlider = document.getElementById('volumeSlider');
if (volumeSlider) {
volumeSlider.addEventListener('input', (event) => {
userSettings.masterAudioLevel = event.target.value;
saveSettings();
});
}
// Checkbox settings
setupSettingsToggleListeners();
// Make panel draggable
setupPanelDragging();
// Start/stop dungeon button
$('body').on('click', "button:contains('Start Dungeon')", function() {
stopAlert();
});
// Keyboard shortcuts
setupKeyboardShortcuts();
}
// Show/hide settings panel
function toggleSettingsPanel() {
if (elements.settingsPanel.style.display === 'none' || elements.settingsPanel.style.display === '') {
updateSettingsPanelPosition();
elements.settingsPanel.style.display = 'block';
} else {
elements.settingsPanel.style.display = 'none';
}
}
// Graph panel implementation placeholder
function showGraphPanel() {
console.log('Graph panel functionality not yet implemented');
// To be implemented
}
// Combat panel implementation placeholder
function showCombatPanel() {
console.log('Combat panel functionality not yet implemented');
// To be implemented
}
// Setup toggle listeners for settings checkboxes
function setupSettingsToggleListeners() {
const toggleSettings = [
'autoAudioAlert', 'autoDesktopAlert', 'dungeonAudioAlert', 'dungeonDesktopAlert',
'bossAudioAlert', 'bossDesktopAlert', 'eventAudioAlert', 'eventDesktopAlert',
'whisperAudioAlert', 'whisperDesktopAlert', 'landAudioAlert', 'masteryAudioAlert',
'effectAudioAlert', 'watchtowerAudioAlert', 'watchtowerDesktopAlert', 'bonusExpAudioAlert',
'eventAudioAlertFinished'
];
toggleSettings.forEach(setting => {
const checkbox = document.getElementById(setting);
if (checkbox) {
checkbox.addEventListener('change', (event) => {
userSettings[setting] = event.target.checked;
saveSettings();
console.log(`${setting} set to ${event.target.checked}`);
});
} else {
console.log(`Checkbox element not found for setting: ${setting}`);
}
});
}
// Setup panel dragging functionality
function setupPanelDragging() {
elements.buttonPanel.addEventListener('mousedown', (e) => {
// Only start dragging if clicking on the panel background or button container
if (e.target === elements.buttonPanel || e.target === elements.buttonContainer) {
isDragging = true;
dragOffsetX = e.clientX - elements.buttonPanel.getBoundingClientRect().left;
dragOffsetY = e.clientY - elements.buttonPanel.getBoundingClientRect().top;
elements.buttonPanel.classList.add('dragging');
}
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
const newX = e.clientX - dragOffsetX;
const newY = e.clientY - dragOffsetY;
// Calculate limits to keep panel within screen bounds
const maxX = window.innerWidth - elements.buttonPanel.offsetWidth;
const maxY = window.innerHeight - elements.buttonPanel.offsetHeight;
// Apply position with limits
elements.buttonPanel.style.left = Math.max(0, Math.min(maxX, newX)) + 'px';
elements.buttonPanel.style.top = Math.max(0, Math.min(maxY, newY)) + 'px';
elements.buttonPanel.style.right = 'auto'; // Clear right positioning
// If panel is near the right edge, reposition the settings panel
updateSettingsPanelPosition();
}
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
elements.buttonPanel.classList.remove('dragging');
// Save panel position to localStorage for persistence
const position = {
left: elements.buttonPanel.style.left,
top: elements.buttonPanel.style.top
};
localStorage.setItem('iqrpgPanelPosition', JSON.stringify(position));
}
});
// Stop dragging if mouse leaves the window
document.addEventListener('mouseleave', () => {
if (isDragging) {
isDragging = false;
elements.buttonPanel.classList.remove('dragging');
}
});
}
// Update settings panel position based on button panel position
function updateSettingsPanelPosition() {
const panelRect = elements.buttonPanel.getBoundingClientRect();
const screenWidth = window.innerWidth;
// If panel is close to the right edge of the screen, position settings to the left
if (panelRect.right + 250 > screenWidth) { // 250px is min-width of settings panel
elements.settingsPanel.style.left = 'auto';
elements.settingsPanel.style.right = '-1px';
} else {
elements.settingsPanel.style.left = '-1px';
elements.settingsPanel.style.right = 'auto';
}
// Match the width to the button panel
elements.settingsPanel.style.width = (elements.buttonPanel.offsetWidth + 2) + 'px';
}
// Setup keyboard shortcuts
function setupKeyboardShortcuts() {
document.addEventListener('keyup', function(e) {
if (e.altKey === true) {
let index = -1;
const channels = $('.chat-channels').children();
if (isNaN(e.key)) {
let direction = null;
if (e.which === 38) { // Up arrow
direction = true;
} else if (e.which === 40) { // Down arrow
direction = false;
}
if (direction !== null) {
let chatChannels = channels;
if (e.shiftKey === true) {
chatChannels = $('.chat-channels').children('.new-message, .active-channel');
}
index = chatChannels.index($('.active-channel'));
if (direction) {
index--;
} else {
index++;
}
}
} else {
if (e.ctrlKey === true) {
index = -1 + parseInt(e.key);
}
}
if (index >= 0 && index < channels.length) {
channels[index].click();
$('#chatInput').focus();
}
}
});
}
// WebSocket interception
function setupWebSocketInterception() {
const OldSocket = window.WebSocket;
window.WebSocket = function WebSocket(url, protocols) {
console.log('IQRPG+ Socket Monitor Initilized...');
const socket = new OldSocket(...arguments);
socket.addEventListener('message', handleWebSocketMessage);
return socket;
};
}
// Handle WebSocket messages
function handleWebSocketMessage(event) {
try {
const message = JSON.parse(event.data);
switch(message.type){
case 'playersOnline':
case 'loadMessages':
case 'addItemsToUser':
case 'notification':
case 'bonus':
break;
case 'event':
debugInfo('Event Data:');
debugInfo(message);
handleGameEvent(message);
break;
case 'msg':
handleChatMessage(message);
break;
case 'boss':
break;
default:
debugInfo(message);
}
} catch (error) {
console.error('Error handling WebSocket message:', error);
}
}
// Handle game events (woodcutting, mining, etc)
function handleGameEvent(message) {
if (!message.data || !message.data.type) return;
const eventType = message.data.type;
const timeRemaining = message.data.timeRemaining || 0;
const currentTime = Date.now();
// Create a unique signature for this event
// Combine type, time remaining, and any other distinguishing factors
const eventSignature = `${eventType}-${timeRemaining}`;
// Check if we've seen this exact event recently (within 1 second)
if (processedEvents.has(eventSignature)) {
const lastProcessedTime = processedEvents.get(eventSignature);
// Only ignore if the same exact event was processed very recently (500ms)
// This prevents double notifications while still allowing rapid consecutive events
if (currentTime - lastProcessedTime < 500) {
debugInfo('Ignoring duplicate event: ' + eventSignature);
return;
}
}
// Update the processed events map with this event
processedEvents.set(eventSignature, currentTime);
// Clean up old events from the map (older than 10 seconds)
processedEvents.forEach((timestamp, signature) => {
if (currentTime - timestamp > 10000) {
processedEvents.delete(signature);
}
});
if (eventType === "woodcutting" && userSettings.eventAlert_Woodcutting) {
handleResourceEvent('Woodcutting', timeRemaining);
} else if (eventType === "mining" && userSettings.eventAlert_Mining) {
handleResourceEvent('Mining', timeRemaining);
} else if (eventType === "quarrying" && userSettings.eventAlert_Quarrying) {
handleResourceEvent('Quarrying', timeRemaining);
} else {
debugInfo('Unsupported Event - ' + eventType);
}
}
// Handle resource events (common code for woodcutting, mining, quarrying)
function handleResourceEvent(eventName, timeRemaining) {
if (userSettings.eventAudioAlert) {
playSound(userSettings.eventAlertSoundURL);
}
if (userSettings.eventDesktopAlert) {
showNotification(`IQRPG Event!`, `${eventName} event has started!`);
}
// Set timeout for end of event
setTimeout(function() {
if (userSettings.eventAudioAlertFinished) {
playSound(userSettings.eventAlertSoundURL);
}
if (userSettings.eventDesktopAlert) {
showNotification(`IQRPG Event Finished!`, `${eventName} event has ended!`);
}
}, timeRemaining * 10);
}
// Handle chat messages
function handleChatMessage(message) {
if (!message.data || !message.data.type) return;
switch(message.data.type) {
case 'clanGlobal':
if (message.data.msg && message.data.msg.startsWith('The watchtower')) {
if (userSettings.watchtowerAudioAlert) {
playSound(userSettings.watchtowerAlertSoundURL);
}
if (userSettings.watchtowerDesktopAlert) {
showNotification('IQRPG Watchtower!', message.data.msg);
}
}
break;
case 'pm-from':
handleWhisper(message);
break;
case 'eventGlobal':
if (message.data.msg && message.data.msg.startsWith('A rift to the dark realm has opened')) {
if (userSettings.eventAudioAlert) {
playSound(userSettings.bossAlertSoundURL, 0.1);
}
if (userSettings.eventDesktopAlert) {
showNotification('IQRPG Boss!', 'A rift to the dark realm has opened!');
}
}
break;
case 'pm-to':
case 'msg':
case 'global':
case 'me':
break;
default:
debugInfo('Unsupported msg type:' + message.data.type);
debugInfo(message);
break;
}
}
// Handle whispers/private messages
function handleWhisper(message) {
if (userSettings.whisperAlertOnlyWhenTabIsInactive) {
if (document.hidden) {
triggerWhisperAlert(message);
}
} else {
triggerWhisperAlert(message);
}
}
// Trigger whisper alerts (sound and notification)
function triggerWhisperAlert(message) {
if (userSettings.whisperAudioAlert) {
playSound(userSettings.whisperAlertSoundURL);
}
if (userSettings.whisperDesktopAlert) {
if (canSendDesktopAlert) {
showNotification('IQRPG Whisper!', message.data.username + ': ' + message.data.msg);
canSendDesktopAlert = false;
setTimeout(() => { canSendDesktopAlert = true; }, 10000);
}
}
}
// Setup observers for DOM changes
function setupObservers() {
const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
const obsConfig = { childList: true, characterData: true, attributes: true, subtree: true };
// Main observer for title changes and auto remaining
observers.main = new MutationObserver(handleMutations);
// Land observer for land timer
observers.land = new MutationObserver(handleLandMutations);
// Mastery observer for mastery levels
observers.mastery = new MutationObserver(handleMasteryMutations);
// Initialize observers with delay to ensure elements are available
setTimeout(function() {
try {
const actionTimer = $("div.action-timer__text")[0];
const title = $("head title")[0];
if (actionTimer) {
observers.main.observe(actionTimer, obsConfig);
}
if (title) {
observers.main.observe(title, obsConfig);
}
if (userSettings.landAudioAlert) {
const landSection = $(".main-section")[2];
if (landSection) {
observers.land.observe(landSection, obsConfig);
}
}
if (userSettings.masteryAudioAlert) {
const masteries = $(".clickable > .flex.space-between > .green-text");
if (masteries.length > 0) {
masteries.each(function(index) {
observers.mastery.observe(masteries[index], obsConfig);
});
}
}
} catch (error) {
console.error("Error setting up observers:", error);
}
}, 500);
}
// Handle main mutations (title changes, auto remaining)
function handleMutations(mutationRecords) {
try {
// Check for effects
if (userSettings.effectAudioAlert) {
checkEffects();
}
// Check for bonus exp
if (userSettings.bonusExpAudioAlert) {
checkBonusExp();
}
mutationRecords.forEach(function(mutation) {
// Handle title changes
if (mutation.type == "childList") {
if (mutation.target.nodeName == "TITLE") {
handleTitleChange(mutation.target.innerHTML);
}
}
// Handle auto remaining changes
if (mutation.type == "characterData") {
handleAutoRemaining(mutation.target.data);
}
});
} catch (error) {
console.error("Error handling mutations:", error);
}
}
// Handle title changes
function handleTitleChange(title) {
console.log('Title changed to:', title);
console.log('Current settings status - dungeonAudioAlert:', userSettings.dungeonAudioAlert,
'bossAudioAlert:', userSettings.bossAudioAlert,
'watchtowerAudioAlert:', userSettings.watchtowerAudioAlert);
switch (title) {
case 'Dungeon Complete Idle Quest RPG':
if (userSettings.dungeonDesktopAlert) {
showNotification('IQRPG Dungeon Alert!', 'You have completed your dungeon!');
}
if (!isAlerting && userSettings.dungeonAudioAlert && canPlayMoreAlerts()) {
console.log('Starting dungeon audio alert. dungeonAudioAlert setting:', userSettings.dungeonAudioAlert);
startAlert();
}
break;
case 'Clan Boss Defeated Idle Quest RPG':
if (userSettings.watchtowerDesktopAlert) {
showNotification('IQRPG Watchtower Alert!', 'Your clan has defeated the boss!');
}
if (userSettings.watchtowerAudioAlert && canPlayMoreAlerts()) {
playSound(userSettings.bossDefeatedSoundURL);
}
break;
case 'All Mobs Defeated Idle Quest RPG':
if (userSettings.watchtowerDesktopAlert) {
showNotification('IQRPG Watchtower Alert!', 'All mobs have been defeated!');
}
if (!isAlerting && userSettings.watchtowerAudioAlert && canPlayMoreAlerts()) {
startAlert();
}
break;
case 'Boss Defeated Idle Quest RPG':
if (userSettings.bossDesktopAlert) {
showNotification('IQRPG Boss Alert!', 'The boss has been defeated!');
}
if (userSettings.bossAudioAlert) {
playSound(userSettings.bossDefeatedSoundURL);
}
break;
case 'ALERT':
break;
default:
stopAlert();
currAutoAudioPlays = 0;
}
}
// Handle auto remaining changes
function handleAutoRemaining(data) {
if (!data) return;
try {
const autosRemaining = parseInt(data.replace('Autos Remaining: ', ''));
if (isNaN(autosRemaining)) return;
if (autosRemaining <= userSettings.autoAlertNumber && userSettings.autoAlertNumber) {
if (autosRemaining == userSettings.autoAlertNumber && userSettings.autoDesktopAlert) {
showNotification('IQRPG Auto Alert!', 'You have ' + userSettings.autoAlertNumber + ' remaining!');
}
// Only start sound alerts if auto audio alerts are enabled
if (!isAlerting && userSettings.autoAudioAlert && canPlayMoreAlerts()) {
console.log('Starting auto audio alert. autoAudioAlert setting:', userSettings.autoAudioAlert);
startAlert();
}
} else {
stopAlert();
currAutoAudioPlays = 0;
}
} catch (error) {
console.error("Error handling auto remaining:", error);
}
}
// Handle land mutations
function handleLandMutations(mutationRecords) {
mutationRecords.forEach(function(mutation) {
if (mutation.type == 'characterData') {
if (mutation.target.data == '00:00') {
if (userSettings.landAudioAlert) {
playSound(userSettings.landAlertSoundURL);
}
}
}
});
}
// Handle mastery mutations
function handleMasteryMutations(mutationRecords) {
mutationRecords.forEach(function(mutation) {
if (mutation.type == 'characterData') {
const level = parseInt(mutation.target.data);
if (!isNaN(level) && level % userSettings.masteryEveryXLevels == 0) {
if (userSettings.masteryAudioAlert) {
playSound(userSettings.masteryAlertSoundURL);
}
}
}
});
}
// Check for effects
function checkEffects() {
try {
const effects = $(".main-section__body > div > .flex.space-between > .green-text");
if (!effects || !effects.length) return;
effects.each(function(index) {
const effectLeft = $(effects[index])[0].innerHTML;
if (effectLeft == userSettings.effectAutoLeft) {
if (userSettings.effectAudioAlert) {
playSound(userSettings.effectAlertSoundURL);
}
}
});
} catch (error) {
console.error("Error checking effects:", error);
}
}
// Check for bonus exp
function checkBonusExp() {
try {
const bonusExpSpan = $('.main-section__body> div > div > div > span.exp-text');
const hasBonus = bonusExpSpan != null && bonusExpSpan.length != 0;
if (hasBonus && !bonusExp) {
bonusExp = true;
if (userSettings.bonusExpAudioAlert) {
playSound(userSettings.bonusExpAlertSoundURL);
}
} else if (!hasBonus && bonusExp) {
bonusExp = false;
}
} catch (error) {
console.error("Error checking bonus exp:", error);
}
}
// Check if more alerts can be played
function canPlayMoreAlerts() {
return currAutoAudioPlays <= userSettings.autoMaxNumberOfAudioAlerts || userSettings.autoMaxNumberOfAudioAlerts == 0;
}
// Start alert sounds
function startAlert() {
if (isAlerting || !canPlayMoreAlerts()) return;
isAlerting = true;
currAutoAudioPlays++;
// Check if auto audio alert is enabled before playing sound
if (userSettings.autoAudioAlert) {
playSound(userSettings.autoAlertSoundURL);
}
const repeatInterval = userSettings.autoAlertRepeatInSeconds * 1000;
alertTimer = setInterval(() => {
if (canPlayMoreAlerts()) {
currAutoAudioPlays++;
// Check if auto audio alert is enabled before playing sound
if (userSettings.autoAudioAlert) {
playSound(userSettings.autoAlertSoundURL);
}
} else {
stopAlert();
}
}, repeatInterval);
}
// Stop alert sounds
function stopAlert() {
isAlerting = false;
if (alertTimer) {
clearInterval(alertTimer);
alertTimer = null;
}
}
// Show desktop notification
function showNotification(title, text) {
if (desktopNotificationOnCooldown) return;
try {
desktopNotificationOnCooldown = true;
setTimeout(() => { desktopNotificationOnCooldown = false; }, 7000);
if (!("Notification" in window)) {
console.error("This browser does not support desktop notifications");
return;
}
if (Notification.permission === "granted") {
const notification = new Notification(title, { body: text });
setupNotificationBehavior(notification);
} else if (Notification.permission !== "denied") {
Notification.requestPermission().then(function(permission) {
if (permission === "granted") {
const notification = new Notification(title, { body: text });
setupNotificationBehavior(notification);
}
});
}
} catch (error) {
console.error("Error showing notification:", error);
}
}
// Setup notification behavior
function setupNotificationBehavior(notification) {
if (!notification) return;
notification.onclick = function() {
window.focus();
this.close();
};
setTimeout(() => {
if (notification) notification.close();
}, 7000);
}
// Play sound with volume control
function playSound(sound, volume = null) {
try {
if (!sound) return;
// Added debug log for sound playback
console.log('Playing sound:', sound, 'with volume:',
(volume !== null ? volume : userSettings.masterAudioLevel),
'Master audio level:', userSettings.masterAudioLevel);
const audio = new Audio(sound);
audio.volume = volume !== null ? volume : userSettings.masterAudioLevel;
const playPromise = audio.play();
if (playPromise) {
playPromise.catch(error => {
console.error("Error playing sound:", error);
});
}
} catch (error) {
console.error("Error playing sound:", error);
}
}
// Debug logging
function debugInfo(msg) {
if (userSettings.showDebugInfo) {
console.log(msg);
}
}
return {
init
};
})();
// Initialize the script when the document is ready
$(document).ready(function() {
IQRPG_Plus.init();
});