EP Hacks

EP Answer Helper + Anti-Snitch + Maths Solver + MCQ + Gaps + Auto-Skip

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         EP Hacks
// @namespace    http://tampermonkey.net/
// @version      6.0
// @description  EP Answer Helper + Anti-Snitch + Maths Solver + MCQ + Gaps + Auto-Skip
// @match        https://app.educationperfect.com/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
  "use strict";

  // ── EXAM BYPASS (silent, always on) ───────────────────────────
  document.addEventListener('contextmenu', e => e.stopImmediatePropagation(), true);
  document.addEventListener('selectstart', e => e.stopImmediatePropagation(), true);
  document.addEventListener('copy',        e => e.stopImmediatePropagation(), true);

  // ── ANTI-SNITCH ───────────────────────────────────────────────
  let antiSnitchActive = true;

  function stopSnitch(e) {
    if (antiSnitchActive) e.stopImmediatePropagation();
  }

  try {
    Object.defineProperty(document, 'visibilityState', { get: () => antiSnitchActive ? 'visible' : 'hidden', configurable: true });
    Object.defineProperty(document, 'hidden',          { get: () => antiSnitchActive ? false : true, configurable: true });
  } catch(e) {}

  document.addEventListener('visibilitychange', stopSnitch, true);
  window.addEventListener('blur',     stopSnitch, true);
  window.addEventListener('focus',    stopSnitch, true);
  window.addEventListener('pagehide', stopSnitch, true);

  setInterval(() => {
    if (!antiSnitchActive) return;
    document.dispatchEvent(new MouseEvent('mousemove', {
      bubbles: true,
      clientX: 200 + Math.random() * 400,
      clientY: 200 + Math.random() * 300,
    }));
  }, 3000);

  // ── AUTO-SKIP ─────────────────────────────────────────────────
  let autoSkipActive = false;

  setInterval(() => {
    try {
      if (!autoSkipActive) return;
      const infoSlide = document.querySelector('.h-group.v-align-center.expanded-content.information.selected');
      if (!infoSlide) return;
      document.querySelectorAll('.continue.arrow.action-bar-button.v-group.ng-isolate-scope button').forEach(btn => btn.click());
    } catch(e) {}
  }, 100);

  // ── HELPERS ───────────────────────────────────────────────────
  function getScope() {
    try {
      if (!window.angular) return null;
      // Try lp-answer-input first (works for all question types incl. exams)
      const lpEl = document.querySelector('.lp-answer-input');
      if (lpEl) return angular.element(lpEl).scope();
      // Fallback to answer-text input
      const input = [...document.querySelectorAll('#answer-text')].find(el => el.tagName === 'INPUT');
      if (input) return angular.element(input).scope();
      return null;
    } catch(e) { return null; }
  }

  function getQuestion() {
    try {
      const scope = getScope();
      return scope?.self?.model?._currentQuestion || null;
    } catch(e) { return null; }
  }

  function parseArr(expr) {
    const match = expr?.match(/\[([^\]]+)\]/);
    if (!match) return null;
    return match[1].split(',').map(s => s.replace(/['"]/g, '').trim());
  }

  function resolveVars(vars, rng) {
    const resolved = { rng };
    vars?.forEach(v => {
      if (v.Name === 'rng') return;
      // Direct value — always resolve regardless of rng
      if (v.value !== null && v.value !== undefined && !v.expression) {
        resolved[v.Name] = String(v.value);
      } else if (v.expression?.includes('[rng]')) {
        const arr = parseArr(v.expression);
        if (arr) resolved[v.Name] = arr[rng];
      }
    });
    return resolved;
  }

  // Check question text for degree symbol requirement
  function needsDegreeSymbol() {
    try {
      const text = document.querySelector('.question-text, [class*="question-body"], .lp-answer-input')?.closest('[class*="question"], [class*="game"]')?.innerText || '';
      return /degree symbol|include.*°|°.*symbol/i.test(text);
    } catch(e) { return false; }
  }

  // ── ANSWER GETTERS ────────────────────────────────────────────

  // 1. Standard language answer
  function getStandardAnswer() {
    try {
      const q = getQuestion();
      if (!q) return null;
      const answer = q.validAnswers?.[0]?.outputString;
      if (!answer) return null;
      return { answer, prompt: q.specifiedDisplayName || null, type: 'standard' };
    } catch(e) { return null; }
  }

  // 2. MCQ answer
  function getMCQAnswer() {
    try {
      const q = getQuestion();
      if (!q || q.questionType !== 6) return null;
      const components = q.questionDef?.Components;
      if (!components?.length) return null;
      // Must have Options with Correct flag AND visible MCQ buttons on screen
      if (!components[0]?.Options?.some(o => o.Correct === 'true' || o.Correct === true)) return null;
      if (!document.querySelector('.mcq-preview-option')) return null;

      const vars = q.questionGeneratedVariables;
      const rng = vars?.find(v => v.Name === 'rng')?.value || 0;
      const resolved = resolveVars(vars, rng);

      const correct = components[0].Options.find(o => o.Correct === 'true');
      if (!correct) return null;

      const tmpl = correct.TextTemplate;
      // Pre-substitute all \var{} in template for use throughout
      const tmplResolved = tmpl.replace(/(?:\\\\|\\)?var\{(\w+)\}/g, (_, k) => resolved[k] !== undefined ? resolved[k] : k);
      const trigFn = tmpl.includes('sin') ? 'sin' : tmpl.includes('cos') ? 'cos' : tmpl.includes('tan') ? 'tan' : null;

      // Extract frac numerator/denominator from resolved template
      const fracParts = tmplResolved.match(/frac\{([^}]+(?:\{[^}]*\}[^}]*)*)\}\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/);
      const numPart = fracParts?.[1] || '';
      const denPart = fracParts?.[2] || '';

      // Check if numerator/denominator contains a plain number (not trig)
      // After resolution, a numeric part will just be a number like "5.9"
      const numPartIsNumber = /^[\d.]+$/.test(numPart.trim()) || (/^[\d.]+$/.test(numPart.replace(/[^0-9.]/g,''))) && !/(sin|cos|tan)/.test(numPart);
      const numPartHasVar = !/(sin|cos|tan)/.test(numPart);
      const denPartHasVar = !/(sin|cos|tan)/.test(denPart);

      // Get numeric values from parts
      const numVarVal = numPart.replace(/[^0-9.]/g,'') || null;
      const denVarVal = denPart.replace(/[^0-9.]/g,'') || null;

      // Extract simple {x}/{y} pattern
      const simpleFrac = tmpl.match(/\{(\w+)\}\/\{(\w+)\}/);
      const numVal = simpleFrac ? resolved[simpleFrac[1]] : null;
      const denVal = simpleFrac ? resolved[simpleFrac[2]] : null;

      // Highlight correct button
      const activeContainer = document.querySelector('.multi-choice-component');
      const buttons = activeContainer
        ? [...activeContainer.querySelectorAll('.mcq-preview-option')]
        : [...document.querySelectorAll('.mcq-preview-option')];

      // Clear all borders first
      document.querySelectorAll('.mcq-preview-option').forEach(btn => {
        btn.style.outline = '';
        btn.style.borderRadius = '';
      });

      let bestBtn = null;
      let bestScore = -1;

      buttons.forEach(btn => {
        const text = btn.textContent.toLowerCase().replace(/\s/g, '');
        let score = 0;

        // Trig function match
        if (trigFn) {
          if (text.includes(trigFn)) score += 5;
          else score -= 10;
        }

        // Number order — check if numeric value appears before or after trig function
        if (trigFn && (numVarVal || denVarVal)) {
          const numericVal = String(numVarVal || denVarVal);
          const fnIdx = text.indexOf(trigFn);
          const numIdx = text.indexOf(numericVal);
          if (fnIdx !== -1 && numIdx !== -1) {
            if (numPartHasVar) {
              // Number should be BEFORE trig fn (numerator)
              if (numIdx < fnIdx) score += 6;
              else score -= 6;
            } else {
              // Number should be AFTER trig fn (denominator)
              if (numIdx > fnIdx) score += 6;
              else score -= 6;
            }
          }
        }

        // Number order for {opp}/{hyp} style
        if (!numVarVal && numVal && denVal) {
          const ni = text.indexOf(String(numVal));
          const di = text.indexOf(String(denVal));
          if (ni !== -1 && di !== -1 && ni < di) score += 5;
        }

        // Text match — substitute all vars into template and match against button
        {
          // Use pre-resolved template
          let resolvedTmpl = tmplResolved.replace(/`/g, '').replace(/['"]/g, '').toLowerCase();
          Object.keys(resolved).forEach(k => {
            resolvedTmpl = resolvedTmpl.split('{' + k + '}').join(String(resolved[k]));
          });
          const cleanTmpl = resolvedTmpl.replace(/\\[a-zA-Z]+/g, '').replace(/[^a-z0-9]/g, '');
          const cleanText = text.replace(/[^a-z0-9]/g, '');
          if (cleanTmpl.length > 2 && cleanText.includes(cleanTmpl)) score += 8;
        }

        if (score > bestScore) { bestScore = score; bestBtn = btn; }
      });

      if (bestBtn && bestScore > 0) {
        bestBtn.style.outline = '3px solid #3fb950';
        bestBtn.style.borderRadius = '6px';
      }

      return {
        answer: 'Select option with green border',
        prompt: trigFn && numVarVal ? `${trigFn}(${numVarVal}/${denVarVal})` : tmpl.replace(/`/g,'').replace(/\\[a-zA-Z]+/g,'').slice(0,40),
        type: 'mcq',
      };
    } catch(e) { return null; }
  }

  // 3. Fill in the gaps
  function getFillGapsAnswer() {
    try {
      const q = getQuestion();
      if (!q) return null;
      const comp = q.questionDef?.Components?.find(c => c.ComponentTypeCode === 'FILL_IN_GAPS_COMPONENT');
      if (!comp?.Gaps?.length) return null;

      const answers = comp.Gaps.map((gap, i) => `Box ${i + 1}: ${gap.CorrectOptions?.[0] || '?'}`);
      return {
        answer: answers.join('  |  '),
        prompt: `Fill in ${comp.Gaps.length} gap${comp.Gaps.length > 1 ? 's' : ''}`,
        type: 'gaps',
      };
    } catch(e) { return null; }
  }

  // 4. Maths (generated variables)
  function getMathsAnswer() {
    try {
      const q = getQuestion();
      if (!q || q.questionType !== 6) return null;
      const vars = q.questionGeneratedVariables;
      if (!vars?.length) return null;
      // Skip if there are visible MCQ buttons (real MCQ question)
      if (document.querySelector('.mcq-preview-option')) return null;

      const rng = vars.find(v => v.Name === 'rng')?.value || 0;
      const resolved = {};
      vars.forEach(v => {
        if (v.Name === 'rng') { resolved.rng = rng; return; }
        if (v.value !== null && v.value !== undefined && !v.expression) {
          resolved[v.Name] = v.value;
        } else if (v.expression?.includes('[rng]')) {
          const arr = parseArr(v.expression);
          if (arr) resolved[v.Name] = parseFloat(arr[rng]);
        }
      });

      const answerVar = vars.find(v => v.Name === 'answer' || v.Name === 'ans' || v.Name === 'rans');
      if (!answerVar?.expression) return null;

      let expr = answerVar.expression
        .replace(/^fix\((.+),(\d+)\)$/, 'parseFloat(($1).toFixed($2))')
        .replace(/\bpi\b/g,    'Math.PI')
        .replace(/\bcos\b/g,   'Math.cos')
        .replace(/\bsin\b/g,   'Math.sin')
        .replace(/\btan\b/g,   'Math.tan')
        .replace(/\bsqrt\b/g,  'Math.sqrt')
        .replace(/\babs\b/g,   'Math.abs')
        .replace(/\bround\b/g, 'Math.round')
        .replace(/(?<!Math\.)\basin\b/g,  'Math.asin')
        .replace(/(?<!Math\.)\bacos\b/g,  'Math.acos')
        .replace(/(?<!Math\.)\batan\b/g,  'Math.atan');

      // Sort keys longest first to avoid partial replacements
      Object.keys(resolved).sort((a, b) => b.length - a.length).forEach(k => {
        expr = expr.replaceAll(k, String(resolved[k]));
      });

      const answer = eval(expr);
      if (isNaN(answer) || !isFinite(answer)) return null;

      // Check if degrees symbol needed
      const deg = needsDegreeSymbol() ? '°' : '';

      return {
        answer: String(answer) + deg,
        prompt: `🧮 ${answerVar.expression}`,
        type: 'maths',
      };
    } catch(e) { return null; }
  }

  function getAnswer() {
    return getStandardAnswer() || getMCQAnswer() || getFillGapsAnswer() || getMathsAnswer();
  }

  // ── BUILD UI ──────────────────────────────────────────────────
  function buildUI() {
    document.getElementById('ep-hacks-panel')?.remove();
    document.getElementById('ep-hacks-reopen')?.remove();

    const reopenBtn = document.createElement('div');
    reopenBtn.id = 'ep-hacks-reopen';
    reopenBtn.style.cssText = `
      position:fixed;top:16px;right:16px;z-index:999999;
      width:32px;height:32px;border-radius:50%;
      background:#58a6ff;color:#0f1117;
      font-size:18px;font-weight:700;
      display:none;align-items:center;justify-content:center;
      cursor:pointer;box-shadow:0 4px 12px rgba(0,0,0,0.4);
    `;
    reopenBtn.textContent = '⚡';
    document.body.appendChild(reopenBtn);

    const panel = document.createElement('div');
    panel.id = 'ep-hacks-panel';
    panel.style.cssText = `
      position:fixed;top:16px;right:16px;z-index:999999;
      background:#0f1117;color:#e2e8f0;
      border:1px solid #30363d;border-radius:12px;
      padding:16px 20px;font-family:'Fira Code',monospace;
      font-size:13px;min-width:240px;
      box-shadow:0 8px 32px rgba(0,0,0,0.6);
    `;
    panel.innerHTML = `
      <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;">
        <span style="font-size:14px;font-weight:700;color:#58a6ff;">⚡ EP Hacks</span>
        <button id="ep-hacks-close" style="background:none;border:none;color:#8b949e;cursor:pointer;font-size:18px;padding:0;line-height:1;">×</button>
      </div>

      <div style="margin-bottom:14px;">
        <div style="font-size:10px;color:#8b949e;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:4px;">Answer</div>
        <div id="ep-hacks-prompt" style="font-size:11px;color:#484f58;margin-bottom:4px;">—</div>
        <div id="ep-hacks-answer" style="font-size:22px;font-weight:700;color:#3fb950;min-height:28px;letter-spacing:0.02em;cursor:pointer;" title="Click to copy">—</div>
        <div id="ep-hacks-type" style="font-size:10px;color:#484f58;margin-top:4px;"></div>
      </div>

      <div style="height:1px;background:#21262d;margin-bottom:14px;"></div>

      <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
        <div>
          <div style="font-size:12px;font-weight:600;color:#e2e8f0;">Anti-Snitch</div>
          <div style="font-size:10px;color:#8b949e;">Hides tab switching</div>
        </div>
        <div id="ep-snitch-toggle" style="width:40px;height:22px;border-radius:11px;background:#238636;cursor:pointer;position:relative;transition:background 0.2s;">
          <div id="ep-snitch-knob" style="position:absolute;top:3px;left:21px;width:16px;height:16px;border-radius:50%;background:#fff;transition:left 0.2s;"></div>
        </div>
      </div>

      <div style="display:flex;justify-content:space-between;align-items:center;">
        <div>
          <div style="font-size:12px;font-weight:600;color:#e2e8f0;">Auto-Skip Info</div>
          <div style="font-size:10px;color:#8b949e;">Skips learning slides</div>
        </div>
        <div id="ep-skip-toggle" style="width:40px;height:22px;border-radius:11px;background:#484f58;cursor:pointer;position:relative;transition:background 0.2s;">
          <div id="ep-skip-knob" style="position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#fff;transition:left 0.2s;"></div>
        </div>
      </div>
    `;
    document.body.appendChild(panel);

    // Close / reopen
    document.getElementById('ep-hacks-close').addEventListener('click', () => {
      panel.style.display = 'none';
      reopenBtn.style.display = 'flex';
    });
    reopenBtn.addEventListener('click', () => {
      panel.style.display = '';
      reopenBtn.style.display = 'none';
    });

    // Anti-snitch toggle
    let snitchOn = true;
    document.getElementById('ep-snitch-toggle').addEventListener('click', () => {
      snitchOn = !snitchOn;
      antiSnitchActive = snitchOn;
      document.getElementById('ep-snitch-toggle').style.background = snitchOn ? '#238636' : '#484f58';
      document.getElementById('ep-snitch-knob').style.left = snitchOn ? '21px' : '3px';
    });

    // Auto-skip toggle
    document.getElementById('ep-skip-toggle').addEventListener('click', () => {
      autoSkipActive = !autoSkipActive;
      document.getElementById('ep-skip-toggle').style.background = autoSkipActive ? '#238636' : '#484f58';
      document.getElementById('ep-skip-knob').style.left = autoSkipActive ? '21px' : '3px';
    });
  }

  // ── ANSWER LOOP ───────────────────────────────────────────────
  function startAnswerLoop() {
    let lastAnswer = null;

    setInterval(() => {
      const data = getAnswer();
      if (!data?.answer || data.answer === lastAnswer) return;
      lastAnswer = data.answer;

      const promptEl = document.getElementById('ep-hacks-prompt');
      const answerEl = document.getElementById('ep-hacks-answer');
      const typeEl   = document.getElementById('ep-hacks-type');

      if (promptEl) promptEl.textContent = data.prompt || '—';

      if (answerEl) {
        if (data.type === 'gaps') {
          answerEl.innerHTML = data.answer.split('  |  ')
            .map(a => `<div style="font-size:15px;margin-bottom:4px;">${a}</div>`)
            .join('');
        } else {
          answerEl.textContent = data.answer;
        }

        // Auto-copy for non-MCQ, non-gaps
        if (data.type !== 'mcq' && data.type !== 'gaps') {
          navigator.clipboard.writeText(data.answer).catch(() => {});
        }

        // Click to copy
        answerEl.onclick = () => {
          const text = data.type === 'gaps'
            ? data.answer.replace(/  \|  /g, ', ')
            : data.answer;
          navigator.clipboard.writeText(text).then(() => {
            const orig = answerEl.style.color;
            answerEl.style.color = '#58a6ff';
            setTimeout(() => answerEl.style.color = orig, 500);
          }).catch(() => {});
        };
      }

      if (typeEl) typeEl.textContent =
        data.type === 'maths'  ? '🧮 Maths' :
        data.type === 'mcq'    ? '🔵 Multiple Choice' :
        data.type === 'gaps'   ? '✏️ Fill in Gaps' :
        '📝 Language';

    }, 500);
  }

  // ── INIT ──────────────────────────────────────────────────────
  // Wait for Angular to be ready
  const init = setInterval(() => {
    if (!window.angular) return;
    const lpEl = document.querySelector('.lp-answer-input');
    const input = [...document.querySelectorAll('#answer-text')].find(el => el.tagName === 'INPUT');
    if (!lpEl && !input) return;
    clearInterval(init);
    buildUI();
    startAnswerLoop();
  }, 300);

})();