Skip to content

Instantly share code, notes, and snippets.

@fidel-perez
Last active May 19, 2025 10:06
Show Gist options
  • Save fidel-perez/e84baad66d0f8c51e5a9d69f7ccaffb5 to your computer and use it in GitHub Desktop.
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.
// ==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