Torn Egg Helper Compact

Compact manual egg detector for pages you manually visit

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Torn Egg Helper Compact
// @namespace    jez.torn.egg.compact
// @version      3.0
// @description  Compact manual egg detector for pages you manually visit
// @match        https://www.torn.com/*
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const STORAGE_KEY = 'jez_torn_egg_compact_v3';
  const PANEL_ID = 'jez-egg-compact-panel';
  let lastUrl = '';
  let observerStarted = false;
  let checkTimer = null;
  let collapsed = true;

  function loadData() {
    try {
      const raw = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
      return {
        checked: raw.checked || {},
        found: raw.found || {}
      };
    } catch (e) {
      return { checked: {}, found: {} };
    }
  }

  function saveData(data) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
  }

  function getPageKey() {
    const u = new URL(window.location.href);
    return u.pathname + u.search;
  }

  function isVisible(el) {
    if (!el) return false;
    const s = getComputedStyle(el);
    if (s.display === 'none' || s.visibility === 'hidden' || s.opacity === '0') return false;
    const r = el.getBoundingClientRect();
    return r.width > 0 && r.height > 0;
  }

  function looksLikeEggText(str) {
    const s = String(str || '').toLowerCase();
    return s.includes('egg') || s.includes('easter') || s.includes('hunt');
  }

  function detectEggCandidates() {
    const nodes = document.querySelectorAll('img, div, span, a, button, svg, [class], [id], [title], [aria-label], [alt]');
    const candidates = [];

    nodes.forEach(el => {
      const parts = [
        el.id || '',
        typeof el.className === 'string' ? el.className : '',
        el.getAttribute?.('title') || '',
        el.getAttribute?.('aria-label') || '',
        el.getAttribute?.('alt') || '',
        el.getAttribute?.('src') || '',
        el.textContent ? el.textContent.trim().slice(0, 60) : ''
      ];

      const joined = parts.join(' | ');
      if (looksLikeEggText(joined) && isVisible(el)) {
        candidates.push({ el, text: joined });
      }
    });

    return candidates;
  }

  function detectEgg() {
    const candidates = detectEggCandidates();
    return {
      found: candidates.length > 0,
      candidates
    };
  }

  function flash(msg, good) {
    const old = document.getElementById('jez-egg-flash');
    if (old) old.remove();

    const n = document.createElement('div');
    n.id = 'jez-egg-flash';
    n.textContent = msg;
    n.style.position = 'fixed';
    n.style.top = '10px';
    n.style.right = '10px';
    n.style.zIndex = '99999999';
    n.style.padding = '8px 10px';
    n.style.borderRadius = '8px';
    n.style.fontSize = '12px';
    n.style.fontWeight = '700';
    n.style.color = '#fff';
    n.style.background = good ? '#1f8b24' : '#444';
    n.style.boxShadow = '0 4px 12px rgba(0,0,0,0.35)';
    document.body.appendChild(n);
    setTimeout(() => n.remove(), 1800);
  }

  function removePanel() {
    const old = document.getElementById(PANEL_ID);
    if (old) old.remove();
  }

  function highlightCandidate(el) {
    try {
      el.style.outline = '3px solid lime';
      el.style.outlineOffset = '2px';
      el.scrollIntoView({ behavior: 'smooth', block: 'center' });
    } catch (e) {}
  }

  function makeButton(text, onClick) {
    const btn = document.createElement('button');
    btn.textContent = text;
    btn.style.border = 'none';
    btn.style.borderRadius = '6px';
    btn.style.padding = '5px 7px';
    btn.style.fontSize = '11px';
    btn.style.fontWeight = '700';
    btn.style.cursor = 'pointer';
    btn.style.marginRight = '6px';
    btn.style.marginTop = '6px';
    btn.onclick = onClick;
    return btn;
  }

  function drawPanel(result) {
    removePanel();

    const data = loadData();
    const pageKey = getPageKey();

    const panel = document.createElement('div');
    panel.id = PANEL_ID;
    panel.style.position = 'fixed';
    panel.style.right = '8px';
    panel.style.bottom = '8px';
    panel.style.zIndex = '99999998';
    panel.style.background = 'rgba(0,0,0,0.92)';
    panel.style.color = '#fff';
    panel.style.border = '1px solid rgba(255,255,255,0.14)';
    panel.style.borderRadius = '10px';
    panel.style.boxShadow = '0 6px 16px rgba(0,0,0,0.35)';
    panel.style.fontSize = '11px';
    panel.style.lineHeight = '1.25';
    panel.style.width = collapsed ? '160px' : '260px';
    panel.style.maxHeight = collapsed ? 'unset' : '40vh';
    panel.style.overflow = 'auto';
    panel.style.padding = '8px';

    const titleRow = document.createElement('div');
    titleRow.style.display = 'flex';
    titleRow.style.justifyContent = 'space-between';
    titleRow.style.alignItems = 'center';

    const title = document.createElement('div');
    title.textContent = 'Egg Helper';
    title.style.fontWeight = '800';
    title.style.fontSize = '12px';
    titleRow.appendChild(title);

    const toggle = document.createElement('button');
    toggle.textContent = collapsed ? '+' : '–';
    toggle.style.border = 'none';
    toggle.style.borderRadius = '5px';
    toggle.style.width = '22px';
    toggle.style.height = '22px';
    toggle.style.cursor = 'pointer';
    toggle.onclick = () => {
      collapsed = !collapsed;
      drawPanel(result);
    };
    titleRow.appendChild(toggle);

    panel.appendChild(titleRow);

    const status = document.createElement('div');
    status.style.marginTop = '6px';
    status.style.fontWeight = '700';
    status.style.color = result.found ? '#7dff7d' : '#ddd';
    status.textContent = result.found ? 'Egg: possible match' : 'Egg: none seen';
    panel.appendChild(status);

    const stats = document.createElement('div');
    stats.style.marginTop = '4px';
    stats.textContent = `Checked ${Object.keys(data.checked).length} | Flagged ${Object.keys(data.found).length}`;
    panel.appendChild(stats);

    if (!collapsed) {
      const page = document.createElement('div');
      page.style.marginTop = '6px';
      page.style.wordBreak = 'break-word';
      page.textContent = pageKey;
      panel.appendChild(page);

      const btnRow = document.createElement('div');
      btnRow.appendChild(makeButton('Recheck', () => runCheck(true)));
      btnRow.appendChild(makeButton('Clear', () => {
        localStorage.removeItem(STORAGE_KEY);
        flash('Log cleared', true);
        runCheck(true);
      }));
      btnRow.appendChild(makeButton('Copy', async () => {
        const txt = Object.keys(loadData().found).join('\n') || 'No flagged pages yet.';
        try {
          await navigator.clipboard.writeText(txt);
          flash('Copied', true);
        } catch (e) {
          flash('Copy failed', false);
        }
      }));
      if (result.candidates[0]) {
        btnRow.appendChild(makeButton('Jump', () => highlightCandidate(result.candidates[0].el)));
      }
      panel.appendChild(btnRow);

      const found = Object.keys(data.found).reverse().slice(0, 8);
      const header = document.createElement('div');
      header.style.marginTop = '8px';
      header.style.fontWeight = '800';
      header.textContent = 'Flagged pages';
      panel.appendChild(header);

      if (!found.length) {
        const none = document.createElement('div');
        none.style.opacity = '0.8';
        none.style.marginTop = '4px';
        none.textContent = 'None yet';
        panel.appendChild(none);
      } else {
        found.forEach(p => {
          const row = document.createElement('div');
          row.style.marginTop = '3px';
          row.style.wordBreak = 'break-word';
          row.textContent = p;
          panel.appendChild(row);
        });
      }
    }

    document.body.appendChild(panel);
  }

  function runCheck(showFlash) {
    const data = loadData();
    const pageKey = getPageKey();
    const result = detectEgg();

    data.checked[pageKey] = { at: new Date().toISOString() };

    if (result.found) {
      data.found[pageKey] = { at: new Date().toISOString() };
    } else {
      delete data.found[pageKey];
    }

    saveData(data);
    drawPanel(result);

    if (showFlash) {
      flash(result.found ? 'Possible egg found' : 'Checked page', result.found);
    }
  }

  function scheduleCheck(showFlash) {
    clearTimeout(checkTimer);
    checkTimer = setTimeout(() => runCheck(showFlash), 500);
  }

  function startObserver() {
    if (observerStarted) return;
    observerStarted = true;

    const obs = new MutationObserver(() => {
      scheduleCheck(false);
    });

    obs.observe(document.documentElement || document.body, {
      childList: true,
      subtree: true,
      attributes: true
    });
  }

  function boot() {
    lastUrl = location.href;
    runCheck(true);
    startObserver();

    setInterval(() => {
      if (location.href !== lastUrl) {
        lastUrl = location.href;
        scheduleCheck(true);
      }
    }, 1500);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', boot);
  } else {
    boot();
  }
})();