Skip to content

Instantly share code, notes, and snippets.

@svenbledt
Forked from Rakise/DiscordQuest.md
Created December 20, 2025 03:09
Show Gist options
  • Select an option

  • Save svenbledt/39149a9aa9880b7a36ba0d37466891b0 to your computer and use it in GitHub Desktop.

Select an option

Save svenbledt/39149a9aa9880b7a36ba0d37466891b0 to your computer and use it in GitHub Desktop.

Since the original creator aamiaa dont want improvements/new features to their code. i made this gist with some improvements/QOL stuff

my version still respects discord API

How to use this script:

  1. Accept a quest under Discover -> Quests
  2. Press Ctrl+Shift+I to open DevTools
  3. Go to the Console tab
  4. Paste the following code and hit enter:
Click to expand
{
    // --- 0. Cleanup Previous Instances ---
    const existingUI = document.getElementById('autoquest-overlay');
    if (existingUI) existingUI.remove();
    const existingCanvas = document.getElementById('aq-confetti');
    if (existingCanvas) existingCanvas.remove();
    
    // --- 1. Discord Internals Setup ---
    delete window.$;
    let wpRequire = webpackChunkdiscord_app.push([[Symbol()], {}, r => r]);
    webpackChunkdiscord_app.pop();

    let ApplicationStreamingStore = Object.values(wpRequire.c).find(x => x?.exports?.Z?.__proto__?.getStreamerActiveStreamMetadata).exports.Z;
    let RunningGameStore = Object.values(wpRequire.c).find(x => x?.exports?.ZP?.getRunningGames).exports.ZP;
    let QuestsStore = Object.values(wpRequire.c).find(x => x?.exports?.Z?.__proto__?.getQuest).exports.Z;
    let ChannelStore = Object.values(wpRequire.c).find(x => x?.exports?.Z?.__proto__?.getAllThreadsForParent).exports.Z;
    let GuildChannelStore = Object.values(wpRequire.c).find(x => x?.exports?.ZP?.getSFWDefaultChannel).exports.ZP;
    let FluxDispatcher = Object.values(wpRequire.c).find(x => x?.exports?.Z?.__proto__?.flushWaitQueue).exports.Z;
    let api = Object.values(wpRequire.c).find(x => x?.exports?.tn?.get).exports.tn;

    const isApp = typeof DiscordNative !== "undefined";
    const SLEEP_BUFFER = 5000; 

    // --- 2. VFX: Confetti Engine ---
    const fireConfetti = (duration = 2000, amount = 100) => {
        const canvas = document.createElement('canvas');
        canvas.id = 'aq-confetti';
        canvas.style.position = 'fixed';
        canvas.style.top = '0';
        canvas.style.left = '0';
        canvas.style.width = '100vw';
        canvas.style.height = '100vh';
        canvas.style.pointerEvents = 'none';
        canvas.style.zIndex = '100000';
        document.body.appendChild(canvas);

        const ctx = canvas.getContext('2d');
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;

        const particles = [];
        const colors = ['#5865F2', '#57F287', '#EB459E', '#FEE75C', '#ffffff'];

        for (let i = 0; i < amount; i++) {
            particles.push({
                x: Math.random() * canvas.width,
                y: Math.random() * canvas.height - canvas.height,
                vx: Math.random() * 4 - 2,
                vy: Math.random() * 4 + 2,
                color: colors[Math.floor(Math.random() * colors.length)],
                size: Math.random() * 8 + 4,
                rotation: Math.random() * 360
            });
        }

        let start = null;
        const animate = (timestamp) => {
            if (!start) start = timestamp;
            const progress = timestamp - start;
            ctx.clearRect(0, 0, canvas.width, canvas.height);

            particles.forEach(p => {
                p.x += p.vx;
                p.y += p.vy;
                p.rotation += 2;
                
                ctx.save();
                ctx.translate(p.x, p.y);
                ctx.rotate(p.rotation * Math.PI / 180);
                ctx.fillStyle = p.color;
                ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);
                ctx.restore();

                if (p.y > canvas.height) p.y = -20;
            });

            if (progress < duration) {
                requestAnimationFrame(animate);
            } else {
                canvas.remove();
            }
        };
        requestAnimationFrame(animate);
    };

    // --- 3. UI Builder & State Management ---
    
    const colors = {
        bg: '#2b2d31', 
        header: '#1e1f22',
        primary: '#5865F2',
        success: '#23a559',
        text: '#dbdee1',
        subtext: '#949ba4',
        barBg: '#3f4147'
    };

    const styles = `
        #autoquest-overlay {
            position: fixed;
            top: 100px;
            left: 100px;
            width: 400px;
            height: auto;
            min-width: 300px;
            min-height: 100px;
            max-height: 80vh;
            background: rgba(43, 45, 49, 0.98);
            border-radius: 12px;
            box-shadow: 0 12px 32px rgba(0,0,0,0.4);
            font-family: 'gg sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
            color: ${colors.text};
            z-index: 99999;
            overflow: hidden;
            display: flex;
            flex-direction: column;
            border: 1px solid rgba(255,255,255,0.08);
            backdrop-filter: blur(12px);
            transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1), 
                        height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
                        border-radius 0.3s,
                        opacity 0.2s,
                        transform 0.2s;
        }
        
        /* Minimized State */
        #autoquest-overlay.minimized {
            width: 60px !important;
            height: 60px !important;
            min-width: 0 !important; 
            min-height: 0 !important;
            border-radius: 50% !important;
            cursor: pointer;
            overflow: hidden;
            background: ${colors.primary};
            box-shadow: 0 4px 12px rgba(88, 101, 242, 0.4);
            border: 2px solid rgba(255,255,255,0.2);
        }
        #autoquest-overlay.minimized #autoquest-header,
        #autoquest-overlay.minimized #autoquest-content,
        #autoquest-overlay.minimized #autoquest-footer,
        #autoquest-overlay.minimized .aq-resize-handle {
            display: none;
        }
        #autoquest-overlay.minimized::after {
            content: "AQ";
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: white;
            font-weight: 900;
            font-size: 20px;
            pointer-events: none;
        }
        #autoquest-overlay.minimized:hover {
            transform: scale(1.1);
        }

        #autoquest-header {
            background: rgba(30, 31, 34, 0.8);
            padding: 14px 16px;
            font-weight: 700;
            display: flex;
            justify-content: space-between;
            align-items: center;
            border-bottom: 1px solid rgba(255,255,255,0.05);
            cursor: default;
            user-select: none;
            flex-shrink: 0;
        }
        .header-controls { display: flex; gap: 8px; }
        
        #autoquest-content {
            padding: 16px;
            flex: 1;
            overflow-y: auto;
            min-height: 0; 
        }
        #autoquest-content::-webkit-scrollbar { width: 6px; }
        #autoquest-content::-webkit-scrollbar-track { background: transparent; }
        #autoquest-content::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.4); border-radius: 4px; }
        
        .quest-card {
            position: relative;
            background: rgba(30, 31, 34, 0.4);
            border-radius: 8px;
            padding: 12px;
            margin-bottom: 10px;
            border: 1px solid rgba(255,255,255,0.05);
            transition: transform 0.2s, border-color 0.2s;
            display: flex;
            gap: 12px;
            align-items: center;
            overflow: hidden;
        }
        .quest-card:hover {
            border-color: rgba(255,255,255,0.2);
            transform: translateY(-2px);
        }
        
        .quest-info {
            flex: 1;
            min-width: 0; 
            z-index: 1;
        }
        .quest-title {
            font-weight: 700;
            font-size: 14px;
            margin-bottom: 4px;
            color: #fff;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            text-shadow: 0 1px 2px rgba(0,0,0,0.8);
        }
        .quest-meta {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 6px;
        }
        .quest-type {
            font-size: 10px;
            color: #fff;
            text-transform: uppercase;
            font-weight: 800;
            letter-spacing: 0.5px;
            background: rgba(0,0,0,0.4);
            padding: 2px 6px;
            border-radius: 4px;
            backdrop-filter: blur(4px);
        }
        .quest-timer {
            font-size: 11px;
            font-family: 'Consolas', 'Monaco', monospace;
            color: ${colors.text};
            font-variant-numeric: tabular-nums;
            background: rgba(0,0,0,0.4);
            padding: 2px 4px;
            border-radius: 4px;
        }
        .progress-track {
            height: 6px;
            background: rgba(0,0,0,0.6);
            border-radius: 3px;
            overflow: hidden;
            margin-top: 6px;
        }
        .progress-fill {
            height: 100%;
            background: ${colors.primary};
            width: 0%;
            transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
            position: relative;
            overflow: hidden;
        }
        .progress-fill::after {
            content: '';
            position: absolute;
            top: 0; left: 0; bottom: 0; right: 0;
            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
            transform: translateX(-100%);
            animation: shimmer 2s infinite;
        }
        @keyframes shimmer { 100% { transform: translateX(100%); } }

        .status-text {
            font-size: 11px;
            color: #efefef;
            margin-top: 4px;
            display: flex;
            justify-content: space-between;
            font-weight: 500;
            text-shadow: 0 1px 2px rgba(0,0,0,0.8);
        }
        #autoquest-footer {
            padding: 16px;
            background: rgba(30, 31, 34, 0.8);
            display: flex;
            gap: 10px;
            border-top: 1px solid rgba(255,255,255,0.05);
            flex-shrink: 0;
        }
        .aq-btn {
            flex: 1;
            padding: 10px;
            border: none;
            border-radius: 6px;
            color: #fff;
            font-weight: 600;
            font-size: 13px;
            cursor: pointer;
            transition: all 0.2s;
            position: relative;
            overflow: hidden;
        }
        .aq-btn:active { transform: scale(0.97); }
        .aq-btn:hover { opacity: 0.9; }
        .aq-btn-start { 
            background: linear-gradient(90deg, ${colors.primary}, #4752C4); 
            box-shadow: 0 4px 12px rgba(88, 101, 242, 0.3);
        }
        .aq-btn-icon {
            width: 28px; 
            height: 28px; 
            padding: 0; 
            display: flex; 
            align-items: center; 
            justify-content: center; 
            border-radius: 6px;
            cursor: pointer;
            flex: 0 0 auto;
            background: rgba(255,255,255,0.1);
            color: #dbdee1;
            border: none;
            font-family: monospace;
            font-size: 16px;
        }
        .aq-btn-icon:hover { background: rgba(255,255,255,0.2); color: #fff; }
        .aq-btn-close:hover { background: #da373c; }
        
        .aq-resize-handle {
            position: absolute;
            bottom: 0;
            right: 0;
            width: 20px;
            height: 20px;
            cursor: nwse-resize;
            background: radial-gradient(circle at bottom right, rgba(255,255,255,0.2) 0%, transparent 50%);
            border-bottom-right-radius: 12px;
            z-index: 10;
        }
    `;

    // Inject Styles
    const styleEl = document.createElement('style');
    styleEl.textContent = styles;
    document.head.appendChild(styleEl);

    // Create Main Container
    const container = document.createElement('div');
    container.id = 'autoquest-overlay';
    container.innerHTML = `
        <div id="autoquest-header">
            <span style="display:flex; align-items:center; gap:10px;">
                <img src="https://cdn.discordapp.com/embed/avatars/0.png" style="width:24px; height:24px; border-radius:50%;" id="aq-header-icon">
                AutoQuest
            </span>
            <div class="header-controls">
                <button class="aq-btn-icon" id="aq-refresh-btn" title="Refresh Quests">↻</button>
                <button class="aq-btn-icon" id="aq-min-btn" title="Minimize">_</button>
                <button class="aq-btn-icon aq-btn-close" title="Close">×</button>
            </div>
        </div>
        <div id="autoquest-content">
            <div style="text-align: center; color: #949ba4; padding: 40px 20px;">
                <div style="margin-bottom:10px; font-size:24px;">🔍</div>
                Scanning quests...
            </div>
        </div>
        <div id="autoquest-footer">
            <button id="aq-start-btn" class="aq-btn aq-btn-start">Start All Quests</button>
        </div>
        <div class="aq-resize-handle"></div>
    `;
    document.body.appendChild(container);

    // --- State & Interaction Logic ---
    const header = document.getElementById('autoquest-header');
    const resizeHandle = container.querySelector('.aq-resize-handle');
    const minBtn = document.getElementById('aq-min-btn');
    const refreshBtn = document.getElementById('aq-refresh-btn');
    
    let isDragging = false, isResizing = false, isMinimized = false;
    let dragOffset = { x: 0, y: 0 };
    let resizeStart = { w: 0, h: 0, x: 0, y: 0 };
    let rafId = null;
    let savedSize = { w: 380, h: 500 }; 
    let hasMoved = false; // Flag to track if dragging occurred

    // Minimize Logic
    const toggleMinimize = () => {
        isMinimized = !isMinimized;
        container.classList.toggle('minimized', isMinimized);
        
        if (isMinimized) {
            savedSize = { w: container.offsetWidth, h: container.offsetHeight };
        } else {
            container.style.width = `${savedSize.w}px`;
            container.style.height = `${savedSize.h}px`;
        }
    };

    minBtn.onclick = (e) => { e.stopPropagation(); toggleMinimize(); };
    
    // Global mouse down on container to handle "ball" dragging
    container.onmousedown = (e) => {
        // If maximized, only dragging via header is allowed (handled below)
        // If minimized, dragging anywhere on ball is allowed
        if (!isMinimized) return; // Let header handler do it if maximized

        e.preventDefault(); // Prevent text selection
        isDragging = true;
        hasMoved = false;
        
        const rect = container.getBoundingClientRect();
        dragOffset.x = e.clientX - rect.left;
        dragOffset.y = e.clientY - rect.top;
        container.style.cursor = 'grabbing';
    };

    // Restore on click (if minimized and NOT dragged)
    container.onclick = (e) => {
        if(isMinimized && e.target === container && !hasMoved) toggleMinimize();
    };

    // Header Drag Logic (Maximized)
    header.onmousedown = (e) => {
        if(e.target.closest('button')) return;
        isDragging = true;
        hasMoved = false;
        const rect = container.getBoundingClientRect();
        dragOffset.x = e.clientX - rect.left;
        dragOffset.y = e.clientY - rect.top;
        header.style.cursor = 'grabbing';
    };

    // Resize Logic
    resizeHandle.onmousedown = (e) => {
        e.stopPropagation();
        isResizing = true;
        const rect = container.getBoundingClientRect();
        resizeStart = { w: rect.width, h: rect.height, x: e.clientX, y: e.clientY };
    };

    // Movement
    document.onmousemove = (e) => {
        if (!isDragging && !isResizing) return;
        
        if (isDragging) hasMoved = true;

        if (rafId) cancelAnimationFrame(rafId);
        rafId = requestAnimationFrame(() => {
            if (isDragging) {
                const newLeft = e.clientX - dragOffset.x;
                const newTop = e.clientY - dragOffset.y;
                container.style.left = `${newLeft}px`;
                container.style.top = `${newTop}px`;
            } 
            else if (isResizing && !isMinimized) {
                const deltaX = e.clientX - resizeStart.x;
                const deltaY = e.clientY - resizeStart.y;
                container.style.width = `${Math.max(300, resizeStart.w + deltaX)}px`;
                container.style.height = `${Math.max(200, resizeStart.h + deltaY)}px`;
            }
        });
    };

    document.onmouseup = () => {
        isDragging = false;
        isResizing = false;
        header.style.cursor = 'default';
        container.style.cursor = isMinimized ? 'pointer' : 'default';
        if (rafId) cancelAnimationFrame(rafId);
    };

    container.querySelector('.aq-btn-close').onclick = (e) => {
        e.stopPropagation();
        container.remove();
        styleEl.remove();
        if(autoRefreshInterval) clearInterval(autoRefreshInterval);
    };
    
    // --- UI Helpers ---
    const contentArea = document.getElementById('autoquest-content');
    const timers = new Map();
    let autoRefreshInterval = null;

    const formatTime = (seconds) => {
        if (seconds <= 0) return "00:00";
        const m = Math.floor(seconds / 60);
        const s = Math.floor(seconds % 60);
        return `${m}:${s.toString().padStart(2, '0')}`;
    };

    const updateUI = (questId, percent, status, remainingSecs = null, isDone = false) => {
        const card = document.getElementById(`quest-card-${questId}`);
        if (!card) return;
        
        const bar = card.querySelector('.progress-fill');
        const statusEl = card.querySelector('.status-detail');
        const timerEl = card.querySelector('.quest-timer');

        if(bar) bar.style.width = `${Math.min(100, percent)}%`;
        if(statusEl) statusEl.innerText = status;
        
        if (remainingSecs !== null && timerEl) {
             timerEl.innerText = formatTime(remainingSecs);
        }
        
        if (isDone) {
            if(bar) {
                bar.style.background = colors.success;
                bar.style.boxShadow = "none";
            }
            if(statusEl) {
                statusEl.style.color = colors.success;
                statusEl.innerText = "Completed!";
            }
            if(timerEl) timerEl.innerText = "Done";
            
            if (Math.random() > 0.7) fireConfetti(1000, 30); 

            if (timers.has(questId)) {
                clearInterval(timers.get(questId));
                timers.delete(questId);
            }
        }
    };

    const startLocalTimer = (questId, initialSeconds) => {
        if (timers.has(questId)) clearInterval(timers.get(questId));
        
        let remaining = initialSeconds;
        const card = document.getElementById(`quest-card-${questId}`);
        const timerEl = card?.querySelector('.quest-timer');
        
        if(timerEl) timerEl.innerText = formatTime(remaining);

        const interval = setInterval(() => {
            remaining--;
            if (remaining < 0) remaining = 0;
            if(timerEl) timerEl.innerText = formatTime(remaining);
            if(!document.getElementById(`quest-card-${questId}`)) clearInterval(interval);
        }, 1000);

        timers.set(questId, interval);
    };

    const createQuestCard = (q, type) => {
        const div = document.createElement('div');
        div.className = 'quest-card';
        div.id = `quest-card-${q.id}`;
        
        let typeClass = 'badge-game';
        if(type.includes("VIDEO")) typeClass = 'badge-video';
        if(type.includes("STREAM")) typeClass = 'badge-stream';
        if(type.includes("ACTIVITY")) typeClass = 'badge-activity';

        div.innerHTML = `
            <div class="quest-info">
                <div class="quest-meta">
                    <span class="quest-type ${typeClass}">${type.replace(/_/g, ' ')}</span>
                    <span class="quest-timer">--:--</span>
                </div>
                <div class="quest-title">${q.config.messages.questName}</div>
                <div class="progress-track">
                    <div class="progress-fill" style="width: 0%"></div>
                </div>
                <div class="status-text">
                    <span class="status-detail">Ready to start</span>
                </div>
            </div>
        `;
        return div;
    };

    // --- 4. Task Logic ---

    const handleVideoTask = async (quest, taskName, secondsNeeded, secondsDone) => {
        let remaining = secondsNeeded - secondsDone;
        updateUI(quest.id, (secondsDone/secondsNeeded)*100, "Watching...", remaining);
        startLocalTimer(quest.id, remaining);
        
        const maxFuture = 10, speed = 7, interval = 1;
        const enrolledAt = new Date(quest.userStatus.enrolledAt).getTime();
        
        while (true) {
            const maxAllowed = Math.floor((Date.now() - enrolledAt) / 1000) + maxFuture;
            const diff = maxAllowed - secondsDone;
            const timestamp = secondsDone + speed;

            if (diff >= speed) {
                const res = await api.post({
                    url: `/quests/${quest.id}/video-progress`,
                    body: { timestamp: Math.min(secondsNeeded, timestamp + Math.random()) }
                });
                secondsDone = Math.min(secondsNeeded, timestamp);
                updateUI(quest.id, (secondsDone/secondsNeeded)*100, "Watching...");

                if (res.body.completed_at != null || secondsDone >= secondsNeeded) break;
            }
            await new Promise(r => setTimeout(r, interval * 1000));
        }

        await api.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: secondsNeeded } });
        updateUI(quest.id, 100, "Completed!", 0, true);
    };

    const handleActivityTask = async (quest, taskName, secondsNeeded, secondsDone) => {
        const channelId = ChannelStore.getSortedPrivateChannels()[0]?.id 
            ?? Object.values(GuildChannelStore.getAllGuilds()).find(x => x != null && x.VOCAL.length > 0)?.VOCAL[0].channel.id;
            
        if (!channelId) {
            updateUI(quest.id, 0, "Error: No Voice Channel found");
            return;
        }

        let remaining = secondsNeeded - secondsDone;
        const streamKey = `call:${channelId}:1`;
        
        updateUI(quest.id, 0, "Active...", remaining);
        startLocalTimer(quest.id, remaining);

        while (true) {
            const res = await api.post({
                url: `/quests/${quest.id}/heartbeat`, 
                body: { stream_key: streamKey, terminal: false }
            });
            
            const progress = res.body.progress.PLAY_ACTIVITY.value;
            updateUI(quest.id, (progress/secondsNeeded)*100, "In Activity...");

            if (progress >= secondsNeeded) {
                await api.post({
                    url: `/quests/${quest.id}/heartbeat`, 
                    body: { stream_key: streamKey, terminal: true }
                });
                break;
            }
            
            await new Promise(r => setTimeout(r, 20 * 1000));
        }
        updateUI(quest.id, 100, "Completed!", 0, true);
    };

    const handleDesktopPlayTask = (quest, applicationId, applicationName, secondsNeeded, pid) => {
        return new Promise((resolve) => {
            if (!isApp) {
                updateUI(quest.id, 0, "Error: Desktop App Required");
                return resolve();
            }

            updateUI(quest.id, 0, "Starting Game...", secondsNeeded);
            
            api.get({ url: `/applications/public?application_ids=${applicationId}` }).then(res => {
                const appData = res.body[0];
                const exeName = appData.executables.find(x => x.os === "win32").name.replace(">", "");

                const fakeGame = {
                    cmdLine: `C:\\Program Files\\${appData.name}\\${exeName}`,
                    exeName,
                    exePath: `c:/program files/${appData.name.toLowerCase()}/${exeName}`,
                    hidden: false,
                    isLauncher: false,
                    id: applicationId,
                    name: appData.name,
                    pid: pid,
                    pidPath: [pid],
                    processName: appData.name,
                    start: Date.now(),
                };

                const realGetRunningGames = RunningGameStore.getRunningGames;
                const realGetGameForPID = RunningGameStore.getGameForPID;
                const realGames = RunningGameStore.getRunningGames();
                
                RunningGameStore.getRunningGames = () => [fakeGame];
                RunningGameStore.getGameForPID = (pid) => pid === fakeGame.pid ? fakeGame : null;
                
                FluxDispatcher.dispatch({ type: "RUNNING_GAMES_CHANGE", removed: realGames, added: [fakeGame], games: [fakeGame] });

                const fn = (data) => {
                    let progress = quest.config.configVersion === 1 
                        ? data.userStatus.streamProgressSeconds 
                        : Math.floor(data.userStatus.progress.PLAY_ON_DESKTOP.value);
                    
                    const rem = Math.max(0, secondsNeeded - progress);
                    if (!timers.has(quest.id)) startLocalTimer(quest.id, rem);
                    
                    updateUI(quest.id, (progress/secondsNeeded)*100, "Playing Game...", rem);

                    if (progress >= secondsNeeded) {
                        RunningGameStore.getRunningGames = realGetRunningGames;
                        RunningGameStore.getGameForPID = realGetGameForPID;
                        FluxDispatcher.dispatch({ type: "RUNNING_GAMES_CHANGE", removed: [fakeGame], added: [], games: [] });
                        FluxDispatcher.unsubscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn);
                        updateUI(quest.id, 100, "Completed!", 0, true);
                        resolve();
                    }
                };

                FluxDispatcher.subscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn);
            });
        });
    };

    const handleDesktopStreamTask = (quest, applicationId, applicationName, secondsNeeded, pid) => {
        return new Promise((resolve) => {
            if (!isApp) {
                updateUI(quest.id, 0, "Error: Desktop App Required");
                return resolve();
            }

            updateUI(quest.id, 0, "Starting Stream...", secondsNeeded);

            const realFunc = ApplicationStreamingStore.getStreamerActiveStreamMetadata;
            ApplicationStreamingStore.getStreamerActiveStreamMetadata = () => ({
                id: applicationId,
                pid,
                sourceName: null
            });

            const fn = (data) => {
                let progress = quest.config.configVersion === 1 
                    ? data.userStatus.streamProgressSeconds 
                    : Math.floor(data.userStatus.progress.STREAM_ON_DESKTOP.value);

                const rem = Math.max(0, secondsNeeded - progress);
                if (!timers.has(quest.id)) startLocalTimer(quest.id, rem);

                updateUI(quest.id, (progress/secondsNeeded)*100, "Streaming...", rem);

                if (progress >= secondsNeeded) {
                    ApplicationStreamingStore.getStreamerActiveStreamMetadata = realFunc;
                    FluxDispatcher.unsubscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn);
                    updateUI(quest.id, 100, "Completed!", 0, true);
                    resolve();
                }
            };

            FluxDispatcher.subscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn);
        });
    };

    const processQueue = async (quests, handler) => {
        for (const q of quests) {
             updateUI(q.id, 0, "Waiting in queue...", null);
        }

        for (const quest of quests) {
            try {
                const pid = Math.floor(Math.random() * 30000) + 1000;
                const applicationId = quest.config.application.id;
                const applicationName = quest.config.application.name;
                const taskConfig = quest.config.taskConfig ?? quest.config.taskConfigV2;
                const taskName = Object.keys(taskConfig.tasks).find(k => k.includes("PLAY") || k.includes("STREAM") || k.includes("VIDEO"));
                const secondsNeeded = taskConfig.tasks[taskName].target;
                const currentProgress = quest.userStatus?.progress?.[taskName]?.value ?? 0;
                
                updateUI(quest.id, (currentProgress/secondsNeeded)*100, "Starting...", secondsNeeded - currentProgress);
                await handler(quest, applicationId, applicationName, secondsNeeded, pid);
                
                if (quests.indexOf(quest) < quests.length - 1) {
                    await new Promise(r => setTimeout(r, SLEEP_BUFFER));
                }
            } catch (e) {
                console.error(e);
                updateUI(quest.id, 0, "Error Occurred");
            }
        }
    };

    // --- 5. Initialization ---

    let buckets = { video: [], activity: [], game: [], stream: [] };

    const scanQuests = () => {
        const rawQuests = QuestsStore.quests;
        const questsList = (rawQuests instanceof Map) ? [...rawQuests.values()] : Object.values(rawQuests);

        contentArea.innerHTML = '';
        buckets = { video: [], activity: [], game: [], stream: [] };
        let count = 0;

        for (const q of questsList) {
            const isEnrolled = !!q.userStatus?.enrolledAt;
            const isCompleted = !!q.userStatus?.completedAt;
            const isExpired = new Date(q.config.expiresAt).getTime() <= Date.now();
            
            if (q.id === "1412491570820812933") continue; 
            if (isCompleted || isExpired || !isEnrolled) continue;

            const taskConfig = q.config.taskConfig ?? q.config.taskConfigV2;
            const taskName = ["WATCH_VIDEO", "PLAY_ON_DESKTOP", "STREAM_ON_DESKTOP", "PLAY_ACTIVITY", "WATCH_VIDEO_ON_MOBILE"]
                .find(x => taskConfig.tasks[x] != null);

            if (!taskName) continue;
            const secondsNeeded = taskConfig.tasks[taskName].target;
            const secondsDone = q.userStatus?.progress?.[taskName]?.value ?? 0;
            if (secondsDone >= secondsNeeded) continue;

            // Add to UI
            contentArea.appendChild(createQuestCard(q, taskName));
            
            // Initialize Timer Text
            const rem = Math.max(0, secondsNeeded - secondsDone);
            const tEl = document.getElementById(`quest-card-${q.id}`).querySelector('.quest-timer');
            if(tEl) tEl.innerText = formatTime(rem);
            
            count++;

            if (taskName.includes("VIDEO")) buckets.video.push({ q, taskName, secondsNeeded, secondsDone });
            else if (taskName === "PLAY_ACTIVITY") buckets.activity.push({ q, taskName, secondsNeeded, secondsDone });
            else if (taskName === "PLAY_ON_DESKTOP") buckets.game.push(q);
            else if (taskName === "STREAM_ON_DESKTOP") buckets.stream.push(q);
        }

        if(count === 0) {
            contentArea.innerHTML = '<div style="text-align:center; padding: 40px; color: #949ba4;">No active quests found.<br>Make sure you accepted them!</div>';
            return;
        }
    };

    const startExecution = async (btn) => {
        btn.innerText = 'Tasks Running...';
        btn.disabled = true;
        btn.style.opacity = '0.5';
        btn.style.cursor = 'wait';

        const promises = [];
        
        // Parallel
        buckets.video.forEach(item => promises.push(handleVideoTask(item.q, item.taskName, item.secondsNeeded, item.secondsDone)));
        buckets.activity.forEach(item => promises.push(handleActivityTask(item.q, item.taskName, item.secondsNeeded, item.secondsDone)));
        
        // Sequential Queues
        if (buckets.game.length > 0) promises.push(processQueue(buckets.game, handleDesktopPlayTask));
        if (buckets.stream.length > 0) promises.push(processQueue(buckets.stream, handleDesktopStreamTask));

        await Promise.allSettled(promises);
        
        btn.innerText = 'All Quests Completed';
        btn.style.background = colors.success;
        btn.style.opacity = '1';
        btn.style.cursor = 'default';
        
        fireConfetti(3000, 300);
    };

    const init = () => {
        scanQuests();
        
        const startBtn = document.getElementById('aq-start-btn');
        if(startBtn) startBtn.onclick = () => startExecution(startBtn);

        // Refresh Handler
        if(refreshBtn) {
            refreshBtn.onclick = () => {
                refreshBtn.style.transform = 'rotate(360deg)';
                refreshBtn.style.transition = 'transform 0.5s';
                scanQuests();
                const btn = document.getElementById('aq-start-btn');
                if(btn && buckets.video.length + buckets.game.length + buckets.stream.length + buckets.activity.length > 0) {
                    btn.onclick = () => startExecution(btn);
                }
                setTimeout(() => { refreshBtn.style.transform = 'none'; }, 500);
            };
        }

        // Auto Refresh
        autoRefreshInterval = setInterval(() => {
            // Only auto-refresh if nothing is actively processing to avoid UI jitters
            // (In a real app we'd diff the state, but simple re-scan is safe here if idle)
            // For now, we will just log heartbeat
            // console.log("AutoQuest Heartbeat..."); 
        }, 30000);
    };

    init();
    console.log("AutoQuest UI Loaded");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment