Virtual Pet - Cursor-Friendly

A virtual creature that follows your cursor, that you've nurtured, raised, and played with.

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         Virtual Pet - Cursor-Friendly
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  A virtual creature that follows your cursor, that you've nurtured, raised, and played with.
// @author       Mustafa Hakan
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_notification
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // Wait for body to be available
    function waitForBody(callback) {
        if (document.body) {
            callback();
        } else {
            setTimeout(() => waitForBody(callback), 100);
        }
    }

    // Creature data
    const PET = {
        name: GM_getValue('pet_name', 'Minik'),
        type: GM_getValue('pet_type', 'cat'),
        level: GM_getValue('pet_level', 1),
        xp: GM_getValue('pet_xp', 0),
        hunger: GM_getValue('pet_hunger', 100),
        happiness: GM_getValue('pet_happiness', 100),
        energy: GM_getValue('pet_energy', 100),
        coins: GM_getValue('pet_coins', 0),
        accessories: JSON.parse(GM_getValue('pet_acc', '[]')),
        color: GM_getValue('pet_color', '#ff6b35'),
        born: GM_getValue('pet_born', Date.now()),
        lastFed: GM_getValue('pet_lastFed', Date.now()),
        lastPlayed: GM_getValue('pet_lastPlayed', Date.now())
    };

    // Creature types
    const TYPES = {
        cat: { name: 'Cat', emoji: '🐱', sound: 'Meow!', foods: ['🐟','🥛','🐭'], toys: ['🧶','🎾','📦'] },
        dog: { name: 'Dog', emoji: '🐶', sound: 'Woof!', foods: ['🦴','🥩','🍖'], toys: ['🦴','🎾','🥏'] },
        dragon: { name: 'Dragon', emoji: '🐉', sound: 'Roar!', foods: ['🔥','🍖','💎'], toys: ['🏰','⚔️','💍'] },
        bunny: { name: 'Bunny', emoji: '🐰', sound: 'Hop!', foods: ['🥕','🥬','🍎'], toys: ['🏀','🪁','🎪'] },
        panda: { name: 'Panda', emoji: '🐼', sound: 'Purr!', foods: ['🎋','🍎','🍯'], toys: ['🪵','🧸','🏀'] },
        fox: { name: 'Fox', emoji: '🦊', sound: 'Yip!', foods: ['🍇','🧀','🥚'], toys: ['🪶','🎯','🧩'] }
    };

    const petType = TYPES[PET.type] || TYPES.cat;
    let petEl, statsEl, menuEl;
    let x = window.innerWidth / 2, y = window.innerHeight / 2;
    let targetX = x, targetY = y;
    let velX = 0, velY = 0;
    let animFrame;
    let state = 'idle';
    let stateTimer = 0;
    let particles = [];
    let mouseX = x, mouseY = y;

    function save() {
        try {
            GM_setValue('pet_name', PET.name);
            GM_setValue('pet_type', PET.type);
            GM_setValue('pet_level', PET.level);
            GM_setValue('pet_xp', PET.xp);
            GM_setValue('pet_hunger', PET.hunger);
            GM_setValue('pet_happiness', PET.happiness);
            GM_setValue('pet_energy', PET.energy);
            GM_setValue('pet_coins', PET.coins);
            GM_setValue('pet_acc', JSON.stringify(PET.accessories));
            GM_setValue('pet_color', PET.color);
            GM_setValue('pet_lastFed', PET.lastFed);
            GM_setValue('pet_lastPlayed', PET.lastPlayed);
        } catch(e) {
            console.log('Pet save error:', e);
        }
    }

    function addXP(amount) {
        PET.xp += amount;
        if (PET.xp >= PET.level * 100) {
            PET.xp = 0;
            PET.level++;
            PET.coins += PET.level * 10;
            try {
                GM_notification({ text: `🎉 ${PET.name} reached level ${PET.level}! +${PET.level*10} 💰`, timeout: 3000 });
            } catch(e) {}
            spawnParticles('levelup');
        }
        save();
    }

    function spawnParticles(type) {
        const emojis = {
            eat: ['✨','💫','🌟'],
            play: ['🎈','💝','💖'],
            levelup: ['🎉','🎊','⭐','🌟','💫'],
            sleep: ['💤','💤','💤'],
            love: ['💕','💗','💖']
        };
        const list = emojis[type] || ['✨'];
        for (let i = 0; i < 8; i++) {
            particles.push({
                emoji: list[Math.floor(Math.random() * list.length)],
                x: x, y: y - 20,
                vx: (Math.random() - 0.5) * 4,
                vy: -Math.random() * 4 - 2,
                life: 1,
                size: 16 + Math.random() * 16
            });
        }
    }

    function feedPet() {
        if (PET.hunger >= 100) { showMsg('I\'m full! 😊'); return; }
        if (Date.now() - PET.lastFed < 5000) { showMsg('Not hungry yet! ⏳'); return; }
        PET.hunger = Math.min(100, PET.hunger + 20);
        PET.lastFed = Date.now();
        PET.coins = Math.max(0, PET.coins - 5);
        addXP(10);
        state = 'eating';
        stateTimer = 30;
        spawnParticles('eat');
        showMsg('Yummy! 😋');
        save();
    }

    function playPet() {
        if (PET.energy < 20) { showMsg('Too tired... 😴'); return; }
        if (Date.now() - PET.lastPlayed < 3000) { showMsg('Need a break! ⏳'); return; }
        PET.happiness = Math.min(100, PET.happiness + 20);
        PET.energy = Math.max(0, PET.energy - 20);
        PET.hunger = Math.max(0, PET.hunger - 10);
        PET.lastPlayed = Date.now();
        PET.coins += Math.floor(Math.random() * 10) + 1;
        addXP(15);
        state = 'playing';
        stateTimer = 40;
        spawnParticles('play');
        if (Math.random() < 0.3) {
            PET.coins += 5;
            showMsg('💰 Found treasure! +5 coin');
        } else {
            showMsg('Yay! 🎾');
        }
        save();
    }

    function sleepPet() {
        if (PET.energy > 80) { showMsg('Not sleepy! 😄'); return; }
        state = 'sleeping';
        stateTimer = 60;
        PET.energy = Math.min(100, PET.energy + 30);
        PET.happiness = Math.min(100, PET.happiness + 5);
        spawnParticles('sleep');
        showMsg('Zzz... 😴');
        save();
    }

    function showMsg(text) {
        if (!petEl) return;
        const msg = document.createElement('div');
        msg.textContent = text;
        msg.style.cssText = `
            position: fixed; left: ${x}px; top: ${y - 50}px;
            background: rgba(0,0,0,0.85); color: #fff; padding: 8px 16px;
            border-radius: 20px; font: 13px system-ui; pointer-events: none;
            z-index: 2147483648; animation: petMsg 2s forwards; white-space: nowrap;
        `;
        document.body.appendChild(msg);
        setTimeout(() => { if (msg.parentNode) msg.remove(); }, 2000);
    }

    function changeAccessory() {
        const all = ['🎩','👑','🎀','🕶️','🧢','💍','📿','🎒','🧣','👒','🪖','🎭'];
        const available = all.filter(a => !PET.accessories.includes(a));
        if (available.length === 0) { showMsg('Collected them all! 🏆'); return; }
        const chosen = available[Math.floor(Math.random() * available.length)];
        PET.accessories.push(chosen);
        PET.coins = Math.max(0, PET.coins - 10);
        spawnParticles('love');
        showMsg(`New accessory: ${chosen}!`);
        save();
    }

    function createPet() {
        if (document.getElementById('virtual-pet')) return;
        petEl = document.createElement('div');
        petEl.id = 'virtual-pet';
        petEl.style.cssText = `
            position: fixed; left: ${x}px; top: ${y}px; z-index: 2147483645;
            font-size: ${30 + PET.level * 3}px; cursor: pointer; pointer-events: all;
            transition: none; user-select: none; transform: translate(-50%, -50%);
            filter: drop-shadow(0 5px 15px rgba(0,0,0,0.5));
        `;
        updatePetAppearance();
        document.body.appendChild(petEl);

        petEl.addEventListener('click', (e) => {
            e.stopPropagation();
            if (state === 'sleeping') { showMsg('Shh sleeping... 😴'); return; }
            PET.happiness = Math.min(100, PET.happiness + 5);
            spawnParticles('love');
            if (Math.random() < 0.2) PET.coins++;
            save();
        });

        petEl.addEventListener('dblclick', (e) => {
            e.stopPropagation();
            toggleMenu();
        });
    }

    function updatePetAppearance() {
        if (!petEl) return;
        let display = petType.emoji;
        if (PET.accessories.length > 0) {
            display += PET.accessories[PET.accessories.length - 1];
        }
        if (state === 'sleeping') display += '💤';
        petEl.textContent = display;
        petEl.style.fontSize = (30 + PET.level * 3) + 'px';
        petEl.style.filter = `drop-shadow(0 5px 15px rgba(0,0,0,0.5)) drop-shadow(0 0 ${5+PET.level*2}px ${PET.color})`;
    }

    function createStats() {
        if (document.getElementById('pet-stats')) return;
        statsEl = document.createElement('div');
        statsEl.id = 'pet-stats';
        statsEl.style.cssText = `
            position: fixed; bottom: 20px; left: 20px; background: rgba(15,15,20,0.92);
            border: 1px solid rgba(255,255,255,0.15); border-radius: 16px; padding: 15px;
            z-index: 2147483645; font: 12px system-ui; color: #ccc; min-width: 200px;
            backdrop-filter: blur(20px); box-shadow: 0 10px 30px rgba(0,0,0,0.6);
        `;
        updateStats();
        document.body.appendChild(statsEl);
    }

    function updateStats() {
        if (!statsEl) return;
        statsEl.innerHTML = `
            <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
                <span style="font-size:24px;">${petType.emoji}</span>
                <div>
                    <div style="color:#fff;font-weight:700;">${PET.name}</div>
                    <div style="color:#888;font-size:10px;">${petType.name} • Lv ${PET.level}</div>
                </div>
            </div>
            ${bar('🍗', 'Hunger', PET.hunger, '#f97316')}
            ${bar('😊', 'Happiness', PET.happiness, '#f472b6')}
            ${bar('⚡', 'Energy', PET.energy, '#4ade80')}
            <div style="display:flex;justify-content:space-between;margin-top:8px;">
                <span>⭐ XP: ${PET.xp}/${PET.level*100}</span>
                <span>💰 ${PET.coins}</span>
            </div>
            <div style="display:flex;gap:4px;margin-top:8px;">
                <button class="pet-feed-btn" style="flex:1;padding:8px;border-radius:8px;border:1px solid #f97316;background:#f9731622;color:#f97316;cursor:pointer;font-size:16px;" title="Feed (-5💰)">🍗</button>
                <button class="pet-play-btn" style="flex:1;padding:8px;border-radius:8px;border:1px solid #f472b6;background:#f472b622;color:#f472b6;cursor:pointer;font-size:16px;" title="Play">🎾</button>
                <button class="pet-sleep-btn" style="flex:1;padding:8px;border-radius:8px;border:1px solid #4ade80;background:#4ade8022;color:#4ade80;cursor:pointer;font-size:16px;" title="Sleep">💤</button>
                <button class="pet-acc-btn" style="flex:1;padding:8px;border-radius:8px;border:1px solid #a78bfa;background:#a78bfa22;color:#a78bfa;cursor:pointer;font-size:16px;" title="Accessory (-10💰)">🎀</button>
            </div>
            <div style="color:#666;font-size:9px;text-align:center;margin-top:6px;">Click: Pet • Double-click: Menu</div>
        `;

        // Add event listeners directly
        const feedBtn = statsEl.querySelector('.pet-feed-btn');
        const playBtn = statsEl.querySelector('.pet-play-btn');
        const sleepBtn = statsEl.querySelector('.pet-sleep-btn');
        const accBtn = statsEl.querySelector('.pet-acc-btn');

        if (feedBtn) feedBtn.onclick = feedPet;
        if (playBtn) playBtn.onclick = playPet;
        if (sleepBtn) sleepBtn.onclick = sleepPet;
        if (accBtn) accBtn.onclick = changeAccessory;
    }

    function bar(icon, label, value, color) {
        return `<div style="margin:4px 0;"><span style="font-size:10px;">${icon} ${label}</span><div style="height:6px;background:#333;border-radius:3px;margin-top:2px;"><div style="height:100%;width:${value}%;background:${color};border-radius:3px;transition:width 0.5s;"></div></div></div>`;
    }

    function createMenu() {
        if (menuEl) { menuEl.remove(); menuEl = null; }
        menuEl = document.createElement('div');
        menuEl.id = 'pet-menu';
        menuEl.style.cssText = `
            position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
            background: rgba(15,15,20,0.95); border: 1px solid rgba(255,255,255,0.2);
            border-radius: 20px; padding: 25px; z-index: 2147483649; font: 14px system-ui;
            color: #fff; min-width: 280px; backdrop-filter: blur(30px);
            box-shadow: 0 20px 60px rgba(0,0,0,0.8);
        `;

        const menuHTML = `
            <div style="font-weight:700;font-size:18px;margin-bottom:15px;text-align:center;">🐾 ${PET.name} Menu</div>
            <div style="display:flex;flex-direction:column;gap:8px;">
                <button id="pet-btn-name" style="padding:12px;border-radius:10px;border:1px solid #444;background:#222;color:#fff;cursor:pointer;">✏️ Change Name</button>
                <button id="pet-btn-type" style="padding:12px;border-radius:10px;border:1px solid #444;background:#222;color:#fff;cursor:pointer;">🔄 Change Type</button>
                <button id="pet-btn-color" style="padding:12px;border-radius:10px;border:1px solid #444;background:#222;color:#fff;cursor:pointer;">🎨 Change Color</button>
                <button id="pet-btn-stats" style="padding:12px;border-radius:10px;border:1px solid #444;background:#222;color:#fff;cursor:pointer;">📊 Detailed Stats</button>
            </div>
            <button id="pet-btn-close" style="margin-top:15px;width:100%;padding:10px;border-radius:10px;border:1px solid #444;background:transparent;color:#888;cursor:pointer;">Close</button>
        `;
        menuEl.innerHTML = menuHTML;
        document.body.appendChild(menuEl);

        document.getElementById('pet-btn-name').onclick = () => {
            const name = prompt('New name:', PET.name);
            if (name && name.trim()) { 
                PET.name = name.trim(); 
                save(); 
                updateStats(); 
                updatePetAppearance(); 
                showMsg(`My name is now ${PET.name}! 🎉`); 
            }
        };

        document.getElementById('pet-btn-type').onclick = () => {
            const types = Object.keys(TYPES);
            const current = types.indexOf(PET.type);
            PET.type = types[(current + 1) % types.length];
            save(); 
            updateStats(); 
            updatePetAppearance();
            showMsg(`Now I'm a ${TYPES[PET.type].name}! ${TYPES[PET.type].emoji}`);
        };

        document.getElementById('pet-btn-color').onclick = () => {
            const color = prompt('Color (hex):', PET.color);
            if (color) { 
                PET.color = color; 
                save(); 
                updatePetAppearance(); 
                showMsg('My color changed! ✨'); 
            }
        };

        document.getElementById('pet-btn-stats').onclick = () => {
            const age = Math.floor((Date.now() - PET.born) / 86400000);
            const acc = PET.accessories.length > 0 ? PET.accessories.join(' ') : 'None';
            alert(`${PET.name} - ${petType.name} Lv ${PET.level}\n\n🍗 Hunger: ${PET.hunger}%\n😊 Happiness: ${PET.happiness}%\n⚡ Energy: ${PET.energy}%\n⭐ XP: ${PET.xp}/${PET.level*100}\n💰 Coin: ${PET.coins}\n🎀 Accessories: ${acc}\n📅 Age: ${age} days`);
        };

        document.getElementById('pet-btn-close').onclick = () => {
            if (menuEl) { menuEl.remove(); menuEl = null; }
        };

        // Hover effects
        ['pet-btn-name','pet-btn-type','pet-btn-color','pet-btn-stats'].forEach(id => {
            const btn = document.getElementById(id);
            if (btn) {
                btn.onmouseover = () => { btn.style.background = '#333'; };
                btn.onmouseout = () => { btn.style.background = '#222'; };
            }
        });
    }

    function toggleMenu() {
        if (document.getElementById('pet-menu')) {
            const existing = document.getElementById('pet-menu');
            existing.remove();
            menuEl = null;
        } else {
            createMenu();
        }
    }

    function updatePet() {
        if (!petEl) return;

        const dx = mouseX - x;
        const dy = mouseY - y;
        const dist = Math.sqrt(dx * dx + dy * dy);

        if (state === 'sleeping') {
            velX *= 0.9;
            velY *= 0.9;
        } else if (dist > 30) {
            const speed = 0.08;
            velX += dx * speed;
            velY += dy * speed;
        } else {
            velX *= 0.95;
            velY *= 0.95;
            if (dist < 5 && Math.random() < 0.01) {
                spawnParticles('love');
            }
        }

        const maxSpeed = state === 'playing' ? 12 : 8;
        const spd = Math.sqrt(velX * velX + velY * velY);
        if (spd > maxSpeed) {
            velX = velX / spd * maxSpeed;
            velY = velY / spd * maxSpeed;
        }

        x += velX;
        y += velY;

        x = Math.max(20, Math.min(window.innerWidth - 20, x));
        y = Math.max(20, Math.min(window.innerHeight - 20, y));

        if (stateTimer > 0) {
            stateTimer--;
        } else if (state !== 'idle') {
            state = 'idle';
        }

        petEl.style.left = x + 'px';
        petEl.style.top = y + 'px';

        particles = particles.filter(p => {
            p.x += p.vx;
            p.y += p.vy;
            p.vy += 0.1;
            p.life -= 0.03;
            return p.life > 0;
        });

        updatePetAppearance();
        updateStats();
        renderParticles();

        if (Math.random() < 0.001) {
            PET.hunger = Math.max(0, PET.hunger - 1);
            PET.happiness = Math.max(0, PET.happiness - 0.5);
            PET.energy = Math.min(100, PET.energy + 0.5);
            save();
        }

        animFrame = requestAnimationFrame(updatePet);
    }

    let particlesCanvas, particlesCtx;
    function renderParticles() {
        if (!particlesCanvas) {
            particlesCanvas = document.createElement('canvas');
            particlesCanvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:2147483646;';
            document.body.appendChild(particlesCanvas);
            particlesCanvas.width = window.innerWidth;
            particlesCanvas.height = window.innerHeight;
            particlesCtx = particlesCanvas.getContext('2d');
        }
        if (!particlesCtx) return;
        particlesCtx.clearRect(0, 0, particlesCanvas.width, particlesCanvas.height);
        particles.forEach(p => {
            particlesCtx.globalAlpha = p.life;
            particlesCtx.font = p.size + 'px serif';
            particlesCtx.fillText(p.emoji, p.x, p.y);
        });
        particlesCtx.globalAlpha = 1;
    }

    function init() {
        GM_addStyle(`
            @keyframes petMsg { 
                0% { opacity: 0; transform: translateY(10px); } 
                20% { opacity: 1; transform: translateY(0); } 
                80% { opacity: 1; } 
                100% { opacity: 0; transform: translateY(-30px); } 
            }
        `);

        document.addEventListener('mousemove', (e) => {
            mouseX = e.clientX;
            mouseY = e.clientY;
        });

        document.addEventListener('click', (e) => {
            if (e.target.closest('#pet-menu') || e.target.closest('#virtual-pet') || e.target.closest('#pet-stats')) return;
            if (document.getElementById('pet-menu')) {
                document.getElementById('pet-menu').remove();
                menuEl = null;
            }
        });

        window.addEventListener('resize', () => {
            if (particlesCanvas) {
                particlesCanvas.width = window.innerWidth;
                particlesCanvas.height = window.innerHeight;
            }
        });

        createPet();
        createStats();
        updatePet();

        try {
            GM_notification({ text: `🐾 ${PET.name} is here! Click to pet, double-click for menu.`, timeout: 3000 });
        } catch(e) {}

        console.log('🐾 Virtual Pet loaded!');
    }

    waitForBody(init);
})();