Lichess Predator Vision Overlay

Predator vision thermal overlay with heat shimmer, triangular reticle, numeric stream, and draggable anchored toggle button.

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         Lichess Predator Vision Overlay
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  Predator vision thermal overlay with heat shimmer, triangular reticle, numeric stream, and draggable anchored toggle button.
// @match        https://lichess.org/
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Load saved position or default
    let savedPos = localStorage.getItem('predatorVisionBtnPos');
    let pos = savedPos ? JSON.parse(savedPos) : null;

    // Default: under Terminator Vision button (shift 50px down)
    const defaultTop = pos ? pos.top : 156;
    const defaultRight = pos ? null : 154;

    // Create toggle button
    const btn = document.createElement('button');
    btn.textContent = 'Predator Vision';
    Object.assign(btn.style, {
        position: 'fixed',
        top: `${defaultTop}px`,
        right: defaultRight !== null ? `${defaultRight}px` : '',
        left: pos ? `${pos.left}px` : '',
        zIndex: 999999,
        padding: '8px 14px',
        background: '#004400',
        color: 'lime',
        border: 'none',
        borderRadius: '5px',
        cursor: 'pointer',
        fontWeight: 'bold',
        fontFamily: 'Consolas, monospace',
        userSelect: 'none',
        boxShadow: '0 0 8px lime',
        letterSpacing: '0.05em',
        touchAction: 'none',
    });
    btn.addEventListener('mouseenter', () => btn.style.backgroundColor = '#006600');
    btn.addEventListener('mouseleave', () => btn.style.backgroundColor = '#004400');
    document.body.appendChild(btn);

    // Dragging logic
    let dragging = false, dragStartX = 0, dragStartY = 0, btnStartLeft = 0, btnStartTop = 0;
    btn.addEventListener('mousedown', e => {
        dragging = true;
        dragStartX = e.clientX;
        dragStartY = e.clientY;
        const rect = btn.getBoundingClientRect();
        btnStartLeft = rect.left;
        btnStartTop = rect.top;
        e.preventDefault();
    });
    window.addEventListener('mousemove', e => {
        if (!dragging) return;
        let newLeft = btnStartLeft + (e.clientX - dragStartX);
        let newTop = btnStartTop + (e.clientY - dragStartY);
        newLeft = Math.min(window.innerWidth - btn.offsetWidth, Math.max(0, newLeft));
        newTop = Math.min(window.innerHeight - btn.offsetHeight, Math.max(0, newTop));
        btn.style.left = `${newLeft}px`;
        btn.style.top = `${newTop}px`;
        btn.style.right = '';
        localStorage.setItem('predatorVisionBtnPos', JSON.stringify({left: newLeft, top: newTop}));
    });
    window.addEventListener('mouseup', () => dragging = false);

    // Overlay container
    const overlay = document.createElement('div');
    Object.assign(overlay.style, {
        position: 'fixed',
        top: 0,
        left: 0,
        width: '100vw',
        height: '100vh',
        background: 'rgba(0, 50, 0, 0.6)',  // greener overlay
        zIndex: 999998,
        pointerEvents: 'none',
        display: 'none',
        overflow: 'hidden'
    });

    // Heatmap shimmer filter
    overlay.style.backdropFilter = 'contrast(200%) saturate(250%) hue-rotate(90deg)';

    // Shimmer effect
    const shimmer = document.createElement('div');
    Object.assign(shimmer.style, {
        position: 'absolute',
        width: '100%',
        height: '100%',
        background: 'radial-gradient(rgba(0,255,0,0.1), transparent)',
        mixBlendMode: 'screen',
        animation: 'heatShimmer 2s infinite linear',
        opacity: 0.4
    });
    overlay.appendChild(shimmer);

    // Numeric stream box (only numeric box now)
    const numStream = document.createElement('div');
    Object.assign(numStream.style, {
        position: 'fixed',
        top: '300px',
        right: '20px',
        width: '80px',
        height: '170px',  // reduced height to fit lines better
        color: 'lime',
        fontFamily: 'monospace',
        fontWeight: 'bold',
        fontSize: '14px',
        background: 'rgba(0,0,0,0.3)',
        textShadow: '0 0 10px lime',
        overflow: 'hidden',
        pointerEvents: 'none',
        zIndex: 40,
        userSelect: 'none',
        paddingLeft: '4px',  // slight left shift to avoid cutoff
        lineHeight: '1.2',
        letterSpacing: '0.07em',
        borderRadius: '8px 0 0 8px',
    });
    overlay.appendChild(numStream);

    const chars = '0123456789ABCDEF';
    const lineCount = 10;
    const lines = [];
    for(let i=0; i<lineCount; i++){
        const span = document.createElement('span');
        span.textContent = Array.from({length:12}, () => chars[Math.floor(Math.random()*chars.length)]).join('');
        span.style.opacity = Math.random() * 0.5 + 0.5;
        numStream.appendChild(span);
        if (i < lineCount - 1) {
            numStream.appendChild(document.createElement('br'));  // no br after last line
        }
        lines.push(span);
    }

    function animateNumStream(){
        lines.forEach(line => {
            if(Math.random() < 0.05){ // slower updates
                line.textContent = Array.from({length:12}, () => chars[Math.floor(Math.random()*chars.length)]).join('');
            }
        });
        requestAnimationFrame(animateNumStream);
    }

    // Text labels in corners and top center
    function createTextLabel(text, styles){
        const d = document.createElement('div');
        d.textContent = text.toUpperCase();
        Object.assign(d.style, {
            position: 'fixed',
            color: 'lime',
            fontWeight: '900',
            fontFamily: 'Consolas, monospace',
            fontSize: '1.4rem',
            textShadow: '0 0 8px lime',
            userSelect: 'none',
            pointerEvents: 'none',
            letterSpacing: '0.1em',
            ...styles,
        });
        overlay.appendChild(d);
    }
    createTextLabel('PREDATOR MODE ACTIVATED', {top: '20px', left: '50%', transform: 'translateX(-50%)'});
    createTextLabel('THERMAL TARGET LOCKED', {top: '20px', left: '20px'});
    createTextLabel('SCANNING', {top: '20px', right: '20px'});

    // Triangular reticle element creation & animation
    const reticle = document.createElement('div');
    reticle.innerHTML = `
        <svg width="80" height="80" viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg">
            <polygon points="40,5 75,75 5,75" stroke="lime" stroke-width="2" fill="none" />
            <circle cx="40" cy="50" r="5" fill="lime" />
        </svg>
    `;
    Object.assign(reticle.style, {
        position: 'fixed',
        top: '50%',
        left: '50%',
        transform: 'translate(-50%, -50%)',
        pointerEvents: 'none',
        zIndex: 50,
    });
    overlay.appendChild(reticle);

    let reticleTime = 0;
    function animateReticle(){
        reticleTime += 0.015; // speed up slightly for smoothness
        const radius = 80;
        const centerX = window.innerWidth / 2;
        const centerY = window.innerHeight / 2;

        const x = centerX + Math.cos(reticleTime) * radius;
        const y = centerY + Math.sin(reticleTime) * radius;

        reticle.style.left = `${x}px`;
        reticle.style.top = `${y}px`;

        requestAnimationFrame(animateReticle);
    }

    // Create waiting message at bottom center, big green with sharp border and pulsate
    const waitingMsg = document.createElement('div');
    waitingMsg.textContent = 'WAITING FOR GAME...';
    Object.assign(waitingMsg.style, {
        position: 'fixed',
        bottom: '20px',
        left: '50%',
        transform: 'translateX(-50%)',
        color: 'lime',
        fontWeight: '900',
        fontFamily: 'Consolas, monospace',
        fontSize: '5rem',
        letterSpacing: '0.15em',
        textTransform: 'uppercase',
        userSelect: 'none',
        pointerEvents: 'none',
        whiteSpace: 'nowrap',
        zIndex: 1000000,
        textShadow:
          '-2px -2px 0 #000,' +
          '2px -2px 0 #000,' +
          '-2px 2px 0 #000,' +
          '2px 2px 0 #000',
        animation: 'pulseSharp 2.5s ease-in-out infinite',
        display: 'none'  // initially hidden
    });
    document.body.appendChild(waitingMsg);

    // Keyframes for shimmer and pulse plus pulseSharp for waiting message
    const style = document.createElement('style');
    style.textContent = `
        @keyframes pulseGreen {
            0%, 100% { opacity: 1; text-shadow: 0 0 6px lime; }
            50% { opacity: 0.5; text-shadow: 0 0 2px green; }
        }
        @keyframes heatShimmer {
            0% { transform: translateY(0); }
            50% { transform: translateY(-5px); }
            100% { transform: translateY(0); }
        }
        @keyframes pulseSharp {
            0%, 100% {
                opacity: 1;
                text-shadow:
                  -2px -2px 0 #000,
                  2px -2px 0 #000,
                  -2px 2px 0 #000,
                  2px 2px 0 #000;
            }
            50% {
                opacity: 0.5;
                text-shadow:
                  -1px -1px 0 #222,
                  1px -1px 0 #222,
                  -1px 1px 0 #222,
                  1px 1px 0 #222;
            }
        }
    `;
    document.head.appendChild(style);

    document.body.appendChild(overlay);

    // Toggle overlay on button click
    let active = false;
    btn.addEventListener('click', () => {
        active = !active;
        overlay.style.display = active ? 'block' : 'none';
        waitingMsg.style.display = active ? 'block' : 'none';  // Show waiting message only when overlay is ON
        btn.style.backgroundColor = active ? '#006600' : '#004400';
        if (active) {
            animateNumStream();
            animateReticle();
        }
    });

})();