(() => { 'use strict'; const els = { input: document.getElementById('questInput'), button: document.getElementById('questBtn'), status: document.getElementById('questStatus'), results: document.getElementById('questResults'), toolbar: document.querySelector('.quest-toolbar') }; function showStatus(msg, ok = false) { if (!els.status) return; els.status.textContent = msg; els.status.style.color = ok ? 'var(--fg)' : 'var(--fg-soft)'; } function escapeHTML(s = '') { return s.toString() .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } async function fetchJSON(url) { try { const res = await fetch(url, { cache: 'no-store' }); if (!res.ok) return null; return await res.json(); } catch { return null; } } let allTasks = null; async function loadLocalData() { if (Array.isArray(allTasks)) return allTasks; els.results?.setAttribute('aria-busy', 'true'); try { const json = await fetchJSON('/quests.json'); const tasks = (Array.isArray(json?.tasks) && json.tasks) || (Array.isArray(json?.data?.tasks) && json.data.tasks) || []; // ✅ FIX: assign the cache allTasks = tasks; return allTasks; } finally { els.results?.removeAttribute('aria-busy'); } } function renderQuest(task) { const wrap = document.createElement('article'); wrap.className = 'quest-card trader-card'; const h2 = document.createElement('h2'); h2.className = 'quest-name trader-name'; h2.textContent = task.name || 'Unknown Quest'; wrap.appendChild(h2); if (task.trader || task.wikiLink) { const meta = document.createElement('div'); meta.className = 'quest-meta'; if (task.trader) { const box = document.createElement('div'); box.className = 'quest-trader'; box.style.display = 'flex'; box.style.alignItems = 'center'; if (task.trader.imageLink) { const img = document.createElement('img'); img.src = task.trader.imageLink; img.alt = task.trader.name || 'Trader'; img.loading = 'lazy'; img.width = 36; img.height = 36; img.style.borderRadius = '6px'; img.style.marginRight = '8px'; img.style.border = '1px solid rgba(0,255,102,0.2)'; box.appendChild(img); } const nm = document.createElement('span'); nm.textContent = task.trader.name || 'Trader'; box.appendChild(nm); meta.appendChild(box); } if (task.wikiLink) { const wiki = document.createElement('a'); wiki.href = task.wikiLink; wiki.target = '_blank'; wiki.rel = 'noopener noreferrer'; wiki.className = 'btn'; wiki.style.marginLeft = 'auto'; wiki.textContent = 'Open Wiki'; meta.appendChild(wiki); } wrap.appendChild(meta); } const objectives = Array.isArray(task.objectives) ? task.objectives : []; const ol = document.createElement('ol'); ol.className = 'quest-objectives'; if (!objectives.length) { const li = document.createElement('li'); li.className = 'quest-objective'; li.textContent = 'No objectives listed.'; ol.appendChild(li); } else { for (const obj of objectives) { const li = document.createElement('li'); li.className = 'quest-objective'; const lines = []; const type = obj.type ? `${escapeHTML(obj.type)}` : 'Objective'; const desc = escapeHTML(obj.description || '—'); const maps = (obj.maps && obj.maps.length) ? ` — Map: ${escapeHTML(obj.maps.map(m => m.normalizedName).join(', '))}` : ''; lines.push(`${type}: ${desc}${maps}`); if (typeof obj.count === 'number' && (obj.item || (obj.items && obj.items.length))) { const baseName = obj.item?.name || obj.items?.[0]?.name || 'item'; const fir = obj.foundInRaid ? ' (FIR)' : ''; lines.push(`Hand in: ${escapeHTML(baseName)} × ${obj.count}${escapeHTML(fir)}`); } if (obj.targetNames && obj.targetNames.length && typeof obj.count === 'number') { lines.push(`Eliminate: ${escapeHTML(obj.targetNames.join(', '))} × ${obj.count}`); } li.innerHTML = lines.map(l => `
${l}
`).join(''); ol.appendChild(li); } } wrap.appendChild(ol); const fr = task.finishRewards; if (fr) { const box = document.createElement('div'); box.className = 'quest-rewards'; function addRow(label, valueHTML) { const row = document.createElement('div'); row.innerHTML = `${escapeHTML(label)}: ${valueHTML}`; box.appendChild(row); } if (Array.isArray(fr.items) && fr.items.length) { const grid = document.createElement('div'); grid.style.display = 'grid'; grid.style.gridTemplateColumns = 'repeat(auto-fit, minmax(180px, 1fr))'; grid.style.gap = '8px'; for (const g of fr.items) { const c = document.createElement('div'); c.style.display = 'flex'; c.style.alignItems = 'center'; c.style.gap = '8px'; if (g.item?.iconLink) { const img = document.createElement('img'); img.src = g.item.iconLink; img.alt = g.item.name || ''; img.width = 24; img.height = 24; img.style.border = '1px solid rgba(0,255,102,0.2)'; img.style.borderRadius = '4px'; c.appendChild(img); } const span = document.createElement('span'); const nm = g.item?.name || 'Item'; const ct = (typeof g.count === 'number') ? ` × ${g.count}` : ''; span.textContent = `${nm}${ct}`; c.appendChild(span); grid.appendChild(c); } const row = document.createElement('div'); row.innerHTML = `Items:`; row.appendChild(grid); box.appendChild(row); } if (Array.isArray(fr.traderStanding) && fr.traderStanding.length) { const parts = fr.traderStanding.map(s => { const t = s.trader?.name || 'Trader'; const v = typeof s.standing === 'number' ? `${s.standing > 0 ? '+' : ''}${s.standing}` : (s.standing ?? ''); return `${t}: ${v}`; }); addRow('Standing', escapeHTML(parts.join(', '))); } if (fr.offerUnlock) { const list = Array.isArray(fr.offerUnlock) ? fr.offerUnlock : [fr.offerUnlock]; const ul = document.createElement('ul'); for (const u of list) { const li = document.createElement('li'); const itemName = u?.item?.name || 'Item'; const traderName = u?.trader?.name || 'Trader'; li.textContent = `${itemName} @ ${traderName}`; ul.appendChild(li); } const row = document.createElement('div'); row.innerHTML = `Offer unlocks:`; row.appendChild(ul); box.appendChild(row); } if (fr.skillLevelReward) { const list = Array.isArray(fr.skillLevelReward) ? fr.skillLevelReward : [fr.skillLevelReward]; const ul = document.createElement('ul'); for (const s of list) { const li = document.createElement('li'); const skillObj = s?.skill; const skillName = skillObj?.name || skillObj?.id || 'Skill'; const lvl = typeof s?.level === 'number' ? ` +${s.level}` : ''; li.textContent = `${skillName}${lvl}`; ul.appendChild(li); } const row = document.createElement('div'); row.innerHTML = `Skill rewards:`; row.appendChild(ul); box.appendChild(row); } if (fr.traderUnlock) { const list = Array.isArray(fr.traderUnlock) ? fr.traderUnlock : [fr.traderUnlock]; const ul = document.createElement('ul'); for (const t of list) { const li = document.createElement('li'); li.textContent = t?.name || t?.id || 'Trader unlock'; ul.appendChild(li); } const row = document.createElement('div'); row.innerHTML = `Trader unlocks:`; row.appendChild(ul); box.appendChild(row); } if (fr.craftUnlock) { const list = Array.isArray(fr.craftUnlock) ? fr.craftUnlock : [fr.craftUnlock]; const ul = document.createElement('ul'); for (const p of list) { const li = document.createElement('li'); li.textContent = p?.name || p?.id || 'Craft unlock'; ul.appendChild(li); } const row = document.createElement('div'); row.innerHTML = `Craft unlocks:`; row.appendChild(ul); box.appendChild(row); } if (fr.achievement) { const list = Array.isArray(fr.achievement) ? fr.achievement : [fr.achievement]; const line = list.map(a => a?.name || a?.id || 'Achievement').join(', '); addRow('Achievement', escapeHTML(line)); } if (fr.customization) { const list = Array.isArray(fr.customization) ? fr.customization : [fr.customization]; const line = list.map(c => c?.name || c?.id || 'Customization').join(', '); addRow('Customization', escapeHTML(line)); } if (box.children.length) wrap.appendChild(box); } return wrap; } function getOrCreateAltMatchesContainer() { const id = 'questAltMatches'; let box = document.getElementById(id); if (box) { box.hidden = false; return box; } const results = document.getElementById('questResults'); if (!results || !results.parentNode) return null; box = document.createElement('div'); box.id = id; box.className = 'quest-alt-matches'; results.parentNode.insertBefore(box, results); return box; } function renderMatchList(matches) { const altBox = getOrCreateAltMatchesContainer(); if (!altBox) return; altBox.innerHTML = ''; if (!matches.length) { altBox.hidden = true; return; } const details = document.createElement('details'); const summary = document.createElement('summary'); summary.textContent = `Show ${matches.length} match(es)`; details.appendChild(summary); const ul = document.createElement('ul'); ul.style.listStyle = 'disc'; ul.style.paddingLeft = '1.25rem'; for (const m of matches) { const li = document.createElement('li'); const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'linklike'; btn.textContent = m.trader?.name ? `${m.name} — ${m.trader.name}` : m.name; btn.addEventListener('click', () => openTaskDetails(m.id)); li.appendChild(btn); ul.appendChild(li); } details.appendChild(ul); altBox.appendChild(details); } async function openTaskDetails(id) { try { showStatus('Loading task…'); els.results.innerHTML = ''; const list = await loadLocalData(); const task = list.find(t => t?.id === id); if (!task) { showStatus('Task not found', false); return; } const card = renderQuest(task); els.results.appendChild(card); showStatus('Ready', true); } catch (err) { console.error('[QuestSearch] open details failed:', err); showStatus('Failed to load task details.', false); } } async function search() { const q = (els.input?.value || '').trim(); const alt = getOrCreateAltMatchesContainer(); els.results.innerHTML = ''; if (alt) alt.innerHTML = ''; if (!q) { if (alt) alt.hidden = true; showStatus('Type a quest name to search.'); return; } try { showStatus('Searching…'); const list = await loadLocalData(); const safe = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const re = new RegExp(safe, 'i'); const matches = (Array.isArray(list) ? list : []).filter(t => re.test(t.name || '')); renderMatchList(matches); showStatus(`Found ${matches.length} match(es)`, true); if (matches.length === 1) await openTaskDetails(matches[0].id); } catch (err) { console.error('[QuestSearch] search failed:', err); showStatus('Search failed. Check console.', false); } } let lastEnterTs = 0; const ENTER_DEBOUNCE_MS = 250; els.button?.addEventListener('click', search); els.input?.addEventListener('keydown', (e) => { if (e.key === 'Enter') { const now = Date.now(); if (now - lastEnterTs < ENTER_DEBOUNCE_MS) return; lastEnterTs = now; e.preventDefault(); search(); } }); showStatus('Waiting to search…'); })();