Lichess Predator Vision Overlay

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

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.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==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();
        }
    });

})();