Last active
May 19, 2025 10:06
-
-
Save fidel-perez/e84baad66d0f8c51e5a9d69f7ccaffb5 to your computer and use it in GitHub Desktop.
Melvor idle beep on different conditions, items gathered, produced, mobs killed, etc.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Melvor Idle Success Beep & Tracking | |
// @namespace http://tampermonkey.net/ | |
// @version 0.9.6 | |
// @description ETA for tracked gains & inventory totals. Beeps for targets/idle. UI enhancements & bug fixes. | |
// @author Gemini & User | |
// @match https://melvoridle.com/index_game.php | |
// @match https://*.melvoridle.com/index_game.php | |
// @grant none | |
// @run-at document-idle | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
// --- Script Configuration --- | |
const enableIncreaseDetectionBeep = false; | |
const idleTimeoutSeconds = 60; | |
const idleBeepRepeatSeconds = 15; | |
const FILTER_OFF_VALUE = "FILTER_OFF"; | |
const COMBINED_SUFFIX = "_PLUS_PERFECT"; | |
const PERFECT_SUFFIX = "_perfect"; | |
const recheckIntervalMs = 500; | |
const recheckDurationMs = 3000; | |
// --- End Script Configuration --- | |
// --- Mode & Tracking Configuration --- | |
const MODE_OFF = "MODE_OFF"; | |
const MODE_IDLE_ONLY = "MODE_IDLE_ONLY"; | |
const MODE_INVENTORY_TOTAL = "MODE_INVENTORY_TOTAL"; | |
const MODE_TRACKING = "MODE_TRACKING"; | |
const ENEMY_NAME_SPAN_ID = 'combat-enemy-name'; | |
const TRACK_TYPE_ENEMIES_VALUE = "TRACK_ENEMIES_DEFEATED"; | |
const TRACK_TYPE_ENEMIES_TEXT = "-- Track Enemies Defeated --"; | |
// --- End Mode & Tracking Configuration --- | |
// --- ETA & Timer Configuration --- | |
const MAX_TIMESTAMPS_FOR_AVG = 10; | |
const MIN_TIMESTAMPS_FOR_ETA = 2; | |
const ETA_UPDATE_INTERVAL_MS = 1000; | |
// --- End ETA & Timer Configuration --- | |
console.log(`Melvor Success Beep & Tracking (v0.9.6): Script loading.`); | |
let audioCtx; | |
let itemPreviousStates = {}; | |
let lastRelevantActivityTimestamp = Date.now(); | |
let idleCheckIntervalId = null; | |
let idleBeepIntervalId = null; | |
let discoveredItemNames = new Set(); | |
let lastProcessedNotificationElement = null; | |
let recheckNotificationIntervalId = null; | |
let recheckEndTime = 0; | |
let trackingData = {}; | |
let trackingStatusDisplay = null; | |
let combatEnemyNameObserver = null; | |
let previousEnemyName = '-'; | |
let collapsibleContent = null; | |
let etaDisplayElement = null; | |
let etaUpdateIntervalId = null; | |
const ETA_LAST_CALC_SECS_PROP = 'eta_lastCalculatedSeconds'; | |
const ETA_CALC_TS_PROP = 'eta_calculationTimestamp'; | |
const ETA_EVENT_POINTS_PROP = 'eta_eventPoints'; | |
function addGlobalStyles() { | |
const styleId = 'melvor-beep-styles'; | |
if (document.getElementById(styleId)) return; | |
const styleSheet = document.createElement("style"); | |
styleSheet.id = styleId; | |
styleSheet.textContent = ` | |
input#beepTargetNumberInput::-webkit-outer-spin-button, | |
input#beepTargetNumberInput::-webkit-inner-spin-button { | |
-webkit-appearance: none; | |
margin: 0; | |
} | |
input#beepTargetNumberInput[type=number] { | |
-moz-appearance: textfield; | |
appearance: textfield; | |
} | |
.userscript-button-style { | |
padding: 3px 7px; border: 1px solid #555; border-radius: 4px; | |
background-color: #666; color: #fff; font-size: 14px; cursor: pointer; | |
line-height: 1.2; height: 26px; display: inline-flex; align-items: center; | |
justify-content: center; box-sizing: border-box; user-select: none; | |
} | |
.userscript-button-style:hover { background-color: #777; } | |
`; | |
document.head.appendChild(styleSheet); | |
} | |
function getAudioContext() { | |
if (!audioCtx || audioCtx.state === 'closed') { | |
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } | |
catch (e) { console.error("Melvor Beep: AudioContext not supported.", e); return null; } | |
} | |
if (audioCtx.state === 'suspended') { | |
audioCtx.resume().catch(e => console.error("Melvor Beep: Error resuming AudioContext on get:", e)); | |
} | |
return audioCtx; | |
} | |
function playBeep(conditionLabel = "General", isTrackingAlert = false) { | |
const ctx = getAudioContext(); | |
if (!ctx) { console.warn("Melvor Beep: Cannot play beep, AudioContext unavailable."); return; } | |
if (ctx.state === 'suspended') { | |
ctx.resume().then(() => actuallyPlayBeep(ctx, conditionLabel, isTrackingAlert)) | |
.catch(e => console.error("Melvor Beep: Error resuming AudioContext for beep:", e)); | |
return; | |
} | |
actuallyPlayBeep(ctx, conditionLabel, isTrackingAlert); | |
} | |
function actuallyPlayBeep(ctx, conditionLabel, isTrackingAlert) { | |
try { | |
const oscillator = ctx.createOscillator(); | |
const gainNode = ctx.createGain(); | |
oscillator.connect(gainNode); | |
gainNode.connect(ctx.destination); | |
if (isTrackingAlert) { oscillator.type = 'triangle'; oscillator.frequency.setValueAtTime(783.99, ctx.currentTime); } | |
else if (conditionLabel.startsWith("Cond A")) { oscillator.type = 'sine'; oscillator.frequency.setValueAtTime(523.25, ctx.currentTime); } | |
else if (conditionLabel.startsWith("Cond B")) { oscillator.type = 'sine'; oscillator.frequency.setValueAtTime(659.25, ctx.currentTime); } | |
else if (conditionLabel.startsWith("Idle Alert")) { oscillator.type = 'sawtooth'; oscillator.frequency.setValueAtTime(349.23, ctx.currentTime); } | |
else { oscillator.type = 'sine'; oscillator.frequency.setValueAtTime(440, ctx.currentTime); } | |
gainNode.gain.setValueAtTime(0.25, ctx.currentTime); | |
oscillator.start(ctx.currentTime); | |
oscillator.stop(ctx.currentTime + (conditionLabel.startsWith("Idle Alert") ? 0.3 : 0.2) ); | |
} catch (e) { console.error("Melvor Beep: Error playing sound.", e); } | |
} | |
function updateLastRelevantActivityAndStopIdleBeep(reason = "Relevant activity") { | |
lastRelevantActivityTimestamp = Date.now(); | |
if (idleBeepIntervalId) { | |
clearInterval(idleBeepIntervalId); | |
idleBeepIntervalId = null; | |
console.log(`Melvor Beep: Stopped repeating idle beep due to: ${reason}.`); | |
} | |
} | |
function clearElementBeepStates(element) { | |
if (element && element.dataset) { | |
delete element.dataset.lastBeepedStateA; | |
delete element.dataset.lastRecordedGain; | |
} | |
} | |
function getSelectedMode() { | |
const modeDropdown = document.getElementById('trackingModeDropdown'); | |
return modeDropdown ? modeDropdown.value : MODE_OFF; | |
} | |
function stopEtaUpdateTimer() { | |
if (etaUpdateIntervalId) { | |
clearInterval(etaUpdateIntervalId); | |
etaUpdateIntervalId = null; | |
} | |
} | |
function initializeOrResetTrackingSession(selectionKey) { | |
trackingData = {}; | |
stopEtaUpdateTimer(); | |
const currentMode = getSelectedMode(); | |
if (currentMode !== MODE_TRACKING || !selectionKey || selectionKey === FILTER_OFF_VALUE) { | |
calculateAndShowETA(); | |
return; | |
} | |
const baseData = { | |
startTime: Date.now(), | |
lastBeepedTargetCount: -1, | |
[ETA_EVENT_POINTS_PROP]: [], | |
[ETA_LAST_CALC_SECS_PROP]: null, | |
[ETA_CALC_TS_PROP]: 0 | |
}; | |
if (selectionKey === TRACK_TYPE_ENEMIES_VALUE) { | |
trackingData[TRACK_TYPE_ENEMIES_VALUE] = { | |
...baseData, | |
type: "enemies", | |
enemiesDefeated: 0, | |
}; | |
} else { | |
trackingData[selectionKey] = { | |
...baseData, | |
type: "item", | |
itemsGained: 0, | |
}; | |
} | |
} | |
function ensureInventoryEtaContext(itemKey) { | |
if (!itemPreviousStates[itemKey]) { | |
itemPreviousStates[itemKey] = {}; | |
} | |
if (!itemPreviousStates[itemKey][ETA_EVENT_POINTS_PROP]) { | |
itemPreviousStates[itemKey][ETA_EVENT_POINTS_PROP] = []; | |
} | |
if (itemPreviousStates[itemKey][ETA_LAST_CALC_SECS_PROP] === undefined) { | |
itemPreviousStates[itemKey][ETA_LAST_CALC_SECS_PROP] = null; | |
} | |
if (itemPreviousStates[itemKey][ETA_CALC_TS_PROP] === undefined) { | |
itemPreviousStates[itemKey][ETA_CALC_TS_PROP] = 0; | |
} | |
} | |
function updateTrackingUIDisplay() { | |
if (!trackingStatusDisplay) return; | |
const currentMode = getSelectedMode(); | |
const selectionKey = document.getElementById('itemFilterDropdown')?.value; | |
if (currentMode === MODE_TRACKING && selectionKey && trackingData[selectionKey]) { | |
const trackedEntry = trackingData[selectionKey]; | |
let text = ""; | |
if (trackedEntry.type === "item") { text = `Tracking ${selectionKey.replace(/_/g, ' ')}: Items: ${trackedEntry.itemsGained}`; } | |
else if (trackedEntry.type === "enemies") { text = `Tracking Enemies: Defeated: ${trackedEntry.enemiesDefeated}`; } | |
trackingStatusDisplay.textContent = text; trackingStatusDisplay.style.display = 'block'; | |
} else { | |
trackingStatusDisplay.textContent = ''; trackingStatusDisplay.style.display = 'none'; | |
} | |
} | |
function formatEta(totalSeconds) { | |
if (totalSeconds === null || totalSeconds === undefined || !isFinite(totalSeconds)) { | |
return 'Calculating...'; | |
} | |
if (totalSeconds < 0) { | |
return 'Target Met!'; | |
} | |
if (totalSeconds === 0) { | |
return 'Soon!'; | |
} | |
const hours = Math.floor(totalSeconds / 3600); | |
const minutes = Math.floor((totalSeconds % 3600) / 60); | |
const seconds = Math.floor(totalSeconds % 60); | |
let parts = []; | |
if (hours > 0) parts.push(hours + 'h'); | |
if (minutes > 0) parts.push(minutes + 'm'); | |
if (seconds > 0 || (hours === 0 && minutes === 0)) parts.push(seconds + 's'); | |
return parts.length > 0 ? parts.join(' ') : '0s'; | |
} | |
function updateEtaDisplayUI(etaSeconds) { | |
if (!etaDisplayElement) return; | |
const currentMode = getSelectedMode(); | |
const selectedFilterValue = document.getElementById('itemFilterDropdown')?.value; | |
const targetInput = document.getElementById('beepTargetNumberInput'); | |
if ( (currentMode !== MODE_TRACKING && currentMode !== MODE_INVENTORY_TOTAL) || | |
!selectedFilterValue || selectedFilterValue === FILTER_OFF_VALUE || | |
(currentMode === MODE_INVENTORY_TOTAL && selectedFilterValue === TRACK_TYPE_ENEMIES_VALUE) || | |
!targetInput || targetInput.disabled || targetInput.value.trim() === '') { | |
etaDisplayElement.style.display = 'none'; | |
etaDisplayElement.textContent = ''; | |
stopEtaUpdateTimer(); | |
return; | |
} | |
etaDisplayElement.style.display = 'block'; | |
if (etaSeconds === null) { | |
etaDisplayElement.textContent = 'ETA: Calculating...'; | |
} else { | |
etaDisplayElement.textContent = `ETA: ${formatEta(etaSeconds)}`; | |
} | |
} | |
function calculateAverageTimePerItem(eventPoints) { | |
if (!eventPoints || eventPoints.length < MIN_TIMESTAMPS_FOR_ETA) { | |
return null; | |
} | |
const relevantPoints = eventPoints.slice(-MAX_TIMESTAMPS_FOR_AVG); | |
if (relevantPoints.length < MIN_TIMESTAMPS_FOR_ETA) { | |
return null; | |
} | |
const firstPoint = relevantPoints[0]; | |
const lastPoint = relevantPoints[relevantPoints.length - 1]; | |
if (lastPoint.ts <= firstPoint.ts) { | |
return null; | |
} | |
const totalItemsChanged = lastPoint.totalCount - firstPoint.totalCount; | |
if (totalItemsChanged <= 0) { | |
return null; | |
} | |
const totalDurationMs = lastPoint.ts - firstPoint.ts; | |
return totalDurationMs / totalItemsChanged; | |
} | |
function calculateAndShowETA() { | |
stopEtaUpdateTimer(); | |
const currentMode = getSelectedMode(); | |
const selectionKey = document.getElementById('itemFilterDropdown')?.value; | |
const targetInput = document.getElementById('beepTargetNumberInput'); | |
if ((currentMode !== MODE_TRACKING && currentMode !== MODE_INVENTORY_TOTAL) || | |
!selectionKey || selectionKey === FILTER_OFF_VALUE || | |
(currentMode === MODE_INVENTORY_TOTAL && selectionKey === TRACK_TYPE_ENEMIES_VALUE) || | |
!targetInput || targetInput.disabled || targetInput.value.trim() === '') { | |
updateEtaDisplayUI(null); | |
let contextObject = null; | |
if (currentMode === MODE_TRACKING && trackingData[selectionKey]) contextObject = trackingData[selectionKey]; | |
else if (currentMode === MODE_INVENTORY_TOTAL && itemPreviousStates[selectionKey]) contextObject = itemPreviousStates[selectionKey]; // Might need adjustment for combined keys | |
if (contextObject && contextObject.hasOwnProperty(ETA_LAST_CALC_SECS_PROP)) { // Check if property exists | |
contextObject[ETA_LAST_CALC_SECS_PROP] = null; | |
contextObject[ETA_CALC_TS_PROP] = 0; | |
} | |
return; | |
} | |
const targetVal = parseNumber(targetInput.value); | |
if (isNaN(targetVal) || targetVal <= 0) { | |
updateEtaDisplayUI(null); | |
return; | |
} | |
let currentCount = NaN; | |
let eventPoints = null; | |
let etaContextObject = null; | |
if (currentMode === MODE_TRACKING) { | |
etaContextObject = trackingData[selectionKey]; | |
if (!etaContextObject) { updateEtaDisplayUI(null); return; } | |
currentCount = (etaContextObject.type === "item") ? etaContextObject.itemsGained : etaContextObject.enemiesDefeated; | |
eventPoints = etaContextObject[ETA_EVENT_POINTS_PROP]; | |
} else if (currentMode === MODE_INVENTORY_TOTAL) { | |
let itemKeyForInventoryEta = selectionKey; | |
if (selectionKey.endsWith(COMBINED_SUFFIX)) { | |
const baseName = selectionKey.substring(0, selectionKey.length - COMBINED_SUFFIX.length); | |
const perfectName = baseName + PERFECT_SUFFIX; | |
currentCount = (itemPreviousStates[baseName]?.currentIndividualTotal || 0) + | |
(itemPreviousStates[perfectName]?.currentIndividualTotal || 0); | |
itemKeyForInventoryEta = selectionKey; | |
} else { | |
currentCount = itemPreviousStates[selectionKey]?.currentIndividualTotal || 0; | |
itemKeyForInventoryEta = selectionKey; | |
} | |
if (isNaN(currentCount)) { updateEtaDisplayUI(null); return; } | |
ensureInventoryEtaContext(itemKeyForInventoryEta); | |
etaContextObject = itemPreviousStates[itemKeyForInventoryEta]; | |
eventPoints = etaContextObject[ETA_EVENT_POINTS_PROP]; | |
} | |
if (etaContextObject === null || !eventPoints || isNaN(currentCount)) { // Added !eventPoints check | |
updateEtaDisplayUI(null); | |
return; | |
} | |
if (currentCount >= targetVal) { | |
updateEtaDisplayUI(-1); | |
etaContextObject[ETA_LAST_CALC_SECS_PROP] = -1; | |
etaContextObject[ETA_CALC_TS_PROP] = Date.now(); | |
return; | |
} | |
const remainingCount = targetVal - currentCount; | |
const averageTimePerSingleItemMs = calculateAverageTimePerItem(eventPoints); | |
if (averageTimePerSingleItemMs === null || averageTimePerSingleItemMs <= 0) { | |
updateEtaDisplayUI(null); | |
etaContextObject[ETA_LAST_CALC_SECS_PROP] = null; | |
etaContextObject[ETA_CALC_TS_PROP] = 0; | |
return; | |
} | |
const etaTotalSeconds = (remainingCount * averageTimePerSingleItemMs) / 1000; | |
etaContextObject[ETA_LAST_CALC_SECS_PROP] = etaTotalSeconds; | |
etaContextObject[ETA_CALC_TS_PROP] = Date.now(); | |
updateEtaDisplayUI(etaTotalSeconds); | |
if (etaTotalSeconds > 0 && isFinite(etaTotalSeconds) && !etaUpdateIntervalId) { | |
etaUpdateIntervalId = setInterval(() => { | |
const modeNow = getSelectedMode(); | |
const keyNow = document.getElementById('itemFilterDropdown')?.value; | |
let contextNow = null; | |
let itemKeyForContextLookup = keyNow; | |
if (modeNow === MODE_TRACKING) { | |
contextNow = trackingData[keyNow]; | |
} else if (modeNow === MODE_INVENTORY_TOTAL) { | |
// Determine the correct key for itemPreviousStates, especially for combined items | |
if (keyNow && keyNow.endsWith(COMBINED_SUFFIX)) { | |
itemKeyForContextLookup = keyNow; // Combined key itself | |
} else { | |
itemKeyForContextLookup = keyNow; // Standard item key | |
} | |
if (itemPreviousStates[itemKeyForContextLookup]) { | |
ensureInventoryEtaContext(itemKeyForContextLookup); // Ensure sub-properties exist | |
contextNow = itemPreviousStates[itemKeyForContextLookup]; | |
} | |
} | |
if (!contextNow || !contextNow.hasOwnProperty(ETA_LAST_CALC_SECS_PROP) || contextNow[ETA_LAST_CALC_SECS_PROP] === null || contextNow[ETA_CALC_TS_PROP] === 0 || | |
(modeNow !== getSelectedMode() || keyNow !== document.getElementById('itemFilterDropdown')?.value)) { | |
stopEtaUpdateTimer(); | |
calculateAndShowETA(); | |
return; | |
} | |
if (contextNow[ETA_LAST_CALC_SECS_PROP] < 0) { | |
stopEtaUpdateTimer(); | |
updateEtaDisplayUI(-1); | |
return; | |
} | |
let liveCurrentCount = NaN; | |
const liveTargetVal = parseNumber(document.getElementById('beepTargetNumberInput').value); | |
if(modeNow === MODE_TRACKING && contextNow) { // contextNow here is trackingData[keyNow] | |
liveCurrentCount = (contextNow.type === "item") ? contextNow.itemsGained : contextNow.enemiesDefeated; | |
} else if (modeNow === MODE_INVENTORY_TOTAL && contextNow) { // contextNow here is itemPreviousStates[itemKeyForContextLookup] | |
if (keyNow.endsWith(COMBINED_SUFFIX)) { | |
const base = keyNow.substring(0, keyNow.length - COMBINED_SUFFIX.length); | |
const perfect = base + PERFECT_SUFFIX; | |
liveCurrentCount = (itemPreviousStates[base]?.currentIndividualTotal || 0) + (itemPreviousStates[perfect]?.currentIndividualTotal || 0); | |
} else { | |
liveCurrentCount = itemPreviousStates[keyNow]?.currentIndividualTotal || 0; | |
} | |
} | |
if (!isNaN(liveTargetVal) && !isNaN(liveCurrentCount) && liveCurrentCount >= liveTargetVal) { | |
stopEtaUpdateTimer(); | |
updateEtaDisplayUI(-1); | |
contextNow[ETA_LAST_CALC_SECS_PROP] = -1; | |
return; | |
} | |
const elapsedSinceCalc = (Date.now() - contextNow[ETA_CALC_TS_PROP]) / 1000; | |
let currentRemainingSeconds = contextNow[ETA_LAST_CALC_SECS_PROP] - elapsedSinceCalc; | |
if (currentRemainingSeconds <= 0) { | |
stopEtaUpdateTimer(); | |
calculateAndShowETA(); | |
} else { | |
updateEtaDisplayUI(currentRemainingSeconds); | |
} | |
}, ETA_UPDATE_INTERVAL_MS); | |
} else if (!(etaTotalSeconds > 0 && isFinite(etaTotalSeconds))) { | |
stopEtaUpdateTimer(); | |
} | |
} | |
function updateItemFilterDropdown() { | |
const dropdown = document.getElementById('itemFilterDropdown'); | |
if (!dropdown) return; | |
const currentMode = getSelectedMode(); | |
if (currentMode === MODE_OFF || currentMode === MODE_IDLE_ONLY) { | |
dropdown.innerHTML = ''; | |
const naOption = document.createElement('option'); | |
naOption.value = FILTER_OFF_VALUE; naOption.textContent = "N/A for current mode"; | |
dropdown.appendChild(naOption); dropdown.disabled = true; return; | |
} | |
dropdown.disabled = false; | |
const currentSelection = dropdown.value; | |
dropdown.innerHTML = ''; | |
const defaultOption = document.createElement('option'); | |
defaultOption.value = FILTER_OFF_VALUE; defaultOption.textContent = "-- Filter OFF / No Tracking --"; | |
dropdown.appendChild(defaultOption); | |
const enemiesOption = document.createElement('option'); | |
enemiesOption.value = TRACK_TYPE_ENEMIES_VALUE; enemiesOption.textContent = TRACK_TYPE_ENEMIES_TEXT; | |
dropdown.appendChild(enemiesOption); | |
const separator = document.createElement('option'); | |
separator.disabled = true; separator.textContent = '──────────'; | |
dropdown.appendChild(separator); | |
const displayableIndividualItems = new Set(discoveredItemNames); | |
discoveredItemNames.forEach(itemName => { | |
if (itemName.endsWith(PERFECT_SUFFIX)) { | |
const baseName = itemName.substring(0, itemName.length - PERFECT_SUFFIX.length); | |
displayableIndividualItems.add(baseName); | |
} | |
}); | |
const sortedIndividualItems = Array.from(displayableIndividualItems).sort(); | |
sortedIndividualItems.forEach(itemName => { | |
const option = document.createElement('option'); | |
option.value = itemName; option.textContent = itemName.replace(/_/g, ' '); | |
dropdown.appendChild(option); | |
}); | |
const combinedOptionsProcessed = new Set(); | |
discoveredItemNames.forEach(itemName => { | |
let baseNameForCombinedOption = null; | |
if (itemName.endsWith(PERFECT_SUFFIX)) { | |
baseNameForCombinedOption = itemName.substring(0, itemName.length - PERFECT_SUFFIX.length); | |
} else if (discoveredItemNames.has(itemName + PERFECT_SUFFIX) || itemName.includes("_cooked")) { | |
baseNameForCombinedOption = itemName; | |
} | |
if (baseNameForCombinedOption && !combinedOptionsProcessed.has(baseNameForCombinedOption)) { | |
const perfectVersionName = baseNameForCombinedOption + PERFECT_SUFFIX; | |
if (discoveredItemNames.has(baseNameForCombinedOption) || discoveredItemNames.has(perfectVersionName)) { | |
const combinedOptionValue = baseNameForCombinedOption + COMBINED_SUFFIX; | |
if (!Array.from(dropdown.options).some(opt => opt.value === combinedOptionValue)) { | |
const option = document.createElement('option'); | |
option.value = combinedOptionValue; option.textContent = `${baseNameForCombinedOption.replace(/_/g, ' ')} (+Perfect)`; | |
dropdown.appendChild(option); | |
} | |
combinedOptionsProcessed.add(baseNameForCombinedOption); | |
} | |
} | |
}); | |
const specialOptions = Array.from(dropdown.options).slice(0, 3); | |
const itemOptions = Array.from(dropdown.options).slice(3); | |
itemOptions.sort((a, b) => a.textContent.localeCompare(b.textContent)); | |
dropdown.innerHTML = ''; | |
specialOptions.forEach(opt => dropdown.appendChild(opt)); | |
itemOptions.forEach(opt => dropdown.appendChild(opt)); | |
if (Array.from(dropdown.options).some(opt => opt.value === currentSelection)) { dropdown.value = currentSelection; } | |
else { dropdown.value = FILTER_OFF_VALUE; } | |
} | |
function updateMainInputUIState() { | |
const currentMode = getSelectedMode(); | |
const selectedFilterValue = document.getElementById('itemFilterDropdown')?.value || FILTER_OFF_VALUE; | |
const qtyTargetInput = document.getElementById('beepTargetNumberInput'); | |
const greaterThanToggleLabel = document.querySelector('label[for="beepIfGreaterThanTargetToggle"]'); | |
const itemFilterDropdown = document.getElementById('itemFilterDropdown'); | |
if (!qtyTargetInput || !greaterThanToggleLabel || !itemFilterDropdown) return; | |
qtyTargetInput.disabled = false; | |
greaterThanToggleLabel.style.display = 'inline-flex'; | |
if (currentMode === MODE_OFF || currentMode === MODE_IDLE_ONLY) { | |
qtyTargetInput.placeholder = "N/A"; qtyTargetInput.title = "Not applicable for current mode."; | |
qtyTargetInput.disabled = true; qtyTargetInput.value = ''; | |
greaterThanToggleLabel.style.display = 'none'; | |
} else if (currentMode === MODE_INVENTORY_TOTAL) { | |
if (selectedFilterValue === TRACK_TYPE_ENEMIES_VALUE || selectedFilterValue === FILTER_OFF_VALUE) { | |
qtyTargetInput.placeholder = "N/A"; qtyTargetInput.title = "Inventory total not applicable for this selection."; | |
qtyTargetInput.disabled = true; if (selectedFilterValue !== FILTER_OFF_VALUE) qtyTargetInput.value = ''; | |
} else { | |
qtyTargetInput.placeholder = "Inv. Target"; qtyTargetInput.title = "Target for selected item's total in inventory."; | |
} | |
} else { // MODE_TRACKING | |
if (selectedFilterValue === FILTER_OFF_VALUE) { | |
qtyTargetInput.placeholder = "N/A"; qtyTargetInput.title = "Select an item or 'Track Enemies' to set a tracking target."; | |
qtyTargetInput.disabled = true; qtyTargetInput.value = ''; | |
} else if (selectedFilterValue === TRACK_TYPE_ENEMIES_VALUE) { | |
qtyTargetInput.placeholder = "Enemy Target"; qtyTargetInput.title = "Target for # enemies defeated since tracking started."; | |
} else { | |
qtyTargetInput.placeholder = "Item Target"; qtyTargetInput.title = "Target for # selected item gained since tracking started."; | |
} | |
} | |
updateItemFilterDropdown(); | |
updateTrackingUIDisplay(); | |
calculateAndShowETA(); | |
} | |
function setupUI() { | |
const existingUI = document.getElementById('beepScriptUIContainer'); | |
if (existingUI) { | |
updateItemFilterDropdown(); updateMainInputUIState(); updateTrackingUIDisplay(); calculateAndShowETA(); return; | |
} | |
const uiContainer = document.createElement('div'); | |
uiContainer.id = 'beepScriptUIContainer'; | |
Object.assign(uiContainer.style, { | |
position: 'fixed', bottom: '10px', right: '230px', padding: '8px', | |
backgroundColor: 'rgba(30, 30, 30, 0.95)', color: '#e0e0e0', zIndex: '10001', | |
borderRadius: '8px', fontFamily: '"Inter", "Helvetica Neue", Helvetica, Arial, sans-serif', | |
fontSize: '12px', boxShadow: '0 2px 10px rgba(0,0,0,0.5)', | |
border: '1px solid rgba(255,255,255,0.1)', display: 'flex', | |
flexDirection: 'column', gap: '6px', minWidth: '230px' | |
}); | |
const headerControlsDiv = document.createElement('div'); | |
headerControlsDiv.id = 'beepScriptHeaderControls'; | |
Object.assign(headerControlsDiv.style, { | |
display: 'flex', flexDirection: 'row', alignItems: 'center', gap: '8px', justifyContent: 'space-between' | |
}); | |
addTrackingModeControls(headerControlsDiv); | |
addClearFilterButtonToHeader(headerControlsDiv); | |
uiContainer.appendChild(headerControlsDiv); | |
collapsibleContent = document.createElement('div'); | |
collapsibleContent.id = 'beepScriptCollapsibleContent'; | |
Object.assign(collapsibleContent.style, { | |
display: 'none', flexDirection: 'column', gap: '6px', paddingTop: '6px' | |
}); | |
const filterDropdownContainer = document.createElement('div'); | |
filterDropdownContainer.id = 'filterDropdownContainer'; | |
addItemFilterDropdown(filterDropdownContainer); | |
collapsibleContent.appendChild(filterDropdownContainer); | |
const row2InputControls = document.createElement('div'); | |
row2InputControls.id = 'row2InputControlsContainer'; | |
Object.assign(row2InputControls.style, { | |
display: 'flex', flexDirection: 'row', alignItems: 'center', gap: '8px' | |
}); | |
const mainInput = document.createElement('input'); | |
mainInput.type = 'number'; mainInput.id = 'beepTargetNumberInput'; | |
Object.assign(mainInput.style, { | |
width: '70px', padding: '4px 6px', border: '1px solid #555', | |
borderRadius: '4px', backgroundColor: '#444', color: '#fff', fontSize: '12px', | |
textAlign: 'center' | |
}); | |
mainInput.addEventListener('input', () => { | |
updateLastRelevantActivityAndStopIdleBeep("Target input changed"); | |
if (lastProcessedNotificationElement && document.body.contains(lastProcessedNotificationElement)) { | |
clearElementBeepStates(lastProcessedNotificationElement); | |
} | |
const currentSelectionKey = document.getElementById('itemFilterDropdown')?.value; | |
if (getSelectedMode() === MODE_TRACKING && currentSelectionKey && currentSelectionKey !== FILTER_OFF_VALUE && trackingData[currentSelectionKey]) { | |
trackingData[currentSelectionKey].lastBeepedTargetCount = -1; | |
} | |
calculateAndShowETA(); | |
}); | |
row2InputControls.appendChild(mainInput); | |
addGreaterThanToggle(row2InputControls); | |
collapsibleContent.appendChild(row2InputControls); | |
addTrackingStatusDisplay(collapsibleContent); | |
addEtaDisplay(collapsibleContent); | |
uiContainer.appendChild(collapsibleContent); | |
document.body.appendChild(uiContainer); | |
updateItemFilterDropdown(); updateMainInputUIState(); updateTrackingUIDisplay(); calculateAndShowETA(); | |
} | |
function addTrackingModeControls(container) { | |
const modeDiv = document.createElement('div'); | |
modeDiv.style.display = 'flex'; modeDiv.style.alignItems = 'center'; | |
const modeLabel = document.createElement('label'); modeLabel.textContent = "Mode:"; | |
modeLabel.style.marginRight = '4px'; modeLabel.style.whiteSpace = 'nowrap'; | |
const modeDropdown = document.createElement('select'); modeDropdown.id = 'trackingModeDropdown'; | |
Object.assign(modeDropdown.style, { | |
padding: '4px 6px', border: '1px solid #555', borderRadius: '4px', | |
backgroundColor: '#444', color: '#fff', fontSize: '12px', minWidth: '90px' | |
}); | |
const offOption = document.createElement('option'); offOption.value = MODE_OFF; offOption.textContent = "Off"; | |
const idleOnlyOption = document.createElement('option'); idleOnlyOption.value = MODE_IDLE_ONLY; idleOnlyOption.textContent = "Idle Only"; | |
const invTotalOption = document.createElement('option'); invTotalOption.value = MODE_INVENTORY_TOTAL; invTotalOption.textContent = "Inv. Total"; | |
const trackingOption = document.createElement('option'); trackingOption.value = MODE_TRACKING; trackingOption.textContent = "Tracking"; | |
modeDropdown.appendChild(offOption); modeDropdown.appendChild(idleOnlyOption); | |
modeDropdown.appendChild(invTotalOption); modeDropdown.appendChild(trackingOption); | |
modeDropdown.value = MODE_OFF; | |
modeDropdown.addEventListener('change', (event) => { | |
const newMode = event.target.value; | |
updateLastRelevantActivityAndStopIdleBeep("Tracking mode changed"); | |
if (collapsibleContent) { | |
collapsibleContent.style.display = (newMode === MODE_OFF) ? 'none' : 'flex'; | |
} | |
const currentFilterSelection = document.getElementById('itemFilterDropdown')?.value; | |
if (newMode === MODE_TRACKING) { | |
initializeOrResetTrackingSession(currentFilterSelection); | |
} else { | |
trackingData = {}; // Clear session tracking if not in tracking mode | |
} | |
updateMainInputUIState(); | |
}); | |
modeDiv.append(modeLabel, modeDropdown); | |
container.appendChild(modeDiv); | |
} | |
function addClearFilterButtonToHeader(container) { | |
const clearFilterButton = document.createElement('button'); | |
clearFilterButton.textContent = '❌'; | |
clearFilterButton.classList.add('userscript-button-style'); | |
clearFilterButton.style.padding = '3px 6px'; | |
clearFilterButton.style.fontSize = '12px'; | |
clearFilterButton.title = "Clear filter, discovered items, stop tracking & set mode to Off"; | |
clearFilterButton.addEventListener('click', () => { | |
const filterDropdown = document.getElementById('itemFilterDropdown'); | |
const modeDropdown = document.getElementById('trackingModeDropdown'); | |
discoveredItemNames.clear(); | |
itemPreviousStates = {}; | |
trackingData = {}; | |
if (modeDropdown) { | |
modeDropdown.value = MODE_OFF; | |
if (collapsibleContent) { | |
collapsibleContent.style.display = 'none'; | |
} | |
} | |
if (filterDropdown) { filterDropdown.value = FILTER_OFF_VALUE; } | |
updateLastRelevantActivityAndStopIdleBeep("Filter cleared & Mode set to Off"); | |
if (lastProcessedNotificationElement && document.body.contains(lastProcessedNotificationElement)) { | |
clearElementBeepStates(lastProcessedNotificationElement); | |
} | |
stopAndClearRecheckInterval(); | |
updateMainInputUIState(); | |
console.log("Melvor Beep: All data cleared. Mode set to OFF."); | |
}); | |
container.appendChild(clearFilterButton); | |
} | |
function addItemFilterDropdown(container) { | |
const filterDropdown = document.createElement('select'); filterDropdown.id = 'itemFilterDropdown'; | |
Object.assign(filterDropdown.style, { | |
padding: '4px 6px', border: '1px solid #555', borderRadius: '4px', | |
backgroundColor: '#444', color: '#fff', fontSize: '12px', | |
minWidth: '100%', maxWidth: '100%' | |
}); | |
filterDropdown.addEventListener('change', () => { | |
const selectedValue = filterDropdown.value; | |
itemPreviousStates = {}; | |
updateLastRelevantActivityAndStopIdleBeep("Filter selection changed"); | |
if (lastProcessedNotificationElement && document.body.contains(lastProcessedNotificationElement)) { | |
clearElementBeepStates(lastProcessedNotificationElement); | |
} | |
if (getSelectedMode() === MODE_TRACKING) { | |
initializeOrResetTrackingSession(selectedValue); | |
} else { | |
trackingData = {}; | |
} | |
updateMainInputUIState(); | |
}); | |
container.appendChild(filterDropdown); | |
} | |
function addTrackingStatusDisplay(container) { | |
trackingStatusDisplay = document.createElement('div'); trackingStatusDisplay.id = 'trackingStatusDisplay'; | |
Object.assign(trackingStatusDisplay.style, { | |
padding: '4px', marginTop: '2px', backgroundColor: 'rgba(10,10,10,0.8)', | |
borderRadius: '4px', fontSize: '11px', minHeight: '15px', color: '#66D9EF', | |
display: 'none', textAlign: 'center', width: '100%' | |
}); | |
container.appendChild(trackingStatusDisplay); | |
} | |
function addEtaDisplay(container) { | |
etaDisplayElement = document.createElement('div'); | |
etaDisplayElement.id = 'etaDisplayElement'; | |
Object.assign(etaDisplayElement.style, { | |
padding: '4px', | |
marginTop: '4px', | |
backgroundColor: 'rgba(10,10,10,0.7)', | |
borderRadius: '4px', | |
fontSize: '11px', | |
minHeight: '15px', | |
color: '#A6E22E', | |
display: 'none', | |
textAlign: 'center', | |
width: '100%' | |
}); | |
etaDisplayElement.textContent = 'ETA: Calculating...'; | |
container.appendChild(etaDisplayElement); | |
} | |
function addGreaterThanToggle(container) { | |
const toggleCheckbox = document.createElement('input'); | |
toggleCheckbox.type = 'checkbox'; toggleCheckbox.id = 'beepIfGreaterThanTargetToggle'; | |
toggleCheckbox.checked = true; toggleCheckbox.style.display = 'none'; | |
const toggleLabel = document.createElement('label'); | |
toggleLabel.htmlFor = 'beepIfGreaterThanTargetToggle'; toggleLabel.textContent = '🔔'; | |
toggleLabel.title = "Beep if quantity is > target (applies to current mode's target)"; | |
toggleLabel.classList.add('userscript-button-style'); | |
Object.assign(toggleLabel.style, { minWidth: '26px' }); | |
const setBellStyle = () => { | |
if (toggleCheckbox.checked) { toggleLabel.style.backgroundColor = 'darkgreen'; toggleLabel.style.color = 'white'; } | |
else { toggleLabel.style.backgroundColor = '#555'; toggleLabel.style.color = '#ccc'; } | |
}; | |
toggleCheckbox.addEventListener('change', () => { setBellStyle(); updateLastRelevantActivityAndStopIdleBeep("Bell toggle changed"); }); | |
setBellStyle(); | |
container.appendChild(toggleCheckbox); container.appendChild(toggleLabel); | |
} | |
function parseNumber(text) { | |
if (typeof text !== 'string' || text.trim() === '') return NaN; | |
return parseInt(text.trim().replace(/,/g, ''), 10); | |
} | |
function stopAndClearRecheckInterval() { | |
if (recheckNotificationIntervalId) { clearInterval(recheckNotificationIntervalId); recheckNotificationIntervalId = null; } | |
} | |
function processGameNotification(gameNotificationElement, isRecheck = false) { | |
if (!gameNotificationElement || !document.body.contains(gameNotificationElement)) { | |
if (lastProcessedNotificationElement === gameNotificationElement) { stopAndClearRecheckInterval(); lastProcessedNotificationElement = null; } return; | |
} | |
const currentMode = getSelectedMode(); | |
if (currentMode === MODE_OFF) return; | |
const selectedFilterValue = document.getElementById('itemFilterDropdown')?.value; | |
const itemIdentifierElement = gameNotificationElement.querySelector('img.newNotification-img'); | |
const itemIdSrc = itemIdentifierElement ? itemIdentifierElement.src : null; | |
const itemName = itemIdSrc ? itemIdSrc.substring(itemIdSrc.lastIndexOf('/') + 1).replace(/\.png(\?.*)?$/, '') : "N/A"; | |
const descriptionElement = gameNotificationElement.querySelector('div.flex-notify > span.text-white.font-size-xs.justify-vertical-center.mr-1'); | |
const descriptionText = descriptionElement ? descriptionElement.textContent.trim() : ""; | |
if (!isRecheck) { | |
if (lastProcessedNotificationElement !== gameNotificationElement) { | |
stopAndClearRecheckInterval(); | |
// No need to clearElementBeepStates for the old element IF it's truly gone/different. | |
// The new element starts fresh. | |
lastProcessedNotificationElement = gameNotificationElement; | |
} | |
recheckEndTime = Date.now() + recheckDurationMs; | |
if (!recheckNotificationIntervalId) { | |
recheckNotificationIntervalId = setInterval(() => { | |
if (Date.now() > recheckEndTime || !lastProcessedNotificationElement || !document.body.contains(lastProcessedNotificationElement)) { | |
stopAndClearRecheckInterval(); lastProcessedNotificationElement = null; return; | |
} | |
processGameNotification(lastProcessedNotificationElement, true); | |
}, recheckIntervalMs); | |
} | |
} else { if (gameNotificationElement === lastProcessedNotificationElement) recheckEndTime = Date.now() + recheckDurationMs; } | |
let activityShouldResetIdle = false; | |
if (itemName !== "N/A" && descriptionText === "") { | |
if (currentMode === MODE_IDLE_ONLY) { activityShouldResetIdle = true; } | |
else if (currentMode === MODE_INVENTORY_TOTAL || currentMode === MODE_TRACKING) { | |
if (selectedFilterValue && selectedFilterValue !== FILTER_OFF_VALUE) { activityShouldResetIdle = true; } | |
} | |
} | |
if (!isRecheck && activityShouldResetIdle) { updateLastRelevantActivityAndStopIdleBeep(`Item notification (${itemName})`); } | |
const plusAmountGainedElement = gameNotificationElement.querySelector('div.mr-2 span.text-success.font-w700.font-size-sm, div.mr-2 span.text-success'); | |
const textPlusAmountGained = plusAmountGainedElement ? plusAmountGainedElement.textContent.trim() : null; | |
const plusAmountGainedNum = textPlusAmountGained && textPlusAmountGained.startsWith('+') ? parseNumber(textPlusAmountGained.substring(1)) : 0; | |
const textNewTotalQuantityElement = gameNotificationElement.querySelector('div.flex-notify > span.text-warning.font-size-xs'); | |
const textNewTotalQuantity = textNewTotalQuantityElement ? textNewTotalQuantityElement.textContent.trim() : null; | |
const newTotalQuantityNum = parseNumber(textNewTotalQuantity); | |
if (itemName !== "N/A" && descriptionText === "" && !isNaN(newTotalQuantityNum)) { | |
if (!itemPreviousStates[itemName]) itemPreviousStates[itemName] = {}; | |
ensureInventoryEtaContext(itemName); | |
itemPreviousStates[itemName].currentIndividualTotal = newTotalQuantityNum; | |
if (!isRecheck && !discoveredItemNames.has(itemName)) { | |
discoveredItemNames.add(itemName); | |
if (currentMode !== MODE_IDLE_ONLY && currentMode !== MODE_OFF) { updateItemFilterDropdown(); } | |
} | |
} | |
if (currentMode === MODE_IDLE_ONLY) return; | |
const targetInputElement = document.getElementById('beepTargetNumberInput'); | |
const greaterThanToggleCheckbox = document.getElementById('beepIfGreaterThanTargetToggle'); | |
let targetVal = NaN; | |
if (targetInputElement && !targetInputElement.disabled && targetInputElement.value !== '') { targetVal = parseNumber(targetInputElement.value); } | |
// MODE_INVENTORY_TOTAL | |
if (currentMode === MODE_INVENTORY_TOTAL && selectedFilterValue && selectedFilterValue !== FILTER_OFF_VALUE && selectedFilterValue !== TRACK_TYPE_ENEMIES_VALUE) { | |
if (descriptionText === "" && itemName !== "N/A") { | |
let qtyToCompare = NaN; | |
let itemKeyForInvContext = selectedFilterValue; | |
let itemMatchesFilter = false; | |
let logItemName = itemName.replace(/_/g, ' '); | |
if (selectedFilterValue.endsWith(COMBINED_SUFFIX)) { | |
const baseFilterName = selectedFilterValue.substring(0, selectedFilterValue.length - COMBINED_SUFFIX.length); | |
const perfectFilterName = baseFilterName + PERFECT_SUFFIX; | |
if (itemName === baseFilterName || itemName === perfectFilterName) { | |
ensureInventoryEtaContext(baseFilterName); // Ensure base item has context if needed for individual total | |
ensureInventoryEtaContext(perfectFilterName); // Ensure perfect item has context | |
const baseTotal = itemPreviousStates[baseFilterName]?.currentIndividualTotal || 0; | |
const perfectTotal = itemPreviousStates[perfectFilterName]?.currentIndividualTotal || 0; | |
qtyToCompare = baseTotal + perfectTotal; | |
itemMatchesFilter = true; | |
logItemName = `${baseFilterName.replace(/_/g, ' ')} (+Perfect sum: ${qtyToCompare})`; | |
itemKeyForInvContext = selectedFilterValue; // Use combined key for ETA context | |
ensureInventoryEtaContext(itemKeyForInvContext); | |
} | |
} else if (itemName === selectedFilterValue) { | |
qtyToCompare = newTotalQuantityNum; | |
itemMatchesFilter = true; | |
itemKeyForInvContext = selectedFilterValue; | |
ensureInventoryEtaContext(itemKeyForInvContext); | |
} | |
if (itemMatchesFilter && !isNaN(qtyToCompare)) { | |
const itemStateContext = itemPreviousStates[itemKeyForInvContext]; | |
const existingTimestamps = itemStateContext[ETA_EVENT_POINTS_PROP]; | |
const lastRecordedEvent = existingTimestamps.length > 0 ? existingTimestamps[existingTimestamps.length-1] : null; | |
if (!lastRecordedEvent || lastRecordedEvent.totalCount !== qtyToCompare) { | |
if (!lastRecordedEvent || qtyToCompare > lastRecordedEvent.totalCount) { | |
existingTimestamps.push({ ts: Date.now(), totalCount: qtyToCompare }); | |
if (existingTimestamps.length > MAX_TIMESTAMPS_FOR_AVG) { | |
itemStateContext[ETA_EVENT_POINTS_PROP] = existingTimestamps.slice(-MAX_TIMESTAMPS_FOR_AVG); | |
} | |
if (!isNaN(targetVal)) calculateAndShowETA(); | |
} | |
} | |
if (!isNaN(targetVal)) { | |
const lastProcessedTotalCondA = itemStateContext.lastProcessedTotalCondA === undefined ? -Infinity : itemStateContext.lastProcessedTotalCondA; | |
const justCrossed = lastProcessedTotalCondA < targetVal && qtyToCompare >= targetVal; | |
const exactMatch = qtyToCompare === targetVal; | |
const greaterAndToggleChecked = greaterThanToggleCheckbox?.checked && qtyToCompare > targetVal; | |
let triggerBeepCondA = false; let reasonCondA = ""; | |
if (justCrossed) { triggerBeepCondA = true; reasonCondA = "CROSSED INV. THRESHOLD"; } | |
else if (exactMatch && lastProcessedTotalCondA !== qtyToCompare) { triggerBeepCondA = true; reasonCondA = "EXACT INV. QTY MATCH"; } | |
else if (greaterAndToggleChecked && qtyToCompare > lastProcessedTotalCondA && qtyToCompare > targetVal) { triggerBeepCondA = true; reasonCondA = "GREATER THAN INV. QTY"; } | |
const condAStateKey = `${itemKeyForInvContext}-${qtyToCompare}-A-${reasonCondA}`; | |
if (triggerBeepCondA && gameNotificationElement.dataset.lastBeepedStateA !== condAStateKey) { | |
console.log(`%cMelvor Beep (Cond A - Inv. Total): ${reasonCondA}! Item: ${logItemName}, Qty (${qtyToCompare}) vs Target (${targetVal}).`, "color: lightgreen; font-weight: bold;"); | |
playBeep("Cond A (Inv. Target)"); gameNotificationElement.dataset.lastBeepedStateA = condAStateKey; | |
if(activityShouldResetIdle) updateLastRelevantActivityAndStopIdleBeep(`Cond A beep (${itemName})`); | |
} else if (!triggerBeepCondA && gameNotificationElement.dataset.lastBeepedStateA) { delete gameNotificationElement.dataset.lastBeepedStateA; } | |
itemStateContext.lastProcessedTotalCondA = qtyToCompare; | |
} | |
} | |
} | |
} | |
// MODE_TRACKING for Items | |
else if (currentMode === MODE_TRACKING && selectedFilterValue && selectedFilterValue !== FILTER_OFF_VALUE && selectedFilterValue !== TRACK_TYPE_ENEMIES_VALUE) { | |
const trackedItemEntry = trackingData[selectedFilterValue]; | |
let itemIsRelevantToSelectedFilter = false; | |
if (selectedFilterValue.endsWith(COMBINED_SUFFIX)) { | |
const baseFilterName = selectedFilterValue.substring(0, selectedFilterValue.length - COMBINED_SUFFIX.length); | |
const perfectFilterName = baseFilterName + PERFECT_SUFFIX; | |
if (itemName === baseFilterName || itemName === perfectFilterName) { | |
itemIsRelevantToSelectedFilter = true; | |
} | |
} else if (itemName === selectedFilterValue) { | |
itemIsRelevantToSelectedFilter = true; | |
} | |
if (trackedItemEntry && trackedItemEntry.type === "item" && itemIsRelevantToSelectedFilter && !isNaN(plusAmountGainedNum)) { | |
let lastRecordedGain = 0; | |
if (gameNotificationElement.dataset.lastRecordedGain !== undefined) { | |
lastRecordedGain = parseNumber(gameNotificationElement.dataset.lastRecordedGain); | |
if (isNaN(lastRecordedGain)) lastRecordedGain = 0; | |
} | |
let actualDelta = 0; | |
if (plusAmountGainedNum > lastRecordedGain) { | |
actualDelta = plusAmountGainedNum - lastRecordedGain; | |
} | |
// --- DEBUG LOGGING for Gain Calculation --- | |
if (itemIsRelevantToSelectedFilter && (plusAmountGainedNum > 0 || actualDelta > 0)) { // Log if relevant and any potential gain | |
console.log(`Melvor Beep (Track Gain Debug - ${itemName}): Notification +Amount: ${plusAmountGainedNum}, Element LastRecorded: ${lastRecordedGain}, Calculated Delta: ${actualDelta}, IsRecheck: ${isRecheck}, Element:`, gameNotificationElement); | |
} | |
// --- END DEBUG LOGGING --- | |
gameNotificationElement.dataset.lastRecordedGain = String(plusAmountGainedNum); | |
if (actualDelta > 0) { | |
trackedItemEntry.itemsGained += actualDelta; | |
updateTrackingUIDisplay(); | |
trackedItemEntry[ETA_EVENT_POINTS_PROP].push({ ts: Date.now(), totalCount: trackedItemEntry.itemsGained }); | |
if (trackedItemEntry[ETA_EVENT_POINTS_PROP].length > MAX_TIMESTAMPS_FOR_AVG) { | |
trackedItemEntry[ETA_EVENT_POINTS_PROP] = trackedItemEntry[ETA_EVENT_POINTS_PROP].slice(-MAX_TIMESTAMPS_FOR_AVG); | |
} | |
if (!isNaN(targetVal)) calculateAndShowETA(); | |
if(activityShouldResetIdle) updateLastRelevantActivityAndStopIdleBeep(`Tracked item gain (${itemName})`); | |
} | |
if (!isNaN(targetVal) && trackedItemEntry.itemsGained > 0) { | |
const currentCount = trackedItemEntry.itemsGained; const lastBeeped = trackedItemEntry.lastBeepedTargetCount; | |
const greaterAndToggleChecked = greaterThanToggleCheckbox?.checked && currentCount > targetVal; | |
const justCrossed = lastBeeped < targetVal && currentCount >= targetVal; | |
const exactMatch = currentCount === targetVal; | |
let triggerBeepTrack = false; let reasonTrack = ""; | |
if (justCrossed) { triggerBeepTrack = true; reasonTrack = "CROSSED TRACKED ITEM THRESHOLD"; } | |
else if (exactMatch && lastBeeped !== currentCount ) { triggerBeepTrack = true; reasonTrack = "EXACT TRACKED ITEM QTY"; } | |
else if (greaterAndToggleChecked && currentCount > lastBeeped && currentCount > targetVal ) { triggerBeepTrack = true; reasonTrack = "GREATER THAN TRACKED ITEM QTY"; } | |
if (triggerBeepTrack && lastBeeped !== currentCount) { | |
console.log(`%cMelvor Beep (Tracking Alert): ${reasonTrack}! Item: ${selectedFilterValue.replace(/_/g, ' ')}, Tracked Qty (${currentCount}) vs Target (${targetVal}).`, "color: #FFD700; font-weight: bold;"); | |
playBeep("Tracked Item Target Met", true); trackedItemEntry.lastBeepedTargetCount = currentCount; | |
if(activityShouldResetIdle) updateLastRelevantActivityAndStopIdleBeep(`Tracked item target beep (${itemName})`); | |
} | |
} | |
} | |
} | |
if (enableIncreaseDetectionBeep && descriptionText === "" && itemName !== "N/A" && !isNaN(plusAmountGainedNum) && plusAmountGainedNum > 0 && !isNaN(newTotalQuantityNum)) { | |
let itemMatchesFilterCondB = (selectedFilterValue === FILTER_OFF_VALUE || selectedFilterValue === TRACK_TYPE_ENEMIES_VALUE || | |
itemName === selectedFilterValue || | |
(selectedFilterValue.endsWith(COMBINED_SUFFIX) && | |
(itemName === selectedFilterValue.substring(0, selectedFilterValue.length - COMBINED_SUFFIX.length) || | |
itemName === selectedFilterValue.substring(0, selectedFilterValue.length - COMBINED_SUFFIX.length) + PERFECT_SUFFIX))); | |
if (itemMatchesFilterCondB) { | |
if (!itemPreviousStates[itemName]) itemPreviousStates[itemName] = {}; | |
const itemState = itemPreviousStates[itemName]; | |
const calculatedPreviousTotal = newTotalQuantityNum - plusAmountGainedNum; | |
if (itemState.previousTotalCondB !== undefined && | |
itemState.previousTotalCondB === calculatedPreviousTotal && | |
newTotalQuantityNum > itemState.previousTotalCondB) { | |
if (itemState.lastBeepedCondBTotal !== newTotalQuantityNum) { | |
playBeep("Cond B (Increase)"); | |
itemState.lastBeepedCondBTotal = newTotalQuantityNum; | |
if(activityShouldResetIdle) updateLastRelevantActivityAndStopIdleBeep(`Cond B beep (${itemName})`); | |
} | |
} | |
itemState.previousTotalCondB = newTotalQuantityNum; | |
} | |
} | |
} | |
function observeEnemyName() { | |
const enemyNameSpan = document.getElementById(ENEMY_NAME_SPAN_ID); | |
if (!enemyNameSpan) { setTimeout(observeEnemyName, 2000); return; } | |
previousEnemyName = enemyNameSpan.textContent ? enemyNameSpan.textContent.trim() : '-'; | |
if (combatEnemyNameObserver) combatEnemyNameObserver.disconnect(); | |
combatEnemyNameObserver = new MutationObserver(mutations => { | |
const currentEnemyName = enemyNameSpan.textContent ? enemyNameSpan.textContent.trim() : '-'; | |
if (currentEnemyName === previousEnemyName) return; | |
const currentMode = getSelectedMode(); | |
if (currentMode !== MODE_OFF) { | |
if (previousEnemyName === '-' && currentEnemyName !== '-') { | |
if (currentMode === MODE_IDLE_ONLY) updateLastRelevantActivityAndStopIdleBeep(`Enemy appeared (${currentEnemyName})`); | |
} else if (currentEnemyName === '-' && previousEnemyName !== '-') { | |
if (currentMode === MODE_IDLE_ONLY) updateLastRelevantActivityAndStopIdleBeep(`Enemy vanished (was ${previousEnemyName})`); | |
if (currentMode === MODE_TRACKING) { | |
const selectedFilterValue = document.getElementById('itemFilterDropdown')?.value; | |
if (selectedFilterValue === TRACK_TYPE_ENEMIES_VALUE) { | |
const trackedEnemyEntry = trackingData[TRACK_TYPE_ENEMIES_VALUE]; | |
if (trackedEnemyEntry && trackedEnemyEntry.type === "enemies") { | |
trackedEnemyEntry.enemiesDefeated++; | |
updateTrackingUIDisplay(); | |
trackedEnemyEntry[ETA_EVENT_POINTS_PROP].push({ ts: Date.now(), totalCount: trackedEnemyEntry.enemiesDefeated }); | |
if (trackedEnemyEntry[ETA_EVENT_POINTS_PROP].length > MAX_TIMESTAMPS_FOR_AVG) { | |
trackedEnemyEntry[ETA_EVENT_POINTS_PROP] = trackedEnemyEntry[ETA_EVENT_POINTS_PROP].slice(-MAX_TIMESTAMPS_FOR_AVG); | |
} | |
const targetInputVal = parseNumber(document.getElementById('beepTargetNumberInput').value); | |
if (!isNaN(targetInputVal)) calculateAndShowETA(); | |
updateLastRelevantActivityAndStopIdleBeep(`Enemy defeated (${previousEnemyName}) while tracking enemies`); | |
const targetInputElement = document.getElementById('beepTargetNumberInput'); | |
let targetVal = NaN; | |
if (targetInputElement && !targetInputElement.disabled && targetInputElement.value !== '') { targetVal = parseNumber(targetInputElement.value); } | |
if (!isNaN(targetVal)) { | |
const currentCount = trackedEnemyEntry.enemiesDefeated; const lastBeeped = trackedEnemyEntry.lastBeepedTargetCount; | |
const greaterThanToggleCheckbox = document.getElementById('beepIfGreaterThanTargetToggle'); | |
const greaterAndToggleChecked = greaterThanToggleCheckbox?.checked && currentCount > targetVal; | |
const justCrossed = lastBeeped < targetVal && currentCount >= targetVal; | |
const exactMatch = currentCount === targetVal; | |
let triggerBeepTrack = false; let reasonTrack = ""; | |
if (justCrossed) { triggerBeepTrack = true; reasonTrack = "CROSSED TRACKED ENEMY THRESHOLD"; } | |
else if (exactMatch && lastBeeped !== currentCount ) { triggerBeepTrack = true; reasonTrack = "EXACT TRACKED ENEMY QTY"; } | |
else if (greaterAndToggleChecked && currentCount > lastBeeped && currentCount > targetVal) { triggerBeepTrack = true; reasonTrack = "GREATER THAN TRACKED ENEMY QTY"; } | |
if (triggerBeepTrack && lastBeeped !== currentCount) { | |
console.log(`%cMelvor Beep (Tracking Alert): ${reasonTrack}! Enemies Defeated (${currentCount}) vs Target (${targetVal}).`, "color: #FFD700; font-weight: bold;"); | |
playBeep("Tracked Enemy Target Met", true); trackedEnemyEntry.lastBeepedTargetCount = currentCount; | |
updateLastRelevantActivityAndStopIdleBeep(`Tracked enemy target beep`); | |
} | |
} | |
} | |
} | |
} | |
} else if (currentEnemyName !== '-' && previousEnemyName !== '-' && currentEnemyName !== previousEnemyName) { | |
if (currentMode === MODE_IDLE_ONLY) updateLastRelevantActivityAndStopIdleBeep(`Enemy changed (to ${currentEnemyName})`); | |
} | |
} | |
previousEnemyName = currentEnemyName; | |
}); | |
combatEnemyNameObserver.observe(enemyNameSpan, { characterData: true, childList: true, subtree: true }); | |
} | |
function startIdleChecker() { | |
if (idleCheckIntervalId) { clearInterval(idleCheckIntervalId); } | |
idleCheckIntervalId = setInterval(() => { | |
const currentMode = getSelectedMode(); | |
if (currentMode === MODE_OFF) { if (idleBeepIntervalId) { clearInterval(idleBeepIntervalId); idleBeepIntervalId = null; } return; } | |
let isMonitoringActive = false; | |
if (currentMode === MODE_IDLE_ONLY) { isMonitoringActive = true; } | |
else { | |
const selectedFilterValue = document.getElementById('itemFilterDropdown')?.value; | |
const targetInput = document.getElementById('beepTargetNumberInput'); | |
if (selectedFilterValue && selectedFilterValue !== FILTER_OFF_VALUE && targetInput && targetInput.value.trim() !== '' && !targetInput.disabled) { | |
isMonitoringActive = true; | |
} | |
} | |
if (!isMonitoringActive) { if (idleBeepIntervalId) { clearInterval(idleBeepIntervalId); idleBeepIntervalId = null; } return; } | |
const now = Date.now(); | |
const elapsedSinceRelevantActivity = now - lastRelevantActivityTimestamp; | |
const timeoutMs = idleTimeoutSeconds * 1000; | |
if (elapsedSinceRelevantActivity > timeoutMs) { | |
if (!idleBeepIntervalId) { | |
let idleReason = ""; | |
if (currentMode === MODE_IDLE_ONLY) { idleReason = "Idle Only mode active"; } | |
else { const selectedFilterValue = document.getElementById('itemFilterDropdown')?.value || "N/A"; idleReason = `monitoring for "${selectedFilterValue.replace(/_/g, ' ')}" with a target`; } | |
console.log(`%cMelvor Beep: Idle timeout reached (${idleTimeoutSeconds}s) because no relevant activity detected for ${idleReason}. Starting idle beeps. Last activity: ${new Date(lastRelevantActivityTimestamp).toLocaleTimeString()}`, "color: tomato; font-weight:bold;"); | |
playBeep("Idle Alert (Initial)"); | |
idleBeepIntervalId = setInterval(() => playBeep("Idle Alert (Repeating)"), idleBeepRepeatSeconds * 1000); | |
} | |
} else { | |
if (idleBeepIntervalId) { clearInterval(idleBeepIntervalId); idleBeepIntervalId = null; } | |
} | |
}, 1000); | |
} | |
function startMainObserver() { | |
const targetNode = document.body; | |
const config = { childList: true, subtree: true, characterData: true, characterDataOldValue: false }; | |
const callback = (mutationsList) => { | |
let processedNotificationsInBatch = new Set(); | |
for (const mutation of mutationsList) { | |
let targetElementForProcessing = null; | |
if (mutation.type === 'childList') { | |
if (mutation.target.nodeType === Node.ELEMENT_NODE && mutation.target.matches('game-notification')) targetElementForProcessing = mutation.target; | |
else if (mutation.target.nodeType === Node.ELEMENT_NODE) targetElementForProcessing = mutation.target.closest('game-notification'); | |
if (!targetElementForProcessing) { | |
mutation.addedNodes.forEach(node => { | |
if (node.nodeType === Node.ELEMENT_NODE) { | |
if (node.tagName === 'GAME-NOTIFICATION') targetElementForProcessing = node; | |
else if (node.querySelector) targetElementForProcessing = node.querySelector('game-notification') || targetElementForProcessing; | |
} | |
}); | |
} | |
} else if (mutation.type === 'characterData') { targetElementForProcessing = mutation.target.parentElement ? mutation.target.parentElement.closest('game-notification') : null; } | |
if (targetElementForProcessing && !processedNotificationsInBatch.has(targetElementForProcessing)) { | |
processGameNotification(targetElementForProcessing, false); processedNotificationsInBatch.add(targetElementForProcessing); | |
} | |
} | |
}; | |
const observer = new MutationObserver(callback); | |
observer.observe(targetNode, config); | |
} | |
function initializeScript() { | |
addGlobalStyles(); | |
setupUI(); | |
startMainObserver(); | |
observeEnemyName(); | |
startIdleChecker(); | |
setTimeout(() => { getAudioContext(); }, 1000); | |
} | |
if (document.readyState === 'interactive' || document.readyState === 'complete') { | |
initializeScript(); | |
} else { | |
document.addEventListener('DOMContentLoaded', initializeScript); | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment