EP Hacks

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         EP Hacks
// @namespace    http://tampermonkey.net/
// @version      7.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();
  }

  // Spoof visibility and focus APIs
  function applyAntiSnitchSpoof() {
    try {
      Object.defineProperty(document, 'visibilityState', { get: () => antiSnitchActive ? 'visible' : 'hidden', configurable: true });
      Object.defineProperty(document, 'hidden',          { get: () => antiSnitchActive ? false : true, configurable: true });
      // Fix 9: patch hasFocus too
      const origHasFocus = document.hasFocus.bind(document);
      document.hasFocus = () => antiSnitchActive ? true : origHasFocus();
    } catch(e) {}
  }
  applyAntiSnitchSpoof();

  // Register all event interceptors once
  document.addEventListener('visibilitychange',       stopSnitch, true);
  window.addEventListener('blur',                     stopSnitch, true);
  window.addEventListener('focus',                    stopSnitch, true);
  window.addEventListener('pagehide',                 stopSnitch, true);
  document.addEventListener('fullscreenchange',       stopSnitch, true);
  document.addEventListener('webkitfullscreenchange', stopSnitch, true);

  // Fake mouse movement to keep activity tracker happy
  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;

  // Fix 11: broader info slide detection
  setInterval(() => {
    try {
      if (!autoSkipActive) return;
      // Try specific selector first, then broader fallback
      const infoSlide =
        document.querySelector('.h-group.v-align-center.expanded-content.information.selected') ||
        document.querySelector('[class*="information"][class*="selected"]') ||
        document.querySelector('[class*="info-slide"][class*="active"]');
      if (!infoSlide) return;
      document.querySelectorAll('.continue.arrow.action-bar-button.v-group.ng-isolate-scope button, [class*="continue"][class*="action"] button').forEach(btn => btn.click());
    } catch(e) {}
  }, 100);

  // ── HELPERS ───────────────────────────────────────────────────

  // Fix 1: Angular fallback + Fix 14: broader scope search
  function getScope() {
    try {
      if (!window.angular) {
        console.warn('[EP Hacks] Angular not found');
        return null;
      }
      const selectors = ['.lp-answer-input', '[class*="answer-input"]', '[class*="lp-answer"]'];
      for (const sel of selectors) {
        const el = document.querySelector(sel);
        if (el) {
          const scope = angular.element(el).scope();
          if (scope?.self?.model) return scope;
        }
      }
      // Fallback to answer-text input
      const input = [...document.querySelectorAll('#answer-text, [id*="answer"]')].find(el => el.tagName === 'INPUT');
      if (input) return angular.element(input).scope();
      return null;
    } catch(e) { console.warn('[EP Hacks] getScope error:', 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;
      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;
  }

  function needsDegreeSymbol() {
    try {
      const pageText = document.body?.innerText || '';
      return /degree symbol|include.*°|°.*symbol/i.test(pageText);
    } 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) { console.warn('[EP Hacks] Standard answer error:', 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;
      if (!components[0]?.Options?.some(o => o.Correct === 'true' || o.Correct === true)) return null;
      // Fix 15: check visible buttons not just presence
      const visibleBtns = [...document.querySelectorAll('.mcq-preview-option')].filter(b => b.offsetParent !== null);
      if (!visibleBtns.length) 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' || o.Correct === true);
      if (!correct) return null;

      const tmpl = correct.TextTemplate;
      // Substitute \var{name} with resolved values
      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 parts from resolved template
      const fracParts = tmplResolved.match(/frac\{([^}]+(?:\{[^}]*\}[^}]*)*)\}\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/);
      const numPart = fracParts?.[1] || '';
      const denPart = fracParts?.[2] || '';
      const numPartHasVar = !/(sin|cos|tan)/.test(numPart);
      const numVarVal = numPart.replace(/[^0-9.]/g, '') || null;
      const denVarVal = denPart.replace(/[^0-9.]/g, '') || null;

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

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

      // Clear all borders
      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 position relative to 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 && numIdx < fnIdx) score += 6;
            else if (numPartHasVar && numIdx > fnIdx) score -= 6;
            else if (!numPartHasVar && numIdx > fnIdx) score += 6;
            else if (!numPartHasVar && numIdx < fnIdx) score -= 6;
          }
        }

        // Simple opp/hyp order
        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 vars and compare
        {
          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';
      }

      const displayPrompt = tmplResolved.replace(/`/g, '').replace(/['"]/g, '').replace(/\\[a-zA-Z]+/g, '').trim().slice(0, 40);
      return { answer: 'Select option with green border', prompt: displayPrompt, type: 'mcq' };
    } catch(e) { console.warn('[EP Hacks] MCQ error:', 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) { console.warn('[EP Hacks] Gaps error:', 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 visible MCQ buttons on screen
      const visibleBtns = [...document.querySelectorAll('.mcq-preview-option')].filter(b => b.offsetParent !== null);
      if (visibleBtns.length) 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 => ['answer', 'ans', 'rans'].includes(v.Name));
      if (!answerVar?.expression) return null;

      // Fix 7: global fix() replacement
      let expr = answerVar.expression
        .replace(/fix\(([^,]+),(\d+)\)/g, '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\.)asin\b/g, 'Math.asin')
        .replace(/(?<!Math\.)acos\b/g, 'Math.acos')
        .replace(/(?<!Math\.)atan\b/g, 'Math.atan');

      // Fix 5: word boundary replacement
      Object.keys(resolved).sort((a, b) => b.length - a.length).forEach(k => {
        expr = expr.replace(new RegExp('\\b' + k + '\\b', 'g'), String(resolved[k]));
      });

      // Fix 6: debug eval errors
      let answer;
      try {
        answer = eval(expr);
      } catch(e) {
        console.warn('[EP Hacks] Maths eval error:', expr, e);
        return null;
      }

      if (isNaN(answer) || !isFinite(answer)) return null;
      const deg = needsDegreeSymbol() ? '°' : '';
      return { answer: String(answer) + deg, prompt: `🧮 ${answerVar.expression}`, type: 'maths' };
    } catch(e) { console.warn('[EP Hacks] Maths error:', 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;margin-bottom:10px;">
        <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>
      <div style="height:1px;background:#21262d;margin-bottom:10px;"></div>
      <button id="ep-exit-fullscreen" style="
        width:100%;padding:7px;background:#21262d;color:#8b949e;
        border:1px solid #30363d;border-radius:6px;cursor:pointer;
        font-size:11px;font-family:'Fira Code',monospace;text-align:left;">
        ⛶ Enter Fullscreen
      </button>
    `;
    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
    document.getElementById('ep-snitch-toggle').addEventListener('click', () => {
      antiSnitchActive = !antiSnitchActive;
      document.getElementById('ep-snitch-toggle').style.background = antiSnitchActive ? '#238636' : '#484f58';
      document.getElementById('ep-snitch-knob').style.left = antiSnitchActive ? '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';
    });

    // Fullscreen toggle
    const fsBtn = document.getElementById('ep-exit-fullscreen');
    let reallyFullscreen = false;
    fsBtn.addEventListener('click', () => {
      if (reallyFullscreen) {
        document.exitFullscreen().catch(() => {});
        reallyFullscreen = false;
        fsBtn.textContent = '⛶ Enter Fullscreen';
      } else {
        document.documentElement.requestFullscreen().catch(() => {});
        reallyFullscreen = true;
        fsBtn.textContent = '⛶ Exit Fullscreen';
      }
    });
  }

  // ── 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;
        }
        if (data.type !== 'mcq' && data.type !== 'gaps') {
          navigator.clipboard.writeText(data.answer).catch(() => {});
        }
        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 — Fix 8: MutationObserver instead of weak polling ────
  let uiBuilt = false;

  function tryInit() {
    if (uiBuilt) return;
    if (!window.angular) return;
    const lpEl = document.querySelector('.lp-answer-input, [class*="lp-answer"]');
    const input = [...document.querySelectorAll('#answer-text, [id*="answer-text"]')].find(el => el.tagName === 'INPUT');
    if (!lpEl && !input) return;
    uiBuilt = true;
    buildUI();
    startAnswerLoop();
  }

  // Fix 13: MutationObserver watches for EP's SPA navigation
  const observer = new MutationObserver(() => {
    tryInit();
    // Rebuild UI if panel was removed by EP's re-render
    if (uiBuilt && !document.getElementById('ep-hacks-panel') && !document.getElementById('ep-hacks-reopen')) {
      uiBuilt = false;
      tryInit();
    }
  });

  observer.observe(document.documentElement, { childList: true, subtree: true });

  // Also keep a fallback interval in case MutationObserver misses early load
  const initInterval = setInterval(() => {
    tryInit();
    if (uiBuilt) clearInterval(initInterval);
  }, 300);

})();