[ Disclaimer, Create new user --- Wiki markup help, Install P99 ]
Difference between revisions of "MediaWiki:HuntingGuide.js"
From Project 1999 Wiki
| (24 intermediate revisions by one user not shown) | |||
| Line 28: | Line 28: | ||
}; | }; | ||
| − | var | + | var ALL_ERAS = ['Classic', 'Kunark', 'Velious']; |
| − | + | ||
| − | + | ||
| − | + | ||
| − | + | ||
| − | + | ||
| − | + | ||
| − | + | ||
| − | + | ||
| − | + | ||
| − | + | ||
| − | + | ||
| − | + | ||
| − | + | ||
| − | + | ||
| − | ]; | + | |
var subErasShort = [ | var subErasShort = [ | ||
'cla', | 'cla', | ||
| Line 61: | Line 46: | ||
var KUNARK_INDEX = 6; | var KUNARK_INDEX = 6; | ||
var VELIOUS_INDEX = 11; | var VELIOUS_INDEX = 11; | ||
| − | var isWithinEra = function(era, | + | var isWithinEra = function(era, subEra) { |
| − | var index = | + | var index = subErasShort.indexOf(subEra); |
| + | |||
switch (era) { | switch (era) { | ||
| − | case ' | + | case 'cla': return index < KUNARK_INDEX; |
| − | case ' | + | case 'kun': return index >= KUNARK_INDEX && index < VELIOUS_INDEX; |
| − | case ' | + | case 'vel': return index >= VELIOUS_INDEX; |
} | } | ||
} | } | ||
| Line 91: | Line 77: | ||
wizard: 'WIZ', | wizard: 'WIZ', | ||
}; | }; | ||
| − | var monsterTypes = ['Goblin', 'Guard', 'Undead']; | + | var monsterTypes = ['Animal', 'Goblin', 'Guard', 'Undead']; |
| + | |||
| + | var animals = []; | ||
| + | |||
| + | var fetchPageOfAnimals = function(url) { | ||
| + | return fetch(url) | ||
| + | .then(function(response){return response.text()}) | ||
| + | .then(function (html) { | ||
| + | var $html = $(html); | ||
| + | |||
| + | $html | ||
| + | .find('.mw-content-ltr table td li a') | ||
| + | .map(function (i, x) { | ||
| + | animals.push(x.innerText.toLowerCase()); | ||
| + | }) | ||
| + | return $html.find('[title="Category:Animal"]')[0].href; | ||
| + | }); | ||
| + | }; | ||
| + | |||
| + | |||
| + | var fetchAllAnimals = function() { | ||
| + | return fetchPageOfAnimals('https://wiki.project1999.com/Category:Animal') | ||
| + | .then(fetchPageOfAnimals) | ||
| + | .then(function(){ return animals; }); | ||
| + | } | ||
// Add radio buttons (since MediaWiki won't allow them as content) | // Add radio buttons (since MediaWiki won't allow them as content) | ||
| Line 106: | Line 116: | ||
'<li><span style="font-weight:bold">Level:</span> <input id="level-filter" style="width:30px"/></li>'+ | '<li><span style="font-weight:bold">Level:</span> <input id="level-filter" style="width:30px"/></li>'+ | ||
'<li><span style="font-weight:bold">Zone:</span> <input id="zone-filter" style="width:50em"/></li>' + | '<li><span style="font-weight:bold">Zone:</span> <input id="zone-filter" style="width:50em"/></li>' + | ||
| − | '<li><span style="font-weight:bold">Era:</span> ' + | + | '<li><span style="font-weight:bold">Era:</span> ' + ALL_ERAS.map(buildEraCheckbox).join('') + '</li>' + |
'<li><button id="clear-filters">Clear Filters</button></li>' + | '<li><button id="clear-filters">Clear Filters</button></li>' + | ||
'</ul>'); | '</ul>'); | ||
| Line 179: | Line 189: | ||
selectedEras | selectedEras | ||
) { | ) { | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | var monstersPromise = selectedMonster ==='Animal' | |
| − | + | ? fetchAllAnimals() | |
| − | + | : Promise.resolve([selectedMonster]); | |
| − | + | selectedMonster = selectedMonster.toLowerCase(); | |
| − | + | // To know what monsters count as animals, we have to go get a list of all animals | |
| + | return monstersPromise.then(function(monsters) { | ||
| + | return cleanedSpots.filter(function (spot) { | ||
| + | var classMatches = !selectedClass || spot.for.includes(toClassAbbreviation(selectedClass)); | ||
| + | var monsterMatches = | ||
| + | !selectedMonster || | ||
| + | monsters.map(monster => monster.toLowerCase()).some(function(monster){ | ||
| + | return spot.monsters.toLowerCase().includes(monster) || | ||
| + | spot.notes.toLowerCase().includes(monster); | ||
| + | }); | ||
| − | + | var minAndMax = spot.soloLevel.split('-'); | |
| − | + | var minLevel = parseFloat(minAndMax[0]); | |
| − | + | var maxLevel = parseFloat(minAndMax[1] || minAndMax[0]); | |
| + | var levelMatches = !selectedLevel || (minLevel <= selectedLevel && maxLevel >= selectedLevel); | ||
| − | + | var lowerCaseZone = spot.zone.toLowerCase(); | |
| − | + | var zonePattern = new RegExp(selectedZone.toLowerCase()); | |
| + | var zoneMatches = !selectedZone || lowerCaseZone.match(zonePattern); | ||
| + | |||
| + | var lowerCaseEra = spot.era.toLowerCase().trim(); | ||
| + | var eraMatches = selectedEras.length === 3 || isSubEraOf(selectedEras, lowerCaseEra); | ||
| + | |||
| + | return classMatches && monsterMatches && levelMatches && zoneMatches && eraMatches; | ||
| + | }); | ||
| + | }) | ||
| − | |||
| − | |||
}; | }; | ||
| Line 224: | Line 244: | ||
*/ | */ | ||
var handleAnyFilteringElementChange = function (e) { | var handleAnyFilteringElementChange = function (e) { | ||
| − | var $target = $(e.target); | + | if (e) { // Zone Input and Clear call this without an e |
| − | + | var $target = $(e.target); | |
| − | + | if ($target.is('.was-checked')) { | |
| − | + | // Let users "uncheck" a radio button by clicking it again | |
| − | + | $target.attr('checked', false); | |
| − | + | } else { | |
| − | + | // ... but if they're not un-checking something, uncheck the "no filters" radio button | |
| − | + | // (and differentiate that it's been clicked by adding a class ... if we didn't use a class | |
| − | + | // and just cheked .is(':checked'), it would always be true from the click itself) | |
| − | + | $target.addClass('was-checked'); | |
| + | $('.no-filter').attr('checked', false); | ||
| + | } | ||
} | } | ||
| Line 254: | Line 276: | ||
$filterResults.toggle(willFilter); | $filterResults.toggle(willFilter); | ||
| − | + | getFilteredSpots( | |
selectedClass, | selectedClass, | ||
selectedMonster, | selectedMonster, | ||
| Line 260: | Line 282: | ||
selectedZone, | selectedZone, | ||
selectedEras | selectedEras | ||
| − | ) | + | ).then(function(filteredSpots) { |
| − | + | setResults(filteredSpots); | |
| + | }); | ||
}; | }; | ||
$('.class-filter, .monster-filter').click(handleAnyFilteringElementChange); | $('.class-filter, .monster-filter').click(handleAnyFilteringElementChange); | ||
$('#level-filter').keyup(handleAnyFilteringElementChange); | $('#level-filter').keyup(handleAnyFilteringElementChange); | ||
$('#zone-filter').keyup(function (e) { | $('#zone-filter').keyup(function (e) { | ||
| − | if (e.target.value.length > 3) handleAnyFilteringElementChange; | + | if (e.target.value.length > 3) handleAnyFilteringElementChange(); |
}); | }); | ||
$('.era-filter').change(handleAnyFilteringElementChange); | $('.era-filter').change(handleAnyFilteringElementChange); | ||
Latest revision as of 17:42, 27 June 2025
// Basic DOM manipulation onReady (since the wiki won't let us add <input> tags in the wiki text
var buildEraCheckbox = function (era) {
return (
'<label><input checked class="era-filter" type="checkbox" value="' +
era.substr(0, 3).toLowerCase() +
'"/>' +
era +
'</label>'
);
};
// Create filter results table
var $filterResults = $('table.eoTable3.wikitable.sortable').eq(0).clone().hide();
var $headerRow = $('.hunting-guide-row:first').clone();
var clearFilterResults = function () {
return $filterResults.empty().append($headerRow);
};
clearFilterResults();
$('h4:contains("Hunting Spots 1-4")').before($filterResults);
var toTitleCase = function(original){
return original.split(' ').map(function toTitleCaseSingleWord(word) {
return word[0].toUpperCase() + word.substr(1);
}).join(' ');
};
var ALL_ERAS = ['Classic', 'Kunark', 'Velious'];
var subErasShort = [
'cla',
'fea',
'hat',
'tem',
'sky',
'pai',
'kun',
'hol',
'epi',
'war',
'sto',
'vel',
'ch2.0'
];
var KUNARK_INDEX = 6;
var VELIOUS_INDEX = 11;
var isWithinEra = function(era, subEra) {
var index = subErasShort.indexOf(subEra);
switch (era) {
case 'cla': return index < KUNARK_INDEX;
case 'kun': return index >= KUNARK_INDEX && index < VELIOUS_INDEX;
case 'vel': return index >= VELIOUS_INDEX;
}
}
var isSubEraOf = function(eras, subera) {
return eras.some(function(era) {
return isWithinEra(era, subera);
});
}
var classAbbreviations = {
bard: 'BRD',
cleric: 'CLR',
druid: 'DRU',
enchanter: 'ENC',
magician: 'MAG',
monk: 'MNK',
necromancer: 'NEC',
paladin: 'PAL',
ranger: 'RNG',
rogue: 'ROG',
'shadow knight': 'SHD',
shaman: 'SHM',
warrior: 'WAR',
wizard: 'WIZ',
};
var monsterTypes = ['Animal', 'Goblin', 'Guard', 'Undead'];
var animals = [];
var fetchPageOfAnimals = function(url) {
return fetch(url)
.then(function(response){return response.text()})
.then(function (html) {
var $html = $(html);
$html
.find('.mw-content-ltr table td li a')
.map(function (i, x) {
animals.push(x.innerText.toLowerCase());
})
return $html.find('[title="Category:Animal"]')[0].href;
});
};
var fetchAllAnimals = function() {
return fetchPageOfAnimals('https://wiki.project1999.com/Category:Animal')
.then(fetchPageOfAnimals)
.then(function(){ return animals; });
}
// Add radio buttons (since MediaWiki won't allow them as content)
$('#filter-ui').html(
'<ul><li><strong>Class:</strong>' +
Object.keys(classAbbreviations).map(function(eqClass){
return '<label class="class-filter filter-link"><input type="radio" name="class-filter" />' + toTitleCase(eqClass) + '</label>';
}).join('') + '</li>' +
'<li><strong>Monster:</strong>' +
monsterTypes.map(function(monster){
return '<label class="monster-filter filter-link"><input type="radio" name="monster-filter" />' + monster + '</label>';
}).join('') + '</li>' +
'<li><span style="font-weight:bold">Level:</span> <input id="level-filter" style="width:30px"/></li>'+
'<li><span style="font-weight:bold">Zone:</span> <input id="zone-filter" style="width:50em"/></li>' +
'<li><span style="font-weight:bold">Era:</span> ' + ALL_ERAS.map(buildEraCheckbox).join('') + '</li>' +
'<li><button id="clear-filters">Clear Filters</button></li>' +
'</ul>');
//$('.filter-link').each(function (i, el) {
// var $el = $(el);
// var eqClass = $el.text();
// var htmlClass = $el.attr('class');
// var name = htmlClass.split('-', 1)[0];
// var inputHtml = '<input type="radio" name="' + name + '" />';
// $el.replaceWith('<label class="' + htmlClass + '">' + inputHtml + eqClass + '</label>');
//});
// Get jQuery object of every row in the hunting guide (because all of the filter functions need it)
var $rows = $('.wikitable tbody tr');
/**
* Build a in-memory data object with the data parsed from each row (which will tell us which
* levels/classes should be shown for any filter).
* @example a "spot" created by this function for critters in Butcherblock Mountains: {
* soloLevel: '04-06',
* groupLevel: '03-05',
* zone: 'Butcherblock Mountains',
* area: 'Greater Faydark zoneline',
* monsters: 'Assorted Critters ',
* xpMod: '100%',
* notes: '',
* $tr: *a jQuery object pointing to the <tr> that all this data originally came from*
* }
*/
var spots = [];
$rows.each(function (i, el) {
var groupLevel = $(el).children('td:eq(1)').text();
groupLevel = groupLevel.trim() === '-' ? null : groupLevel;
var $tr = $(el);
if ($tr.children('th').length) return; // Header row
spots.push({
soloLevel: $tr.children('td:eq(0)').text(),
groupLevel: groupLevel,
zone: $tr.children('td:eq(2)').text(),
area: $tr.children('td:eq(3)').text(),
monsters: $tr.children('td:eq(4)').text(),
xpMod: $tr.children('td:eq(5)').text(),
era: $tr.children('td:eq(6)').text(),
image: $tr.children('td:eq(7)').text(),
notes: $tr.children('td:eq(8)').text(),
for: $tr.find('.for').text(),
$tr: $tr,
});
});
// Clear garbage entries (wish I had ES6 filter ..)
var cleanedSpots = [];
$.each(spots, function (i, spot) {
if (spot.soloLevel && spot.zone) cleanedSpots.push(spot);
});
$('#numSpots').text(cleanedSpots.length);
var toClassAbbreviation = function (eqClass) {
return classAbbreviations[eqClass.toLowerCase()];
};
var getFilteredSpots = function (
selectedClass,
selectedMonster,
selectedLevel,
selectedZone,
selectedEras
) {
var monstersPromise = selectedMonster ==='Animal'
? fetchAllAnimals()
: Promise.resolve([selectedMonster]);
selectedMonster = selectedMonster.toLowerCase();
// To know what monsters count as animals, we have to go get a list of all animals
return monstersPromise.then(function(monsters) {
return cleanedSpots.filter(function (spot) {
var classMatches = !selectedClass || spot.for.includes(toClassAbbreviation(selectedClass));
var monsterMatches =
!selectedMonster ||
monsters.map(monster => monster.toLowerCase()).some(function(monster){
return spot.monsters.toLowerCase().includes(monster) ||
spot.notes.toLowerCase().includes(monster);
});
var minAndMax = spot.soloLevel.split('-');
var minLevel = parseFloat(minAndMax[0]);
var maxLevel = parseFloat(minAndMax[1] || minAndMax[0]);
var levelMatches = !selectedLevel || (minLevel <= selectedLevel && maxLevel >= selectedLevel);
var lowerCaseZone = spot.zone.toLowerCase();
var zonePattern = new RegExp(selectedZone.toLowerCase());
var zoneMatches = !selectedZone || lowerCaseZone.match(zonePattern);
var lowerCaseEra = spot.era.toLowerCase().trim();
var eraMatches = selectedEras.length === 3 || isSubEraOf(selectedEras, lowerCaseEra);
return classMatches && monsterMatches && levelMatches && zoneMatches && eraMatches;
});
})
};
/**
* Replaces the contents of the search results UI with the rows of the provided hunting spots
*/
var setResults = function (spots) {
// Clear the results (but add back the header row), then append (clones of) the result rows
var $resultsTbody = clearFilterResults();
var resultRows = spots.map(function (spot) {
return spot.$tr.clone();
});
$resultsTbody.append(resultRows);
if (spots.length) return; // Show "no matches" UI only if there are no matches
$resultsTbody.append(
'<tr><th colspan="50" style="color:red; font-weight: bold; font-size: 2em; padding: 1em;">' +
'No results for those filters</th></tr>'
);
};
/**
* Re-filter the page when one of the filtering UI elements changes
*/
var handleAnyFilteringElementChange = function (e) {
if (e) { // Zone Input and Clear call this without an e
var $target = $(e.target);
if ($target.is('.was-checked')) {
// Let users "uncheck" a radio button by clicking it again
$target.attr('checked', false);
} else {
// ... but if they're not un-checking something, uncheck the "no filters" radio button
// (and differentiate that it's been clicked by adding a class ... if we didn't use a class
// and just cheked .is(':checked'), it would always be true from the click itself)
$target.addClass('was-checked');
$('.no-filter').attr('checked', false);
}
}
var selectedClass = $('.class-filter :checked').parent().text();
var selectedMonster = $('.monster-filter :checked').parent().text();
var selectedLevel = $('#level-filter').val();
var selectedZone = $('#zone-filter').val();
var selectedEras = $('.era-filter:checked')
.map(function (i, el) {
return $(el).val();
})
.toArray();
// Are we actually filtering out anything?
var willFilter =
selectedClass || selectedMonster || selectedLevel || selectedZone || selectedEras.length < 3;
// If so, hide the normal UI and show results (otherwise show normal UI)
$('h4, .wikitable').toggle(!willFilter);
$filterResults.toggle(willFilter);
getFilteredSpots(
selectedClass,
selectedMonster,
selectedLevel,
selectedZone,
selectedEras
).then(function(filteredSpots) {
setResults(filteredSpots);
});
};
$('.class-filter, .monster-filter').click(handleAnyFilteringElementChange);
$('#level-filter').keyup(handleAnyFilteringElementChange);
$('#zone-filter').keyup(function (e) {
if (e.target.value.length > 3) handleAnyFilteringElementChange();
});
$('.era-filter').change(handleAnyFilteringElementChange);
$('#clear-filters').click(function () {
// Clear the other radio buttons (the browser won't do it since they have different names)
$('.filter-link :radio').attr('checked', false);
// Clear the text inputs
$('#level-filter').val('');
$('#zone-filter').val('');
// Re-check all three era checkboxes
$('.era-filter').attr('checked', true);
handleAnyFilteringElementChange();
});