MediaWiki:AchievementTracker.js: Difference between revisions
From EQOA Sandstorm Wiki
Jump to navigationJump to search
DevRenodor (talk | contribs) No edit summary Tags: Manual revert Reverted |
DevRenodor (talk | contribs) No edit summary |
||
| (2 intermediate revisions by the same user not shown) | |||
| Line 1,279: | Line 1,279: | ||
} | } | ||
} | } | ||
function renderRequirements(row) { | function renderRequirements(row) { | ||
| Line 1,470: | Line 1,471: | ||
} | } | ||
if (wanted === 'bestiary') { | |||
return ( | |||
subcategory === 'bestiary' || | |||
category === 'bestiary' | |||
); | |||
} | |||
return ( | return ( | ||
Latest revision as of 21:39, 16 July 2026
function initEqoaItemAutoDrops(root) {
root = root && root.jquery ? root[0] : (root || document);
var droppedBox = root.querySelector('.eqoa-auto-droppedby');
var zoneBox = root.querySelector('.eqoa-auto-zone');
if (!droppedBox || !zoneBox || droppedBox.classList.contains('eqoa-auto-drop-ready')) return;
droppedBox.classList.add('eqoa-auto-drop-ready');
var itemName =
(root.querySelector('.eqoa-misc-inspect-name') && root.querySelector('.eqoa-misc-inspect-name').textContent.trim()) ||
(root.querySelector('.item-header') && root.querySelector('.item-header').textContent.trim()) ||
mw.config.get('wgTitle');
if (!itemName) return;
function cleanText(text) {
return String(text || '').replace(/\s+/g, ' ').trim();
}
function norm(text) {
return cleanText(text).toLowerCase();
}
function esc(text) {
return String(text || '').replace(/[&<>"']/g, function (ch) {
return {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}[ch];
});
}
function pageLink(title) {
return '<a href="' + mw.util.getUrl(title) + '">' + esc(title) + '</a>';
}
function shouldReplace(box) {
var text = norm(box.textContent);
return !text || text === 'loading...' || text === 'unknown' || text === 'none';
}
var api = new mw.Api();
var foundDrops = [];
var foundZones = {};
var wanted = norm(itemName);
api.get({
action: 'query',
generator: 'backlinks',
gbltitle: itemName,
gbllimit: 'max',
prop: 'info',
format: 'json'
}).then(function (data) {
var pages = data.query && data.query.pages ? Object.values(data.query.pages) : [];
pages = pages.filter(function (page) {
return page.ns === 0;
});
if (!pages.length) {
if (shouldReplace(droppedBox)) droppedBox.textContent = 'Unknown';
if (shouldReplace(zoneBox)) zoneBox.textContent = 'Unknown';
return;
}
return Promise.all(pages.map(function (page) {
return api.get({
action: 'parse',
page: page.title,
prop: 'text',
format: 'json'
}).then(function (parsed) {
var html = parsed.parse && parsed.parse.text ? parsed.parse.text['*'] : '';
var doc = new DOMParser().parseFromString(html, 'text/html');
var rows = doc.querySelectorAll('.eqoa-npc-zone-table tr');
rows.forEach(function (row) {
var cells = row.querySelectorAll('td');
if (cells.length < 7) return;
var zone = cleanText(cells[0].textContent);
var level = cleanText(cells[1].textContent);
var dropsText = norm(cells[3].textContent);
if (!zone || dropsText.indexOf(wanted) === -1) return;
foundZones[zone] = true;
foundDrops.push({
npc: page.title,
zone: zone,
level: level
});
});
}).catch(function () {});
})).then(function () {
var dropHtml;
var zoneHtml;
if (foundDrops.length) {
dropHtml = foundDrops.map(function (drop) {
var levelText = drop.level ? ' <span class="eqoa-auto-drop-level">(Lv ' + esc(drop.level) + ')</span>' : '';
return pageLink(drop.npc) + levelText;
}).join('<br>');
zoneHtml = Object.keys(foundZones).sort().map(pageLink).join('<br>');
} else {
dropHtml = 'Unknown';
zoneHtml = 'Unknown';
}
if (shouldReplace(droppedBox)) droppedBox.innerHTML = dropHtml;
if (shouldReplace(zoneBox)) zoneBox.innerHTML = zoneHtml;
});
});
}
mw.hook('wikipage.content').add(initEqoaItemAutoDrops);
document.addEventListener('DOMContentLoaded', function () {
initEqoaItemAutoDrops(document);
});
function initEqoaItemAutoMerchantsAndQuests(root) {
root = root && root.jquery ? root[0] : (root || document);
var merchantBox = root.querySelector('.eqoa-auto-merchants');
var questBox = root.querySelector('.eqoa-auto-quests');
if ((!merchantBox && !questBox) || root.querySelector('.eqoa-auto-item-links-ready')) return;
var marker = document.createElement('span');
marker.className = 'eqoa-auto-item-links-ready';
marker.style.display = 'none';
root.appendChild(marker);
var itemName =
(root.querySelector('.eqoa-misc-inspect-name') && root.querySelector('.eqoa-misc-inspect-name').textContent.trim()) ||
(root.querySelector('.eqoa-itembox-name') && root.querySelector('.eqoa-itembox-name').textContent.trim()) ||
mw.config.get('wgTitle');
if (!itemName) return;
function cleanText(text) {
return String(text || '').replace(/\s+/g, ' ').trim();
}
function norm(text) {
return cleanText(text).toLowerCase();
}
function esc(text) {
return String(text || '').replace(/[&<>"']/g, function (ch) {
return {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}[ch];
});
}
function pageLink(title) {
return '<a href="' + mw.util.getUrl(title) + '">' + esc(title) + '</a>';
}
function shouldReplace(box) {
if (!box) return false;
var text = norm(box.textContent);
return !text || text === 'loading...' || text === 'unknown' || text === 'none';
}
var wanted = norm(itemName);
var api = new mw.Api();
var merchants = {};
var quests = {};
api.get({
action: 'query',
generator: 'backlinks',
gbltitle: itemName,
gbllimit: 'max',
prop: 'info',
format: 'json'
}).then(function (data) {
var pages = data.query && data.query.pages ? Object.values(data.query.pages) : [];
pages = pages.filter(function (page) {
return page.ns === 0 && page.title !== mw.config.get('wgTitle');
});
if (!pages.length) {
if (shouldReplace(merchantBox)) merchantBox.textContent = 'None';
if (shouldReplace(questBox)) questBox.textContent = 'None';
return;
}
return Promise.all(pages.map(function (page) {
return api.get({
action: 'parse',
page: page.title,
prop: 'text',
format: 'json'
}).then(function (parsed) {
var html = parsed.parse && parsed.parse.text ? parsed.parse.text['*'] : '';
var doc = new DOMParser().parseFromString(html, 'text/html');
var sellsBox = doc.querySelector('.eqoa-merchant-sells');
if (sellsBox && norm(sellsBox.textContent).indexOf(wanted) !== -1) {
merchants[page.title] = true;
}
var questPage = doc.querySelector('.quest-template');
if (questPage && norm(doc.body.textContent).indexOf(wanted) !== -1) {
quests[page.title] = true;
}
}).catch(function () {});
})).then(function () {
var merchantTitles = Object.keys(merchants).sort();
var questTitles = Object.keys(quests).sort();
if (shouldReplace(merchantBox)) {
merchantBox.innerHTML = merchantTitles.length
? merchantTitles.map(pageLink).join('<br>')
: 'None';
}
if (shouldReplace(questBox)) {
questBox.innerHTML = questTitles.length
? questTitles.map(pageLink).join('<br>')
: 'None';
}
});
});
}
mw.hook('wikipage.content').add(initEqoaItemAutoMerchantsAndQuests);
document.addEventListener('DOMContentLoaded', function () {
initEqoaItemAutoMerchantsAndQuests(document);
});
function initZoneDropsFromNpcPages(root) {
root = root && root.jquery ? root[0] : (root || document);
var zoneName = String(mw.config.get('wgTitle') || '').replace(/_/g, ' ');
var dropsPanel = root.querySelector('[data-tab-panel="items"], [data-tab-panel="drops"]');
if (!zoneName || !dropsPanel || dropsPanel.classList.contains('eqoa-zone-drops-from-npcs-ready')) {
return;
}
dropsPanel.classList.add('eqoa-zone-drops-from-npcs-ready');
var npcCategory = zoneName + ' NPCs';
var api = new mw.Api();
function normalizeText(value) {
return String(value || '')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}
function normalizePageName(value) {
return String(value || '')
.replace(/_/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function escapeHtml(value) {
return String(value || '').replace(/[&<>"']/g, function (char) {
return {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}[char];
});
}
function getGeneratedItemByName(name) {
var wanted = normalizeText(name);
var items = window.EQOA_GENERATED_ITEMS || [];
for (var i = 0; i < items.length; i++) {
if (normalizeText(items[i].name) === wanted) {
return items[i];
}
}
return null;
}
function isGearItem(item) {
if (!item) return false;
return !!(
item.slot ||
item.lvl ||
item.level ||
item.class ||
item.dmg ||
item.ac ||
item.dur ||
item.gearhp
);
}
function formatItemClass(item) {
if (!item || !item.class) return '';
if (Array.isArray(item.class)) {
return item.class.join(', ');
}
return String(item.class || '');
}
function getItemLevel(item) {
if (!item) return '';
return item.lvl || item.level || '';
}
function parseNpcPageForDrops(html, npcName) {
var doc = new DOMParser().parseFromString(html, 'text/html');
var rows = doc.querySelectorAll('.eqoa-npc-zone-table tr');
var results = [];
rows.forEach(function (row) {
var cells = row.querySelectorAll('td');
if (cells.length < 4) return;
var rowZone = normalizeText(cells[0].textContent);
if (rowZone !== normalizeText(zoneName)) {
return;
}
cells[3].querySelectorAll('a').forEach(function (link) {
var itemName = normalizePageName(link.textContent);
if (!itemName) return;
results.push({
name: itemName,
npc: npcName
});
});
});
return results;
}
function renderDropsTable(drops) {
var seen = {};
var uniqueDrops = [];
drops.forEach(function (drop) {
var key = normalizeText(drop.name);
if (seen[key]) return;
seen[key] = true;
uniqueDrops.push(drop);
});
uniqueDrops.sort(function (a, b) {
return a.name.localeCompare(b.name);
});
var html = ''
+ '<div class="zone-drops-auto-wrapper">'
+ '<table id="items-found-here-table" class="wikitable sortable zone-data-table zone-drops-auto-table">'
+ '<thead>'
+ '<tr>'
+ '<th>Name</th>'
+ '<th>Level</th>'
+ '<th>Class</th>'
+ '</tr>'
+ '</thead>'
+ '<tbody>';
if (!uniqueDrops.length) {
html += '<tr><td colspan="3">No drops found.</td></tr>';
} else {
uniqueDrops.forEach(function (drop) {
var item = getGeneratedItemByName(drop.name);
var gearItem = isGearItem(item);
var level = gearItem ? getItemLevel(item) : '';
var itemClass = gearItem ? formatItemClass(item) : '';
var itemUrl = mw.util.getUrl(drop.name);
html += ''
+ '<tr>'
+ '<td>'
+ '<a class="class-item-tooltip-link" data-class-item-name="' + encodeURIComponent(drop.name) + '" href="' + itemUrl + '">'
+ escapeHtml(drop.name)
+ '</a>'
+ '</td>'
+ '<td>' + escapeHtml(level) + '</td>'
+ '<td>' + escapeHtml(itemClass) + '</td>'
+ '</tr>';
});
}
html += '</tbody></table></div>';
dropsPanel.innerHTML = html;
if (typeof initGeneratedItemTooltipsForWiki === 'function') {
initGeneratedItemTooltipsForWiki(dropsPanel);
}
if (typeof applyClassItemRarityStyle === 'function') {
dropsPanel.querySelectorAll('.class-item-tooltip-link').forEach(function (link) {
var itemName = decodeURIComponent(link.getAttribute('data-class-item-name') || link.textContent || '');
var item = getGeneratedItemByName(itemName);
if (item) {
applyClassItemRarityStyle(link, item);
}
});
}
if (typeof initItemsFoundHereSortControls === 'function') {
initItemsFoundHereSortControls(dropsPanel);
}
if (window.jQuery && jQuery.fn.tablesorter) {
jQuery(dropsPanel).find('table.sortable').tablesorter();
}
}
api.get({
action: 'query',
list: 'categorymembers',
cmtitle: 'Category:' + npcCategory,
cmlimit: '500',
format: 'json'
}).then(function (data) {
var members = (data.query && data.query.categorymembers) || [];
if (!members.length) {
renderDropsTable([]);
return;
}
return Promise.all(members.map(function (member) {
return api.get({
action: 'parse',
page: member.title,
prop: 'text',
format: 'json'
}).then(function (pageData) {
var html = pageData.parse && pageData.parse.text && pageData.parse.text['*'];
if (!html) return [];
return parseNpcPageForDrops(html, member.title);
}).catch(function () {
return [];
});
}));
}).then(function (dropGroups) {
if (!dropGroups) return;
renderDropsTable([].concat.apply([], dropGroups));
}).catch(function () {
renderDropsTable([]);
});
}
mw.hook('wikipage.content').add(initZoneDropsFromNpcPages);
document.addEventListener('DOMContentLoaded', function () {
initZoneDropsFromNpcPages(document);
});
setTimeout(function () {
initZoneDropsFromNpcPages(document);
}, 500);
setTimeout(function () {
initZoneDropsFromNpcPages(document);
}, 1500);
function initEqoaScreenshotLightbox(root) {
root = root && root.jquery ? root[0] : (root || document);
var cards = Array.from(root.querySelectorAll('.eqoa-zone-screenshot-card'));
if (!cards.length) return;
var images = cards.map(function (card) {
return card.querySelector('img');
}).filter(Boolean);
if (!images.length) return;
function getFullImageUrl(img) {
var src = img.currentSrc || img.src || '';
var match = src.match(/^(.*\/images\/)thumb\/(.+?)\/[^\/?#]+(\?.*)?$/);
if (match) {
return match[1] + match[2];
}
return src;
}
function getLightbox() {
var box = document.querySelector('.eqoa-screenshot-lightbox');
if (!box) {
box = document.createElement('div');
box.className = 'eqoa-screenshot-lightbox';
box.innerHTML =
'<button type="button" class="eqoa-screenshot-lightbox-close">×</button>' +
'<button type="button" class="eqoa-screenshot-lightbox-prev">‹</button>' +
'<img class="eqoa-screenshot-lightbox-image" alt="Screenshot">' +
'<button type="button" class="eqoa-screenshot-lightbox-next">›</button>' +
'<div class="eqoa-screenshot-lightbox-counter"></div>';
document.body.appendChild(box);
}
return box;
}
var currentIndex = 0;
function showImage(index) {
var box = getLightbox();
var img = box.querySelector('.eqoa-screenshot-lightbox-image');
var counter = box.querySelector('.eqoa-screenshot-lightbox-counter');
currentIndex = (index + images.length) % images.length;
img.src = getFullImageUrl(images[currentIndex]);
counter.textContent = 'Screenshot ' + (currentIndex + 1) + ' / ' + images.length;
box.classList.add('is-open');
document.body.classList.add('eqoa-screenshot-lightbox-open');
}
function closeLightbox() {
var box = getLightbox();
box.classList.remove('is-open');
document.body.classList.remove('eqoa-screenshot-lightbox-open');
}
function nextImage() {
showImage(currentIndex + 1);
}
function prevImage() {
showImage(currentIndex - 1);
}
images.forEach(function (img, index) {
if (img.classList.contains('eqoa-screenshot-lightbox-ready')) return;
img.classList.add('eqoa-screenshot-lightbox-ready');
img.addEventListener('click', function (event) {
event.preventDefault();
showImage(index);
});
});
var box = getLightbox();
box.querySelector('.eqoa-screenshot-lightbox-close').onclick = closeLightbox;
box.querySelector('.eqoa-screenshot-lightbox-next').onclick = nextImage;
box.querySelector('.eqoa-screenshot-lightbox-prev').onclick = prevImage;
box.addEventListener('click', function (event) {
if (event.target === box) {
closeLightbox();
}
});
document.addEventListener('keydown', function (event) {
if (!box.classList.contains('is-open')) return;
if (event.key === 'Escape') closeLightbox();
if (event.key === 'ArrowRight') nextImage();
if (event.key === 'ArrowLeft') prevImage();
});
}
mw.hook('wikipage.content').add(initEqoaScreenshotLightbox);
document.addEventListener('DOMContentLoaded', function () {
initEqoaScreenshotLightbox(document);
});
/* Add Class under Race / Transformation on Ultimate Build output */
(function () {
var CLASS_NAMES = {
ALC: 'Alchemist',
BRD: 'Bard',
CL: 'Cleric',
DRD: 'Druid',
ENC: 'Enchanter',
MAG: 'Magician',
MNK: 'Monk',
NEC: 'Necromancer',
PAL: 'Paladin',
RAN: 'Ranger',
RGE: 'Rogue',
ROG: 'Rogue',
SK: 'Shadowknight',
SHA: 'Shaman',
WAR: 'Warrior',
WIZ: 'Wizard'
};
function cleanText(value) {
return String(value || '').replace(/\s+/g, ' ').trim();
}
function normalizeClassName(value) {
var text = cleanText(value).replace(/^Class:\s*/i, '');
var upper = text.toUpperCase();
if (CLASS_NAMES[upper]) {
return CLASS_NAMES[upper];
}
return Object.keys(CLASS_NAMES).map(function (key) {
return CLASS_NAMES[key];
}).find(function (name) {
return name.toLowerCase() === text.toLowerCase();
}) || text;
}
function getSelectedClassName() {
var globals = [
window.CB_SELECTED_CLASS,
window.selectedClass,
window.currentClass,
window.eqoaSelectedClass
];
var found;
var selects;
var active;
var checked;
found = globals.map(normalizeClassName).find(Boolean);
if (found) return found;
selects = document.querySelectorAll('select');
found = Array.prototype.slice.call(selects).map(function (select) {
var option = select.options[select.selectedIndex];
return normalizeClassName((option && option.textContent) || select.value);
}).find(function (value) {
return Object.keys(CLASS_NAMES).map(function (key) {
return CLASS_NAMES[key];
}).indexOf(value) !== -1;
});
if (found) return found;
active = document.querySelectorAll(
'[data-class].is-active, [data-class].active, [data-class].selected,' +
'[data-cb-class].is-active, [data-cb-class].active, [data-cb-class].selected,' +
'[data-player-class].is-active, [data-player-class].active, [data-player-class].selected'
);
found = Array.prototype.slice.call(active).map(function (node) {
return normalizeClassName(
node.getAttribute('data-class') ||
node.getAttribute('data-cb-class') ||
node.getAttribute('data-player-class') ||
node.textContent
);
}).find(Boolean);
if (found) return found;
checked = document.querySelectorAll('input[type="radio"]:checked, input[type="checkbox"]:checked');
found = Array.prototype.slice.call(checked).map(function (input) {
if (!/class/i.test(input.name + ' ' + input.id + ' ' + input.className)) return '';
return normalizeClassName(input.value);
}).find(Boolean);
return found || '';
}
function findRaceLabel(root) {
return Array.prototype.slice.call((root || document).querySelectorAll('*')).find(function (node) {
return cleanText(node.textContent) === 'Race / Transformation' && !node.children.length;
});
}
function findSmallBlock(label) {
var current = label.parentElement;
var best = label.parentElement;
while (current && current !== document.body) {
if (
cleanText(current.textContent).indexOf('Race / Transformation') !== -1 &&
cleanText(current.textContent).length <= 160
) {
best = current;
}
current = current.parentElement;
}
return best;
}
function writeClassBlock(block, className) {
var walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT);
var textNodes = [];
var node;
while ((node = walker.nextNode())) {
if (cleanText(node.nodeValue)) {
textNodes.push(node);
}
}
if (textNodes[0]) {
textNodes[0].nodeValue = textNodes[0].nodeValue.replace(cleanText(textNodes[0].nodeValue), 'Class');
}
if (textNodes[1]) {
textNodes[1].nodeValue = textNodes[1].nodeValue.replace(cleanText(textNodes[1].nodeValue), className);
} else {
block.appendChild(document.createTextNode(className));
}
}
function addUltimateBuildClass(root) {
var className = getSelectedClassName();
var raceLabel = findRaceLabel(root);
var raceBlock;
var parent;
var existing;
var classBlock;
if (!className || !raceLabel) return;
raceBlock = findSmallBlock(raceLabel);
parent = raceBlock.parentNode;
existing = Array.prototype.slice.call(parent.children).find(function (child) {
return child.classList && child.classList.contains('cb-ultimate-class-injected');
});
classBlock = existing || raceBlock.cloneNode(true);
classBlock.classList.add('cb-ultimate-class-injected');
writeClassBlock(classBlock, className);
if (!existing) {
parent.insertBefore(classBlock, raceBlock.nextSibling);
}
}
function runUltimateBuildClassUpdate() {
addUltimateBuildClass(document);
}
if (window.mw && mw.hook) {
mw.hook('wikipage.content').add(function ($content) {
addUltimateBuildClass($content && $content[0] ? $content[0] : document);
});
}
document.addEventListener('DOMContentLoaded', runUltimateBuildClassUpdate);
document.addEventListener('change', function () {
setTimeout(runUltimateBuildClassUpdate, 50);
});
document.addEventListener('click', function () {
setTimeout(runUltimateBuildClassUpdate, 50);
});
setTimeout(runUltimateBuildClassUpdate, 500);
setTimeout(runUltimateBuildClassUpdate, 1500);
}());
/* Zone NPC tab: use NPCFriendly/NPCMerchant |race= as Family and |level= as Level when blank */
(function () {
var npcFriendlyDataCache = {};
var observerStarted = false;
function normalizeTitleFromRow(nameCell) {
var link = nameCell ? nameCell.querySelector('a') : null;
var href;
var match;
if (link) {
href = link.getAttribute('href') || '';
match = href.match(/[?&]title=([^&]+)/);
if (match) return decodeURIComponent(match[1]).replace(/_/g, ' ');
match = href.match(/\/wiki\/([^?#]+)/);
if (match) return decodeURIComponent(match[1]).replace(/_/g, ' ');
return link.textContent.replace(/\s+/g, ' ').trim();
}
return nameCell ? nameCell.textContent.replace(/\s+/g, ' ').trim() : '';
}
function extractTemplateParam(source, templateName, paramName) {
var start = source.search(new RegExp('{{\\s*' + templateName + '\\b', 'i'));
var chunk;
var match;
if (start === -1) return '';
chunk = source.slice(start);
match = chunk.match(new RegExp(
'(?:^|\\n)\\s*\\|\\s*' + paramName + '\\s*=\\s*([\\s\\S]*?)(?=\\n\\s*\\|\\s*[A-Za-z0-9_ -]+\\s*=|\\n\\s*}}|$)',
'i'
));
return match ? match[1].trim() : '';
}
function getNpcFriendlyTemplateData(source) {
var templateName = '';
var race = '';
var level = '';
if (/{{\s*NPCFriendly\b/i.test(source)) {
templateName = 'NPCFriendly';
} else if (/{{\s*NPCMerchant\b/i.test(source)) {
templateName = 'NPCMerchant';
}
if (!templateName) {
return {
race: '',
level: ''
};
}
race = extractTemplateParam(source, templateName, 'race');
level = extractTemplateParam(source, templateName, 'level') || extractTemplateParam(source, templateName, 'lvl');
return {
race: race,
level: level
};
}
function parseWikiText(api, text) {
if (!text) return Promise.resolve('');
return api.get({
action: 'parse',
text: text,
contentmodel: 'wikitext',
prop: 'text',
formatversion: 2
}).then(function (data) {
return data && data.parse && data.parse.text ? data.parse.text : text;
}).catch(function () {
return text;
});
}
function loadNpcFriendlyData(title) {
var api;
if (!title) {
return Promise.resolve({
raceHtml: '',
levelHtml: ''
});
}
if (npcFriendlyDataCache[title]) {
return npcFriendlyDataCache[title];
}
api = new mw.Api();
npcFriendlyDataCache[title] = api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];
var source = page && page.revisions && page.revisions[0] && page.revisions[0].slots.main.content;
var templateData;
if (!source) {
return {
raceHtml: '',
levelHtml: ''
};
}
templateData = getNpcFriendlyTemplateData(source);
return Promise.all([
parseWikiText(api, templateData.race),
parseWikiText(api, templateData.level)
]).then(function (parsed) {
return {
raceHtml: parsed[0] || '',
levelHtml: parsed[1] || ''
};
});
}).catch(function () {
return {
raceHtml: '',
levelHtml: ''
};
});
return npcFriendlyDataCache[title];
}
function initZoneNpcFriendlyData(root) {
root = root && root.jquery ? root[0] : (root || document);
root.querySelectorAll('.zone-monster-table tbody tr, .zone-npc-list-wrapper tbody tr').forEach(function (row) {
var nameCell = row.querySelector('.zone-monster-name');
var levelCell = row.querySelector('.zone-monster-level');
var familyCell = row.querySelector('.zone-monster-family');
var title;
if (!nameCell) return;
if (row.classList.contains('eqoa-npc-friendly-data-loading')) return;
title = normalizeTitleFromRow(nameCell);
if (!title) return;
row.classList.add('eqoa-npc-friendly-data-loading');
loadNpcFriendlyData(title).then(function (data) {
if (familyCell && !familyCell.textContent.replace(/\s+/g, ' ').trim() && data.raceHtml) {
familyCell.innerHTML = data.raceHtml;
}
if (levelCell && !levelCell.textContent.replace(/\s+/g, ' ').trim() && data.levelHtml) {
levelCell.innerHTML = data.levelHtml;
}
row.classList.add('eqoa-npc-friendly-data-ready');
});
});
}
function startNpcFriendlyDataObserver() {
var target;
if (observerStarted || !window.MutationObserver) return;
target = document.querySelector('.eqoa-zone-tabs') || document.querySelector('#mw-content-text') || document.body;
if (!target) return;
observerStarted = true;
new MutationObserver(function () {
initZoneNpcFriendlyData(document);
}).observe(target, {
childList: true,
subtree: true
});
}
mw.hook('wikipage.content').add(function (root) {
initZoneNpcFriendlyData(root);
startNpcFriendlyDataObserver();
});
document.addEventListener('DOMContentLoaded', function () {
initZoneNpcFriendlyData(document);
startNpcFriendlyDataObserver();
});
setTimeout(function () {
initZoneNpcFriendlyData(document);
startNpcFriendlyDataObserver();
}, 500);
setTimeout(function () {
initZoneNpcFriendlyData(document);
}, 1500);
setTimeout(function () {
initZoneNpcFriendlyData(document);
}, 3000);
}());
function initEqoaAchievements(root) {
root = root && root.jquery ? root[0] : (root || document);
var pages = Array.prototype.slice.call(
root.querySelectorAll('.eqoa-achievements-page')
);
if (
root.classList &&
root.classList.contains('eqoa-achievements-page')
) {
pages.unshift(root);
}
pages.forEach(function (page) {
var rows = Array.prototype.slice.call(
page.querySelectorAll('.eqoa-achievement-row')
);
var controls = page.querySelector(
'[data-eqoa-achievement-controls]'
);
if (!rows.length || !controls) {
return;
}
var storagePrefix = 'eqoa-achievement-complete:';
var tabs = [
{
label: 'All',
key: 'All',
subtabs: []
},
{
label: 'Character',
key: 'Character',
subtabs: []
},
{
label: 'World',
key: 'World',
subtabs: [
'Bestiary',
'Arctic',
'Desert',
'Forest',
'Marine',
'Mountain',
'Plains',
'Swamp',
'Deep Jungle',
'Rathe Mountains',
'Red Desert',
'Wastelands',
'Wetlands'
]
},
{
label: 'Quest',
key: 'Quest',
subtabs: [
'Path Quests',
'17',
'23',
'25',
'27',
'33',
'35',
'37',
'43',
'45',
'47',
'53',
'55',
'57',
'59',
'Misc'
]
},
{
label: 'Dungeons and Raids',
key: 'Dungeons and Raids',
subtabs: [
'Classic',
'Permafrost',
'Isle of Dread',
'Plane of Sky',
'Plane of Disease',
"Solusek's Eye",
'Zaoran Plateau'
]
},
{
label: 'Exploration',
key: 'Exploration',
subtabs: [
'Treasure Chest',
'Misc'
]
},
{
label: 'Professions and Hobbies',
key: 'Professions and Hobbies',
subtabs: [
'Armorsmith',
'Jewelry',
'Tailoring',
'Weaponsmith',
'Fishing'
]
}
];
function clean(value) {
return String(value || '')
.replace(/\s+/g, ' ')
.trim();
}
function norm(value) {
return clean(value).toLowerCase();
}
function get(row, attr) {
return clean(row.getAttribute(attr));
}
function normalizedCategory(row) {
var category = norm(
get(row, 'data-achievement-category')
);
if (category === 'questing') {
return 'quest';
}
if (
category === 'dungeon and raids' ||
category === 'dungeons and raids' ||
category === 'dungeon' ||
category === 'raids'
) {
return 'dungeons and raids';
}
if (
category === 'profession' ||
category === 'hobbies'
) {
return 'professions and hobbies';
}
return category;
}
function getName(row) {
return get(row, 'data-achievement-name');
}
function storageKey(row) {
return storagePrefix + norm(getName(row));
}
function isComplete(row) {
try {
return localStorage.getItem(
storageKey(row)
) === '1';
} catch (error) {
return false;
}
}
function setComplete(row, value) {
try {
if (value) {
localStorage.setItem(
storageKey(row),
'1'
);
} else {
localStorage.removeItem(
storageKey(row)
);
}
} catch (error) {
// Ignore unavailable localStorage.
}
}
function parseRequires(value) {
return String(value || '')
.split(/\s*\|\|\s*|\s*;\s*|\r?\n/)
.map(clean)
.filter(Boolean);
}
function findRowByName(name) {
var wanted = norm(name);
return rows.find(function (row) {
return norm(getName(row)) === wanted;
});
}
function clearBrokenIconText(icon) {
if (!icon) {
return;
}
Array.prototype.slice.call(
icon.childNodes
).forEach(function (node) {
if (
node.nodeType === 3 &&
/^[\s?]+$/.test(node.nodeValue || '')
) {
node.remove();
}
});
icon.querySelectorAll(
'.mw-broken-media, a.new'
).forEach(function (bad) {
bad.remove();
});
if (
!icon.querySelector('img') &&
!icon.querySelector(
'.eqoa-achievement-icon-fallback'
)
) {
var fallback = document.createElement('span');
fallback.className =
'eqoa-achievement-icon-fallback';
fallback.textContent = '?';
icon.appendChild(fallback);
}
}
function renderRequirements(row) {
var box = row.querySelector(
'.eqoa-achievement-requirements'
);
if (!box) {
return;
}
var requirements = parseRequires(
get(row, 'data-achievement-requires')
);
box.innerHTML = '';
if (!requirements.length) {
row.classList.remove(
'is-requirements-open'
);
return;
}
var title = document.createElement('div');
title.className =
'eqoa-achievement-requirements-title';
title.textContent = 'Required Achievements';
box.appendChild(title);
requirements.forEach(function (requirement) {
var target = findRowByName(requirement);
var button = document.createElement('button');
button.type = 'button';
button.className =
'eqoa-achievement-requirement';
button.textContent = requirement;
if (target && isComplete(target)) {
button.classList.add('is-completed');
}
button.addEventListener(
'click',
function (event) {
event.preventDefault();
event.stopPropagation();
if (!target) {
return;
}
target.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
target.classList.add(
'is-highlighted'
);
setTimeout(function () {
target.classList.remove(
'is-highlighted'
);
}, 1400);
}
);
box.appendChild(button);
});
}
function ensureCheckButton(row) {
var holder = row.querySelector(
'.eqoa-achievement-check-placeholder'
);
if (!holder) {
return;
}
var button = holder.querySelector(
'.eqoa-achievement-check'
);
if (!button) {
button = document.createElement('button');
button.type = 'button';
button.className =
'eqoa-achievement-check';
button.setAttribute(
'aria-label',
'Toggle achievement'
);
button.addEventListener(
'click',
function (event) {
event.preventDefault();
event.stopPropagation();
setComplete(
row,
!isComplete(row)
);
refresh();
}
);
holder.innerHTML = '';
holder.appendChild(button);
}
var completed = isComplete(row);
button.classList.toggle(
'is-checked',
completed
);
button.setAttribute(
'aria-pressed',
completed ? 'true' : 'false'
);
button.textContent = completed ? '✓' : '';
}
function rowMatchesMain(row, main, sub) {
var category = normalizedCategory(row);
var subcategory = norm(
get(row, 'data-achievement-subcategory')
);
var biome = norm(
get(row, 'data-achievement-biome')
);
var questtype = norm(
get(row, 'data-achievement-questtype')
);
var npcfamily = norm(
get(row, 'data-achievement-npcfamily')
);
var zone = norm(
get(row, 'data-achievement-zone')
);
var raid = norm(
get(row, 'data-achievement-raid')
);
var dungeon = norm(
get(row, 'data-achievement-dungeon')
);
var wanted = norm(sub);
if (main === 'All') {
return true;
}
if (main === 'Character') {
return category === 'character';
}
if (main === 'World') {
if (
category !== 'world' &&
!biome &&
!npcfamily
) {
return false;
}
if (!wanted) {
return true;
}
if (wanted === 'bestiary') {
return (
subcategory === 'bestiary' ||
category === 'bestiary'
);
}
return (
biome === wanted ||
subcategory === wanted
);
}
if (main === 'Quest') {
if (
category !== 'quest' &&
!questtype
) {
return false;
}
if (!wanted) {
return true;
}
return (
questtype === wanted ||
subcategory === wanted ||
norm('Lv' + questtype) === wanted
);
}
if (main === 'Dungeons and Raids') {
if (
category !== 'dungeons and raids' &&
!raid &&
!dungeon
) {
return false;
}
if (!wanted) {
return true;
}
return (
zone === wanted ||
raid === wanted ||
dungeon === wanted ||
subcategory === wanted
);
}
if (main === 'Exploration') {
if (category !== 'exploration') {
return false;
}
if (!wanted) {
return true;
}
return subcategory === wanted;
}
if (main === 'Professions and Hobbies') {
if (
category !==
'professions and hobbies'
) {
return false;
}
if (!wanted) {
return true;
}
return subcategory === wanted;
}
return false;
}
function buildControls() {
if (
controls.classList.contains(
'eqoa-achievement-controls-ready'
) &&
controls.querySelector(
'.eqoa-achievement-main-tabs'
)
) {
return;
}
controls.classList.add(
'eqoa-achievement-controls-ready'
);
controls.innerHTML = '';
var mainTabs = document.createElement('div');
mainTabs.className =
'eqoa-achievement-main-tabs';
var subTabs = document.createElement('div');
subTabs.className =
'eqoa-achievement-subtabs';
var filters = document.createElement('div');
filters.className =
'eqoa-achievement-filter-row';
var search = document.createElement('input');
search.type = 'search';
search.className =
'eqoa-achievement-search';
search.placeholder =
'Search achievements';
var showLabel =
document.createElement('label');
showLabel.className =
'eqoa-achievement-toggle-label';
var show = document.createElement('input');
show.type = 'checkbox';
show.className =
'eqoa-achievement-show-completed';
show.checked = true;
showLabel.appendChild(show);
showLabel.appendChild(
document.createTextNode(
' Show Completed'
)
);
filters.appendChild(search);
filters.appendChild(showLabel);
controls.appendChild(mainTabs);
controls.appendChild(subTabs);
controls.appendChild(filters);
search.addEventListener(
'input',
refresh
);
show.addEventListener(
'change',
refresh
);
}
function renderTabs() {
var mainTabs = controls.querySelector(
'.eqoa-achievement-main-tabs'
);
var subTabs = controls.querySelector(
'.eqoa-achievement-subtabs'
);
if (!mainTabs || !subTabs) {
return;
}
var activeMain =
controls.getAttribute(
'data-active-main'
) || 'All';
var activeSub =
controls.getAttribute(
'data-active-sub'
) || '';
var activeTab =
tabs.find(function (tab) {
return tab.key === activeMain;
}) || tabs[0];
mainTabs.innerHTML = '';
subTabs.innerHTML = '';
tabs.forEach(function (tab) {
var button =
document.createElement('button');
button.type = 'button';
button.textContent = tab.label;
button.setAttribute(
'data-achievement-tab',
tab.key
);
if (tab.key === activeTab.key) {
button.classList.add('is-active');
}
button.addEventListener(
'click',
function () {
controls.setAttribute(
'data-active-main',
tab.key
);
controls.setAttribute(
'data-active-sub',
''
);
refresh();
}
);
mainTabs.appendChild(button);
});
if (!activeTab.subtabs.length) {
subTabs.hidden = true;
return;
}
subTabs.hidden = false;
var allButton =
document.createElement('button');
allButton.type = 'button';
allButton.textContent = 'All';
allButton.setAttribute(
'data-achievement-subtab',
''
);
if (!activeSub) {
allButton.classList.add('is-active');
}
allButton.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
controls.setAttribute(
'data-active-sub',
''
);
refresh();
});
subTabs.appendChild(allButton);
activeTab.subtabs.forEach(function (sub) {
var button =
document.createElement('button');
button.type = 'button';
button.textContent = sub;
button.setAttribute(
'data-achievement-subtab',
sub
);
if (sub === activeSub) {
button.classList.add('is-active');
}
button.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
controls.setAttribute(
'data-active-sub',
sub
);
refresh();
});
subTabs.appendChild(button);
});
}
function updateTracker() {
var counted = rows.filter(function (row) {
return (
normalizedCategory(row) !==
'feats of strength'
);
});
var completed =
counted.filter(isComplete).length;
var total = counted.length;
var percent = total
? Math.round(
completed / total * 100
)
: 0;
var earnedPoints = counted
.filter(isComplete)
.reduce(function (sum, row) {
var value = parseInt(
get(
row,
'data-achievement-points'
),
10
);
return sum + (
isNaN(value) ? 0 : value
);
}, 0);
var totalPoints = counted
.reduce(function (sum, row) {
var value = parseInt(
get(
row,
'data-achievement-points'
),
10
);
return sum + (
isNaN(value) ? 0 : value
);
}, 0);
var percentBox = page.querySelector(
'.eqoa-achievement-total-percent'
);
var countBox = page.querySelector(
'.eqoa-achievement-total-count'
);
var pointsValue = page.querySelector(
'.eqoa-achievement-points-summary-value'
);
var pointsMeta = page.querySelector(
'.eqoa-achievement-points-summary-meta'
);
if (percentBox) {
percentBox.textContent =
percent + '%';
}
if (countBox) {
countBox.textContent =
completed +
' / ' +
total +
' Completed';
}
if (pointsValue) {
pointsValue.textContent =
earnedPoints;
}
if (pointsMeta) {
pointsMeta.textContent =
earnedPoints +
' / ' +
totalPoints +
' earned';
}
var groups = {
Character: function (row) {
return (
normalizedCategory(row) ===
'character'
);
},
World: function (row) {
return (
normalizedCategory(row) ===
'world' ||
normalizedCategory(row) ===
'bestiary' ||
!!get(
row,
'data-achievement-biome'
) ||
!!get(
row,
'data-achievement-npcfamily'
)
);
},
Quest: function (row) {
return (
normalizedCategory(row) ===
'quest' ||
!!get(
row,
'data-achievement-questtype'
)
);
},
'Dungeons and Raids': function (row) {
return (
normalizedCategory(row) ===
'dungeons and raids' ||
!!get(
row,
'data-achievement-raid'
) ||
!!get(
row,
'data-achievement-dungeon'
)
);
},
Exploration: function (row) {
return (
normalizedCategory(row) ===
'exploration'
);
},
'Professions and Hobbies':
function (row) {
return (
normalizedCategory(row) ===
'professions and hobbies'
);
}
};
Object.keys(groups).forEach(
function (groupName) {
var box = page.querySelector(
'[data-achievement-summary="' +
groupName +
'"] span'
);
if (!box) {
return;
}
var groupRows =
counted.filter(
groups[groupName]
);
var groupDone =
groupRows.filter(
isComplete
).length;
box.textContent =
groupDone +
' / ' +
groupRows.length;
}
);
}
function refresh() {
buildControls();
renderTabs();
var activeMain =
controls.getAttribute(
'data-active-main'
) || 'All';
var activeSub =
controls.getAttribute(
'data-active-sub'
) || '';
var searchInput =
controls.querySelector(
'.eqoa-achievement-search'
);
var search = norm(
searchInput ? searchInput.value : ''
);
var showCompleted =
controls.querySelector(
'.eqoa-achievement-show-completed'
);
var shouldShowCompleted =
!showCompleted ||
showCompleted.checked;
rows.forEach(function (row) {
clearBrokenIconText(
row.querySelector(
'.eqoa-achievement-icon'
)
);
ensureCheckButton(row);
renderRequirements(row);
var complete = isComplete(row);
row.classList.toggle(
'is-completed',
complete
);
if (
!row.classList.contains(
'eqoa-achievement-click-ready'
)
) {
row.classList.add(
'eqoa-achievement-click-ready'
);
row.addEventListener(
'click',
function (event) {
if (
event.target.closest(
'.eqoa-achievement-check, ' +
'.eqoa-achievement-requirement, ' +
'input, label'
)
) {
return;
}
var requirements =
parseRequires(
get(
row,
'data-achievement-requires'
)
);
if (!requirements.length) {
return;
}
event.preventDefault();
event.stopPropagation();
row.classList.toggle(
'is-requirements-open'
);
}
);
}
var matchesTab = rowMatchesMain(
row,
activeMain,
activeSub
);
var searchableText = [
row.textContent,
get(
row,
'data-achievement-name'
),
get(
row,
'data-achievement-category'
),
get(
row,
'data-achievement-subcategory'
),
get(
row,
'data-achievement-biome'
),
get(
row,
'data-achievement-questtype'
),
get(
row,
'data-achievement-npcfamily'
),
get(
row,
'data-achievement-zone'
),
get(
row,
'data-achievement-raid'
),
get(
row,
'data-achievement-dungeon'
),
get(
row,
'data-achievement-requires'
)
].join(' ');
var matchesSearch =
!search ||
norm(searchableText).indexOf(
search
) !== -1;
var matchesCompleted =
shouldShowCompleted ||
!complete;
var isVisible =
matchesTab &&
matchesSearch &&
matchesCompleted;
row.classList.toggle(
'is-filtered-out',
!isVisible
);
row.hidden = !isVisible;
row.setAttribute(
'aria-hidden',
isVisible ? 'false' : 'true'
);
});
Array.prototype.forEach.call(
page.querySelectorAll('.eqoa-achievement-list > p'),
function (paragraph) {
var text = String(paragraph.textContent || '')
.replace(/\u00a0/g, ' ')
.trim();
var hasMeaningfulContent =
text.length > 0 ||
paragraph.querySelector(
'img, a, input, button, .eqoa-achievement-row'
);
paragraph.style.display =
hasMeaningfulContent ? '' : 'none';
}
);
updateTracker();
}
refresh();
});
}
mw.hook('wikipage.content').add(
initEqoaAchievements
);
document.addEventListener(
'DOMContentLoaded',
function () {
initEqoaAchievements(document);
}
);
setTimeout(function () {
initEqoaAchievements(document);
}, 500);
setTimeout(function () {
initEqoaAchievements(document);
}, 1500);