antimarty fortune cookie script

Kingdom of Loathing fortune cookie tracker

// antimarty's fortune cookie script, based in part on:
// csemaj's KoL Script 
//     Copyright (c) 2007, James Cammarata
//     Based on code written by Byung Kim (Tard) http://kol.dashida.com and OneTonTomato's scripts
// toggle preference code from lukifer's mrscript
//	   http://www.noblesse-oblige.org/lukifer/scripts/
// script update code based on DrEvi1's hatrack helper, which credits Picklish
// fixes for lag combatting inventory screens by Firvagor
// now using Charon's account options tab code
// Released under the GPL license
// http://www.gnu.org/copyleft/gpl.html
//
// ==UserScript==
// @name		   antimarty fortune cookie script
// @namespace	   antimarty
// @include		   *kingdomofloathing.com/*.php*
// @include		   *127.0.0.1:600*/*.php*
// @include		   *localhost:600*/*.php*
// @include		   localhost:18481/*.php*
// @version		   0.5.8
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// 
// @description	   Kingdom of Loathing fortune cookie tracker
// ==/UserScript==

// released versions:
// Version 0.5.7 - clan speakeasy, new topmenu frame rearrangement, gm grant stuff, move to Greasy Fork
// Version 0.5.5 - kolhs semis; red fox; bram the stoker
// Version 0.5.4 - new undersea SRs
// Version 0.5.3 - new giant castle SRs, fix oxygenarian detection
// Version 0.5.2 - updated for new level 8 and level 9 semis, and typo for friars
// Version 0.5.1 - new hidden temple semi rare
// Version 0.5.0 - add last location info to char pane if no popups
// Version 0.4.9 - bug fix for deep fat friars' adventure name
// Version 0.4.8 - bug fix for auto-attack combat macros, change version check
// Version 0.4.7 - add disable popups option as FF4 workaround
// Version 0.4.6 - bug fix for noncombat semis
// Version 0.4.5 - Show initial bracket for new ascension, bug fix for puttied/faxed/arrow'd SR monsters
// Version 0.4.4 - ascii, kge fights from fax/putty/camera/arrowing aren't semis; support new options page
// Version 0.4.3 - add elf alley semi
// Version 0.4.2 - add the pixel stopwatch
// Version 0.4.1, use the turnsplayed native counter
// Version 0.4.0, display a predicted cookie window if no cookie eaten
// Version 0.3.9, support for billiards room semi
// Version 0.3.8, sanity check numbers when user loads charsheet
// Version 0.3.7, misc bug fixes
// Version 0.3.6, fixes (courtesy Firvagor) for crimbo anti-lag changes to inventory screens; clear counter after semi-rare
// Version 0.3.5, filter impossible cookie values; check for updates automatically
// Version 0.3.4, support for hobopolis adventures
// Version 0.3.3, bugfixes encore
// Version 0.3.2, bugfixes
// Version 0.3.1, bugfixes
// Version 0.3.0, remember most recent semi-rare. Re-fix bugs re-introduced in 0.2.6
//     for unascended chars and compact mode, thanks to bad version control.
// Version 0.2.6, reduced overhead, and bug fixes
// Version 0.2.5, compact mode support and support for unascended chars
// Version 0.2.3, improved sanity checker
// Version 0.2.0, includes sanity checker
// Version 0.1.1, bugfixes
// Version 0.1.0 initial beta

// Known bugs:
// doesn't work in the kolmafia relay browser if you have this option set:
//	 [] Force results to reload inline for [use] links
// ... because the results page never reloads, so greasemonkey scripts don't run
// may not save data correctly if Firefox doesn't exit cleanly (Greasemonkey issue)

// basic logic to do this without retrieving character pane data:
// - store an internal counter for adventures used
// - look at the adventures remaining
// - every time the pane is refreshed:
//	 - if the remaining adventures decreases, decrease the counter by that amount
//	 - if stays the same, do nothing
//	 - if increases, do nothing
// warn when hit zero
//
// read char sheet as a sanity check when the counter doesn't seem to be
// decrementing normally

// (temporarily?) unused, with move to Greasy Fork
/* var currentVersion = "0.5.7";
var scriptSite = "http://userscripts.org/scripts/show/13180"
// this is autogenerated by userscripts.org from Userscript @ comments above, use to reduce bandwidth on version check
var scriptURL = "http://userscripts.org/scripts/source/13180.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 version number of script on the web
function CheckScriptVersion(data)
{
// alert("inside version check");
    // 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 username = document.getElementsByTagName("b");
	if (!username || username.length < 1) return false;
	username = username[0];
	if (!username) return false;
	username = username.firstChild;
	if (!username) return false;
	// 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 false;
	username = username.nodeValue;
	if (!username) return false;
	username = username.toLowerCase();
//	alert("found username " + username + ", fullmode: " + fullmode);
	return {'username': username, 'fullmode': fullmode};
}

////////////////////////////////////////////////////////////////////////////////
// parse the char sheet (not the sidepane) for the player name
function getPlayerNameFromCharsheet(data) {
	// it's an href with syntax something like 
	// showplayer.php?who=PlayerID">PlayerName</a>
	var playerName = /showplayer\.php\?who\=\d+\">([^<]+)<\/a/i.exec(data);  // sometimes this fails, don't know why
	if(playerName)
		return playerName[1].toLowerCase();
	else
		return null;
}

////////////////////////////////////////////////////////////////////////////////
// parse the char sheet (not pane) for the total adventure count
// taken from the csemaj cookie script
// the "(this run)" only appears if you have ascended 
function getTurnsPlayed(data) {
	var turncount = "0";
	if(data.indexOf("Turns Played (this run)") >= 0) {
// alert("parsing datasheet for turns for an ascended char");
		turncount = /Turns Played \(this run\)[^>]*>(<[^>]+>)*([\d,]+)/i.exec(data)[2];
	}
	else {
// alert("parsing datasheet for turns for an UNascended char");
		turncount = /Turns Played[^>]*>(<[^>]+>)*([\d,]+)/i.exec(data)[2];
	}
// alert("found turncount=" + turncount);
	return parseInt(turncount.replace(',',''),10);
}

function getDaysPlayed(data) {
	var dayCount = 0;
	if(data.indexOf("Days Played (this run)") >= 0) {
// alert("parsing datasheet for days for an ascended char");
		dayCount = /Days Played \(this run\)[^>]*>(<[^>]+>)*([\d,]+)/i.exec(data)[2];
	}
	else {
// alert("parsing datasheet for days for an UNascended char");
		dayCount = /Days Played[^>]*>(<[^>]+>)*([\d,]+)/i.exec(data)[2];
	}
// alert("found dayCount=" + dayCount);
	return parseInt(dayCount.replace(',',''),10);
}

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

// alert("got pwdHash: " + pwdHash);
	return pwdHash;
}

// parse the charpane info for the turnsplayed counter
// updates at the end of the adventure, not during the fight
function getTurnsplayedVar(data){
    var turnsplayed = /turnsthisrun \= (\d+)/i.exec(data)[1];

	return parseInt(turnsplayed);
}

////////////////////////////////////////////////////////////////////////////////
// parse the main page for the name of the (noncombat) adventure - assuming 
// it's the next bolded thing after "Adventure Results:"
function getAdventureName(data) {
    var adventureName = "";
    if ( data.indexOf( "Adventure Results:" ) != -1 ) {   // kol mafia can put up stuff that requires this check
        adventureName = /Results:<\/b>.*?<b>(.*?)<\/b>/i.exec(data)[1];
    }
    // the hobopolis rares are choice adventures, not normal noncombats
    else if(document.location.pathname.indexOf("choice.php") != -1) {
	// look for adventure name as something in bold on blue background
        adventureName = /bgcolor=\"blue\"><b>(.*?)<\/b>/i.exec(data)[1];
    }
	
	// check for known broken adventures
	if(adventureName == "") {
		if(data.indexOf( "There once was a bleary-eyed cyclops" ) != -1 ){
			adventureName = "The Bleary-Eyed Cyclops";
		}
	}

// alert("got adventure name: " + adventureName);
    return adventureName;
}

////////////////////////////////////////////////////////////////////////////////
// parse the main page for the name of the adventure
function getMonsterName(data) {
	var monsterName = /id=\"monname\"> *(.*?)<\/span>/i.exec(data);
	if(monsterName) 
		monsterName = monsterName[1].toLowerCase();
	else 
		return {'monsterName': "", 'fromPutty': false};
	
	// don't count monsters from putty/fax/camera/arrowing
	var fromPutty = data.indexOf("You put the copied monster on the ground") != -1 ? true : false;
	if(!fromPutty)
		fromPutty = data.indexOf("You gaze at the photocopy") != -1 ? true : false;
	if(!fromPutty)
		fromPutty = data.indexOf("You reach down to open up the camera") != -1 ? true : false;
	if(!fromPutty)
		fromPutty = data.indexOf("hear a wolf whistle from behind you") != -1 ? true : false;
	if(!fromPutty)
		fromPutty = data.indexOf("You play back the recording") != -1 ? true : false;
	if(!fromPutty)
		fromPutty = data.indexOf("%%") != -1 ? true : false;  // some sort of error we see a lot with copied monsters
		
//  alert("got monster name: *" + monsterName + "*; from putty = " + fromPutty);
	return {'monsterName': monsterName, 'fromPutty': fromPutty};
}

// process a noncombat result for semi-rareness
var semis =
{
  noncombats: {
	"Play Misty For Me"		:	"the Haunted Kitchen",
	"Like the Sunglasses, But Less Comfortable"	:	"the Haunted Library",
	"The Pilsbury Doughjerk"	:	"the Haunted Pantry",
	"The Bleary-Eyed Cyclops"	:	"the Limerick Dungeon",  // I don't see the title appear when I get this adv.
	"In the Still of the Alley"	:	"the Sleazy Back Alley",
	"Natural Selection"		:	"the Goatlet",
	"Not Quite as Cold as Ice"	:	"the Lair of the Ninja Snowmen",
	"Prior to Always"		:	"a Battlefield",
	"How Does He Smell?"		:	"the Batrat and Ratbat Burrow",
	"All The Rave"			:	"the Castle Top Floor",
	"Le Chauve-Souris du Parfum"	:	"Guano Junction",
	"Hands On"			:	"the Harem",
	"You Can Top Our Desserts, But You Can't Beat Our Meats" :	"the Laboratory",
	"Rokay, Raggy!"			:	"the Menagerie Level 2",
	"A Menacing Phantom"		:	"the Misspelled Cemetery",
	"Lunchboxing"			:	"the Outskirts of Cobb's Knob",
	"Filth, Filth, and More Filth"	:	"South of the Border",
	"It's The Only Way To Be Sure"	:	"the Deep Fat Friars",
	"It's the Only Way to be Sure"	:	"Pandamonium slums",
	"Two Sizes Too Small"		:	"the Hidden City",
	"Some Bricks Do, In Fact, Hang in the Air"  :	"the Inexplicable Door",
	"Blaaargh! Blaaargh!"		:	"the Spooky Forest",
	"Monty of County Crisco"	:	"Whitey's Grove",
	"The Latest Sorcerous Developments"	:	"the Hippy Camp (pre-war)",
	"Sand in the Vaseline"		:	"the Orcish Frat House (pre-war)",
	"Yo Ho Ho and a Bottle of Whatever This Is"	:	"the Obligatory Pirate's Cove",
	"A Tight Squeeze"		:	"BurnBarrel Blvd.",
	"Cold Comfort"			:	"Exposure Esplanade",
	"Juicy!"			:	"the Heap",
	"Flowers for You"		:	"the Ancient Hobo Burial Ground",
	"Maybe It's a Sexy Snake!"	:	"the Purple Light District",
	"What a Tosser"				:	"Elf Alley",
	"A Shark's Chum"			:	"The Haunted Billiards Room",
	"The Time This Fire"		:	"Vanya's Castle Chapel",
	"Oh, <i>There</i> Have it Gone" : "A-Boo Peek",
	"Synecdoche, Twin Peak"	: "Twin Peak",
	"It's a Gas Gas Gas"	: "Oil Peak",
	"Fit and Finish"		: "the Castle Basement",
	"Ahead of the Game"		: "the Castle Ground Floor",
	"Razor, Scooter"		: "the Dive Bar",
	"Deeps Impact"			: "the Briny Deeps",
	"The Haggling"			: "the Brinier Deepers",
	"Camera On, James"		: "the Wreck of the Edgar Fitzsimmons",
	"Dragon the Line"		: "Madness Reef",
	"A Drawer of Chests"	: "the Mer-Kin Outpost",
	"Through the Locking Glass"	: "the Hallowed Halls",
	"Clay Is Great, But Leather Is Bether" : "Ye Olde Medievale Villagee",
	"Where There's Smoke...":	"The Copperhead Club",
	"Methinks the Protesters Doth Protest Too Little": "A Mob of Zeppelin Protesters",
	// fake ones for testing
//	"A Dash of Boulder"		:	"the Dungeons of Doom - Boulder",
//	"Meat Score!"			:	"the Treasury - Meat Score",
	
  },
  combats: {  // use lowercase
	"some bad ascii art"		:	"the Valley Beyond The Orc Chasm",
	"a knob goblin elite guard captain" : 	"the Cobb's Knob Kitchens",
	"a knob goblin embezzler" 	:	"the Treasury",
	"a c. h. u. m. chieftain"	: 	"a Maze of Sewer Tunnels",
	"baa'baa'bu'ran"			:	"the Hidden Temple",
	"a 7-foot dwarf foreman" 	: "Itznotyerzitz Mine",
	"a moister oyster"			: "An Octopus's Garden",
	"françois verte, art teacher" : "Art Class",  // some encoding issue with this one
	"x-fingered shop teacher" : 	"Shop Class", // need to resolve the X (1-11)
	"mrs. k, the chemistry teacher"	: "Chemistry class",
	"the red fox"	: "The Red Zeppelin",
	"bram the stoker" : "The Haunted Boiler Room",
	"a full-length mirror" : "The Haunted Storage Room",
	// fake ones for testing
//	"a knob goblin elite guardsman"	:	"the Treasury",
//      "a swarm of killer bees"	:	"the Dungeons of Doom",
  },
};	  

function clearCounters(playerName) {
		GM_setValue(playerName+"_lastSemiTurn",-1);
		GM_setValue(playerName+"_lastSemiLocation","");
		
		GM_setValue(playerName+"_luckyTurn1", -1);
		GM_setValue(playerName+"_luckyTurn2", -1);
		GM_setValue(playerName+"_luckyTurn3", -1);
}

function checkForNoncombatSemi(data) {
	theAdv = getAdventureName(document.body.innerHTML);
// alert("in checkForNoncombatSemi(), adv name = " + theAdv);
// alert("semis.noncombats[theAdv] = " + semis.noncombats[theAdv]);
	
	if(semis.noncombats[theAdv] != undefined) {
		var playerName = GM_getValue("currentPlayer"); 
		var turncount = GM_getValue(playerName+"_turncount", 0);
//		var turncount = getTurnsplayedVar(top.frames[0].document);

// alert("Found semirare adv \'" + theAdv + "\' in area " + semis.noncombats[theAdv] + " on tuncount " + turncount + " for player " + playerName);
		// found a semi, clear counters
		// this might backfire if using the counter as a generic countdown for other purposes, e.g. wossname tracking
		clearCounters(playerName);

		GM_setValue(playerName+"_lastSemiTurn",turncount+1);  // the turncount var isn't updated yet
		GM_setValue(playerName+"_allowBracket",true);
		GM_setValue(playerName+"_lastSemiLocation",semis.noncombats[theAdv]);

		// trigger char pane refresh with the new info
		top.frames[0].location.reload();
	}
}

function checkForCombatSemi(data) {
	var monster = getMonsterName(document.body.innerHTML);
	theAdv = monster.monsterName; 

	if(monster.fromPutty == false && semis.combats[theAdv] != undefined) {
		var playerName = GM_getValue("currentPlayer"); 
		var turncount = GM_getValue(playerName+"_turncount", 0);
//		var turncount = getTurnsplayedVar(top.frames[0].document);

// alert("Found semi rare adv \'" + theAdv + "\' in area " + semis.combats[theAdv] + " - for player " + playerName + " on turn " + turncount );
		// found a semi, clear counters
		// this might backfire if using the counter as a generic countdown for other purposes, e.g. wossname tracking
		clearCounters(playerName);

		GM_setValue(playerName+"_lastSemiTurn",turncount+1);  // the turncount var isn't updated yet
		GM_setValue(playerName+"_allowBracket",true);
		GM_setValue(playerName+"_lastSemiLocation",semis.combats[theAdv]);

		// trigger char pane refresh with the new info
		top.frames[0].location.reload();
	}
}

////////////////////////////////////////////////////////////////////////////////
// callback function to process the main charsheet as a sanity check after 
// something questionable was spotted, or just on a new session
// **** mostly deprecated ****
function sanityCheckCharsheet(data) {
// alert("entering sanity check");
	var playerName = getPlayerNameFromCharsheet(data);
	GM_setValue("currentPlayer", playerName);
	
	var turncount = getTurnsPlayed(data); // total turns played
	
	if(isNaN(turncount) ) {
// alert("sanityCheckCharsheet - unable to parse either turncount (" + turncount + ") or remaining adv count (" + remainingAdventureCount + "), aborting");
		return;	 // hopefully will try again with more success, can't continue
	}

	GM_setValue(playerName+"_turncount", turncount);  // I'll try to keep this up to date

	// if the previous turncount makes no sense, zero it. After ascending would be an example
	var lastSemiTurn = GM_getValue(playerName+"_lastSemiTurn",-1);
	if( lastSemiTurn > turncount+1) {
		GM_setValue(playerName+"_lastSemiTurn",-1);
		GM_setValue(playerName+"_lastSemiLocation","");
	}
	var prevDayCount = GM_getValue(playerName+"_dayCount",-1);

	var dayCount = getDaysPlayed(data);
	if(!isNaN(dayCount)) {
		if(dayCount < prevDayCount) { // must have ascended
			GM_setValue(playerName+"_lastSemiTurn",-1);
			GM_setValue(playerName+"_lastSemiLocation","");
		}
		GM_setValue(playerName+"_dayCount",dayCount);
	}

	// set a flag is we are oxy, since it affects the cookie spacing
	if( data.match("Oxygenarian"))
		GM_setValue(playerName+"_isOxy", true);
	else	
		GM_setValue(playerName+"_isOxy", false);
	
}

////////////////////////////////////////////////////////////////////////////////
// watch for the lucky numbers result on the main page
function processMain(doc) {
	var countdown1 = -1;
	var countdown2 = -1;
	var countdown3 = -1;
	var pos = -1;

// alert("in processMain()");	  
	///////NEW STUFF (Firvagor) - changed the if line to use the div instead of the body, since the results, and a hidden field is made inside the div so infinite looping doesn't occur////////// 
	//if the hidden field is found, don't do anything
	if (doc.getElementById("cookiedone")!=null){return;}

	var playerName = GM_getValue( "currentPlayer");
	var divElement = doc.getElementById("effdiv");
	
	var bqElement = doc.getElementsByTagName("blockquote");
//	if(bqElement[0])
//		alert("processMain: speakeasy quote = " + bqElement[0]);

	if ( divElement ) {
 // alert("found divElement");

	doc.getElementById("effdiv").innerHTML += "<input type='hidden' id='cookiedone' name='completed' value='yes'>"; 
		if((pos = doc.getElementById("effdiv").innerHTML.indexOf( "Lucky numbers:" )) != -1 ) {

 // alert("found lucky numbers");
			// need to parse the text, format is "Lucky numbers: ###, ###, ###"
			var cookieText = doc.getElementById("effdiv").innerHTML.substring(pos, pos+30);

			// first one starts after the colon, next two after commas
			pos = cookieText.indexOf(":");
			cookieText = cookieText.substring(pos+1);
			countdown1 = parseInt(cookieText);

			pos = cookieText.indexOf(",");
			cookieText = cookieText.substring(pos+1);
			countdown2 = parseInt(cookieText);

			pos = cookieText.indexOf(",");
			cookieText = cookieText.substring(pos+1);
			countdown3 = parseInt(cookieText);

			// elegantly sort them
			{
				var temp;
				if(countdown1 > countdown2) {
					temp = countdown1; countdown1 = countdown2; countdown2 = temp;
				}
				if(countdown2 > countdown3) {
					temp = countdown2; countdown2 = countdown3; countdown3 = temp;
				}
				if(countdown1 > countdown2) {
					temp = countdown1; countdown1 = countdown2; countdown2 = temp;
				}
			}
//		alert("processMain: cookie cookieText=" + cookieText + ", lucky numbers are " + countdown1 + ", " + countdown2 + ", " + countdown3);
		}
	}
		
	// speakeasy drink; format is "burp-speak the number <b>30</b>"
	// doesn't work, is there really an effdiv for this? didn't see it in the text
	// try a blockquote instead... doesn't sound too reliable
//		else if((pos = doc.getElementById("effdiv").innerHTML.indexOf( "burp-speak the number" )) != -1 ) {
	if(bqElement[0] && (pos = bqElement[0].innerHTML.indexOf( "burp-speak the number" )) != -1 ) {
 // alert("processing speakeasy");
		bqElement[0].innerHTML += "<input type='hidden' id='cookiedone' name='completed' value='yes'>"; 
		
		var cookieText = bqElement[0].innerHTML.substring(pos, pos+30);

		// just one number, bolded
		pos = cookieText.indexOf("<b>");
		cookieText = cookieText.substring(pos+3);
		countdown1 = parseInt(cookieText);
		countdown2 = -1;
		countdown3 = -1;
// alert("processMain: speakeasy cookieText=" + cookieText + ", lucky numbers are " + countdown1 + ", " + countdown2 + ", " + countdown3);
	}

	if(countdown1>=0 || countdown2>=0 || countdown3>=0) {
	   // filter out nonsense values: 
	   // - too high ( > 200 since last semi, 120 for oxy)
	   // - too low ( < 160 since last semi, or < 100 if oxy)
	   // the filters only apply if you found a semi recently; otherwise the counter starts
	   // after a would-be semi, or ascension, with your first adventure.php location
	   // adventure, but it's too much trouble to keep track
	   GM_setValue(playerName+"_allowSanityCheck", false);	 // lock out the other pane from panicking
															 // must call processCharsheet now, to clear this
	   var allowBracket = GM_getValue(playerName+"_allowBracket",false);
	   GM_setValue(playerName+"_allowBracket",false);  // no reason to put up reminder after this

	   var turncount = GM_getValue(playerName+"_turncount", 0);
	   var lastSemiTurn = GM_getValue(playerName+"_lastSemiTurn",-1);
	   var wasOxy = GM_getValue(playerName+"_isOxy", false);
	   GM_setValue(playerName+"_isOxy", false);  // ate a cookie, must not be oxy
	   var minCount = 160; var maxCount = 200;
	   if( wasOxy ) { 
			minCount = 100; maxCount = 120; 
	   }
	   if(turncount < 70) {  // newly ascended
			minCount = 70; maxCount = 80; lastSemiTurn = 0;
		}	   
	   var turnsSinceSemi = turncount - lastSemiTurn;

 // alert("filtering lucky numbers for " + playerName + " based on turncount=" + turncount + ", lastSemiTurn=" + lastSemiTurn + ", wasOxy=" + wasOxy + ", turnsSinceSemi = " + turnsSinceSemi );
	   // eliminate counters that are too high on an absolute basis, then try to
	   // eliminate ones too high and low based on last semi 
	   if(countdown1 > maxCount) countdown1 = -1;
	   if(countdown2 > maxCount) countdown2 = -1;
	   if(countdown3 > maxCount) countdown3 = -1;
	   
	   if(lastSemiTurn != -1 && turnsSinceSemi <= maxCount && allowBracket) {
		   if( countdown1 + turnsSinceSemi > maxCount ) countdown1 = -1;
		   if( countdown2 + turnsSinceSemi > maxCount ) countdown2 = -1;
		   if( countdown3 + turnsSinceSemi > maxCount ) countdown3 = -1;
	   }
		   
	   if(lastSemiTurn != -1) {
		   if( countdown1 + turnsSinceSemi < minCount ) countdown1 = -1;
		   if( countdown2 + turnsSinceSemi < minCount ) countdown2 = -1;
		   if( countdown3 + turnsSinceSemi < minCount ) countdown3 = -1;
	   }
	   
		// now to compare them vs existing numbers
		// if there is no match, stick them in as the current numbers
		// if there is a match, use that and zero the other two
	   var oldLuckyTurn1 = GM_getValue(playerName+"_luckyTurn1",-1);
	   var oldLuckyTurn2 = GM_getValue(playerName+"_luckyTurn2",-1);
	   var oldLuckyTurn3 = GM_getValue(playerName+"_luckyTurn3",-1);
	   var oldCountdown1 = oldLuckyTurn1 != -1 ? oldLuckyTurn1 - turncount : -1; 
	   var oldCountdown2 = oldLuckyTurn2 != -1 ? oldLuckyTurn2 - turncount : -1; 
	   var oldCountdown3 = oldLuckyTurn3 != -1 ? oldLuckyTurn3 - turncount : -1;

		var noPopups = GM_getValue("noPopups", false);
	   
	   // need to watch out for -1 matching -1 since we reset values above
	   // check for self-match in new cookie values first
	   if(countdown1 != -1 && (countdown1==countdown2 || countdown1 == countdown3)) {
		  if(!noPopups) alert(countdown1 + " turns until a semi-rare!");
		  countdown2 = -1;
		  countdown3 = -1;
	   }
	   else if(countdown2 != -1 && countdown2 == countdown3) {
		  if(!noPopups) alert(countdown2 + " turns until a semi-rare!");
		  countdown1 = -1;
		  countdown3 = -1;
	   }
	   else if (countdown1 != -1 && (countdown1 == oldCountdown1 || countdown1 == oldCountdown2 || countdown1 == oldCountdown3 || countdown2 == -1 && countdown3 == -1)) {
		  if(!noPopups) alert(countdown1 + " turns until a semi-rare!");
		  countdown2 = -1;
		  countdown3 = -1;
	   }
	   else if (countdown2 != -1 && (countdown2 == oldCountdown1 || countdown2 == oldCountdown2 || countdown2 == oldCountdown3 || countdown1 == -1 && countdown3 == -1)) {
		  if(!noPopups) alert(countdown2 + " turns until a semi-rare!");
		  countdown1 = -1;
		  countdown3 = -1;
	   }
	   else if (countdown3 != -1 && (countdown3 == oldCountdown1 || countdown3 == oldCountdown2 || countdown3 == oldCountdown3 || countdown1 == -1 && countdown2 == -1)) {
		  if(!noPopups) alert(countdown3 + " turns until a semi-rare!");
		  countdown1 = -1;
		  countdown2 = -1;
	   }
	   
	   GM_setValue(playerName+"_luckyTurn1",countdown1 != -1 ? countdown1 + turncount : -1);
	   GM_setValue(playerName+"_luckyTurn2",countdown2 != -1 ? countdown2 + turncount : -1);
	   GM_setValue(playerName+"_luckyTurn3",countdown3 != -1 ? countdown3 + turncount : -1);

	   // load and parse the main charsheet to set the behind the scene variables that 
	   // convert the cookie countdown into actual adventure number for each cookie
//	   GM_get(baseURL + charSheet, processCharsheet);

		// trigger char pane refresh with the new info
		top.frames[0].location.reload();
	}
}

////////////////////////////////////////////////////////////////////////////////
function manualCookieEntry() {
	var playerName = getPlayerNameFromCharpane().username;
// alert("player name: " + playerName);
	GM_setValue("currentPlayer", playerName);  // store for other functions that need to know who's playing
	var noPopups = GM_getValue("noPopups", false);
	if(noPopups) return;  // need to figure out some way to enter values without using a popup
	
	var turncount = GM_getValue(playerName+"_turncount", -1);
	var luckyTurn1 = GM_getValue(playerName+"_luckyTurn1", -1);
	var luckyTurn2 = GM_getValue(playerName+"_luckyTurn2", -1);
	var luckyTurn3 = GM_getValue(playerName+"_luckyTurn3", -1);
	
	var oldCountdown1 = luckyTurn1 > 0 ? luckyTurn1 - turncount : -1;
	var oldCountdown2 = luckyTurn2 > 0 ? luckyTurn2 - turncount : -1;
	var oldCountdown3 = luckyTurn3 > 0 ? luckyTurn3 - turncount : -1;

	var countdown1 = -1;
	var countdown2 = -1;
	var countdown3 = -1;
	var displayText = "";
	
	if(oldCountdown1 > -1) 
		displayText += oldCountdown1;
	if(oldCountdown2 > -1) {
		if(displayText != "") displayText += ", ";
		displayText += oldCountdown2;
	}
	if(oldCountdown3 > -1) {
		if(displayText != "") displayText += ", ";
		displayText += oldCountdown3;
	}

	var lastSemiTurn = GM_getValue(playerName+"_lastSemiTurn",-1);
	var lastSemiLocation = GM_getValue(playerName+"_lastSemiLocation","");
	var promptText = "";
	if(lastSemiTurn > -1 && turncount >= lastSemiTurn ) {
		promptText = "Last semirare found " + (turncount - lastSemiTurn) + " turns ago, at " + lastSemiLocation + ".\n\n";
	}
	else {
		promptText = "Last semirare unknown.\n\n"
	}
	cookieText = prompt(promptText + "Enter 1-3 new cookie vals (-1 to clear)\n", displayText);

	if(cookieText) { // will be null if user cancels
		countdown1 = parseInt(cookieText);
		var pos = cookieText.indexOf(",");
		if(pos > 0) {
			cookieText = cookieText.substring(pos+1);
			countdown2 = parseInt(cookieText);
			pos = cookieText.indexOf(",");
			
			if(pos > 0) {
				cookieText = cookieText.substring(pos+1);
				countdown3 = parseInt(cookieText);
			}
		}
// alert("manual text entry, got values " + countdown1 + ", " + countdown2 + ", " + countdown3);
		GM_setValue(playerName+"_luckyTurn1",countdown1+turncount);
		GM_setValue(playerName+"_luckyTurn2",countdown2+turncount);
		GM_setValue(playerName+"_luckyTurn3",countdown3+turncount);
		GM_setValue(playerName+"_needSanityCheck", false);	// just in case we were in the middle of something
		GM_setValue(playerName+"_allowBracket",false);  // no reason to put up reminder after this

		// load and parse the main charsheet to set the behind the scene variables that 
		// convert the cookie countdown into actual adventure number for each cookie
//		GM_get(baseURL + charSheet, processCharsheet);
		// trigger char pane refresh with the new info
		top.frames[0].location.reload();

	}
}

////////////////////////////////////////////////////////////////////////////////
// this function must be called when we are in the char sidepane
function getPlayerLevel(data) {
	var playerLevel = /Level (\d+)/i.exec(data);  // full mode
	if( !playerLevel)
		playerLevel = /Lvl\. (\d+)/i.exec(data);  // compact mode
	if( playerLevel)
		return parseInt(playerLevel[1],10);

	// normal level checks fail if astral spirit
	if(data.indexOf("Astral Spirit") != -1)
		return 0; // astral spirit
	else 
		return -1; // error
}

////////////////////////////////////////////////////////////////////////////////
// this function must be called when we are in the char sidepane
function updateCharacterPane() {
	var a = getPlayerNameFromCharpane();
	var playerName = a.username;
	var fullmode = a.fullmode;
	if( playerName == null )  // not sure why we sometimes see this, but doesn't seem to be at critical times
		return;

	GM_setValue("currentPlayer", playerName);  // store for other functions that need to know who's playing
	
	// if astral plane, need to reset counters
	// getPlayerLevel() returns 0 for astral plane
	var playerLevel = getPlayerLevel(document.documentElement.innerHTML);
	if(playerLevel == 0) {
		// clear the counters, no point in doing anything else
		clearCounters(playerName);
		return;
	}

	// check the session ID to see if we are still in the same session
	// if a new session, check if an update is available
	var pwdHash = getPwdHash(document.documentElement.innerHTML);
	var oldPwdHash = GM_getValue(playerName + "_pwdHash", 0);
	if(pwdHash != oldPwdHash) {
		// new session
		GM_setValue(playerName + "_pwdHash", pwdHash);
		
		// run sanity check on new session, make sure we haven't missed anything
// alert("calling sanity check");
		GM_get(baseURL + charSheet, sanityCheckCharsheet);
		
		// check for a new version of script if none seen already (asynch call, will run in parallel)
		// (temporarily?) removed for use with greasy fork
/* 		var webVer = GM_getValue("webVersion", "Error");
		if(webVer == "Error" || webVer <= currentVersion) {
//			alert("calling version check");
			GM_get(scriptURL, CheckScriptVersion);
		}
 */	
	}

	// new native counter!
	var turncount = getTurnsplayedVar(document.documentElement.innerHTML);
	var oldTurncount = GM_getValue(playerName+"_turncount", 0);
	GM_setValue(playerName+"_turncount", turncount);

	var lastSemiTurn = GM_getValue(playerName+"_lastSemiTurn",-1);
	var lastSemiLocation = GM_getValue(playerName+"_lastSemiLocation","");
	if(lastSemiTurn > turncount + 1) { // turncount var lags behind when fighting, so can have an off-by-1 issue
		// could happen if ascended
		lastSemiTurn = -1;
		GM_setValue(playerName+"_lastSemiTurn",-1);
		GM_setValue(playerName+"_lastSemiLocation","");
	}

	var luckyTurn1 = GM_getValue(playerName+"_luckyTurn1", -1);
	var luckyTurn2 = GM_getValue(playerName+"_luckyTurn2", -1);
	var luckyTurn3 = GM_getValue(playerName+"_luckyTurn3", -1);

	// if anything hit zero, warn the user
	var noPopups = GM_getValue("noPopups", false);
	var atSemi = false;
	if( turncount == luckyTurn1	|| turncount == luckyTurn2	|| turncount == luckyTurn3) {
		atSemi = true;
		if(!noPopups && turncount != oldTurncount) {
			var alertText = "Fortune cookie countdown hit zero!";
			if(lastSemiTurn > -1 && turncount >= lastSemiTurn) {
				alertText += "\n\nLast semirare found " + (turncount - lastSemiTurn) + " turns ago, at " + lastSemiLocation + ".";
			}
			else {
				alertText += "\n\nLast semirare unknown.";
			}
			confirm(alertText);
		}
	}
	
	var displayText = "";
	if(luckyTurn1 >= turncount) 
		displayText += luckyTurn1 - turncount;
	if(luckyTurn2 >= turncount) {
		if(displayText != "") displayText += ", ";
		displayText += luckyTurn2 - turncount;
	}
	if(luckyTurn3 >= turncount) {
		if(displayText != "") displayText += ", ";
		displayText += luckyTurn3 - turncount;
	}
	if(atSemi && noPopups)
		displayText += " (last SR: " + (lastSemiLocation == "" ? "unknown" : lastSemiLocation) + ")";
	if(atSemi && fullmode) {  // shows the html literally in compact mode
		displayText  = "<font color=\"red\"><b>" + displayText + "</b></font>";
	}

	// display turn var to figure out exactly how it works
//	displayText += " (tp=" + turnsplayedVar+")";
   
	var hideIfZeros = GM_getValue("hideIfZeros", false);

    if(displayText != ""
    || hideIfZeros == false ) {
		// display an FC bracket if no cookie eaten, and have found a semi recently
		var isOxy = GM_getValue(playerName+"_isOxy", false);
	    var turnsSinceSemi = turncount - lastSemiTurn;
	    var minCount = 160; var maxCount = 200;
	    if( isOxy ) { 
	    	minCount = 100; maxCount = 120; 
	    }
		if(displayText == "" && lastSemiTurn != -1 && turnsSinceSemi <= maxCount ) {
			displayText = "[" + Math.max(0, minCount - turnsSinceSemi) + "...";
			displayText += Math.max(0, maxCount - turnsSinceSemi) + "]";
		} else if (displayText == "" && turncount < 70) {  // default just-ascended bracket
			displayText = "[" + Math.max(0, 70 - turncount)  + "...";
			displayText += Math.max(0, 80 - turncount) + "]";
		}
		
		if(fullmode) {
			var newElement = document.createElement("FONT");
			newElement.innerHTML = "<b>"
			  + "<font size=2>Cookie Count: " 
			  + displayText
			  + "</font></b><br><br>"; ;
			newElement.setAttribute("onmouseover", 'this.style.opacity="0.5"');
			newElement.setAttribute("onmouseout", 'this.style.opacity="1"');
			newElement.setAttribute("id", 'fortuneCookieCounter');
			newElement.addEventListener("click", manualCookieEntry, true);
			newElement.align = "center";
	
			var elements = document.getElementsByTagName( "FONT" );
			for ( var i = 0; i < elements.length; ++i ){
				if ( elements[i].innerHTML.indexOf( "Last Adventure" ) != -1 ){
					// insert our before this one
					elements[i].parentNode.insertBefore(newElement,elements[i]);
					break;
				}
			}
		}
		else { // compact mode - different layout, make a table row and two data element
			var newTR = document.createElement('tr');

			var newElement = document.createElement("td");
			newElement.appendChild(document.createTextNode("Cookie:"));
			newElement.align = "right";
			newTR.appendChild(newElement);
			
			newElement = document.createElement("td");
			newElement.setAttribute("onmouseover", 'this.style.opacity="0.5"');
			newElement.setAttribute("onmouseout", 'this.style.opacity="1"');
			newElement.setAttribute("id", 'fortuneCookieCounter');
			newElement.addEventListener("click", manualCookieEntry, true);
			newElement.align = "left";
			newElement.style.fontWeight = "bold";
			newElement.appendChild(document.createTextNode(displayText));
			newTR.appendChild(newElement);
			
			var elements = document.getElementsByTagName( "TR" );
			var done = false;
			// if the last adventures script is running, insert before, else append to table
			for ( var i = 1; i < elements.length; ++i ){
				// normally "Adv", might be "Last Adventures" if that script is running
				if ( elements[i].innerHTML.indexOf( "Last Adventures" ) != -1 ){
					// insert ours before this one - experiment, back up one more
					elements[i].parentNode.insertBefore(newTR,elements[i-1]);
					done = true;
					break;
				}
			}
			if(!done) { // normal, no last adv script
				for ( var i = 0; i < elements.length; ++i ){
					 if ( elements[i].innerHTML.indexOf( "Adv" ) != -1 ){
						// insert ours at end of the table in compact mode
						elements[i].parentNode.appendChild(newTR);
						break;
					 }
				}
			}
		}
    }
}   

////////////////////////////////////////////////////////////////////////////////
// set preferences
// code stolen from mr script
// not set on a per-player basis, would need to add code to parse account pane for char name
function toggleHideCountdown() {
//	  alert("in toggleHideCountdown()");

	var hideIfZeros = GM_getValue("hideIfZeros", false);
	hideIfZeros = !hideIfZeros;
	
	GM_setValue("hideIfZeros", hideIfZeros);
		
	// trigger char pane refresh with the new info
	top.frames[0].location.reload();
    var msg = document.getElementById('cookiecountertoggle');
    msg.innerHTML = (hideIfZeros ? "Hiding" : "Showing") + " Inactive Counter - Click to Change";
}

function togglePopups() {
//	  alert("in toggleHideCountdown()");

	var noPopups = GM_getValue("noPopups", false);
	noPopups = !noPopups;
	
	GM_setValue("noPopups", noPopups);
		
	// trigger char pane refresh with the new info
    var msg = document.getElementById('cookiepopups');
    msg.innerHTML = (noPopups ? "Popups Disabled " : "Using Popups") + " (disable for FF4) - Click to Change";
	top.frames[0].location.reload();
}

// --------------------------------------------
// ---------- account menu option -------------
// --------------------------------------------

// Charon's code
function buildPrefs() {
    if (!document.querySelector('#privacy'))
        return;
    if (!document.querySelector('#scripts')) {
        //scripts tab is not built, do it here
        var scripts = document.querySelector('ul').appendChild(document.createElement('li'));
        scripts.id = 'scripts';
        var a = scripts.appendChild(document.createElement('a'));
        a.href = '#';
        var img = a.appendChild(document.createElement('img'));
        img.src = 'http://images.kingdomofloathing.com/itemimages/cmonkey1.gif';
        img.align = 'absmiddle';
        img.border = '0';
        img.style.paddingRight = '10px';
        a.appendChild(document.createTextNode('Scripts'));
        a.addEventListener('click', function (e) {
                //make our new tab active when clicked, clear out the #guts div and add our settings to it
                e.stopPropagation();
                document.querySelector('.active').className = '';
                document.querySelector('#scripts').className = 'active';
                document.querySelector('#guts').innerHTML = '<div class="scaffold"></div>';
                document.querySelector('#guts').appendChild(getSettings());
            }, false);
    } else {
        //script tab already exists
         document.querySelector('#scripts').firstChild.addEventListener('click', function (e) {
                //some other script is doing the activation work, just add our settings
                e.stopPropagation();
                document.querySelector('#guts').appendChild(getSettings());
            }, false);
    }
}

function getSettings() {
    //build our settings and return them for appending
    var guts = document.body.appendChild(document.createElement('div'));
    guts.id = 'cookieprefs';
    var subhead = guts.appendChild(document.createElement('div'));
    subhead.className = 'subhead';
    subhead.textContent = 'Fortune Cookie Counter';
    var section = guts.appendChild(document.createElement('div'));
    section.className = 'indent';
    //call function in main script to actually make the settings
    section.appendChild(buildSettings());
    return guts;
}

function buildSettings() {
	var prefLink = document.createElement('a');
	var hideIfZeros = GM_getValue("hideIfZeros", false);

	prefLink.setAttribute('href','javascript:void(0)');
	prefLink.setAttribute('id','cookiecountertoggle');
    prefLink.innerHTML = (hideIfZeros ? "Hiding" : "Showing") + " Inactive Counter - Click to Change";
	prefLink.addEventListener("click", toggleHideCountdown, true);

	var prefAnchor = document.createElement('a');
	prefAnchor.setAttribute('name','opt'); prefAnchor.innerHTML = " ";
	var pDiddy = document.createElement('p');
	with(pDiddy)
	{	appendChild(prefAnchor);
		appendChild(prefLink);
	}

	prefLink = document.createElement('a');
	var noPopups = GM_getValue("noPopups", false);

	prefLink.setAttribute('href','javascript:void(0)');
	prefLink.setAttribute('id','cookiepopups');
    prefLink.innerHTML = (noPopups ? "Popups Disabled " : "Using Popups") + " (disable for FF4) - Click to Change";
	prefLink.addEventListener("click", togglePopups, true);

	prefAnchor = document.createElement('p');
	prefAnchor.setAttribute('name','opt'); prefAnchor.innerHTML = " ";
	
	with(pDiddy)
	{	appendChild(prefAnchor);
		appendChild(prefLink);
	}
	
	// add a link to update, if an update is available
	// (temporarily?) removed for use with Greasy Fork
/* 	var webVer = GM_getValue("webVersion");
	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 cookie 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);
		
		pDiddy.appendChild(newElement); 
	}
 */
	return pDiddy;
}

////////////////////////////////////////////////////////////////////////////////
// main prog, just call the proper routine if we are on a pane we care about
var nodeBody   = document.getElementsByTagName("body").item(0);
var textBody   = "";
var baseURL	   = "";
var charSheet = "charsheet.php";
var playerName = GM_getValue( "currentPlayer");

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

// do normal page processing independently of whether started sanity check
if ( document.location.pathname.indexOf("charpane.php") != -1 ) {
	//////NEW STUFF (Firvagor) - since the charpane refreshes every time something is used, if the results section is found, run main function using manually located mainpane (can only use indexing for some reason)/////
	if (top.frames[2].document.getElementById("effdiv")!=null)  // was frames[1] until aug 2014 topmenu update
		processMain(top.frames[2].document);
	updateCharacterPane();
}
else if ( document.location.pathname.indexOf("inventory.php") != -1
  || document.location.pathname.indexOf("clan_viplounge.php")!= -1){ 
	 //alert("calling processMain() for " + document.location.pathname);
	 //////NEW STUFF (Firvagor) - passes in current document instead of nothing, just to define the frame used/////
	GM_setValue(playerName+"_fighting",0);
	processMain(document);
}
else if ( document.location.pathname.indexOf("charsheet.php") != -1){ 
//	alert("calling sanityCheckCharsheet() for " + document.location.pathname);
	GM_setValue(playerName+"_fighting",0);
	sanityCheckCharsheet(document.body.innerHTML);
}
else if ( document.location.pathname.indexOf("account.php") != -1 ) {
//	  alert("calling set prefs for " + document.location.pathname);
	GM_setValue(playerName+"_fighting",0);
    buildPrefs();
//	setPrefs();
}
else if ( document.location.pathname.indexOf("adventure.php") != -1
	|| document.location.pathname.indexOf("choice.php") != -1) {
	// adventure.php should trap all "normal" noncombats
	// choice noncombats are in choice.php - now needed for hobopolis
	// for the fortune cookie combat rares, need to process fight.php
	GM_setValue(playerName+"_fighting",0);
	checkForNoncombatSemi(document.body.innerHTML);
}
else if ( document.location.pathname.indexOf("fight.php") != -1 ) {
	// see if we are actively fighting (no asynch calls allowed while fighting!)
	var fighting = GM_getValue(playerName+"_fighting",0)
	// alert("prefight check - page = " + document.location.pathname + ", fighting = " + fighting);
	{
		if( fighting == 0) {
			//  just started a fight, see if it's a combat semi (orc chasm, kitchens, treasury)
			checkForCombatSemi(document.body.innerHTML);
		}
//	alert("incrementing fighting from " + fighting);
		fighting = fighting + 1;  // also count rounds, prevents doing things twice in same fight
		// fighting, but maybe we're done, look for adventure again tag
		// I forget why we look at paragraphs here!
//		var paragraphs = document.getElementsByTagName( "p" );
//		for ( var i = 0; i < paragraphs.length; ++i ) {
//			if ( paragraphs[i].innerHTML.indexOf( "Adventure Again (" ) != -1
//			|| 	paragraphs[i].innerHTML.indexOf("Go back to Your Inventory") != -1) {
			if ( document.body.innerHTML.indexOf( "Adventure Again (" ) != -1
			|| 	document.body.innerHTML.indexOf("Go back to Your Inventory") != -1) {
				fighting = 0;
//				break;
			}
//		}
	}
	GM_setValue(playerName+"_fighting",fighting);
//	alert("post fight - fighting = " + fighting);

}
else {
//	  alert("ignoring " + document.location.pathname);
}

// do sanity check (load charsheet asynchronously) if requested - not while fighting
if(GM_getValue(playerName+"_needSanityCheck",false) == true
&& GM_getValue(playerName+"_allowSanityCheck",false) == true ) {
	// alert("need sanity check flag detected!");
	// no sanity check allowed if fighting
	if( GM_getValue(playerName+"_fighting",0) == 0) {
	// alert("calling sanity check");	 
	   GM_setValue(playerName+"_needSanityCheck", false);
//	   alert("calling sanity check");
	   GM_get(baseURL + charSheet, sanityCheckCharsheet);
	} 
}