Skip to content

Instantly share code, notes, and snippets.

@Domiii
Last active January 21, 2026 15:03
Show Gist options
  • Select an option

  • Save Domiii/52cf49d780ec8c9f01771973c36197af to your computer and use it in GitHub Desktop.

Select an option

Save Domiii/52cf49d780ec8c9f01771973c36197af to your computer and use it in GitHub Desktop.
This script types for you automatically on www.typingclub.com: 1. Open the website 2. Blaze past the tutorials 3. Go into a level 4. Open Console 5. Paste the script and press ENTER
/**
* This script types for you automatically on www.typingclub.com:
* 1. Open the website
* 2. Blaze past the tutorials
* 3. Go into a level
* 4. Open Console
* 5. Paste the script and press ENTER
*/
// NOTE: When delay (in ms between two strokes) is too low, the site might bug out and the result page will not be shown
const minDelay = 60;
const maxDelay = 60;
const keyOverrides = {
[String.fromCharCode(160)]: ' ' // convert hardspace to normal space
};
function getTargetCharacters() {
const els = Array.from(document.querySelectorAll('.token span.token_unit'));
const chrs = els
.map(el => {
// get letter to type from each letter DOM element
if (el.firstChild?.classList?.contains('_enter')) {
// special case: ENTER
return '\n';
}
let text = el.textContent[0];
return text;
})
.map(c => keyOverrides.hasOwnProperty(c) ? keyOverrides[c] : c); // convert special characters
return chrs;
}
function recordKey(chr) {
// send it straight to the internal API
window.core.record_keydown_time(chr);
}
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
async function autoPlay(finish) {
const chrs = getTargetCharacters();
for (let i = 0; i < chrs.length - (!finish); ++i) {
const c = chrs[i];
recordKey(c);
//console.log(c, c.charCodeAt());
await sleep(Math.random() * (maxDelay - minDelay) + minDelay);
}
}
// ############################################################################################################
// old utilities
// ############################################################################################################
// /**
// * @see https://stackoverflow.com/questions/8942678/keyboardevent-in-chrome-keycode-is-0/12522752#12522752
// */
// function simulateKey(chr, el) {
// _simulateKey(chr, 'keydown', el);
// _simulateKey(chr, 'keypress', el);
// }
// function _simulateKey(chr, type, el) {
// var eventObj = document.createEventObject ?
// document.createEventObject() : document.createEvent("Events");
// if (eventObj.initEvent) {
// eventObj.initEvent(type || "keydown", true, true);
// }
// let keyCode = chr.charCodeAt(0);
// eventObj.key = chr[0];
// eventObj.keyCode = keyCode;
// eventObj.which = keyCode;
// eventObj.isTrusted = true;
// el = el || document.body;
// // console.log(keyCode, eventObj);
// el.dispatchEvent ? el.dispatchEvent(eventObj) : el.fireEvent("onkeydown", eventObj);
// }
// document.addEventListener("keydown", function (e) {
// console.log('down', e);
// });
// document.addEventListener("keypress", function (e) {
// console.log('press', e);
// });
//$($('.menu-btn')[0].parentNode).prepend('<button onclick=\'simulateKeyPress("c")\'>sim</button>');
// simulateKey('a', $('input')[0]);
// ############################################################################################################
// go!
// ############################################################################################################
autoPlay(true);
@ethanmccc
Copy link

someone pls make a new code

@DonMarmolejo727
Copy link

¿Alguien ha encontrado la solución? Creo que ya no funciona, probablemente necesitemos hacer otro código

https://github.com/DonMarmolejo727/TypingClub-Extractor.git

Hello brother, try using this code that was developed by a great team of programmers.

@DonMarmolejo727
Copy link

Que alguien cree un código nuevo, por favor

Alright bro, check out my repository :)

@ThunderCoding456
Copy link

ThunderCoding456 commented Dec 6, 2025 via email

@Anonimus1231234
Copy link

someone pls make a new code

Yeah we need one, pls someone make one

@zane2342
Copy link

zane2342 commented Dec 7, 2025 via email

@nayfujii
Copy link

nayfujii commented Dec 7, 2025

Hey I was using your code and it stopped working is there anyway you could help fix or update it?
here’s a screenshot of what I’m seeing
Image 12-7-25 at 13 11

@zane2342
Copy link

zane2342 commented Dec 7, 2025 via email

@zane2342
Copy link

zane2342 commented Dec 7, 2025 via email

@Anonimus1231234
Copy link

Anonimus1231234 commented Dec 11, 2025

Did you or someone find the solution??? Im trying to fix it but this is a little more difficult and I can't find a solution

@zane2342
Copy link

zane2342 commented Dec 12, 2025 via email

@Anonimus1231234
Copy link

Yeah np just asking C:

@zane2342
Copy link

zane2342 commented Dec 12, 2025 via email

@zane2342
Copy link

zane2342 commented Dec 12, 2025 via email

@DeadHnd
Copy link

DeadHnd commented Dec 15, 2025

maybe cuz its not your code? Wowie.

@CuteDogJay
Copy link

CuteDogJay commented Dec 15, 2025 via email

@zane2342
Copy link

zane2342 commented Dec 15, 2025 via email

@hello212231
Copy link

can any one share the new code pls

@zane2342
Copy link

zane2342 commented Dec 16, 2025 via email

@comoxyOG
Copy link

// Typing Club Automator - AUTO START
(function() {
'use strict';

if (window.typingClubBot) {
    console.log('Bot already running! Close it first with: window.typingClubBot.close()');
    return;
}

console.log('🚀 Loading Typing Club Bot...');

const gui = document.createElement('div');
gui.id = 'typing-bot-gui';
gui.style.cssText = `
    position: fixed;
    top: 20px;
    right: 20px;
    width: 340px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    border-radius: 15px;
    box-shadow: 0 10px 40px rgba(0,0,0,0.5);
    z-index: 2147483647;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    color: white;
`;

gui.innerHTML = `
    <div style="padding: 15px 20px; border-bottom: 1px solid rgba(255,255,255,0.2); display: flex; justify-content: space-between; align-items: center; cursor: move;" id="bot-header">
        <div style="font-weight: 700; font-size: 16px;">🎯 Typing Club Bot</div>
        <button id="bot-close" style="background: rgba(255,255,255,0.2); border: none; color: white; width: 28px; height: 28px; border-radius: 50%; cursor: pointer; font-size: 18px;">×</button>
    </div>
    
    <div style="padding: 20px;">
        <div style="background: rgba(255,255,255,0.1); padding: 10px; border-radius: 8px; font-size: 11px; margin-bottom: 10px;">
            <strong style="display: block; margin-bottom: 5px;">📍 Detected:</strong>
            <span id="level-info">Waiting...</span>
        </div>
        
        <div style="margin-bottom: 18px;">
            <label style="display: block; font-size: 12px; font-weight: 600; margin-bottom: 8px; text-transform: uppercase;">Speed</label>
            <div style="background: rgba(255,255,255,0.15); padding: 12px; border-radius: 10px;">
                <input type="range" id="speed-slider" min="30" max="190" value="70" style="width: 100%; margin: 8px 0; cursor: pointer;">
                <div style="text-align: center; font-size: 20px; font-weight: 700; margin-top: 5px;"><span id="speed-value">70</span> WPM</div>
            </div>
        </div>
        
        <div style="margin-bottom: 18px;">
            <label style="display: block; font-size: 12px; font-weight: 600; margin-bottom: 8px; text-transform: uppercase;">Accuracy</label>
            <div style="background: rgba(255,255,255,0.15); padding: 12px; border-radius: 10px;">
                <input type="range" id="accuracy-slider" min="92" max="100" value="97" style="width: 100%; margin: 8px 0; cursor: pointer;">
                <div style="text-align: center; font-size: 20px; font-weight: 700; margin-top: 5px;"><span id="accuracy-value">97</span>%</div>
            </div>
        </div>
        
        <div style="display: flex; align-items: center; gap: 10px; background: rgba(255,255,255,0.15); padding: 12px; border-radius: 10px; margin-bottom: 10px;">
            <input type="checkbox" id="auto-advance" checked style="width: 20px; height: 20px; cursor: pointer;">
            <label for="auto-advance" style="cursor: pointer; flex: 1; font-size: 14px;">Auto-advance</label>
        </div>
        
        <button id="start-btn" style="width: 100%; padding: 12px; border: none; border-radius: 10px; font-size: 14px; font-weight: 700; cursor: pointer; text-transform: uppercase; background: #10ac84; color: white; margin-bottom: 10px;">▶ Start Bot</button>
        <button id="stop-btn" disabled style="width: 100%; padding: 12px; border: none; border-radius: 10px; font-size: 14px; font-weight: 700; cursor: pointer; text-transform: uppercase; background: #ff6b6b; color: white; margin-bottom: 10px; opacity: 0.5;">⏹ Stop</button>
        
        <div id="status" style="background: rgba(255,255,255,0.15); padding: 12px; border-radius: 10px; text-align: center; font-size: 13px; font-weight: 600; margin-top: 15px;">Ready</div>
        
        <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 15px;">
            <div style="background: rgba(255,255,255,0.15); padding: 10px 8px; border-radius: 8px; text-align: center;">
                <div style="font-size: 18px; font-weight: 700;" id="chars-typed">0</div>
                <div style="font-size: 9px; opacity: 0.8; margin-top: 3px;">CHARS</div>
            </div>
            <div style="background: rgba(255,255,255,0.15); padding: 10px 8px; border-radius: 8px; text-align: center;">
                <div style="font-size: 18px; font-weight: 700;" id="levels-completed">0</div>
                <div style="font-size: 9px; opacity: 0.8; margin-top: 3px;">LEVELS</div>
            </div>
        </div>
    </div>
`;

document.body.appendChild(gui);

const speedSlider = document.getElementById('speed-slider');
const speedValue = document.getElementById('speed-value');
const accuracySlider = document.getElementById('accuracy-slider');
const accuracyValue = document.getElementById('accuracy-value');
const autoAdvance = document.getElementById('auto-advance');
const startBtn = document.getElementById('start-btn');
const stopBtn = document.getElementById('stop-btn');
const status = document.getElementById('status');
const levelInfo = document.getElementById('level-info');
const charsTypedEl = document.getElementById('chars-typed');
const levelsCompletedEl = document.getElementById('levels-completed');
const closeBtn = document.getElementById('bot-close');
const header = document.getElementById('bot-header');

let botRunning = false;
let charsTypedCount = 0;
let levelsCompleted = 0;

speedSlider.addEventListener('input', (e) => speedValue.textContent = e.target.value);
accuracySlider.addEventListener('input', (e) => accuracyValue.textContent = e.target.value);

// Draggable
let isDragging = false;
let currentX, currentY, initialX, initialY;

header.addEventListener('mousedown', (e) => {
    initialX = e.clientX - gui.offsetLeft;
    initialY = e.clientY - gui.offsetTop;
    isDragging = true;
});

document.addEventListener('mousemove', (e) => {
    if (isDragging) {
        e.preventDefault();
        currentX = e.clientX - initialX;
        currentY = e.clientY - initialY;
        gui.style.left = currentX + 'px';
        gui.style.top = currentY + 'px';
        gui.style.right = 'auto';
    }
});

document.addEventListener('mouseup', () => isDragging = false);

function detectLevel() {
    // Try to find typing text
    const typable = document.querySelector('div.typable');
    if (typable && typable.textContent) {
        const text = typable.textContent.trim().replace(/\s+/g, ' ');
        levelInfo.textContent = `${text.length} chars detected`;
        return { text, type: 'typing' };
    }
    
    // Check for games - look for game container or canvas
    const gameContainer = document.querySelector('.game-container, .game-canvas, canvas, #game, [class*="game"]');
    if (gameContainer) {
        levelInfo.textContent = 'Game detected';
        return { text: null, type: 'game' };
    }
    
    return { text: null, type: null };
}

function typeChar(char, field) {
    const keyCode = char.charCodeAt(0);
    let code = 'Unidentified';
    let shiftKey = false;
    
    // Map special characters properly
    const specialChars = {
        ' ': { code: 'Space', keyCode: 32 },
        '"': { code: 'Quote', keyCode: 222, shiftKey: true },
        "'": { code: 'Quote', keyCode: 222, shiftKey: false },
        ',': { code: 'Comma', keyCode: 188 },
        '.': { code: 'Period', keyCode: 190 },
        '!': { code: 'Digit1', keyCode: 49, shiftKey: true },
        '?': { code: 'Slash', keyCode: 191, shiftKey: true },
        ':': { code: 'Semicolon', keyCode: 186, shiftKey: true },
        ';': { code: 'Semicolon', keyCode: 186 },
        '-': { code: 'Minus', keyCode: 189 },
        '(': { code: 'Digit9', keyCode: 57, shiftKey: true },
        ')': { code: 'Digit0', keyCode: 48, shiftKey: true }
    };
    
    if (specialChars[char]) {
        code = specialChars[char].code;
        shiftKey = specialChars[char].shiftKey || false;
    } else if (/[a-z]/.test(char)) {
        code = `Key${char.toUpperCase()}`;
        shiftKey = false;
    } else if (/[A-Z]/.test(char)) {
        code = `Key${char}`;
        shiftKey = true;
    } else if (/[0-9]/.test(char)) {
        code = `Digit${char}`;
        shiftKey = false;
    }
    
    const opts = {
        key: char,
        code: code,
        keyCode: keyCode,
        which: keyCode,
        shiftKey: shiftKey,
        bubbles: true,
        cancelable: true,
        view: window
    };
    
    field.dispatchEvent(new KeyboardEvent('keydown', opts));
    field.dispatchEvent(new KeyboardEvent('keypress', opts));
    field.value += char;
    field.dispatchEvent(new Event('input', { bubbles: true }));
    field.dispatchEvent(new Event('change', { bubbles: true }));
    field.dispatchEvent(new KeyboardEvent('keyup', opts));
}

async function processLevel() {
    if (!botRunning) return;
    
    const level = detectLevel();
    
    if (!level.text && level.type !== 'game') {
        status.textContent = '⚠️ No text or game found';
        console.log('❌ Could not find text or game');
        stopBot();
        return;
    }
    
    // Handle games
    if (level.type === 'game') {
        console.log('🎮 Game detected, auto-playing...');
        status.textContent = '🎮 Playing game...';
        
        // Wait for game to load
        await new Promise(r => setTimeout(r, 2000));
        
        // Spam keys that games commonly use
        const gameKeys = ['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Enter', 'KeyW', 'KeyA', 'KeyS', 'KeyD'];
        
        for (let i = 0; i < 50; i++) {
            if (!botRunning) return;
            
            // Press random game key
            const randomKey = gameKeys[Math.floor(Math.random() * gameKeys.length)];
            const keyOpts = {
                key: randomKey.includes('Arrow') || randomKey.includes('Space') ? randomKey.replace('Arrow', '').replace('Key', '') : randomKey.replace('Key', '').toLowerCase(),
                code: randomKey,
                keyCode: randomKey === 'Space' ? 32 : randomKey.includes('Arrow') ? (randomKey === 'ArrowUp' ? 38 : randomKey === 'ArrowDown' ? 40 : randomKey === 'ArrowLeft' ? 37 : 39) : randomKey.charCodeAt(3),
                bubbles: true,
                cancelable: true,
                view: window
            };
            
            document.dispatchEvent(new KeyboardEvent('keydown', keyOpts));
            document.dispatchEvent(new KeyboardEvent('keyup', keyOpts));
            
            // Also click randomly on screen
            if (i % 5 === 0) {
                const x = Math.random() * window.innerWidth;
                const y = Math.random() * window.innerHeight;
                const el = document.elementFromPoint(x, y);
                if (el && !el.closest('#typing-bot-gui')) {
                    el.click();
                }
            }
            
            await new Promise(r => setTimeout(r, 100));
        }
        
        console.log('🎮 Game sequence complete');
        status.textContent = '✅ Game complete!';
        
        if (autoAdvance.checked) {
            await new Promise(r => setTimeout(r, 6000));
            
            // Click to advance
            document.body.click();
            const clickables = document.querySelectorAll('button, .btn, [role="button"]');
            clickables.forEach(el => {
                if (el.offsetParent !== null && !el.closest('#typing-bot-gui')) {
                    el.click();
                }
            });
            
            await new Promise(r => setTimeout(r, 1000));
            if (botRunning) processLevel();
        } else {
            stopBot();
        }
        return;
    }
    
    // Handle typing (existing code)
    const text = level.text;
    console.log('📝 Text:', text);
    status.textContent = '👆 Starting lesson...';
    
    // Try multiple ways to trigger the lesson
    // Method 1: Click the typing area
    const typingArea = document.querySelector('.tpmodes, .typable, .inview');
    if (typingArea) {
        console.log('Clicking typing area...');
        typingArea.click();
    }
    
    // Method 2: Click anywhere on body
    document.body.click();
    
    // Method 3: Press a key to trigger
    document.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', keyCode: 32, bubbles: true }));
    
    await new Promise(r => setTimeout(r, 500));
    
    // Find the input field (might take a moment to appear)
    let input = null;
    for (let attempt = 0; attempt < 30; attempt++) {
        const allInputs = document.querySelectorAll('input[type="text"]');
        console.log(`Attempt ${attempt}: Found ${allInputs.length} inputs`);
        
        for (const inp of allInputs) {
            console.log('  Input:', {
                visible: inp.offsetParent !== null,
                ariaHidden: inp.getAttribute('aria-hidden'),
                style: inp.style.cssText
            });
            
            // Accept ANY text input, even if hidden or aria-hidden
            if (inp.type === 'text') {
                input = inp;
                console.log('✅ Using this input!');
                break;
            }
        }
        
        if (input) break;
        await new Promise(r => setTimeout(r, 100));
    }
    
    if (!input) {
        status.textContent = '⚠️ Input not found';
        console.log('❌ No input field found at all');
        stopBot();
        return;
    }
    
    console.log('🚀 Typing...');
    input.value = '';
    input.focus();
    await new Promise(r => setTimeout(r, 100));
    
    const wpm = parseInt(speedSlider.value);
    const accuracy = parseInt(accuracySlider.value);
    const baseDelay = 60000 / (wpm * 5);
    
    status.textContent = `⌨️ Typing...`;
    
    for (let i = 0; i < text.length; i++) {
        if (!botRunning) return;
        
        const char = text[i];
        const shouldError = Math.random() * 100 > accuracy;
        
        if (shouldError && /[a-z]/i.test(char)) {
            const wrong = String.fromCharCode(char.charCodeAt(0) + (Math.random() > 0.5 ? 1 : -1));
            typeChar(wrong, input);
            await new Promise(r => setTimeout(r, baseDelay * 0.5));
            input.value = input.value.slice(0, -1);
            await new Promise(r => setTimeout(r, baseDelay * 0.3));
        }
        
        typeChar(char, input);
        
        charsTypedCount++;
        charsTypedEl.textContent = charsTypedCount;
        
        let delay = baseDelay * (0.8 + Math.random() * 0.4);
        if (['.', '!', '?'].includes(char)) delay *= 2;
        else if (char === ' ') delay *= 1.3;
        
        await new Promise(r => setTimeout(r, delay));
    }
    
    console.log('✅ Complete!');
    levelsCompleted++;
    levelsCompletedEl.textContent = levelsCompleted;
    status.textContent = '✅ Complete! Waiting...';
    
    if (autoAdvance.checked) {
        // Wait 6 seconds for level to finish processing
        await new Promise(r => setTimeout(r, 6000));
        
        // ONE TIME: Spam Enter on everything
        console.log('Pressing Enter to advance...');
        const enterOpts = {
            key: 'Enter',
            code: 'Enter',
            keyCode: 13,
            which: 13,
            bubbles: true,
            cancelable: true,
            view: window
        };
        
        // Fire on document, input, and body
        document.dispatchEvent(new KeyboardEvent('keydown', enterOpts));
        document.dispatchEvent(new KeyboardEvent('keypress', enterOpts));
        document.dispatchEvent(new KeyboardEvent('keyup', enterOpts));
        
        if (input) {
            input.dispatchEvent(new KeyboardEvent('keydown', enterOpts));
            input.dispatchEvent(new KeyboardEvent('keypress', enterOpts));
            input.dispatchEvent(new KeyboardEvent('keyup', enterOpts));
        }
        
        document.body.dispatchEvent(new KeyboardEvent('keydown', enterOpts));
        document.body.dispatchEvent(new KeyboardEvent('keypress', enterOpts));
        document.body.dispatchEvent(new KeyboardEvent('keyup', enterOpts));
        
        // ONE TIME: Click everything
        console.log('Clicking to advance...');
        document.body.click();
        
        // Click center of screen
        const centerX = window.innerWidth / 2;
        const centerY = window.innerHeight / 2;
        const centerEl = document.elementFromPoint(centerX, centerY);
        if (centerEl && !centerEl.closest('#typing-bot-gui')) {
            centerEl.click();
        }
        
        // Click all buttons
        const clickables = document.querySelectorAll('button, .btn, [role="button"], div[onclick], a');
        clickables.forEach(el => {
            if (el.offsetParent !== null && !el.closest('#typing-bot-gui')) {
                el.click();
            }
        });
        
        // Wait then restart
        await new Promise(r => setTimeout(r, 1000));
        if (botRunning) processLevel();
    } else {
        stopBot();
    }
}

function startBot() {
    botRunning = true;
    startBtn.disabled = true;
    stopBtn.disabled = false;
    stopBtn.style.opacity = '1';
    console.log('🤖 Bot started');
    processLevel();
}

function stopBot() {
    botRunning = false;
    startBtn.disabled = false;
    stopBtn.disabled = true;
    stopBtn.style.opacity = '0.5';
    status.textContent = '⏹ Stopped';
}

function closeBot() {
    stopBot();
    gui.remove();
    window.typingClubBot = null;
}

startBtn.addEventListener('click', startBot);
stopBtn.addEventListener('click', stopBot);
closeBtn.addEventListener('click', closeBtn);

window.typingClubBot = { start: startBot, stop: stopBot, close: closeBot };

setTimeout(detectLevel, 500);
console.log('✅ Bot loaded! Just click Start Bot.');

})();

@zane2342
Copy link

zane2342 commented Dec 20, 2025 via email

@theteknojunkie456
Copy link

god bless you guys man THNAK YOU

@comoxyOG
Copy link

comoxyOG commented Dec 30, 2025 via email

@zane2342
Copy link

zane2342 commented Dec 31, 2025 via email

@hello212231
Copy link

this might sound dum but what do i copy and paste from that code? , because i copied all of it but i didn't work

@comoxyOG
Copy link

comoxyOG commented Jan 8, 2026 via email

@hello212231
Copy link

Yea i know that but when i paste it in the console it dosen't work.

@comoxyOG
Copy link

comoxyOG commented Jan 9, 2026 via email

@Lionsbest9
Copy link

/*

  • Typing Club Automator - AUTO START

  • Refined by: Lionsbest9

  • Idea Developed by: Edman
    */
    (function() {
    'use strict';

    if (window.typingClubBot) {
    console.log('Bot already running! Close it first with: window.typingClubBot.close()');
    return;
    }

    console.log('🚀 Loading Typing Club Bot v2.0 (Developed by Blaise Adelan, Idea by Yanis)...');

    const gui = document.createElement('div');
    gui.id = 'typing-bot-gui';
    gui.style.cssText = position: fixed; top: 20px; right: 20px; width: 380px; /* Slightly wider */ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; box-shadow: 0 10px 40px rgba(0,0,0,0.5); z-index: 2147483647; font-family: 'Segoe UI', Arial, sans-serif; /* More common system fonts */ color: white; transition: width 0.2s ease-in-out, height 0.2s ease-in-out; overflow: hidden; /* Hide content when minimized */;

    gui.innerHTML = `


    🎯 Typing Club Bot



    ×

     <div id="bot-content" style="padding: 20px;">
         <div style="background: rgba(255,255,255,0.1); padding: 10px; border-radius: 8px; font-size: 11px; margin-bottom: 10px;">
             <strong style="display: block; margin-bottom: 5px;">📍 Detected:</strong>
             <span id="level-info">Waiting...</span>
         </div>
    
         <div style="margin-bottom: 18px;">
             <label style="display: block; font-size: 12px; font-weight: 600; margin-bottom: 8px; text-transform: uppercase;">Speed</label>
             <div style="background: rgba(255,255,255,0.15); padding: 12px; border-radius: 10px;">
                 <input type="range" id="speed-slider" min="30" max="250" value="70" style="width: 100%; margin: 8px 0; cursor: pointer;">
                 <div style="text-align: center; font-size: 20px; font-weight: 700; margin-top: 5px;"><span id="speed-value">70</span> WPM</div>
             </div>
         </div>
    
         <div style="margin-bottom: 18px;">
             <label style="display: block; font-size: 12px; font-weight: 600; margin-bottom: 8px; text-transform: uppercase;">Accuracy</label>
             <div style="background: rgba(255,255,255,0.15); padding: 12px; border-radius: 10px;">
                 <input type="range" id="accuracy-slider" min="90" max="100" value="97" style="width: 100%; margin: 8px 0; cursor: pointer;">
                 <div style="text-align: center; font-size: 20px; font-weight: 700; margin-top: 5px;"><span id="accuracy-value">97</span>%</div>
             </div>
         </div>
    
         <div style="display: flex; align-items: center; gap: 10px; background: rgba(255,255,255,0.15); padding: 12px; border-radius: 10px; margin-bottom: 10px;">
             <input type="checkbox" id="auto-advance" checked style="width: 20px; height: 20px; cursor: pointer;">
             <label for="auto-advance" style="cursor: pointer; flex: 1; font-size: 14px;">Auto-advance to next lesson</label>
         </div>
          <div style="display: flex; align-items: center; gap: 10px; background: rgba(255,255,255,0.15); padding: 12px; border-radius: 10px; margin-bottom: 10px;">
             <input type="checkbox" id="handle-games" style="width: 20px; height: 20px; cursor: pointer;">
             <label for="handle-games" style="cursor: pointer; flex: 1; font-size: 14px;">Attempt game auto-skip (unreliable)</label>
         </div>
    
    
         <button id="start-btn" style="width: 100%; padding: 12px; border: none; border-radius: 10px; font-size: 14px; font-weight: 700; cursor: pointer; text-transform: uppercase; background: #10ac84; color: white; margin-bottom: 10px;">▶ Start Bot</button>
         <button id="stop-btn" disabled style="width: 100%; padding: 12px; border: none; border-radius: 10px; font-size: 14px; font-weight: 700; cursor: pointer; text-transform: uppercase; background: #ff6b6b; color: white; margin-bottom: 10px; opacity: 0.5;">⏹ Stop</button>
    
         <div id="status" style="background: rgba(255,255,255,0.15); padding: 12px; border-radius: 10px; text-align: center; font-size: 13px; font-weight: 600; margin-top: 15px;">Ready</div>
    
         <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 15px;">
             <div style="background: rgba(255,255,255,0.15); padding: 10px 8px; border-radius: 8px; text-align: center;">
                 <div style="font-size: 18px; font-weight: 700;" id="chars-typed">0</div>
                 <div style="font-size: 9px; opacity: 0.8; margin-top: 3px;">CHARS</div>
             </div>
             <div style="background: rgba(255,255,255,0.15); padding: 10px 8px; border-radius: 8px; text-align: center;">
                 <div style="font-size: 18px; font-weight: 700;" id="levels-completed">0</div>
                 <div style="font-size: 9px; opacity: 0.8; margin-top: 3px;">LESSONS</div>
             </div>
         </div>
     </div>
    

    `;

    document.body.appendChild(gui);

    const speedSlider = document.getElementById('speed-slider');
    const speedValue = document.getElementById('speed-value');
    const accuracySlider = document.getElementById('accuracy-slider');
    const accuracyValue = document.getElementById('accuracy-value');
    const autoAdvance = document.getElementById('auto-advance');
    const handleGames = document.getElementById('handle-games'); // New checkbox
    const startBtn = document.getElementById('start-btn');
    const stopBtn = document.getElementById('stop-btn');
    const status = document.getElementById('status');
    const levelInfo = document.getElementById('level-info');
    const charsTypedEl = document.getElementById('chars-typed');
    const levelsCompletedEl = document.getElementById('levels-completed');
    const closeBtn = document.getElementById('bot-close');
    const minimizeBtn = document.getElementById('bot-minimize'); // New minimize button
    const header = document.getElementById('bot-header');
    const botContent = document.getElementById('bot-content'); // Main content area

    let botRunning = false;
    let charsTypedCount = 0;
    let levelsCompleted = 0;
    let isMinimized = false;
    let originalGuiHeight = gui.offsetHeight; // Store initial height

    speedSlider.addEventListener('input', (e) => speedValue.textContent = e.target.value);
    accuracySlider.addEventListener('input', (e) => accuracyValue.textContent = e.target.value);

    // Draggable
    let isDragging = false;
    let currentX, currentY, initialX, initialY;

    header.addEventListener('mousedown', (e) => {
    if (!e.target.closest('button')) { // Don't drag if clicking buttons in header
    initialX = e.clientX - gui.offsetLeft;
    initialY = e.clientY - gui.offsetTop;
    isDragging = true;
    gui.style.cursor = 'grabbing';
    }
    });

    document.addEventListener('mousemove', (e) => {
    if (isDragging) {
    e.preventDefault();
    currentX = e.clientX - initialX;
    currentY = e.clientY - initialY;
    gui.style.left = currentX + 'px';
    gui.style.top = currentY + 'px';
    gui.style.right = 'auto'; // Disable right positioning once dragged
    }
    });

    document.addEventListener('mouseup', () => {
    isDragging = false;
    gui.style.cursor = 'grab';
    });

    minimizeBtn.addEventListener('click', () => {
    isMinimized = !isMinimized;
    if (isMinimized) {
    originalGuiHeight = gui.offsetHeight; // Save current height before minimizing
    gui.style.height = header.offsetHeight + 'px';
    botContent.style.display = 'none';
    minimizeBtn.textContent = '☐'; // Change to square/max button
    } else {
    gui.style.height = originalGuiHeight + 'px'; // Restore saved height
    botContent.style.display = 'block';
    minimizeBtn.textContent = '—'; // Change back to minimize
    }
    });

    // Key mapping for KeyboardEvent 'code' and 'key' properties
    const KEY_MAP = {
    ' ': { key: ' ', code: 'Space', keyCode: 32 },
    ',': { key: ',', code: 'Comma', keyCode: 188 },
    '.': { key: '.', code: 'Period', keyCode: 190 },
    ';': { key: ';', code: 'Semicolon', keyCode: 186 },
    "'": { key: "'", code: 'Quote', keyCode: 222 },
    '-': { key: '-', code: 'Minus', keyCode: 189 },
    '/': { key: '/', code: 'Slash', keyCode: 191 },
    '[': { key: '[', code: 'BracketLeft', keyCode: 219 },
    ']': { key: ']', code: 'BracketRight', keyCode: 221 },
    '\': { key: '\', code: 'Backslash', keyCode: 220 },
    '': { key: '', code: 'Backquote', keyCode: 192 },
    '=': { key: '=', code: 'Equal', keyCode: 187 },
    '{': { key: '{', code: 'BracketLeft', keyCode: 219, shiftKey: true },
    '}': { key: '}', code: 'BracketRight', keyCode: 221, shiftKey: true },
    '|': { key: '|', code: 'Backslash', keyCode: 220, shiftKey: true },
    ':': { key: ':', code: 'Semicolon', keyCode: 186, shiftKey: true },
    '"': { key: '"', code: 'Quote', keyCode: 222, shiftKey: true },
    '<': { key: '<', code: 'Comma', keyCode: 188, shiftKey: true },
    '>': { key: '>', code: 'Period', keyCode: 190, shiftKey: true },
    '?': { key: '?', code: 'Slash', keyCode: 191, shiftKey: true },
    '': { key: '', code: 'Backquote', keyCode: 192, shiftKey: true },
    '!': { key: '!', code: 'Digit1', keyCode: 49, shiftKey: true },
    '@': { key: '@', code: 'Digit2', keyCode: 50, shiftKey: true },
    '#': { key: '#', code: 'Digit3', keyCode: 51, shiftKey: true },
    '$': { key: '$', code: 'Digit4', keyCode: 52, shiftKey: true },
    '%': { key: '%', code: 'Digit5', keyCode: 53, shiftKey: true },
    '^': { key: '^', code: 'Digit6', keyCode: 54, shiftKey: true },
    '&': { key: '&', code: 'Digit7', keyCode: 55, shiftKey: true },
    '': { key: '', code: 'Digit8', keyCode: 56, shiftKey: true },
    '(': { key: '(', code: 'Digit9', keyCode: 57, shiftKey: true },
    ')': { key: ')', code: 'Digit0', keyCode: 48, shiftKey: true },
    '': { key: '', code: 'Minus', keyCode: 189, shiftKey: true },
    '+': { key: '+', code: 'Equal', keyCode: 187, shiftKey: true }
    };

    // Populate Key_MAP for a-z A-Z 0-9
    for (let i = 0; i < 26; i++) {
    const charLower = String.fromCharCode('a'.charCodeAt(0) + i);
    const charUpper = String.fromCharCode('A'.charCodeAt(0) + i);
    KEY_MAP[charLower] = { key: charLower, code: Key${charUpper}, keyCode: charUpper.charCodeAt(0) };
    KEY_MAP[charUpper] = { key: charUpper, code: Key${charUpper}, keyCode: charUpper.charCodeAt(0), shiftKey: true };
    }
    for (let i = 0; i < 10; i++) {
    const char = String.fromCharCode('0'.charCodeAt(0) + i);
    KEY_MAP[char] = { key: char, code: Digit${char}, keyCode: char.charCodeAt(0) };
    }

    function typeChar(char, targetElement) {
    let eventProps = KEY_MAP[char];

     if (!eventProps) {
         console.warn(`Character '${char}' not found in key map, using fallback.`);
         eventProps = {
             key: char,
             code: `Key${char.toUpperCase()}`, // Best guess
             keyCode: char.charCodeAt(0),
             shiftKey: char.toUpperCase() === char && char.toLowerCase() !== char // Guess for uppercase
         };
     }
    
     const opts = {
         key: eventProps.key,
         code: eventProps.code,
         keyCode: eventProps.keyCode,
         which: eventProps.keyCode,
         shiftKey: eventProps.shiftKey || false,
         bubbles: true,
         cancelable: true,
         view: window
     };
    
     // Dispatch key events
     targetElement.dispatchEvent(new KeyboardEvent('keydown', opts));
     targetElement.dispatchEvent(new KeyboardEvent('keypress', opts)); // Some apps still listen to keypress
    
     // Update target element's content/value
     if (targetElement.isContentEditable) {
         const selection = window.getSelection();
         const range = document.createRange();
         if (selection.focusNode) {
             range.setStart(selection.focusNode, selection.focusOffset);
             range.insertNode(document.createTextNode(char));
             range.collapse(false);
             selection.removeAllRanges();
             selection.addRange(range);
         } else {
              targetElement.textContent += char; // Fallback if no selection
         }
     } else if (targetElement.tagName === 'INPUT' || targetElement.tagName === 'TEXTAREA') {
         const start = targetElement.selectionStart;
         const end = targetElement.selectionEnd;
         targetElement.value = targetElement.value.substring(0, start) + char + targetElement.value.substring(end);
         targetElement.setSelectionRange(start + 1, start + 1);
     }
    
     // Dispatch input/change events for reactivity
     targetElement.dispatchEvent(new Event('input', { bubbles: true }));
     targetElement.dispatchEvent(new Event('change', { bubbles: true }));
     targetElement.dispatchEvent(new KeyboardEvent('keyup', opts));
    

    }

    function detectLevel() {
    // Look for the element that contains the text to be typed
    // TypingClub uses divs with 'current-letter' or similar indicators
    const currentLetterSpan = document.querySelector('span.current-letter');
    let textContainer = currentLetterSpan ? currentLetterSpan.closest('div.sentence') || currentLetterSpan.closest('div.typable') : null;

     if (!textContainer) {
         textContainer = document.querySelector('div.word.active') || document.querySelector('div[data-lesson-text]');
     }
    
     let fullText = '';
     if (textContainer) {
         // Reconstruct text from child nodes, preserving spaces/structure
         fullText = Array.from(textContainer.querySelectorAll('span, div')).map(el => {
             if (el.classList.contains('current-cursor') || el.classList.contains('current-letter') || el.classList.contains('typed')) {
                 return ''; // Ignore already typed/cursor elements for full text
             }
             return el.textContent;
         }).join('').replace(/\s+/g, ' ').trim(); // Clean up multiple spaces
    
         if (fullText.length > 0) {
             levelInfo.textContent = `${fullText.length} chars detected`;
             return { text: fullText, type: 'typing' };
         }
     }
    
     // Alternative for when lessons just started and there's a big block of text
     const alternativeTextContainer = document.querySelector('.typable, .typing-text, [data-lesson-text]');
     if (alternativeTextContainer && alternativeTextContainer.textContent.trim().length > 10) {
         fullText = alternativeTextContainer.textContent.trim().replace(/\s+/g, ' ');
         levelInfo.textContent = `${fullText.length} chars detected (fallback)`;
         return { text: fullText, type: 'typing' };
     }
    
    
     // Check for games
     const gameContainer = document.querySelector('.game-container, canvas, #game, [class*="game-screen"]');
     if (gameContainer) {
         levelInfo.textContent = 'Game detected';
         return { text: null, type: 'game' };
     }
    
     levelInfo.textContent = 'No active lesson/game found.';
     return { text: null, type: null };
    

    }

    async function findAndFocusInput() {
    let inputField = null;
    for (let attempt = 0; attempt < 50; attempt++) { // Increased attempts
    // Prioritize the element indicated as the current target
    inputField = document.querySelector('span.current-letter, div.current-letter, .user-input, input[type="text"]:focus');

         // Find an active input field that is currently visible or contenteditable
         if (!inputField || !inputField.offsetParent) { // If not found or not visible
             const potentialInputs = document.querySelectorAll('input[type="text"], textarea, [contenteditable="true"], .typingArea');
             for (const inp of potentialInputs) {
                 // Check if it's visible and interactable, or if it's the `contenteditable` container
                 if ( (inp.offsetParent !== null && inp.checkVisibility && inp.checkVisibility()) || inp.isContentEditable) {
                     inputField = inp;
                     break;
                 }
             }
         }
    
         if (inputField) {
             console.log('✅ Found active input field:', inputField);
             // Try to focus it if it's not contenteditable (contenteditable can cause issues with focus)
             if (!inputField.isContentEditable) {
                  inputField.focus();
             }
             // Clear any pre-existing value if it's an input
             if (inputField.tagName === 'INPUT' || inputField.tagName === 'TEXTAREA') {
                 inputField.value = '';
             } else if (inputField.isContentEditable) {
                 inputField.textContent = ''; // Clear contenteditable
             }
             return inputField;
         }
         await new Promise(r => setTimeout(r, 100));
     }
     return null;
    

    }

    async function processLevel() {
    if (!botRunning) {
    console.log("Bot stopped, ceasing level processing.");
    return;
    }

     const level = detectLevel();
    
     if (!level.text && level.type !== 'game') {
         status.textContent = '⚠️ No lesson text or game found. Retrying in 3s...';
         console.log('❌ Could not find text or game. Retrying...');
         await new Promise(r => setTimeout(r, 3000));
         if (botRunning) processLevel(); // Retry
         return;
     }
    
     // Handle games
     if (level.type === 'game') {
         status.textContent = '🎮 Game detected!';
         console.log('🎮 Game detected.');
    
         if (handleGames.checked) {
             status.textContent = '🎮 Attempting to skip game...';
             console.warn('Attempting to skip game is highly unreliable and may not work.');
             // Try to find common "Skip" or "Next" buttons immediately
             const skipBtn = document.querySelector('button.skip, a.skip, .skip-button, button.next, a.next, .next-button, .go-button, .btn-primary, [role="button"]'); // Added more skip options
             if (skipBtn && skipBtn.offsetParent !== null && skipBtn.checkVisibility()) {
                  console.log('Found a potential skip/next button, clicking it.');
                  skipBtn.click();
                  await new Promise(r => setTimeout(r, 2000)); // Give time for transition
                  levelsCompleted++;
                  levelsCompletedEl.textContent = levelsCompleted;
                  if (botRunning) processLevel();
                  return;
             } else {
                 status.textContent = '🎮 Game found, no clear skip button. Stopped.';
                 stopBot(); // Stop if no skip button found
                 return;
             }
         } else {
             status.textContent = '🎮 Game found (game handling disabled). Stopped.';
             stopBot();
             return;
         }
     }
    
     // Handle typing (existing code)
     const text = level.text;
     status.textContent = '👆 Starting lesson...';
    
     // Focus on the typing area initially
     let typingAreaClickTarget = document.querySelector('.typing-widget, [data-lesson-text], .wrapper span.current-letter');
     if (typingAreaClickTarget) {
         console.log('Attempting to click typing area:', typingAreaClickTarget);
         typingAreaClickTarget.click();
     } else {
          document.body.click(); // Fallback general click
     }
     await new Promise(r => setTimeout(r, 500)); // Small delay for focus to take effect
    
     const input = await findAndFocusInput();
    
     if (!input) {
         status.textContent = '⚠️ Input field not found. Retrying in 3s...';
         console.log('❌ No active input field found. Retrying...');
         await new Promise(r => setTimeout(r, 3000));
         if (botRunning) processLevel(); // Retry
         return;
     }
    
     console.log('🚀 Typing into:', input);
     status.textContent = `⌨️ Typing...`;
    
     const wpm = parseInt(speedSlider.value);
     const accuracy = parseInt(accuracySlider.value);
     const baseDelay = 60000 / (wpm * 5); // Time per char in ms
    
     for (let i = 0; i < text.length; i++) {
         if (!botRunning) return;
    
         const char = text[i];
         const shouldError = Math.random() * 100 > accuracy;
    
         // Simulate mistype and backspace for realism
         if (shouldError && /[a-zA-Z0-9]/.test(char)) { // Only mistype letters and numbers
             const wrongCharAscii = char.charCodeAt(0) + (Math.random() > 0.5 ? 1 : -1);
             const wrong = String.fromCharCode(wrongCharAscii);
             if (KEY_MAP[wrong]) { // Ensure the wrong character can be mapped
                 typeChar(wrong, input);
                 charsTypedCount++; // Count the wrong char
                 charsTypedEl.textContent = charsTypedCount;
                 await new Promise(r => setTimeout(r, baseDelay * (0.2 + Math.random() * 0.2))); // Delay for typing error
    
                 // Simulate backspace key
                 const backspaceOpts = {
                     key: 'Backspace',
                     code: 'Backspace',
                     keyCode: 8,
                     which: 8,
                     bubbles: true,
                     cancelable: true,
                     view: window
                 };
                 input.dispatchEvent(new KeyboardEvent('keydown', backspaceOpts));
                 if (input.isContentEditable) {
                      const selection = window.getSelection();
                      if (selection.focusNode && selection.focusOffset > 0) {
                          const range = document.createRange();
                          range.setStart(selection.focusNode, selection.focusOffset - 1);
                          range.setEnd(selection.focusNode, selection.focusOffset);
                          range.deleteContents();
                          range.collapse(false);
                          selection.removeAllRanges();
                          selection.addRange(range);
                      } else {
                           input.textContent = input.textContent.slice(0, -1);
                      }
    
                 } else if (input.tagName === 'INPUT' || input.tagName === 'TEXTAREA') {
                     input.value = input.value.slice(0, -1);
                     input.setSelectionRange(input.value.length, input.value.length);
                 }
                 input.dispatchEvent(new Event('input', { bubbles: true }));
                 input.dispatchEvent(new KeyboardEvent('keyup', backspaceOpts));
                 await new Promise(r => setTimeout(r, baseDelay * (0.2 + Math.random() * 0.2))); // Delay for backspace
             }
         }
    
         typeChar(char, input);
    
         charsTypedCount++;
         charsTypedEl.textContent = charsTypedCount;
    
         let delay = baseDelay * (0.8 + Math.random() * 0.4); // Randomize delay a bit
         if (['.', '!', '?', '\n'].includes(char)) delay *= 2.5; // Longer pause for punctuation/newlines
         else if (char === ' ') delay *= 1.3; // Slightly longer for spaces
    
         await new Promise(r => setTimeout(r, delay));
     }
    
     console.log('✅ Typing complete for this lesson!');
     levelsCompleted++;
     levelsCompletedEl.textContent = levelsCompleted;
     status.textContent = '✅ Lesson complete! Checking next step...';
    
     if (autoAdvance.checked) {
         // Give the page some time to process results and show next button
         await new Promise(r => setTimeout(r, 4000));
    
         // Look for common "Continue", "Next Lesson", "Go" buttons or links
         const advanceButtonSelectors = [
             'button.continue, a.continue, .continue-button, [data-action="next-lesson"]',
             'button.next, a.next, .next-button, [data-action="next"]',
             '.go-button, .btn-primary, [role="button"]' // General buttons
         ];
         let advanceButton = null;
    
         for (const selector of advanceButtonSelectors) {
             advanceButton = document.querySelector(selector);
             if (advanceButton && advanceButton.offsetParent !== null && advanceButton.checkVisibility()) {
                 console.log('Found auto-advance button:', advanceButton);
                 advanceButton.click();
                 break;
             } else if (advanceButton) {
                 console.log('Found button, but not visible or interactable yet:', advanceButton);
             }
             advanceButton = null; // Reset for next selector
         }
    
         if (!advanceButton) {
             console.log('No specific advance button found, trying generic click/Enter...');
             document.body.click(); // Click body as a fallback
             await new Promise(r => setTimeout(r, 500));
             // Try sending Enter key
             const enterOpts = KEY_MAP['\n']; // Or specific enter key code
             if (enterOpts) {
                 document.dispatchEvent(new KeyboardEvent('keydown', enterOpts));
                 document.dispatchEvent(new KeyboardEvent('keyup', enterOpts));
             }
         }
    
         // Wait a bit for the next lesson to load
         await new Promise(r => setTimeout(r, 2000));
         if (botRunning) processLevel(); // Start processing the next lesson
     } else {
         status.textContent = '⏹ Auto-advance disabled. Stopped after lesson.';
         stopBot();
     }
    

    }

    function startBot() {
    botRunning = true;
    charsTypedCount = 0; // Reset metrics on new start
    levelsCompleted = 0;
    charsTypedEl.textContent = '0';
    levelsCompletedEl.textContent = '0';
    startBtn.disabled = true;
    stopBtn.disabled = false;
    stopBtn.style.opacity = '1';
    startBtn.style.background = '#4CAF50'; // Green
    stopBtn.style.background = '#ff6b6b'; // Red
    console.log('🤖 Bot started');
    processLevel();
    }

    function stopBot() {
    botRunning = false;
    startBtn.disabled = false;
    stopBtn.disabled = true;
    stopBtn.style.opacity = '0.5';
    startBtn.style.background = '#10ac84'; // Original green
    stopBtn.style.background = '#ff6b6b'; // Red
    status.textContent = '⏹ Stopped';
    console.log('🤖 Bot stopped.');
    }

    function closeBot() {
    stopBot();
    gui.remove();
    window.typingClubBot = null;
    console.log('Bot GUI closed and removed.');
    }

    startBtn.addEventListener('click', startBot);
    stopBtn.addEventListener('click', stopBot);
    closeBtn.addEventListener('click', closeBot); // Corrected this line

    window.typingClubBot = { start: startBot, stop: stopBot, close: closeBot, gui: gui }; // Expose gui for external control

    // Initial detection after a short delay
    setTimeout(detectLevel, 1000); // Give page a bit more time to render initially
    console.log('✅ Bot loaded! Adjust settings and click "▶ Start Bot" to begin.');

})();

@zane2342
Copy link

zane2342 commented Jan 21, 2026 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment