[ Disclaimer, Create new user --- Wiki markup help, Install P99 ]
Difference between revisions of "MediaWiki:LocMaps.js"
From Project 1999 Wiki
(89 intermediate revisions by one user not shown) | |||
Line 1: | Line 1: | ||
− | + | try { | |
(function() { | (function() { | ||
+ | |||
+ | var buildImgFromFileName = function(fileName) { | ||
+ | return '<img src="/images/' + fileName.replace(/ /g, '_') + '">' | ||
+ | }; | ||
// TODO: Add support for cropping maps that have multiple maps. | // TODO: Add support for cropping maps that have multiple maps. | ||
Line 33: | Line 37: | ||
number + suffix, | number + suffix, | ||
'level ' + number, | 'level ' + number, | ||
− | 'level: ' + number | + | 'level: ' + number, |
+ | 'floor ' + number, | ||
+ | 'floor: ' + number | ||
); | ); | ||
}; | }; | ||
− | var getZoneLevelData = function( | + | var getZoneLevelData = function(zone, text) { |
+ | var levels = zone.levels; | ||
// Check for part "level" aliases (eg. "1st floor" vs. "Level One") | // Check for part "level" aliases (eg. "1st floor" vs. "Level One") | ||
text = text.toLowerCase(); | text = text.toLowerCase(); | ||
− | if (containsAnyNumber(text, 0, 'th') || containsAny(text, 'basement', 'underground')) return levels[0]; | + | if(levels['3'] && levels['3'].image.includes('crystal')) { |
+ | // Special case for Crystal Caverns | ||
+ | if (containsAny(text, 'upper')) return levels[3]; | ||
+ | if (containsAny(text, 'lower')) return levels[2]; | ||
+ | if (containsAny(text, 'town', 'coldain')) return levels[0]; | ||
+ | } | ||
+ | |||
+ | // In most zones "tunnel" = level 0. But in Kurn's there are two level zeroes (tunnels and basement), | ||
+ | // so this next line handles the Kurn's case in particular | ||
+ | if (containsAny(text, 'tunnel') && levels.tunnels) return levels.tunnels; | ||
+ | |||
+ | if (containsAnyNumber(text, 0, 'th') || containsAny(text, 'basement', 'underground', 'tunnel', 'bear pit', 'bear pits', 'cave', 'caves')) return levels[0]; | ||
if (containsAnyNumber(text, 1, 'st') || containsAny(text, 'one', 'first', 'ground')) return levels[1]; | if (containsAnyNumber(text, 1, 'st') || containsAny(text, 'one', 'first', 'ground')) return levels[1]; | ||
if (containsAnyNumber(text, 2, 'nd') || containsAny(text, 'two', 'second')) return levels[2]; | if (containsAnyNumber(text, 2, 'nd') || containsAny(text, 'two', 'second')) return levels[2]; | ||
Line 46: | Line 64: | ||
if (containsAnyNumber(text, 4, 'th') || containsAny(text, 'four', 'fourth')) return levels[4]; | if (containsAnyNumber(text, 4, 'th') || containsAny(text, 'four', 'fourth')) return levels[4]; | ||
if (containsAnyNumber(text, 5, 'th') || containsAny(text, 'five', 'fifth')) return levels[5]; | if (containsAnyNumber(text, 5, 'th') || containsAny(text, 'five', 'fifth')) return levels[5]; | ||
+ | |||
+ | // Special case for Tower of Frozen Shadow, which has two level 6 maps | ||
+ | if (containsAny(text, '6a')) return levels['6A']; | ||
+ | if (containsAny(text, '6b')) return levels['6B']; | ||
+ | |||
if (containsAnyNumber(text, 6, 'th') || containsAny(text, 'six', 'sixth')) return levels[6]; | if (containsAnyNumber(text, 6, 'th') || containsAny(text, 'six', 'sixth')) return levels[6]; | ||
if (containsAnyNumber(text, 7, 'th') || containsAny(text, 'seven', 'seventh')) return levels[7]; | if (containsAnyNumber(text, 7, 'th') || containsAny(text, 'seven', 'seventh')) return levels[7]; | ||
Line 62: | Line 85: | ||
var findZoneData = function(zoneName, locs, nonLocParts) { | var findZoneData = function(zoneName, locs, nonLocParts) { | ||
+ | // Convert someZone (East) to East someZone | ||
+ | if (zoneName.trim().endsWith(')')) { | ||
+ | try { | ||
+ | var nameSplit = zoneName.split('('); | ||
+ | var newName = nameSplit[0].trim(); | ||
+ | var direction = nameSplit[1].split(')')[0].trim(); | ||
+ | if (['east', 'north', 'south', 'west'].includes(direction.toLowerCase())) { | ||
+ | zoneName = direction + ' ' + newName; | ||
+ | } | ||
+ | } catch(err){ /* just use name (it likely won't work, but try) */} | ||
+ | } | ||
+ | |||
var zone = zoneData[zoneName]; | var zone = zoneData[zoneName]; | ||
− | + | // Handle aliases (eg. "North Ro" instead of "Northern Desert of Ro") | |
− | + | if (typeof(zone) === 'string') zone = zoneData[zone]; | |
− | + | if (!zone) return null; | |
− | + | // Handle Multi-Level Zones (with different maps for 1st, 2nd, etc. floor) | |
− | + | if (zone.levels) { | |
+ | var levelOfZone = getZoneLevelData(zone, nonLocParts); | ||
+ | return levelOfZone; | ||
+ | } | ||
− | + | // Handle Zones with alternate maps on the same level (eg. Kelethin in GFay) | |
− | + | if (!zone.alternateMaps) return zone; | |
− | + | for (var i = 0; i < zone.alternateMaps.length; i++) { | |
− | + | var alternateData = zone.alternateMaps[i]; | |
− | + | var allLocsAreWithin = true; | |
− | + | // wish I had ES6 [].every | |
− | + | $.each(locs, function(i, loc) { | |
− | + | allLocsAreWithin = allLocsAreWithin && | |
− | + | locIsWithinAlternateData(loc, alternateData); | |
− | + | }); | |
− | + | if (allLocsAreWithin) return alternateData; | |
+ | }; | ||
+ | return zone; | ||
}; | }; | ||
− | |||
− | |||
var addImageUrl = function(zoneData) { | var addImageUrl = function(zoneData) { | ||
Line 113: | Line 151: | ||
var build$XContainer = function() { | var build$XContainer = function() { | ||
− | return $('<div class="x-container"></div>' | + | return $('<div class="x-container" style="position: relative"></div>'); |
− | + | ||
} | } | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
var build$LightboxFramedMap = function(zoneData) { | var build$LightboxFramedMap = function(zoneData) { | ||
− | var $map = build$ | + | var $map = build$XContainer().append(build$MapImage(zoneData)); |
return addFramingStyles($map, zoneData.width, zoneData.height) | return addFramingStyles($map, zoneData.width, zoneData.height) | ||
}; | }; | ||
Line 136: | Line 170: | ||
} | } | ||
− | var build$ | + | var build$AbsolutePositionFrame = function() { |
− | return $('<div | + | return $('<div style="position: absolute"></div>'); |
− | + | ||
}; | }; | ||
− | var | + | var removeOpenImage = function(e) { |
// Do nothing if there's no open map | // Do nothing if there's no open map | ||
if (!window.$openImg) return false; | if (!window.$openImg) return false; | ||
Line 156: | Line 189: | ||
var build$FullScreenFrame = function() { | var build$FullScreenFrame = function() { | ||
− | return build$ | + | return build$AbsolutePositionFrame() |
.css({ | .css({ | ||
background: 'rgba(0,0,0,0.8)', | background: 'rgba(0,0,0,0.8)', | ||
Line 166: | Line 199: | ||
zIndex: 4 // one higher than #p-search's 3 | zIndex: 4 // one higher than #p-search's 3 | ||
}) | }) | ||
− | .click( | + | .click(removeOpenImage); |
} | } | ||
Line 179: | Line 212: | ||
var build$SmallMap = function(zoneData) { | var build$SmallMap = function(zoneData) { | ||
− | return build$ | + | return build$AbsolutePositionFrame().append( |
+ | build$XContainer().append( | ||
+ | build$MapImage(zoneData))); | ||
} | } | ||
+ | var baseXFontSizeInEm = 2; | ||
var buildX = function(left, top, sizeInEm) { | var buildX = function(left, top, sizeInEm) { | ||
− | sizeInEm = sizeInEm || | + | sizeInEm = sizeInEm || baseXFontSizeInEm; |
return $('<div class="x">x</div>') | return $('<div class="x">x</div>') | ||
.css({ | .css({ | ||
Line 214: | Line 250: | ||
var parseLoc = function(locText) { | var parseLoc = function(locText) { | ||
if (typeof locText !== 'string') return locText; | if (typeof locText !== 'string') return locText; | ||
− | + | try { | |
− | var match = locText.match(/\(? *([\+\-]?\d+\.?\d*), *([\+\-]?\d+\.?\d*)\)?/); | + | var match = locText.match(/\(? *([\+\-]?\d+\.?\d*)\s*, *([\+\-]?\d+\.?\d*)\)?/); |
− | + | return {x: parseFloat(match[2]), y: parseFloat(match[1]) }; | |
+ | } catch (err) { | ||
+ | return locText; | ||
+ | } | ||
} | } | ||
Line 238: | Line 277: | ||
var parseLocString = function(locString) { | var parseLocString = function(locString) { | ||
var nonLocParts = ''; | var nonLocParts = ''; | ||
− | var bits = locString.split(/([\+\-]?\d+\.?\d*\D*,\D*[\+\-]?\d+\.?\d*)/g); | + | // Old regex (failed to match Undead Legionaires loc on TT page, but keeping it around unless |
+ | // the new regex has issues | ||
+ | //var bits = locString.split(/([\+\-]?\d+\.?\d*[^0-9\)]*,\D*[\+\-]?\d+\.?\d*)/g); | ||
+ | var bits = locString.split(/([\+\-]?\d+\.?\d*\s*,\D*[\+\-]?\d+\.?\d*)/g); | ||
var relevantBits = bits.filter(function(part) { | var relevantBits = bits.filter(function(part) { | ||
// Filter out the non-loc parts, but save them | // Filter out the non-loc parts, but save them | ||
Line 277: | Line 319: | ||
$container.css('position', 'relative') | $container.css('position', 'relative') | ||
.prepend($img); | .prepend($img); | ||
− | |||
$img.css({ | $img.css({ | ||
[isAboveHalf ? 'top' : 'bottom']: '1.5em', | [isAboveHalf ? 'top' : 'bottom']: '1.5em', | ||
[isLeftOfHalf ? 'left' : 'right']: '1.5em' | [isLeftOfHalf ? 'left' : 'right']: '1.5em' | ||
− | }) | + | }); |
+ | // TODO: Now, check if the map image is off the page, and if so by how much? | ||
+ | // Add/subtract that much from the top/left/bottom/right to make sure | ||
+ | // the whole map is shown ... but also make sure the link itself isn't covered | ||
} | } | ||
else $('body').append($img); | else $('body').append($img); | ||
Line 293: | Line 337: | ||
*/ | */ | ||
var showMapWithLocs = function(zoneData, locs, $container) { | var showMapWithLocs = function(zoneData, locs, $container) { | ||
− | + | removeOpenImage(); | |
+ | |||
zoneData = addImageUrl(zoneData); | zoneData = addImageUrl(zoneData); | ||
var $map = $container ? build$SmallMap(zoneData) : build$FullMap(zoneData); | var $map = $container ? build$SmallMap(zoneData) : build$FullMap(zoneData); | ||
var $xContainer = $map.find('.x-container'); | var $xContainer = $map.find('.x-container'); | ||
+ | var xSize = baseXFontSizeInEm; | ||
+ | if (locs.length > 5) xSize = 0.8 * baseXFontSizeInEm; | ||
+ | if (locs.length > 15) xSize = 0.5 * baseXFontSizeInEm; | ||
$map.find('img').load(function() { | $map.find('img').load(function() { | ||
− | addXs($xContainer, zoneData, locs); | + | addXs($xContainer, zoneData, locs, xSize); |
}); | }); | ||
return showImage($map, $container); | return showImage($map, $container); | ||
}; | }; | ||
+ | var mapIconHtml = '<img ' + | ||
+ | 'alt="Map Icon.png" ' + | ||
+ | 'height="12" ' + | ||
+ | 'src="/images/thumb/Map_Icon.png/12px-Map_Icon.png" ' + | ||
+ | 'width="12">'; | ||
+ | var addLocLinksToTable = function($table, sharedZone, zoneColumnIndex) { | ||
+ | var areaColumnIndex = $table.find('th:contains("Area")').index(); | ||
+ | var locationColumnIndex = | ||
+ | $table.find('th:contains("Location"), th:contains("Loc")').index(); | ||
+ | |||
+ | |||
+ | $table.find('tr').each(function(i, tr) { | ||
+ | var $tr = $(tr); | ||
+ | var $loc = $tr.find('td:eq(' + locationColumnIndex + ')'); | ||
+ | var loc = $loc.text().trim(); | ||
+ | if (!isLoc(loc)) return; // Non-loc text (eg. "Various") in loc column | ||
+ | |||
+ | // Include the area in case it contains relevant non-loc location info | ||
+ | // (such as a floor number) | ||
+ | var nonLoc = $tr.find('td:eq(' + areaColumnIndex + ')').text().trim(); | ||
+ | var zone = sharedZone || | ||
+ | $tr.find('td:eq(' + zoneColumnIndex + ')').text().trim(); | ||
+ | var $locLink = $('<a class="loc-link" href="' + zone + '" ' + | ||
+ | ' data-loc="' + nonLoc + ', ' + loc + '" ' + | ||
+ | ' data-zone="' + zone + '"' + | ||
+ | '>' + loc + '</a>'); | ||
+ | $loc.html($locLink); | ||
+ | // If we actually have zone data, add the map icon | ||
+ | if (getZoneData(zone, loc, nonLoc)) $loc.append(mapIconHtml); | ||
+ | }); | ||
+ | } | ||
+ | |||
// Handle "Where to obtain" rows for spells | // Handle "Where to obtain" rows for spells | ||
var handleWhereToObtainRows = function() { | var handleWhereToObtainRows = function() { | ||
− | $('table:has(th:contains("Area"))' + | + | $('table'+ |
− | ':has(th:contains("Location") | + | ':has(th:contains("Area"))' + |
− | ':has(th:contains("Zone"))') | + | ':has(th:contains("Location"))'+ |
+ | ':has(th:contains("Loc"))' + | ||
+ | ':has(th:contains("Zone"))'+ | ||
+ | ':not(.zoneTopTable)' + | ||
+ | ':not(:has(th:contains("Zone Spawn Timer:")))' | ||
+ | ) | ||
.each(function(i, table) { | .each(function(i, table) { | ||
var $table = $(table); | var $table = $(table); | ||
− | |||
− | |||
− | |||
var zoneColumnIndex = $table.find('th:contains("Zone")').index(); | var zoneColumnIndex = $table.find('th:contains("Zone")').index(); | ||
− | + | addLocLinksToTable($table, null, zoneColumnIndex); | |
− | $table | + | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
}); | }); | ||
+ | }; | ||
+ | |||
+ | // Handle Zone NPC Locs | ||
+ | var handleZoneNPCRows = function() { | ||
+ | var $npcsInZone = $('i:contains("NPCs that spawn in")'); | ||
+ | if ($npcsInZone.length) { | ||
+ | var match = $npcsInZone.text().match(/\d+ NPCs that spawn in (.*):/); | ||
+ | if (!match) return; | ||
+ | |||
+ | var zone = match[1]; | ||
+ | var $table = $npcsInZone.parent().next() | ||
+ | addLocLinksToTable($table, zone); | ||
+ | } | ||
}; | }; | ||
var getZoneName = function() { | var getZoneName = function() { | ||
try { | try { | ||
− | return $('b:contains("Zone:")'). | + | return $('b:contains("Zone:")').parents('tr:first').text().split('Zone:')[1].trim(); |
} catch (err) { | } catch (err) { | ||
return null; | return null; | ||
Line 358: | Line 439: | ||
var handleLocBoxes = function() { | var handleLocBoxes = function() { | ||
var zoneName = getZoneName(); | var zoneName = getZoneName(); | ||
− | var $ | + | var $bold = $('b:contains("Location:"), b:contains("Loc:")').not('.zoneTopTable b, .toc b'); |
+ | if(!$bold.length) return; | ||
+ | |||
+ | var label = $bold.text().trim(); | ||
+ | var labelNoColon = label.substr(0, label.length - 1); | ||
+ | |||
+ | var $locTd = $bold.parent(); | ||
+ | // The TD can either have the label and the locs, or there could | ||
+ | // be one TD for the label, and a sibling for the loc | ||
+ | var isLabelTd = $locTd.text().trim().endsWith(':'); | ||
+ | if (isLabelTd) $locTd = $locTd.closest('tr').children('td:last'); | ||
+ | |||
var zoneData = getZoneDataIfThereIsALocBox($locTd, zoneName); | var zoneData = getZoneDataIfThereIsALocBox($locTd, zoneName); | ||
if (!zoneData) return; | if (!zoneData) return; | ||
Line 378: | Line 470: | ||
var $map = showMapWithLocs(zoneData, locs, $locTd); | var $map = showMapWithLocs(zoneData, locs, $locTd); | ||
− | $map.parent().one('mouseleave', | + | $map.parent().one('mouseleave', removeOpenImage); |
}) | }) | ||
// When it's clicked, show the map full-screen/lightbox style | // When it's clicked, show the map full-screen/lightbox style | ||
Line 385: | Line 477: | ||
}); | }); | ||
− | + | $bold | |
− | + | .html($('<span>' + labelNoColon + ' </span>') | |
− | + | ||
− | + | ||
.append($link) | .append($link) | ||
.append('<span>:</span>')); | .append('<span>:</span>')); | ||
Line 396: | Line 486: | ||
var startLocFunctionality = function() { | var startLocFunctionality = function() { | ||
handleWhereToObtainRows(); | handleWhereToObtainRows(); | ||
+ | handleZoneNPCRows(); | ||
handleLocBoxes(); | handleLocBoxes(); | ||
}; | }; | ||
Line 411: | Line 502: | ||
} | } | ||
}, timeBetweenTries); | }, timeBetweenTries); | ||
− | |||
− | |||
− | |||
− | |||
− | |||
var showMapForData = function(data, $target) { | var showMapForData = function(data, $target) { | ||
if (!data.loc || ! data.zone) return; | if (!data.loc || ! data.zone) return; | ||
− | var locs = [ | + | var locs = parseLocString(data.loc)[0]; |
− | var zoneData = getZoneData(data.zone, locs); | + | var nonLocParts = parseLocString(data.loc)[1]; |
+ | var zoneData = getZoneData(data.zone, locs, nonLocParts); | ||
if (!zoneData) { | if (!zoneData) { | ||
// If it was a click and not a mouseover | // If it was a click and not a mouseover | ||
− | if ( | + | if ($target) return false; |
− | + | ||
+ | return confirm('We\'re sorry, but ' + data.zone + ' has not been loc mapped yet ... ' + | ||
+ | 'please see "/Loc_Maps" for more information. You can now click "OK" to be taken ' + | ||
+ | 'to the zone\'s page instead, where you can use its map as a guide, or "Cancel" to stay here.'); | ||
} | } | ||
showMapWithLocs(zoneData, locs, $target); | showMapWithLocs(zoneData, locs, $target); | ||
+ | }; | ||
+ | // TODO: Add a new class/event handler/template to trigger this | ||
+ | var addCharacterToMap = function(data, $target) { | ||
+ | if (!data.loc || ! data.zone) return; | ||
+ | |||
+ | var locs = parseLocString(data.loc)[0]; | ||
+ | var nonLocParts = parseLocString(data.loc)[1]; | ||
+ | var zoneData = getZoneData(data.zone, locs, nonLocParts); | ||
+ | if (!zoneData) { | ||
+ | // If it was a click and not a mouseover | ||
+ | if ($target) return false; | ||
+ | |||
+ | alertNoData(data.zone); | ||
+ | return true; | ||
+ | } | ||
+ | // TODO: do next line, but replace $target with a query for the existing map on the page | ||
+ | // Also, instead of making an X, make a number that's provided (via link's data attr) | ||
+ | // showMapWithLocs(zoneData, locs, $target); | ||
}; | }; | ||
// TODO: this is in here because fashion links and loc links share global | // TODO: this is in here because fashion links and loc links share global | ||
// event watchers, but this file needs to be renamed now | // event watchers, but this file needs to be renamed now | ||
− | var | + | |
+ | /** | ||
+ | * Shows an image (triggered by clicking on a link, such | ||
+ | * as a thumbnail or a fashion show link, which contains | ||
+ | * data about the file's URL). | ||
+ | */ | ||
+ | var showImageFromData = function(data, $container) { | ||
if (!data.file) return; | if (!data.file) return; | ||
− | var $img = $( | + | var $img = $(buildImgFromFileName(data.file)); |
var $div = $('<div class="framed-image"></div>') | var $div = $('<div class="framed-image"></div>') | ||
.html($img) | .html($img) | ||
− | var $framedImage = | + | var $framedImage; |
− | showImage($framedImage); | + | if ($container) { |
+ | var $outerFrame = build$AbsolutePositionFrame(); | ||
+ | var $innerFrame = $('<div style="position:relative"></div>'); | ||
+ | $framedImage = $outerFrame.append($innerFrame.append($div)); | ||
+ | } else { | ||
+ | $framedImage = build$FullScreenFrame().html($div); | ||
+ | } | ||
+ | showImage($framedImage, $container); | ||
}; | }; | ||
// Hook up loc-link events | // Hook up loc-link events | ||
$('body') | $('body') | ||
− | .on('click', '.loc-link, .fashion-link', function() { | + | .on('click', '.loc-link, .fashion-link, .thumbnail-link', function() { |
+ | removeOpenImage(); // get rid of any mouseover images first | ||
var data = $(this).data(); | var data = $(this).data(); | ||
− | showMapForData(data) | + | var theClickedLinkShouldRedirect = showMapForData(data) || showImageFromData(data); |
− | + | return theClickedLinkShouldRedirect || false; | |
− | return false; | + | |
}) | }) | ||
− | .on('mouseenter', '.loc-link', function(e) { | + | // Temporarily (?) disable mouseover showing of images |
− | + | //.on('mouseenter', '.loc-link, .fashion-link, .thumbnail-link', function(e) { | |
− | + | // var $this = $(e.target).on('mouseleave', removeOpenImage); | |
− | }); | + | // var data = $(this).data(); |
+ | // showMapForData(data, $this); | ||
+ | // showFashionForData(data, $this); | ||
+ | //}); | ||
Line 460: | Line 584: | ||
// 1) Use this function to find the correct 0,0 point | // 1) Use this function to find the correct 0,0 point | ||
window.testZero = window.testZeroZero = function(zoneData) { | window.testZero = window.testZeroZero = function(zoneData) { | ||
− | + | removeOpenImage(); // test functions don't clean up properly | |
var locs = [{ x: 0, y: 0 }]; | var locs = [{ x: 0, y: 0 }]; | ||
showMapWithLocs(zoneData, locs); | showMapWithLocs(zoneData, locs); | ||
Line 468: | Line 592: | ||
// 2) Use this function to generate a grid of alignment of X's | // 2) Use this function to generate a grid of alignment of X's | ||
window.testGrid = function(zoneData) { | window.testGrid = function(zoneData) { | ||
− | + | removeOpenImage(); // test functions don't clean up properly | |
var $locTd = $('b:contains("Location:")').parent(); | var $locTd = $('b:contains("Location:")').parent(); | ||
var locs = []; | var locs = []; |
Latest revision as of 16:39, 3 August 2025
try { (function() { var buildImgFromFileName = function(fileName) { return '<img src="/images/' + fileName.replace(/ /g, '_') + '">' }; // TODO: Add support for cropping maps that have multiple maps. // For instance, the following CSS styles show only one of the three maps for // Erudin Palace: // background-image:url(/images/Erudinpalace.jpg); // background-position: 250px 0px; width: 275px; height: 272px // Figure out how to make the Xs line up with those styles added var locIsWithinAlternateData = function(loc, alternateData) { return loc.x < alternateData.maxX && loc.x > alternateData.minX && loc.y < alternateData.maxY && loc.y > alternateData.minY; }; /** * Determines if the provided name contains any of the provided words */ // TODO: Add a some polyfill to make this a lot simpler var containsAny = function(name/* word1, word2, etc. */) { for(var arg = 1; arg < arguments.length; ++ arg) { var word = arguments[arg]; if (name.includes(word)) return true; } return false; }; var containsAnyNumber = function(name, number, suffix) { return containsAny.call(null, name, number + suffix, 'level ' + number, 'level: ' + number, 'floor ' + number, 'floor: ' + number ); }; var getZoneLevelData = function(zone, text) { var levels = zone.levels; // Check for part "level" aliases (eg. "1st floor" vs. "Level One") text = text.toLowerCase(); if(levels['3'] && levels['3'].image.includes('crystal')) { // Special case for Crystal Caverns if (containsAny(text, 'upper')) return levels[3]; if (containsAny(text, 'lower')) return levels[2]; if (containsAny(text, 'town', 'coldain')) return levels[0]; } // In most zones "tunnel" = level 0. But in Kurn's there are two level zeroes (tunnels and basement), // so this next line handles the Kurn's case in particular if (containsAny(text, 'tunnel') && levels.tunnels) return levels.tunnels; if (containsAnyNumber(text, 0, 'th') || containsAny(text, 'basement', 'underground', 'tunnel', 'bear pit', 'bear pits', 'cave', 'caves')) return levels[0]; if (containsAnyNumber(text, 1, 'st') || containsAny(text, 'one', 'first', 'ground')) return levels[1]; if (containsAnyNumber(text, 2, 'nd') || containsAny(text, 'two', 'second')) return levels[2]; if (containsAnyNumber(text, 3, 'rd') || containsAny(text, 'three', 'third')) return levels[3]; if (containsAnyNumber(text, 4, 'th') || containsAny(text, 'four', 'fourth')) return levels[4]; if (containsAnyNumber(text, 5, 'th') || containsAny(text, 'five', 'fifth')) return levels[5]; // Special case for Tower of Frozen Shadow, which has two level 6 maps if (containsAny(text, '6a')) return levels['6A']; if (containsAny(text, '6b')) return levels['6B']; if (containsAnyNumber(text, 6, 'th') || containsAny(text, 'six', 'sixth')) return levels[6]; if (containsAnyNumber(text, 7, 'th') || containsAny(text, 'seven', 'seventh')) return levels[7]; if (containsAnyNumber(text, 8, 'th') || containsAny(text, 'eight', 'eighth')) return levels[8]; if (containsAnyNumber(text, 9, 'th') || containsAny(text, 'nine', 'ninth')) return levels[9]; var levelKeys = Object.keys(levels); var keyOfLastLevel = levelKeys[levelKeys.length - 1]; if (containsAny(text, 'top')) return levels[keyOfLastLevel]; // If we still couldn't match, and there is a ground/1st floor, use it return levels[1]; }; var findZoneData = function(zoneName, locs, nonLocParts) { // Convert someZone (East) to East someZone if (zoneName.trim().endsWith(')')) { try { var nameSplit = zoneName.split('('); var newName = nameSplit[0].trim(); var direction = nameSplit[1].split(')')[0].trim(); if (['east', 'north', 'south', 'west'].includes(direction.toLowerCase())) { zoneName = direction + ' ' + newName; } } catch(err){ /* just use name (it likely won't work, but try) */} } var zone = zoneData[zoneName]; // Handle aliases (eg. "North Ro" instead of "Northern Desert of Ro") if (typeof(zone) === 'string') zone = zoneData[zone]; if (!zone) return null; // Handle Multi-Level Zones (with different maps for 1st, 2nd, etc. floor) if (zone.levels) { var levelOfZone = getZoneLevelData(zone, nonLocParts); return levelOfZone; } // Handle Zones with alternate maps on the same level (eg. Kelethin in GFay) if (!zone.alternateMaps) return zone; for (var i = 0; i < zone.alternateMaps.length; i++) { var alternateData = zone.alternateMaps[i]; var allLocsAreWithin = true; // wish I had ES6 [].every $.each(locs, function(i, loc) { allLocsAreWithin = allLocsAreWithin && locIsWithinAlternateData(loc, alternateData); }); if (allLocsAreWithin) return alternateData; }; return zone; }; var addImageUrl = function(zoneData) { zoneData.imageUrl = '/images/' + zoneData.image; return zoneData; }; var getZoneData = function(zoneName, locs, nonLocParts) { var zoneData = findZoneData(zoneName, locs, nonLocParts); if (!zoneData) return null; zoneData.zoneName = zoneName; return addImageUrl(zoneData); }; var build$MapImage = function(zoneData) { return $( '<img alt="Map of ' + zoneData.zoneName + '" ' + ' class="thumbborder" ' + ' height="'+ zoneData.height + '" ' + ' src="' + zoneData.imageUrl + '" ' + ' title="Map of ' + zoneData.zoneName + '"' + ' width="' + zoneData.width + '" ' + '>' ); };`` var build$XContainer = function() { return $('<div class="x-container" style="position: relative"></div>'); } var build$LightboxFramedMap = function(zoneData) { var $map = build$XContainer().append(build$MapImage(zoneData)); return addFramingStyles($map, zoneData.width, zoneData.height) }; var addFramingStyles = function($el, width, height) { return $el.css({ left: '50%', marginLeft: '-' + (width / 2) + 'px', // centering marginTop: '-' + (height / 2) + 'px', opacity: 1, top: '50%' }); } var build$AbsolutePositionFrame = function() { return $('<div style="position: absolute"></div>'); }; var removeOpenImage = function(e) { // Do nothing if there's no open map if (!window.$openImg) return false; // Do nothing if the user clicked to open a map, then moused out of an area // (otherwise it can close immediately) var position = window.$openImg.css('position'); if (e && e.type === 'mouseleave' && position === 'fixed') return false; window.$openImg.remove(); return false; } var build$FullScreenFrame = function() { return build$AbsolutePositionFrame() .css({ background: 'rgba(0,0,0,0.8)', height: '100%', left: 0, position: 'fixed', top: 0, width: '100%', zIndex: 4 // one higher than #p-search's 3 }) .click(removeOpenImage); } var build$FullMap = function(zoneData) { var $frame = build$FullScreenFrame(); if (zoneData) { $frame.html(build$LightboxFramedMap(zoneData)); $frame.zoneData = zoneData; } return $frame; }; var build$SmallMap = function(zoneData) { return build$AbsolutePositionFrame().append( build$XContainer().append( build$MapImage(zoneData))); } var baseXFontSizeInEm = 2; var buildX = function(left, top, sizeInEm) { sizeInEm = sizeInEm || baseXFontSizeInEm; return $('<div class="x">x</div>') .css({ color: 'red', fontSize: sizeInEm + 'em', fontWeight: 'bold', left: left, position: 'absolute', top: top }) } /** * Draws a red "X" on the map at the provided coordinate */ var addX = function($xContainer, zoneData, x, y, xSize) { var left = (zoneData.zeroX || 0) + x * -1 * (zoneData.zoomX || 0.1); var top = (zoneData.zeroY || 0) + y * -1 * (zoneData.zoomY || 0.1); $xContainer.append(buildX(left, top, xSize)); } var addXs = function($xContainer, zoneData, locs, xSize) { $.each(locs, function(i, loc) { addX($xContainer, zoneData, loc.x, loc.y, xSize); }); } var parseLoc = function(locText) { if (typeof locText !== 'string') return locText; try { var match = locText.match(/\(? *([\+\-]?\d+\.?\d*)\s*, *([\+\-]?\d+\.?\d*)\)?/); return {x: parseFloat(match[2]), y: parseFloat(match[1]) }; } catch (err) { return locText; } } var isLoc = function(locBit) { // If we can't split the string by its comma and find a number on either side, it's not a loc try { return locBit.split(',')[0].match(/\d+/) && locBit.split(',')[1].match(/\d+/); } catch (err) { return false; } } /** * Helper function to power parseLocs/extractNonLocText, since they both use * the same logic. * * SIDE NOTE: If only the wiki didn't have to use 1999 JS, and could * destructure return values with ES2015, the two related functions * wouldn't even need to exist :( */ var parseLocString = function(locString) { var nonLocParts = ''; // Old regex (failed to match Undead Legionaires loc on TT page, but keeping it around unless // the new regex has issues //var bits = locString.split(/([\+\-]?\d+\.?\d*[^0-9\)]*,\D*[\+\-]?\d+\.?\d*)/g); var bits = locString.split(/([\+\-]?\d+\.?\d*\s*,\D*[\+\-]?\d+\.?\d*)/g); var relevantBits = bits.filter(function(part) { // Filter out the non-loc parts, but save them if (isLoc(part)) return true; nonLocParts += part; // save as it might be something like "1st floor" }); var locs = relevantBits.map(parseLoc); return [locs, nonLocParts]; }; /** * Extracts all of the locs (objects with x and y properties) from a provided * string such as: * "500, 200" * "(200, 300), (300, 200)" * and returns an array containing any locs found. */ var parseLocs = function(locString) { return parseLocString(locString)[0]; }; /** * Extracts the non-loc parts (eg. "Level 1") from a provided string of text * (presumably from an NPC's location <td> or somewhere similar). Although not * actually a part of the loc, this text may still be useful in determining * which map to show for the loc. */ var extractNonLocText = function(locString) { return parseLocString(locString)[1]; }; var showImage = function($img, $container) { if ($container) { $img.css({ marginTop: 40 }); var isAboveHalf = $container.position().top < ($('body').height() / 2); var isLeftOfHalf = $container.position().left < ($('body').width() / 2); $container.css('position', 'relative') .prepend($img); $img.css({ [isAboveHalf ? 'top' : 'bottom']: '1.5em', [isLeftOfHalf ? 'left' : 'right']: '1.5em' }); // TODO: Now, check if the map image is off the page, and if so by how much? // Add/subtract that much from the top/left/bottom/right to make sure // the whole map is shown ... but also make sure the link itself isn't covered } else $('body').append($img); window.$openImg = $img; return $img; }; /** * Adds a loc map image to the page. */ var showMapWithLocs = function(zoneData, locs, $container) { removeOpenImage(); zoneData = addImageUrl(zoneData); var $map = $container ? build$SmallMap(zoneData) : build$FullMap(zoneData); var $xContainer = $map.find('.x-container'); var xSize = baseXFontSizeInEm; if (locs.length > 5) xSize = 0.8 * baseXFontSizeInEm; if (locs.length > 15) xSize = 0.5 * baseXFontSizeInEm; $map.find('img').load(function() { addXs($xContainer, zoneData, locs, xSize); }); return showImage($map, $container); }; var mapIconHtml = '<img ' + 'alt="Map Icon.png" ' + 'height="12" ' + 'src="/images/thumb/Map_Icon.png/12px-Map_Icon.png" ' + 'width="12">'; var addLocLinksToTable = function($table, sharedZone, zoneColumnIndex) { var areaColumnIndex = $table.find('th:contains("Area")').index(); var locationColumnIndex = $table.find('th:contains("Location"), th:contains("Loc")').index(); $table.find('tr').each(function(i, tr) { var $tr = $(tr); var $loc = $tr.find('td:eq(' + locationColumnIndex + ')'); var loc = $loc.text().trim(); if (!isLoc(loc)) return; // Non-loc text (eg. "Various") in loc column // Include the area in case it contains relevant non-loc location info // (such as a floor number) var nonLoc = $tr.find('td:eq(' + areaColumnIndex + ')').text().trim(); var zone = sharedZone || $tr.find('td:eq(' + zoneColumnIndex + ')').text().trim(); var $locLink = $('<a class="loc-link" href="' + zone + '" ' + ' data-loc="' + nonLoc + ', ' + loc + '" ' + ' data-zone="' + zone + '"' + '>' + loc + '</a>'); $loc.html($locLink); // If we actually have zone data, add the map icon if (getZoneData(zone, loc, nonLoc)) $loc.append(mapIconHtml); }); } // Handle "Where to obtain" rows for spells var handleWhereToObtainRows = function() { $('table'+ ':has(th:contains("Area"))' + ':has(th:contains("Location"))'+ ':has(th:contains("Loc"))' + ':has(th:contains("Zone"))'+ ':not(.zoneTopTable)' + ':not(:has(th:contains("Zone Spawn Timer:")))' ) .each(function(i, table) { var $table = $(table); var zoneColumnIndex = $table.find('th:contains("Zone")').index(); addLocLinksToTable($table, null, zoneColumnIndex); }); }; // Handle Zone NPC Locs var handleZoneNPCRows = function() { var $npcsInZone = $('i:contains("NPCs that spawn in")'); if ($npcsInZone.length) { var match = $npcsInZone.text().match(/\d+ NPCs that spawn in (.*):/); if (!match) return; var zone = match[1]; var $table = $npcsInZone.parent().next() addLocLinksToTable($table, zone); } }; var getZoneName = function() { try { return $('b:contains("Zone:")').parents('tr:first').text().split('Zone:')[1].trim(); } catch (err) { return null; }; }; /** * Determines whether the page has a "Location" <td> (as all NPC pages do) * with parsable locs inside it (as only some do). Also checks that * "loc-mapped" zone data exists for the NPC's zone. */ var getZoneDataIfThereIsALocBox = function($locTd, zoneName) { if (!$locTd.length) return false; // page isn't an NPC page (no loc box) var locs = parseLocs($locTd.text()); if (!locs.length) return false; // no locs existed (or could be parsed) var nonLocParts = extractNonLocText($locTd.text()); return !!getZoneData(zoneName, locs, nonLocParts); // does zone data exist? }; var handleLocBoxes = function() { var zoneName = getZoneName(); var $bold = $('b:contains("Location:"), b:contains("Loc:")').not('.zoneTopTable b, .toc b'); if(!$bold.length) return; var label = $bold.text().trim(); var labelNoColon = label.substr(0, label.length - 1); var $locTd = $bold.parent(); // The TD can either have the label and the locs, or there could // be one TD for the label, and a sibling for the loc var isLabelTd = $locTd.text().trim().endsWith(':'); if (isLabelTd) $locTd = $locTd.closest('tr').children('td:last'); var zoneData = getZoneDataIfThereIsALocBox($locTd, zoneName); if (!zoneData) return; // Get the mob's loc(s) var locs = parseLocs($locTd.text()); var nonLocParts = extractNonLocText($locTd.text()); // Do we have data for that zone's map? var zoneData = getZoneData(zoneName, locs, nonLocParts); if (!zoneData) return; // If not, stop here // Add the mouse-enter link var $link = $(' <a href="#">(Map)</a>') // When it's moused-over, show the map nearby .on('mouseenter', function(e) { // If there is already a map "open" (because of a click), do nothing if (window.$openImg && window.$openImg.parent().length) return false; var $map = showMapWithLocs(zoneData, locs, $locTd); $map.parent().one('mouseleave', removeOpenImage); }) // When it's clicked, show the map full-screen/lightbox style .on('click', function(e) { showMapWithLocs(zoneData, locs); }); $bold .html($('<span>' + labelNoColon + ' </span>') .append($link) .append('<span>:</span>')); }; // Setup event listeners (code "starts" here) var startLocFunctionality = function() { handleWhereToObtainRows(); handleZoneNPCRows(); handleLocBoxes(); }; // Sometimes zoneData isn't ready when this code is, so retry for 10 secs in hope that it becomes available var tries = 10; var timeBetweenTries = 1000; // 1 second var interval = window.setInterval(function() { if (window.zoneData) { window.clearInterval(interval); startLocFunctionality(); } else { tries -= 1; if (!tries) window.clearInterval(interval); // Failure :( } }, timeBetweenTries); var showMapForData = function(data, $target) { if (!data.loc || ! data.zone) return; var locs = parseLocString(data.loc)[0]; var nonLocParts = parseLocString(data.loc)[1]; var zoneData = getZoneData(data.zone, locs, nonLocParts); if (!zoneData) { // If it was a click and not a mouseover if ($target) return false; return confirm('We\'re sorry, but ' + data.zone + ' has not been loc mapped yet ... ' + 'please see "/Loc_Maps" for more information. You can now click "OK" to be taken ' + 'to the zone\'s page instead, where you can use its map as a guide, or "Cancel" to stay here.'); } showMapWithLocs(zoneData, locs, $target); }; // TODO: Add a new class/event handler/template to trigger this var addCharacterToMap = function(data, $target) { if (!data.loc || ! data.zone) return; var locs = parseLocString(data.loc)[0]; var nonLocParts = parseLocString(data.loc)[1]; var zoneData = getZoneData(data.zone, locs, nonLocParts); if (!zoneData) { // If it was a click and not a mouseover if ($target) return false; alertNoData(data.zone); return true; } // TODO: do next line, but replace $target with a query for the existing map on the page // Also, instead of making an X, make a number that's provided (via link's data attr) // showMapWithLocs(zoneData, locs, $target); }; // TODO: this is in here because fashion links and loc links share global // event watchers, but this file needs to be renamed now /** * Shows an image (triggered by clicking on a link, such * as a thumbnail or a fashion show link, which contains * data about the file's URL). */ var showImageFromData = function(data, $container) { if (!data.file) return; var $img = $(buildImgFromFileName(data.file)); var $div = $('<div class="framed-image"></div>') .html($img) var $framedImage; if ($container) { var $outerFrame = build$AbsolutePositionFrame(); var $innerFrame = $('<div style="position:relative"></div>'); $framedImage = $outerFrame.append($innerFrame.append($div)); } else { $framedImage = build$FullScreenFrame().html($div); } showImage($framedImage, $container); }; // Hook up loc-link events $('body') .on('click', '.loc-link, .fashion-link, .thumbnail-link', function() { removeOpenImage(); // get rid of any mouseover images first var data = $(this).data(); var theClickedLinkShouldRedirect = showMapForData(data) || showImageFromData(data); return theClickedLinkShouldRedirect || false; }) // Temporarily (?) disable mouseover showing of images //.on('mouseenter', '.loc-link, .fashion-link, .thumbnail-link', function(e) { // var $this = $(e.target).on('mouseleave', removeOpenImage); // var data = $(this).data(); // showMapForData(data, $this); // showFashionForData(data, $this); //}); // *** HELPER FUNCTIONS *** // Define two helper functions for building new zone definitions // 1) Use this function to find the correct 0,0 point window.testZero = window.testZeroZero = function(zoneData) { removeOpenImage(); // test functions don't clean up properly var locs = [{ x: 0, y: 0 }]; showMapWithLocs(zoneData, locs); }; // 2) Use this function to generate a grid of alignment of X's window.testGrid = function(zoneData) { removeOpenImage(); // test functions don't clean up properly var $locTd = $('b:contains("Location:")').parent(); var locs = []; for (var x = zoneData.maxX; x >= zoneData.minX; x -= zoneData.interval) { for (var y = zoneData.maxY; y >= zoneData.minY; y -= zoneData.interval) { locs.push({ x: x, y: y }); } }; showMapWithLocs(zoneData, locs); }; })(); } catch (err) { console.error(err); // If anything goes wrong, move on but log the error }