HP Zero Alarm

Alert when HP reaches zero in Milky Way Idle combat

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         HP Zero Alarm
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Alert when HP reaches zero in Milky Way Idle combat
// @match        https://www.milkywayidle.com/game*
// @license      MIT
// @grant        none
// ==/UserScript==

(() => {
  "use strict";

  // ============================================================================
  // CONFIGURATION - Change these values to customize behavior
  // ============================================================================

  const CONFIG = {
    hpBarSelector: '[class*="HitpointsBar_hpValue"]', // Matches any class containing this
    checkIntervalMs: 500, // How often to check HP (milliseconds)
    beepFrequencyHz: 380, // Beep pitch (880 = A5 musical note)
    beepDurationSec: 3, // How long each beep lasts
    beepIntervalSec: 1, // Time between continuous beeps
    lowHPThreshold: 1, // Alert when HP drops below this value
  };

  // ============================================================================
  // STATE - Variables that track what's happening
  // ============================================================================

  let wasLowHPOnLastCheck = false; // Prevents beeping multiple times
  let audioContext = null; // Web Audio API context (created lazily)
  let statusBox = null; // The visual indicator box
  let beepInterval = null; // Interval for continuous beeping
  let isMuted = false; // Whether sound is muted

  // ============================================================================
  // VISUAL INDICATOR - Shows what the script is detecting
  // ============================================================================

  function createStatusBox() {
    statusBox = document.createElement("div");

    // Position it in top-right corner, above everything else
    statusBox.style.cssText = `
      position: fixed;
      top: 10px;
      right: 10px;
      padding: 10px 15px;
      background: #333;
      color: white;
      border-radius: 5px;
      font-family: monospace;
      font-size: 14px;
      z-index: 99999;
      box-shadow: 0 2px 10px rgba(0,0,0,0.3);
      cursor: move;
      user-select: none;
    `;

    statusBox.innerHTML = `
      <div style="margin-bottom: 5px; display: flex; justify-content: space-between; align-items: center;">
        <span style="font-weight: bold;">🎯 HP Zero Alarm</span>
        <button id="mute-button" style="
          background: #555;
          border: none;
          color: white;
          padding: 3px 8px;
          border-radius: 3px;
          cursor: pointer;
          font-size: 12px;
          margin-left: 10px;
        ">🔊 Mute</button>
      </div>
      <div id="battle-status" style="margin-bottom: 3px;">
        Battle Tab: <span style="color: #ff4444;">❌ Not Active</span>
      </div>
      <div id="hp-status">
        HP Value: <span style="color: #ff4444;">❌ Not Found</span>
      </div>
    `;

    document.body.appendChild(statusBox);

    // Make it draggable
    makeDraggable(statusBox);

    // Setup mute button
    const muteButton = statusBox.querySelector("#mute-button");
    muteButton.addEventListener("click", (e) => {
      e.stopPropagation(); // Prevent dragging when clicking button
      toggleMute();
    });
  }

  function toggleMute() {
    isMuted = !isMuted;
    const muteButton = statusBox.querySelector("#mute-button");

    if (isMuted) {
      muteButton.textContent = "🔇 Unmute";
      muteButton.style.background = "#aa4444";

      // Stop any currently playing beeps
      if (beepInterval) {
        clearInterval(beepInterval);
        beepInterval = null;
      }
    } else {
      muteButton.textContent = "🔊 Mute";
      muteButton.style.background = "#555";
    }
  }

  function makeDraggable(element) {
    let isDragging = false;
    let offsetX = 0;
    let offsetY = 0;

    element.addEventListener("mousedown", (e) => {
      isDragging = true;
      offsetX = e.clientX - element.getBoundingClientRect().left;
      offsetY = e.clientY - element.getBoundingClientRect().top;
      element.style.cursor = "grabbing";
    });

    document.addEventListener("mousemove", (e) => {
      if (!isDragging) return;

      const x = e.clientX - offsetX;
      const y = e.clientY - offsetY;

      element.style.left = x + "px";
      element.style.top = y + "px";
      element.style.right = "auto"; // Remove right positioning
    });

    document.addEventListener("mouseup", () => {
      if (isDragging) {
        isDragging = false;
        element.style.cursor = "move";
      }
    });
  }

  function updateStatusBox(isBattleActive, currentHP) {
    // Create the box if it doesn't exist yet
    if (!statusBox) {
      createStatusBox();
    }

    const battleStatusText = statusBox.querySelector("#battle-status span");
    const hpStatusText = statusBox.querySelector("#hp-status span");

    // Show if we're on a battle tab
    if (isBattleActive) {
      battleStatusText.innerHTML =
        '✅ <span style="color: #44ff44;">Active</span>';
      statusBox.style.background = "#1a4d1a"; // Dark green background
    } else {
      battleStatusText.innerHTML =
        '❌ <span style="color: #ff4444;">Not Active</span>';
      statusBox.style.background = "#333"; // Gray background
    }

    // Show current HP value with color coding
    if (currentHP !== null) {
      const hpColor = getHPColor(currentHP);
      hpStatusText.innerHTML = `✅ <span style="color: ${hpColor}; font-weight: bold;">${currentHP} HP</span>`;
    } else {
      hpStatusText.innerHTML =
        '❌ <span style="color: #ff4444;">Not Found</span>';
    }
  }

  function getHPColor(hp) {
    if (hp === 0) return "#ff4444"; // Red = dead
    if (hp < CONFIG.lowHPThreshold) return "#ffaa44"; // Orange = low
    return "#44ff44"; // Green = healthy
  }

  function flashStatusBoxRed() {
    if (!statusBox) return;

    statusBox.style.background = "#cc0000"; // Bright red
    statusBox.style.animation = "pulse 0.5s ease-in-out infinite";
  }

  function stopFlashingStatusBox() {
    if (!statusBox) return;

    statusBox.style.background = "#1a4d1a"; // Back to green
    statusBox.style.animation = "";
  }

  // ============================================================================
  // BATTLE DETECTION - Check if we're on a battle tab
  // ============================================================================

  function isOnBattleTab() {
    // Find the currently selected tab
    const activeTab = document.querySelector(
      'button[role="tab"][aria-selected="true"]'
    );

    if (!activeTab) {
      return false; // No tab is selected
    }

    // Check if it's a battle tab (contains "Battle #52" or similar)
    return activeTab.textContent.includes("Battle #");
  }

  // ============================================================================
  // HP DETECTION - Read the current HP value from the page
  // ============================================================================

  function readCurrentHP() {
    // Find all combat units on the page
    const combatUnits = document.querySelectorAll(
      '[class*="CombatUnit_combatUnit"]'
    );

    if (combatUnits.length === 0) {
      return null; // No combat units found
    }

    // In solo play or when you're first, use the first unit
    // In group play, try to find YOUR unit (has your character name)
    let targetUnit = combatUnits[0]; // Default to first unit

    // Try to find your unit by checking if it's the leftmost/first in battle layout
    // The game typically shows your character first
    for (const unit of combatUnits) {
      const hpBar = unit.querySelector(CONFIG.hpBarSelector);
      if (hpBar) {
        targetUnit = unit;
        break; // Use the first one with an HP bar (usually yours)
      }
    }

    // Now find the HP display element within the target unit
    const hpElement = targetUnit.querySelector(CONFIG.hpBarSelector);

    if (!hpElement) {
      return null; // HP element not found
    }

    const displayText = hpElement.textContent.trim();

    // Extract the first number from "1613/1962" format
    const numberMatch = displayText.match(/\d+/);

    if (!numberMatch) {
      return null; // No number found in the text
    }

    return parseInt(numberMatch[0], 10);
  }

  // ============================================================================
  // AUDIO ALERT - Play a beep sound
  // ============================================================================

  function playBeep() {
    // Don't play if muted
    if (isMuted) {
      return;
    }

    // Create audio context on first use (some browsers require user interaction)
    if (!audioContext) {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
    }

    // Resume if browser suspended it (autoplay policy)
    if (audioContext.state === "suspended") {
      audioContext.resume();
    }

    // Create the beep sound
    const oscillator = audioContext.createOscillator();
    const volume = audioContext.createGain();

    oscillator.frequency.value = CONFIG.beepFrequencyHz;
    oscillator.connect(volume);
    volume.connect(audioContext.destination);

    // Play and fade out to avoid clicking sound at the end
    oscillator.start();
    volume.gain.exponentialRampToValueAtTime(
      0.0001,
      audioContext.currentTime + CONFIG.beepDurationSec
    );
    oscillator.stop(audioContext.currentTime + CONFIG.beepDurationSec);
  }

  // ============================================================================
  // MAIN LOGIC - The heart of the script
  // ============================================================================

  function checkHPAndAlert() {
    const isBattleActive = isOnBattleTab();
    const currentHP = readCurrentHP();

    // Always update the visual indicator
    updateStatusBox(isBattleActive, currentHP);

    // Don't do anything if we're not in battle or can't read HP
    if (!isBattleActive || currentHP === null) {
      return;
    }

    // ALARM! HP dropped below threshold
    if (currentHP < CONFIG.lowHPThreshold && !wasLowHPOnLastCheck) {
      console.warn(
        `⚠️ HP BELOW ${CONFIG.lowHPThreshold}! Current: ${currentHP}`
      );
      playBeep();
      flashStatusBoxRed();

      // Start continuous beeping
      beepInterval = setInterval(() => {
        playBeep();
      }, CONFIG.beepIntervalSec * 1000);

      wasLowHPOnLastCheck = true; // Don't restart beeping until HP recovers
    }

    // HP recovered - ready to beep again next time it drops
    if (currentHP >= CONFIG.lowHPThreshold && wasLowHPOnLastCheck) {
      stopFlashingStatusBox();

      // Stop continuous beeping
      if (beepInterval) {
        clearInterval(beepInterval);
        beepInterval = null;
      }

      wasLowHPOnLastCheck = false;
    }
  }

  // ============================================================================
  // STARTUP - Initialize and start monitoring
  // ============================================================================

  function startScript() {
    // Add CSS for the pulsing animation
    const style = document.createElement("style");
    style.textContent = `
      @keyframes pulse {
        0%, 100% { background: #cc0000; }
        50% { background: #ff3333; }
      }
    `;
    document.head.appendChild(style);

    // Create the status indicator
    createStatusBox();

    // Start checking HP repeatedly
    setInterval(checkHPAndAlert, CONFIG.checkIntervalMs);

    console.log(
      `✅ HP Zero Alarm started - checking every ${CONFIG.checkIntervalMs}ms`
    );
  }

  // Wait 1 second for page to load, then start
  setTimeout(startScript, 1000);
})();