[ Disclaimer, Create new user --- Wiki markup help, Install P99 ]
Difference between revisions of "MediaWiki:HuntingGuide.js"
From Project 1999 Wiki
Line 193: | Line 193: | ||
? fetchAllAnimals() | ? fetchAllAnimals() | ||
: Promise.resolve([selectedMonster]); | : Promise.resolve([selectedMonster]); | ||
+ | selectedMonster = selectedMonster.toLowerCase(); | ||
// To know what monsters count as animals, we have to go get a list of all animals | // To know what monsters count as animals, we have to go get a list of all animals | ||
return monstersPromise.then(function(monsters) { | return monstersPromise.then(function(monsters) { | ||
Line 200: | Line 201: | ||
!selectedMonster || | !selectedMonster || | ||
monsters.some(function(monster){ | monsters.some(function(monster){ | ||
− | return spot.monsters.includes(monster) || | + | return spot.monsters.toLowerCase().includes(monster) || |
− | spot.notes.includes(monster); | + | spot.notes.toLowerCase().includes(monster); |
}); | }); | ||
Revision as of 16:15, 17 May 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.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(); });