monster manuel

Version 0.1.7 - The [], spring break monsters

// antimarty's monster manuel helper - pop up monster info for the monster you are fighting
//
// Released under the GPL license
// http://www.gnu.org/copyleft/gpl.html
//
// ==UserScript==
// @name	   monster manuel
// @namespace	antimarty
// @include	   *kingdomofloathing.com/fight.php*
// @include	   *127.0.0.1:600*/fight.php*
// @include	   *localhost:*/fight.php*
// @include	   *kingdomofloathing.com/questlog.php.php*
// @include	   *127.0.0.1:600*/questlog.php*
// @include	   *localhost:*/questlog.php*
// @version		0.1.7
// @grant       GM_getValue
// @grant       GM_setValue
// @grant		GM_xmlhttpRequest
// 
// @description	   Version 0.1.7 - The [], spring break monsters
//
// ==/UserScript==

// released versions:
// Version 0.0.1 - first try
// Version 0.0.2 - fixes for special case monster names
// Version 0.0.3 - more fixes, add update link, general cleanup
// Version 0.0.4 - fernswarthy monster fixes
// Version 0.0.5 - slime tube and hobo monsters, haunted sorority monsters
// Version 0.0.6 - redo popup for non-mafia
// Version 0.0.7 - clean up some minor error handling stuff
// Version 0.0.8 - crimbo 2012 - taco elves and crimbokutown workers
// Version 0.0.9 - "The" Cray-kin, other jar of psychoses stuff, game inform monsters
// Version 0.1.0 - "The" Sierpinski Brothers, "The" Server, mafia wants a password
// Version 0.1.1 - password bug fix
// Version 0.1.2 - mostly undo the password bug fix, and a few things like angry angry bugbears
// Version 0.1.3 - add custom monster entry, then remove it. Plus more names-from-images.
// Version 0.1.4 - dreadsylvania monsters, fixes for GM/Firefox updates
// Version 0.1.5 - add junksprites, halloween monsters

// Known bugs:
// - shows all factoids for multiple monsters with the same name

/* var currentVersion = "0.1.6";
var scriptSite = "http://userscripts.org/scripts/show/150102"
// this is a small file autogenerated by userscripts.org from Userscript @ comments above, use to reduce bandwidth on version check
var scriptURL = "http://userscripts.org/scripts/source/150102.meta.js";  
 */
////////////////////////////////////////////////////////////////////////////////
// Based on a function taken from OneTonTomato's UpUp skill script
function GM_get(target, callback) {
   GM_xmlhttpRequest({
	  method: 'GET',
	  url: target,
	  onload:function(details) {
		 if( typeof callback=='function' ){
			callback(details.responseText);
		 }
	  }
   });
}

// Check for an updated script version
function CheckScriptVersion(data)
{
    // Preemptively set error, in case request fails...
    GM_setValue("webVersion", "Error")

	var m = data.match(/@version\s*([0-9.]+)/);
	if (m)
	{
		GM_setValue("webVersion", m[1]);
	}
}

////////////////////////////////////////////////////////////////////////////////
// parse the char pane for the player name
// revised version! now taken directly from kolpreviousnadventures to handle compact mode
function getPlayerNameFromCharpane() {
	var failed = {'username': "", 'fullmode': true};
	
	if (!top.frames || !top.frames[0]) return failed;
	var username = top.frames[0].document.getElementsByTagName("b");
	
	if (!username || username.length < 1) return failed;
	username = username[0];
	if (!username) return failed;
	username = username.firstChild;
	if (!username) return failed;
	// in full mode the link is <a><b>Name</b></a>
	// in compact mode it's <b><a>Name</a></b>
	// so have to handle this, and also can use it to tell
	// whether it's in compact mode or not.
	var fullmode = true;
	while (username && username.nodeType == 1)
	{
		username = username.firstChild;
		fullmode = false;
	}
	if (!username) return failed;
	username = username.nodeValue;
	if (!username) return failed;
	username = username.toLowerCase();
//	alert("found username " + username + ", fullmode: " + fullmode);
	return {'username': username, 'fullmode': fullmode};
}

// don't strip "The" (and maybe other stuff tbd) from the names of these
var invariableMonsterNames = [
	"El Diablo",
	"five skeleton invaders", // this one shares an image name with procedural stuff
	"The Avatar of Sneaky Pete",
	"The Bat in the Spats",
	"The Beefhemoth",
	"The Big Wisniewski",
	"The Clownlord Beelzebozo",
	"The Cray-Kin",
	"The Frattlesnake",
	"The Free Man",
	"the ghost of Phil Bunion",
	"the gunk",
	"The Landscaper",
	"The Large-Bellied Snitch",
	"The Man",
	"The Sierpinski brothers",
	"The Server",
	"The Snake With Like Ten Heads",
	"The Terrible Pinch",
	"The Thing with No Name",
	"The Thorax",
	"The Unkillable Skeleton"];

// for monsters with name generators, or easier to look at the image names
var monsterImages = {
	"sororghost": "sexy sorority ghost", 
	"sororeton": "sexy sorority skeleton",
	"sororpire": "sexy sorority vampire",
	"sororwolf": "sexy sorority werewolf",
	"sororbie": "sexy sorority zombie",
	"coldhobo": "Cold hobo",
	"hothobo": "Hot hobo",
	"nhobo": "Normal hobo",
	"slhobo": "Sleaze hobo",
	"spookyhobo": "Spooky hobo",
	"stenchhobo": "Stench hobo",
	"elfhobo": "Hobelf",
	"animelf1": "tiny-screwing animelf",
	"animelf2": "plastic-extruding animelf",
	"animelf3": "circuit-soldering animelf",
	"animelf4": "quality control animelf",
	"animelf5": "toy assembling animelf",
	"beergolem": "X Bottles of Beer on a Golem",
	"stonegolem": "X Stone Golem",
	"dimhorror":" X-dimensional horror",
	"hydra": "X-headed Hydra",
	"earbeast": "Beast with X Ears",
	"eyebeast": "Beast with X Eyes",
	"fernghost": "Ghost of Fernswarthy's Grandfather",
	"tacoelf_sign":	"sign-twirling Crimbo elf",
	"tacoelf_taco": "taco-clad Crimbo elf",
	"tacoelf_cart":	"tacobuilding elf",
	"bigskeleton": "procedurally-generated skeleton",
	"faq_miniboss": "Video Game Miniboss",
	"faq_boss": "Video Game Boss",
	"faq_": "Video Game Minion",  // no convenient way to tell strong vs. weak vs. moderate
	"bb_caveman": "angry cavebugbear",  // and all the very very very... angry ones, too
	"dvhotbear": "hot bugbear",
	"dvcoldbear": "cold bugbear",
	"dvspookybear":  "spooky bugbear",
	"dvsleazebear": "sleaze bugbear",
	"dvstenchbear": "stench bugbear",
	"dvhotghost": "hot ghost",
	"dvcoldghost": "cold ghost",
	"dvspookyghost":  "spooky ghost",
	"dvsleazeghost": "sleaze ghost",
	"dvstenchghost": "stench ghost",
	"dvhotskel": "hot skeleton",
	"dvcoldskel": "cold skeleton",
	"dvspookyskel":  "spooky skeleton",
	"dvsleazeskel": "sleaze skeleton",
	"dvstenchskel": "stench skeleton",
	"dvhotvamp": "hot vampire",
	"dvcoldvamp": "cold vampire",
	"dvspookyvamp":  "spooky vampire",
	"dvsleazevamp": "sleaze vampire",
	"dvstenchvamp": "stench vampire",
	"dvhotwolf": "hot werewolf",
	"dvcoldwolf": "cold werewolf",
	"dvspookywolf":  "spooky werewolf",
	"dvsleazewolf": "sleaze werewolf",
	"dvstenchwolf": "stench werewolf",
	"dvhotzom": "hot zombie",
	"dvcoldzom": "cold zombie",
	"dvspookyzom":  "spooky zombie",
	"dvsleazezom": "sleaze zombie",
	"dvstenchzom": "stench zombie",
	"shopteacher": "X-fingered Shop Teacher",
	"js_bender":	"junksprite bender",
	"js_melter":	"junksprite melter",
	"js_sharpener":	"junksprite sharpener",
	"vandalkid": "vandal kid",
	"paulblart":	"suburban security civilian",
	// sloppy seconds sundae, spring break sunken party
	"ssd_cocktail": "Sloppy Seconds Cocktail",
	"ssd_sundae": "Sloppy Seconds Sundae",
	"ssd_burger":	"Sloppy Seconds Burger",
	"fun-gal": "Fun-Guy Playmate",
	"srpainting": "ancestral Spookyraven portrait"
};	  
	
////////////////////////////////////////////////////////////////////////////////
// get monster name (monster manuel version)
function getMonsterName(data) {
	var monsterName = /id=\"monname\"> *(.*?)<\/span>/i.exec(data);
	if(monsterName) 
		monsterName = monsterName[1];
	else 
		return "";
	var imageName = /adventureimages\/(.*?)\.gif/i.exec(data);
	if(imageName) 
		imageName = imageName[1];
	else 
		return "";
// alert("initially found monster name: " + 	monsterName + " (image name: " + imageName + ")");
		
	// strip off leading articles, etc from monster names as seen during fight
	// maybe make this a list of prefixes, it's getting pretty long
	if(invariableMonsterNames.indexOf(monsterName) == -1) {
		if( monsterName.substring(0,2).toLowerCase()=="a ")
		  monsterName = monsterName.substring(2,monsterName.length);
		else if( monsterName.substring(0,3).toLowerCase()=="an ")
		  monsterName = monsterName.substring(3,monsterName.length);
		else if( monsterName.substring(0,5).toLowerCase()=="some ")
		  monsterName = monsterName.substring(5,monsterName.length);
		else if( monsterName.substring(0,4).toLowerCase()=="the ")
		  monsterName = monsterName.substring(4,monsterName.length);
		else if( monsterName.substring(0,3).toLowerCase()=="el ")
		  monsterName = monsterName.substring(3,monsterName.length);
		else if( monsterName.substring(0,3).toLowerCase()=="la ")
		  monsterName = monsterName.substring(3,monsterName.length);

		// translate special cases
		if(monsterName.indexOf("'s butt") != -1)
			monsterName = "[somebody else's butt]";
		else if(monsterName.substring(0,6).toLowerCase()=="shadow" && monsterName.indexOf("Black Bubbles")==-1 && imageName.indexOf("faq_")==-1)
			monsterName = "(shadow opponent)";
		// all slime tube monsters are... "Slime Tube monster", ignore names, just check for the image
		else if(/slime[1-5]/i.exec(imageName) != null )
			monsterName = "Slime Tube monster";
		// more monsters-from-images
		// keep trailing numbers for anime elves (different monsters), strip otherwise (different images for same mob)
		else if(/animelf[1-5]/i.exec(imageName) != null )
			monsterName = monsterImages[imageName];
		else {
			var stripped = /(.*?)[0-9]+/i.exec(imageName);
			if(stripped != null) imageName=stripped[1];
			if(monsterImages[imageName] != undefined)
				monsterName = monsterImages[imageName];
			else if(imageName.substring(0,4).toLowerCase()=="faq_")  // video game minions, how to tell strong vs. weak?
				monsterName = monsterImages["faq_"];
		}
	}

 //  alert("returning monster name: " + 	monsterName + " (image name: " + imageName + ")");
	return monsterName;
}
	
// from one ton tomato's mallsearch script
function getParent(el, pTagName) {
	if (el == null) {
		return null;
	} else if (el.nodeType == 1 && el.tagName.toLowerCase() == pTagName.toLowerCase()) {	// Gecko bug, supposed to be uppercase
		return el;
	} else {
		return getParent(el.parentNode, pTagName);
	}
}

// fill my popup window with a reduced version of manuels facts, for our monster only
function processManual(data){
	var myDocument = document.createElement('div');
	myDocument.innerHTML = data;
	
	// could just get monsterName directly, this is a relic from earlier versions
	var monsterName = /\&monster=(.+)/i.exec(decodeURIComponent(manuelURL));  
	if(monsterName) monsterName = monsterName[1];
 // alert("monster name = " + monsterName);

	// first find the table with our monster; find its parent; use that to find all the monster tables
	if(monsterName) {
		// strip out the javascript href change that loads the game main page
		var allscripts = myDocument.getElementsByTagName("script");
		allscripts[0].parentNode.removeChild(allscripts[0]);
	
		var allElements = myDocument.getElementsByTagName("font");
		var parentNode = 0;
		var monsterTables =0;
		var i;
		for(i=0; i < allElements.length; i++) {
			if(allElements[i].innerHTML.indexOf(monsterName) != -1) {
				parentNode = getParent(allElements[i], "table");
				parentNode = getParent(parentNode, "p");
				monsterTables = parentNode.getElementsByTagName("table");
				break;
			}	 
		}

		// now delete all that parent's children, except our keeper monster
		// monsterTables is a live list, so as we delete items in the doc, list items disappear...
		if(monsterTables) {
			for(i=0; i < monsterTables.length; ) {
				// use the <font></font> tags to eliminate false matches in random text. Harem guards
				// is an example. (why did I change this?)
//				if(monsterTables[i].innerHTML.indexOf(">" + monsterName + "</font>") > -1) {
				if(monsterTables[i].innerHTML.indexOf(">" + monsterName + "<") >= 0
				|| monsterName=="Video Game Minion" && monsterTables[i].innerHTML.indexOf(">" + monsterName)>= 0) {
					// our monster, skip it
					i++;
				}
				else {
					// not our monster, delete it (next target monster table will be at same index)
					// there is also an <a> node in front of each monster
					monsterTables[i].parentNode.removeChild(monsterTables[i].previousSibling);
					monsterTables[i].parentNode.removeChild(monsterTables[i]);
				}
			}
		}
		// delete some junk stuff - other quests header info, back to campsite footer, etc
		if(parentNode.parentNode) {
			parentNode.parentNode.removeChild(parentNode.parentNode.firstChild);
			parentNode.parentNode.removeChild(parentNode.parentNode.firstChild);
			parentNode.parentNode.removeChild(parentNode.parentNode.firstChild);
			parentNode.parentNode.removeChild(parentNode.parentNode.lastChild);
			parentNode.parentNode.removeChild(parentNode.parentNode.lastChild);
		}
		
/* 		// if new script version available, add a link
		var webVer = GM_getValue("webVersion", "Error");
		if (webVer != "Error" && webVer > currentVersion) {  // this is actually a text string comparison, not numerical
			var newElement = document.createElement('p');
			newElement.style.fontSize = "x-small";
			newElement.appendChild(document.createTextNode("New Monster Manuel script version " + webVer + " available: "));

			var hrefElement = document.createElement('a');
			hrefElement.setAttribute('href', scriptSite);
			hrefElement.setAttribute('target', "_blank");
			hrefElement.appendChild(document.createTextNode("here"));
			newElement.appendChild(hrefElement);
			
			parentNode.parentNode.appendChild(newElement); 
		}
 */
		// and finally fill our window
		myWindow.document.documentElement.innerHTML = myDocument.innerHTML;
		myWindow.document.close();
	}
}

// pop up an empty window for our factoid, and call something to fill it
// to do: figure out how to size this appropriately
var myWindow;
function manuelPopup()
{
	myWindow = window.open(baseURL,'manuel','height=400,width=500,scrollbars=yes');
	GM_get(baseURL + manuelURL, processManual);
}

// parse the charpane info for the password hash (use as a session ID)
function getPwdHash(data){
    var pwdHash = /pwdhash \= \"(.*?)\"/i.exec(data); // the .*? is the non-greedy version of .*
	if(pwdHash)
		pwdHash = pwdHash[1];
	else
		pwdHash = "";

	return pwdHash;
}

////////////////////////////////////////////////////////////////////////////////
function monsterNameEntry() {
	var monsterName = prompt("Enter Monster Name to research:\n", "");
	var firstChar = monsterName.toLowerCase().charAt(0) ;
	if(firstChar < 'a' || firstChar > 'z') firstChar = '-';

	manuelURL = "questlog.php?which=6&vl=" + firstChar + "&monster="+monsterName;
	manuelPopup();
}
////////////////////////////////////////////////////////////////////////////////
// currently unused - not really helpful to search unless can cross first-letter sections
function processQuestPage() {
	var entries = document.getElementsByTagName("b");
	var i;
	for(i=0; i < entries.length; i++) {
		if(entries[i].innerHTML == "Other") {
			// append our item here
			var newElement = entries[i].parentNode.parentNode.appendChild(document.createElement('b'));
			newElement.setAttribute("onmouseover", 'this.style.opacity="0.5"');
			newElement.setAttribute("onmouseout", 'this.style.opacity="1"');
			newElement.innerHTML = "&nbsp;Search";
			newElement.addEventListener("click", monsterNameEntry);
		}
	}
}

////////////////////////////////////////////////////////////////////////////////
function processFight() {
	var playerName = getPlayerNameFromCharpane().username;
	var pwdHash = "";
	
	// once per login, check if a new version is available
	// why all this top level stuff sometimes fails at each level, who knows
	if(playerName && top && top.frames[0] && top.frames[0].document && top.frames[0].document.documentElement) {
		pwdHash = getPwdHash(top.frames[0].document.documentElement.innerHTML);
		var oldPwdHash = GM_getValue(playerName + "_pwdHash", 0);
		if(pwdHash != oldPwdHash) {
			// new session
			GM_setValue(playerName + "_pwdHash", pwdHash);
			
/* 			// check for a new version of script if none seen already
			var webVer = GM_getValue("webVersion", "Error");
			if(webVer == "Error" || webVer <= currentVersion)
				GM_get(scriptURL, CheckScriptVersion);
 */		
		}
		// clear any lingering flag that was set with empty player name by old script versions
		GM_setValue("_hasManuel", false);
	}
	
	var monsterName = getMonsterName(document.body.innerHTML);
	var firstChar = monsterName.toLowerCase().charAt(0) ;
	if(firstChar < 'a' || firstChar > 'z') firstChar = '-';
	// apparently mafia now wants a password
	// yay, that change got reverted, and was causing problems anyway
//	manuelURL = "questlog.php?which=6&vl=" + firstChar + "&pwd=" + pwdHash + "&monster="+monsterName;
	manuelURL = "questlog.php?which=6&vl=" + firstChar + "&monster="+monsterName;

	// find the monster's HTML element on the page; add our link
	var monsterSpan = document.getElementById("monname");
	var monsterTable = getParent(monsterSpan, "table");
	if(monsterSpan && monsterTable) {
		var newElement = document.createElement("FONT");
		// make sure we have a factoid to popup - signalled by the attack/defense/hp display
		// or by monster manuel saying something
		if(monsterTable.innerHTML.indexOf("Enemy's Attack Power") != -1
		|| document.body.innerHTML.indexOf("Monster Manuel") != -1 ){
			if(playerName != "")
				GM_setValue(playerName+"_hasManuel", true);  // current monster has factoids, ergo player has manuel

			newElement.innerHTML = "<font size=2> (<u>factoids</u>)</font>"; ;
			newElement.setAttribute("onmouseover", 'this.style.opacity="0.5"');
			newElement.setAttribute("onmouseout", 'this.style.opacity="1"');
			newElement.setAttribute("id", 'manuel');
			newElement.addEventListener("click", manuelPopup, true);
			// always insert a factoid link if there are factoids
			monsterSpan.parentNode.insertBefore(newElement, monsterSpan.nextSibling);
		} else {
			newElement.innerHTML = "<font size=2> (no factoids)</font>"; ;
			newElement.setAttribute("onmouseout", 'this.style.opacity="0.5"');
			newElement.setAttribute("id", 'manuel');
			// no factoids, only insert the link if we know the player has manuel
			if(GM_getValue(playerName+"_hasManuel", false) == true ) 
				monsterSpan.parentNode.insertBefore(newElement, monsterSpan.nextSibling);
		}
	}
}

////////////////////////////////////////////////////////////////////////////////
// main prog, just call the proper routine if we are on a pane we care about
var nodeBody   = document.getElementsByTagName("body").item(0);
var baseURL	   = "";
var manuelURL = "";

if (nodeBody) {
   baseURL = nodeBody.baseURI.substring(0,nodeBody.baseURI.lastIndexOf('/')+1);
}

// our popup gets named after the originating window, which is fight.php, so don't process it
if(window.name != "manuel" && document.location.pathname.indexOf("fight.php") != -1 ) {
	processFight();
}
else if(document.location.pathname.indexOf("questlog.php") != -1 ) {
	// deprecated until I can figure out how to search across different first letters
//	processQuestPage();
}