EP Answer Helper + Anti-Snitch + Maths Solver + Box selection
// ==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);
})();