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:
- Accept a quest under Discover -> Quests
- Press Ctrl+Shift+I to open DevTools
- Go to the
Consoletab - 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");
}