EP Hacks

EP Answer Helper + Anti-Snitch + Maths Solver + Box selection

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

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

(I already have a user script manager, let me install it!)

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.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         EP Hacks
// @namespace    http://tampermonkey.net/
// @version      5.0
// @description  EP Answer Helper + Anti-Snitch + Maths Solver + Box selection
// @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 ───────────────────────────────────────────────
  // Single permanent listener controlled by a flag — no stacking issues
  let antiSnitchActive = true;

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

  function enableAntiSnitch() {
    antiSnitchActive = true;
  }

  function disableAntiSnitch() {
    antiSnitchActive = false;
  }

  // Register listeners once at startup — flag controls whether they fire
  try {
    Object.defineProperty(document, 'visibilityState', { get: () => antiSnitchActive ? 'visible' : document.visibilityState, configurable: true });
    Object.defineProperty(document, 'hidden', { get: () => antiSnitchActive ? false : document.hidden, 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);

  // ── ANSWER READER ─────────────────────────────────────────────
  function getInput() {
    return [...document.querySelectorAll('#answer-text')].find(el => el.tagName === 'INPUT');
  }

  function getScope() {
    const input = getInput();
    if (!input || !window.angular) return null;
    return angular.element(input).scope();
  }

  // Standard answer (language questions)
  function getStandardAnswer() {
    try {
      const lpEl = document.querySelector('.lp-answer-input');
      const scope = lpEl && window.angular ? angular.element(lpEl).scope() : getScope();
      const q = scope?.self?.model?._currentQuestion;
      if (!q) return null;
      const answer = q.validAnswers?.[0]?.outputString;
      const prompt = q.specifiedDisplayName || null;
      if (!answer) return null;
      return { answer, prompt, type: 'standard' };
    } catch(e) { return null; }
  }

  // Maths answer (questionType 6 with generated variables)
  function getMathsAnswer() {
    try {
      // Use .lp-answer-input scope which works for all question formats
      const lpEl = document.querySelector('.lp-answer-input');
      if (!lpEl || !window.angular) return null;
      const scope = angular.element(lpEl).scope();
      const q = scope?.self?.model?._currentQuestion;
      if (!q || q.questionType !== 6) return null;
      const vars = q.questionGeneratedVariables;
      if (!vars || vars.length === 0) return null;

      const rng = vars.find(v => v.Name === 'rng')?.value || 0;

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

      // Resolve all variables — handle both direct values and array expressions
      const resolved = { rng };
      vars.forEach(v => {
        if (v.Name === 'rng') return;
        if (v.value !== null && v.value !== undefined && !v.expression) {
          resolved[v.Name] = v.value;
        } else if (v.expression && v.expression.includes('[rng]')) {
          const arr = parseArr(v.expression);
          if (arr) resolved[v.Name] = parseFloat(arr[rng]);
        }
      });

      // Get answer expression — EP uses 'answer' or 'ans'
      const answerVar = vars.find(v => v.Name === 'answer' || v.Name === 'ans');
      if (!answerVar?.expression) return null;

      // Convert EP expression to JS
      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');

      // Substitute resolved values
      Object.keys(resolved).forEach(k => {
        expr = expr.replaceAll(k, String(resolved[k]));
      });

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

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

  // MCQ answer (questionType 6 with Components)
  function getMCQAnswer() {
    try {
      const lpEl = document.querySelector('.lp-answer-input');
      if (!lpEl || !window.angular) return null;
      const scope = angular.element(lpEl).scope();
      const q = scope?.self?.model?._currentQuestion;
      if (!q || q.questionType !== 6) return null;
      const components = q.questionDef?.Components;
      if (!components?.length) return null;

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

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

      // Resolve variables
      const resolved = { rng };
      vars?.forEach(v => {
        if (v.Name === 'rng') return;
        if (v.value !== null && v.value !== undefined && !v.expression) {
          resolved[v.Name] = String(v.value);
        } else if (v.expression && v.expression.includes('[rng]')) {
          const arr = parseArr(v.expression);
          if (arr) resolved[v.Name] = arr[rng];
        }
      });

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

      const tmpl = correct.TextTemplate;

      // Substitute \var{name} with resolved values
      let resolvedTmpl = tmpl.replace(/\\var\{(\w+)\}/g, (_, k) => resolved[k] || k);

      // Extract key features from template for matching
      const fn = tmpl.includes('sin') ? 'sin' : tmpl.includes('cos') ? 'cos' : tmpl.includes('tan') ? 'tan' : null;

      // Check if length/number is numerator or denominator
      // \frac{\var{length}}{...} = length on top, \frac{...}{\var{length}} = length on bottom
      const fracMatch = tmpl.match(/\\frac\{([^}]+)\}\{([^}]+)\}/);
      const numPart = fracMatch?.[1] || '';
      const denPart = fracMatch?.[2] || '';
      const lengthOnTop = numPart.includes('\\var{length}') || numPart.includes('\\var{adj}') || numPart.includes('\\var{opp}') || numPart.includes('\\var{hyp}');

      // For simple {opp}/{hyp} style templates
      const simpleFrac = tmpl.match(/\{(\w+)\}\/\{(\w+)\}/);
      const numKey = simpleFrac?.[1];
      const denKey = simpleFrac?.[2];
      const numVal = resolved[numKey];
      const denVal = resolved[denKey];

      // Clean template for fallback matching
      const cleanTmpl = resolvedTmpl.replace(/`/g, '').replace(/['"]/g, '').replace(/\\[a-zA-Z]+/g, '').toLowerCase().replace(/\s/g, '').replace(/[^a-z0-9]/g, '');

      // Highlight correct button with green border
      const buttons = document.querySelectorAll('.mcq-preview-option');
      buttons.forEach(btn => { btn.style.outline = ''; btn.style.borderRadius = ''; });

      // Scope buttons to active question container only
      const activeContainer = document.querySelector('.multi-choice-component');
      const scopedButtons = activeContainer
        ? [...activeContainer.querySelectorAll('.mcq-preview-option')]
        : [...buttons];

      // Extract trig function, numerator var, denominator var from template
      const trigFn = tmpl.includes('sin') ? 'sin' : tmpl.includes('cos') ? 'cos' : tmpl.includes('tan') ? 'tan' : null;
      const fracVarMatch = tmpl.match(/\\frac\{\\var\{(\w+)\}\}\{\\var\{(\w+)\}\}/);
      const numVarKey = fracVarMatch?.[1];
      const denVarKey = fracVarMatch?.[2];
      const numVarVal = resolved[numVarKey];
      const denVarVal = resolved[denVarKey];

      // Score each button
      let bestBtn = null;
      let bestScore = -1;

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

        // Must have right trig function
        if (trigFn && text.includes(trigFn)) score += 5;
        else if (trigFn) score -= 10; // wrong function, disqualify

        // Check numerator/denominator order
        if (numVarVal && denVarVal) {
          const ni = text.indexOf(String(numVarVal));
          const di = text.indexOf(String(denVarVal));
          if (ni !== -1 && di !== -1 && ni < di) score += 5;
          else if (ni !== -1 && di !== -1) score -= 5;
        }

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

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

      // Clear all then highlight best
      scopedButtons.forEach(btn => { btn.style.outline = ''; btn.style.borderRadius = ''; });
      if (bestBtn && bestScore > 0) {
        bestBtn.style.outline = '3px solid #3fb950';
        bestBtn.style.borderRadius = '6px';
      }

      // Build display prompt using resolved template
      const displayPrompt = resolvedTmpl
        .replace(/`/g, '').replace(/['"]/g, '')
        .replace(/\\[a-zA-Z]+/g, '').trim() || tmpl.slice(0, 40);

      return {
        answer: 'Select option with green border',
        prompt: displayPrompt,
        type: 'mcq',
      };
    } catch(e) { return null; }
  }

  // Fill in the gaps answer (FILL_IN_GAPS_COMPONENT)
  function getFillGapsAnswer() {
    try {
      const lpEl = document.querySelector('.lp-answer-input');
      if (!lpEl || !window.angular) return null;
      const scope = angular.element(lpEl).scope();
      const q = scope?.self?.model?._currentQuestion;
      if (!q) return null;
      const components = q.questionDef?.Components;
      if (!components?.length) return null;
      const comp = components.find(c => c.ComponentTypeCode === 'FILL_IN_GAPS_COMPONENT');
      if (!comp?.Gaps?.length) return null;

      // Build answer string from all gaps
      const answers = comp.Gaps.map((gap, i) => {
        const correct = gap.CorrectOptions?.[0] || '?';
        return `Box ${i + 1}: ${correct}`;
      });

      return {
        answer: answers.join('  |  '),
        prompt: `Fill in ${comp.Gaps.length} gap${comp.Gaps.length > 1 ? 's' : ''}`,
        type: 'gaps',
      };
    } catch(e) { return null; }
  }

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

  // ── TYPE ANSWER INTO INPUT ────────────────────────────────────
  async function typeAnswer(text) {
    const input = getInput();
    if (!input) return false;
    input.focus();
    input.click();
    for (const char of text) {
      await new Promise(r => setTimeout(r, 60 + Math.random() * 80));
      input.dispatchEvent(new KeyboardEvent('keydown', { key: char, keyCode: char.charCodeAt(0), bubbles: true }));
      input.dispatchEvent(new KeyboardEvent('keypress', { key: char, keyCode: char.charCodeAt(0), bubbles: true }));
      const start = input.selectionStart || 0;
      input.value = input.value.slice(0, start) + char + input.value.slice(start);
      input.selectionStart = input.selectionEnd = start + 1;
      input.dispatchEvent(new InputEvent('input', { data: char, inputType: 'insertText', bubbles: true }));
      input.dispatchEvent(new KeyboardEvent('keyup', { key: char, keyCode: char.charCodeAt(0), bubbles: true }));
    }
    return true;
  }

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

    const reopenBtn = document.createElement('div');
    reopenBtn.id = 'ep-helper-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-helper-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 Helper</span>
        <button id="ep-helper-close" style="
          background:none;border:none;color:#8b949e;
          cursor:pointer;font-size:18px;padding:0;line-height:1;">×</button>
      </div>

      <!-- ANSWER -->
      <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-helper-prompt" style="font-size:11px;color:#484f58;margin-bottom:4px;">—</div>
        <div id="ep-helper-answer" style="
          font-size:22px;font-weight:700;color:#3fb950;
          min-height:28px;letter-spacing:0.02em;">—</div>
        <div id="ep-helper-type" style="font-size:10px;color:#484f58;margin-top:4px;"></div>
      </div>

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

      <!-- ANTI-SNITCH -->
      <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 from EP</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>

      <!-- AUTO-SKIP -->
      <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 instantly</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);

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

    reopenBtn.addEventListener('click', () => {
      panel.style.display = '';
      reopenBtn.style.display = 'none';
    });

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

    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';
    });
  }

  // ── SLIDE TIMER BYPASS ───────────────────────────────────────
  let autoSkipActive = false;

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

  // ── 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-helper-prompt');
      const answerEl = document.getElementById('ep-helper-answer');
      const typeEl = document.getElementById('ep-helper-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 answer to clipboard
        if (data.type !== 'mcq' && data.type !== 'gaps') {
          navigator.clipboard.writeText(data.answer).catch(() => {});
        }
        // Click to copy manually too
        answerEl.title = 'Click to copy';
        answerEl.style.cursor = 'pointer';
        answerEl.onclick = () => {
          navigator.clipboard.writeText(data.answer).then(() => {
            const orig = answerEl.style.color;
            answerEl.style.color = '#58a6ff';
            setTimeout(() => answerEl.style.color = orig, 600);
          }).catch(() => {});
        };
      }
      if (typeEl) typeEl.textContent =
        data.type === 'maths'  ? '🧮 Maths (auto-solved)' :
        data.type === 'mcq'    ? '🔵 Multiple Choice' :
        data.type === 'gaps'   ? '✏️ Fill in the Gaps' :
        '📝 Language';


    }, 500);
  }

  // ── INIT ──────────────────────────────────────────────────────
  const init = setInterval(() => {
    const input = getInput();
    if (input && window.angular) {
      clearInterval(init);
      buildUI();
      startAnswerLoop();
    }
  }, 500);

})();