HP Zero Alarm

Alert when HP reaches zero in Milky Way Idle combat

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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);
})();