This is a wiki for a reason. Anyone can contribute. If you see something that is inaccurate or can be improved, don't ask that it be fixed--just improve it.
[ Disclaimer, Create new user --- Wiki markup help, Install P99 ]

Difference between revisions of "MediaWiki:HuntingGuide.js"

From Project 1999 Wiki
Jump to: navigation, search
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();
});