The West - Best Job Items Selector

By jilly Last update Mar 15, 2011 — Installed 22,089 times.

There are 15 previous versions of this script.

Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

// ---------------------------------------
// The West - Best Job Items Selector
// ---------------------------------------
//
// this script will add button to window with job information
// (any job and also town building and fort building) and if button is pushed, 
// finds best clothes for current job from inventory
// 
// author: Jilly
// version: 1.4.20110315
// licence: 
// 	licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License
// 	http://creativecommons.org/licenses/by-nc-sa/3.0/
//
// ==UserScript==
// @name The West - Best Job Items Selector
// @namespace http://userscripts.org/users/187824
// @description Finds best items for selected job (works also for building in town or fort)
// @include http://*.the-west.*
// @exclude http://www.the-west.*
// @exclude http://forum.the-west.*
// ==/UserScript==

/**
 * Content Script Injection
 *
 * runs script in content scope rather than in greasemonkey sandbox
 * source: http://wiki.greasespot.net/Content_Script_Injection
 */
function contentEval(source) {
  // Check for function input.
  if ('function' == typeof source) {
    // Execute this function with no arguments, by adding parentheses.
    // One set around the function, required for valid syntax, and a
    // second empty set calls the surrounded function.
    source = '(' + source + ')();'
  }
  // Create a script node holding this  source code.
  var script = document.createElement('script');
  script.setAttribute("type", "application/javascript");
  script.textContent = source;
  // Insert the script node into the page, so it will run, and immediately
  // remove it to clean up.
  document.body.appendChild(script);
  document.body.removeChild(script);
}

/*
 * call contentEval so whole code is executed
 * in content scope rather than greasemonkey
 * sandbox
 */
contentEval(function() {
	window.addEvent('domready', function() {
		//set this variable to true if you want
		//to see debugging output with details
		//about calculation
		var debug = false;
		var log = null;
		var sb = null;

		//better processing for string
		//concatenation
		var StringBuffer = function(str) {
			this.strings = new Array("");
			this.append(str);
			return this;
		};

		StringBuffer.prototype.append = function(str) {
			if (str) {
				this.strings.push(str);
			}
			return this;
		};

		StringBuffer.prototype.clear = function() {
			this.strings.length = 1;
			return this;
		};

		StringBuffer.prototype.removeLast = function() {
			this.strings.length -= 1;
			return this;
		};

		StringBuffer.prototype.toString = function() {
			return this.strings.join("");
		};

		// For convenience...
		Date.prototype.format = function (mask, utc) {
			return dateFormat(this, mask, utc);
		};

		/*
		 * Date Format 1.2.3
		 * (c) 2007-2009 Steven Levithan <stevenlevithan.com>
		 * MIT license
		 *
		 * Includes enhancements by Scott Trenda <scott.trenda.net>
		 * and Kris Kowal <cixar.com/~kris.kowal/>
		 *
		 * Accepts a date, a mask, or a date and a mask.
		 * Returns a formatted version of the given date.
		 * The date defaults to the current date/time.
		 * The mask defaults to dateFormat.masks.default.
		 */

		var dateFormat = function () {
			var	token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
				timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
				timezoneClip = /[^-+\dA-Z]/g,
				pad = function (val, len) {
					val = String(val);
					len = len || 2;
					while (val.length < len) val = "0" + val;
					return val;
				};

			// Regexes and supporting functions are cached through closure
			return function (date, mask, utc) {
				var dF = dateFormat;

				// You can't provide utc if you skip other args (use the "UTC:" mask prefix)
				if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) {
					mask = date;
					date = undefined;
				}

				// Passing date through Date applies Date.parse, if necessary
				date = date ? new Date(date) : new Date;
				if (isNaN(date)) throw SyntaxError("invalid date");

				mask = String(dF.masks[mask] || mask || dF.masks["default"]);

				// Allow setting the utc argument via the mask
				if (mask.slice(0, 4) == "UTC:") {
					mask = mask.slice(4);
					utc = true;
				}

				var	_ = utc ? "getUTC" : "get",
					d = date[_ + "Date"](),
					D = date[_ + "Day"](),
					m = date[_ + "Month"](),
					y = date[_ + "FullYear"](),
					H = date[_ + "Hours"](),
					M = date[_ + "Minutes"](),
					s = date[_ + "Seconds"](),
					L = date[_ + "Milliseconds"](),
					o = utc ? 0 : date.getTimezoneOffset(),
					flags = {
						d:    d,
						dd:   pad(d),
						ddd:  dF.i18n.dayNames[D],
						dddd: dF.i18n.dayNames[D + 7],
						m:    m + 1,
						mm:   pad(m + 1),
						mmm:  dF.i18n.monthNames[m],
						mmmm: dF.i18n.monthNames[m + 12],
						yy:   String(y).slice(2),
						yyyy: y,
						h:    H % 12 || 12,
						hh:   pad(H % 12 || 12),
						H:    H,
						HH:   pad(H),
						M:    M,
						MM:   pad(M),
						s:    s,
						ss:   pad(s),
						l:    pad(L, 3),
						L:    pad(L > 99 ? Math.round(L / 10) : L),
						t:    H < 12 ? "a"  : "p",
						tt:   H < 12 ? "am" : "pm",
						T:    H < 12 ? "A"  : "P",
						TT:   H < 12 ? "AM" : "PM",
						Z:    utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
						o:    (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
						S:    ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
					};

				return mask.replace(token, function ($0) {
					return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
				});
			};
		}();

		// Some common format strings
		dateFormat.masks = {
			"default":      "ddd mmm dd yyyy HH:MM:ss",
			shortDate:      "m/d/yy",
			mediumDate:     "mmm d, yyyy",
			longDate:       "mmmm d, yyyy",
			fullDate:       "dddd, mmmm d, yyyy",
			shortTime:      "h:MM TT",
			mediumTime:     "h:MM:ss TT",
			longTime:       "h:MM:ss TT Z",
			isoDate:        "yyyy-mm-dd",
			isoTime:        "HH:MM:ss",
			isoDateTime:    "yyyy-mm-dd'T'HH:MM:ss",
			isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
		};

		// Internationalization strings
		dateFormat.i18n = {
			dayNames: [
				"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
				"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
			],
			monthNames: [
				"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
				"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
			]
		};

		/**
		 * Object representing interface translations
		 */
		var I18n = function() {
			var self = this; //scope variable
			self.lang = "en"; //default language
			self.setLanguage = function(lang) {
				self.lang = lang;
			};
			self.getLanguage = function() {
				return self.lang;
			};
			self.phrases = 
			{
				en:
				{
					no_items_found: "No suitable items found",	
					items_have_been_highlighted: "Suggested items have been highlighted",
					already_wearing_items: "You are already wearing all suggested items",
					item: "item",
					items: "items",
					to_change: "to change",
					button_change_clothes: "Change clothes",
					button_best_items: "Best items",
					processing_wait_please: "Processing, wait please ...",
					done: "Done",
					error: "Error",
					duel_melee: "Melee weapon",
					duel_firearms: "Firearms" 
				},
				cz:
				{
					no_items_found: "Nebylo nalezeno žádné vhodné oblečení",	
					items_have_been_highlighted: "Navrhované oblečení bylo označeno",
					already_wearing_items: "Již máš na sobě všechno navrhované oblečení",
					item: "oblečení",
					items: "oblečení",
					to_change: "na převlečení",
					button_change_clothes: "Převléct oblečení",
					button_best_items: "Nejlepší oblečení",
					processing_wait_please: "Probíhá zpracování, čekejte prosím ...",
					done: "Hotovo",
					error: "Chyba",
					duel_melee: "Chladná zbraň",
					duel_firearms: "Střelná zbraň" 
				}
				/**
				 * here will go other translations
				 */
			};
			/**
			 * returns phrase for current language
			 * if phrase doesn't exist in current language
			 * english phrase is search - if it exist in english, then
			 * english version of 'phrase_name' is returned otherwise
			 * phrase is unknown and therefore '???' string is returned
			 */
			self.getPhrase = function(phrase_name) {
				var phrase = self.phrases[self.lang][phrase_name];
				if (phrase) 
				{
					return phrase;
				}
				else
				{
					var en_phrase = self.phrases["en"][phrase_name];
					return (en_phrase ? en_phrase : "???");
				}
			};
		};

		/**
		 * global variable representing Language instance
		 */
		var lang = new I18n();

		/**
		 * sets interface language based on the domain
		 * of the-west server
		 */
		var setInterfaceLanguage = function() {
			var parts = location.hostname.split(".");
			var domain = parts[parts.length - 1];
			switch(domain) {
				//here may be other languages
				case "cz" : lang.setLanguage("cz"); break;
				default: lang.setLanguage("en");
			}
		};

		/**
		 * adds new line to log buffer
		 */
		var appendToLog = function(text, hideDate) {
			if (sb) {
				sb.append("\n");
				if (!hideDate) {
					var now = new Date();
					sb.append(now.format('"[" mm.dd.yyyy HH:MM:ss","L" ]  "'));
				}
				sb.append(text);
			}
		};

		/**
		 * prints log into debugging window
		 */
		var printLog = function() {
			if (sb && log) {
				log.appendText(sb.toString());
				sb.clear();
			}
		};

		//settings
		var createCookie = function(name,value,days) {
			if (days) {
				var date = new Date();
				date.setTime(date.getTime()+(days*24*60*60*1000));
				var expires = "; expires="+date.toGMTString();
			}
			else var expires = "";
			document.cookie = name+"="+value+expires+"; path=/";
		};

		var readCookie = function(name) {
			var nameEQ = name + "=";
			var ca = document.cookie.split(';');
			for(var i=0;i < ca.length;i++) {
				var c = ca[i];
				while (c.charAt(0)==' ') c = c.substring(1,c.length);
				if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
			}
			return null;
		};

		var eraseCookie = function(name) {
			createCookie(name,"",-1);
		};

		var random = function(lower, upper) {
			return (Math.floor((Math.random() * (upper - lower + 1)) + lower));
		};

		/**
		 * append window with debugging output if debugging is enabled
		 */
		(function() {
			if (debug) {
				log = new Element('textarea');
				log.style.width = '100%';
				log.style.height = '500px';
				$('footer').appendChild(log);
				sb = new StringBuffer();
				if (debug) {
					appendToLog('=========================================================', true);
					appendToLog('  The West - Best Job Items Selector - Debugging Output  ', true);
					appendToLog('=========================================================', true);
					appendToLog('', true);
					printLog();
				}
			}
		})();

		/*
		 * wrapper for items
		 * 
		 * in the-west core script character attributes and skills 
		 * are stored in Character object
		 * 	attributes: 
		 * 		Character.attributes = {}; property is attribute name
		 * 	skills: 
		 * 		Character.skill_names[attribute] = []; array with names
		 */ 	
		var ItemWrapper = function(optobj) {
			//default values
			this.skillBonus = {}; //dynamic programming cache, don't calculate bonus everytime we need it
			this.item = null; //the west item object
			this.wearing = false; //if character is already wearing item or needs to carry it
			//set values
			for (var i in optobj) { this[i] = optobj[i]; }
		};
		/**
		 * returns bonus points of current item for provided 'skill' parameter
		 * if 'skill' is unknown or if item doesn't add any bonus on
		 * current skill returns 0
		 */
		ItemWrapper.prototype.getSkillBonus = function(skill) {
			//bonus not calculated yet
			if (!this.skillBonus[skill]) {
				var obj = this.item.obj;
				var bonus = 0;
				//check skill bonus
				if (obj.bonus.skills.length != 0) {
					if (obj.bonus.skills[skill] != undefined) {
						bonus += obj.bonus.skills[skill];
					}
				}
				//check attribute bonus
				if (obj.bonus.attributes.length != 0) {
					for (var key in obj.bonus.attributes) {
						if (Character.skill_names[key].contains(skill)) {
							bonus += obj.bonus.attributes[key];
						}
					}
				}
				this.skillBonus[skill] = bonus;
			}
			return this.skillBonus[skill];
		};

		ItemWrapper.prototype.toString = function() {
			return (this.item) ? this.item.obj.name : "";
		};

		/**
		 * returns array with all possible wear types
		 *
		 * (in version 1.3 of the-west were added new wear types - pants and belt
		 * maybe, there will be some other in future, so it's better to obtain values
		 * dynamically straight from the core code)
		 */
		var wearTypesList = function() {
			//note: it's not necessary to check all available wear types in game
			//because we only have items from bag or items we are wearing
			//(so types of these items are enough)
			
			var result_set = [];
			
			var bag = Bag.getInstance();
			//first loop through bag object
			for (var key in bag.items) {
				var item = bag.items[key];
				var type = item.get_type();
				if (!result_set.contains(type)) result_set.include(type);
			}

			var wear = Wear.wear;
			//then go through wear items
			for (var key in wear) {
				var item = wear[key];
				var type = item.get_type();
				if (!result_set.contains(type)) result_set.include(type);
			}

			return result_set;
		};
	

		/**
		 * creates map of both inventory and wear items
		 * it is usefull for better later processing
		 * skips all items that we can't wear because of
		 * restrictions (character sex, character class
		 * and item required level)
		 * map structure (key => value)
		 * 	key: item type = "body" | "yield" | "neck" | "foot" | "head" | "right_arm" | "left_arm" | "animal"
		 * 	value: array with items [ item1, item2, ..., itemN ]
		 *
		 * TODO test restrictions: character sex, character class 
		 * 	these restrictions are implemented but not tested yet (but they should work :))
		 */ 
		var getItemsMap = function() {
			if (debug) {
				appendToLog("Creating items map");
			}
			var items = {};
			var sets = {}; //contains items that may be part of a set
			//first go through bag items
			var bag = Bag.getInstance();
			if (debug) appendToLog("\tProcessing bag items ...");
			for (var key in bag.items) {
				var item = bag.items[key];
				//skip items which are not for my character sex
				var characterSex = item.obj.characterSex;
				if (characterSex && characterSex != Character.characterSex) { 
					if (debug) {
						appendToLog("\tSkipping " + item.obj.name + " because it requires " + characterSex + " sex (your sex is " + Character.characterSex + ")");
					}
					continue; 
				}
				//skip items which are not for my character class
				var characterClass = item.obj.characterClass;
				if (characterClass && characterClass != Character.characterClass) { 
					if (debug) {
						appendToLog("\tSkipping " + item.obj.name + " because it requires to you be " + characterClass + " (your class is " + Character.characterClass + ")");
					}
					continue; 
				}
				//skipp items which require higher level than my actual level
				var level = item.obj.level;
				if (level && level > Character.level) { 
					if (debug) {
						appendToLog("\tSkipping " + item.obj.name + " because it requires at least level " + level + " (your level is " + Character.level + ")");
					}
					continue; 
				}
				var type = item.obj.type;
				if (items[type] == undefined) { items[type] = []; }
				if (debug) appendToLog("\tAdding '" + item.obj.name + "'");
				items[type].push(new ItemWrapper({"item": item, "wearing": false}));

				//check if item may be part of a set
				var set = item.obj.set;
				if (set) {
					if (sets[set.key] == undefined) { sets[set.key] = []; }
					if (debug) appendToLog("\t'" + item.obj.name + "' may be part of set ('" + set.name + "'). Adding it to suspected set items.");
					sets[set.key].push(new ItemWrapper({"item": item, "wearing": false}));
				}
			}

			//then go through wear
			//notice that there's no need for checking character sex,
			//character class or item required level - if there was any
			//restriction, the-west server side wouldn't have let to 
			//wear this item before (it would show error)
			var wear = Wear.wear;
			if (debug) appendToLog("\tProcessing wear items ...");
			for (var key in wear) {
				var item = wear[key];
				var type = item.obj.type;
				if (items[type] == undefined) { items[type] = []; }
				if (debug) appendToLog("\tAdding '" + item.obj.name + "'");
				items[type].push(new ItemWrapper({"item": item, "wearing": true}));

				//check if item may be part of a set
				var set = item.obj.set;
				if (set) {
					if (sets[set.key] == undefined) { sets[set.key] = []; }
					if (debug) appendToLog("\t'" + item.obj.name + "' may be part of set ('" + set.name + "'). Adding it to suspected set items.");
					sets[set.key].push(new ItemWrapper({"item": item, "wearing": true}));
				}
			}
			if (debug) {
				appendToLog("Map created");
			}
			return { "items": items, "sets": sets };
		};

		/**
		 * --- wear types ---			
		 * 	type: 
		 * 		"body" | "yield" | "neck" | "foot" | "belt" |
		 * 		"head" | "right_arm" | "left_arm" | "animal" | "pants"
		 *
		 * --- skills & attributes ---
		 * 	skill: 
		 * 		"build" | "punch" | "tough" | "endurance" | "health" |
		 * 		"ride" | "reflex" | "dodge" | "hide" | "swim" |
		 * 		"aim" | "shot" | "pitfall" | "finger_dexterity" | "repair" |
		 * 		"leadership" | "tactic" | "trade" | "animal" | "appearance"
		 */

		/**
		 * map with job ids
		 *
		 * (less chance to make mistake and easy
		 * changes if ids change in future)
		 */
		var JobIds = {
			"building_wind_mills": 44,
			"catching_horses": 48,
			"charlatan": -1, //TODO: complete
			"constructing_a_ranch_house": 84,
			"cowboy": 22,
			"drilling_for_oil": 67,
			"evangelizing": 62,
			"felling_trees" : 27,
			"fire_fighter": 90,
			"harvesting_fields": 8,
			"hunt_buffalo": 52, 
			"hunt_coyotes": 51,
			"hunt_grizzly_bears": -1, //TODO: complete
			"hunt_wolves": 58, 
			"hunting_turkey": 20,
			"installing_a_barbed_wire_fence": 30,
			"milling_grains": 13,
			"mow_pasture": 12,
			"picking_agaves": 86,
			"picking_sugar_cane": 6,
			"shoeing_horses": 88,
			"smuggling": -1, //TODO: complete
			"tanning_deer_skin": 17,
			"transport_ammunition": 50
		};

		/**
		 * represents item sets
		 * there is no option to obtain bonuses dynamically,
		 * so we must hard-code bonuses
		 *
		 * 'bonus' attribute in each set contains only those bonuses
		 * that matter in calculation (for example speed bonus is not
		 * included)
		 *
		 * values were taken from weststats:
		 * 	http://en.weststats.com/Itemsets/
		 *
		 * "strength":		"build"      | "punch"  | "tough"   | "endurance"        | "health"
		 * "flexibility":	"ride"       | "reflex" | "dodge"   | "hide"             | "swim"
		 * "dexterity":		"aim" 	     | "shot"   | "pitfall" | "finger_dexterity" | "repair"
		 * "charisma":		"leadership" | "tactic" | "trade"   | "animal"           | "appearance"
		 *
		 * TODO: complete golden set
		 */
		var ItemSetsBonuses = {
			"set_farmer": {
				"bonus": {
					"2": {
						"attributes": {
							"flexibility": 1,
							"strength": 1
						}
					},
					"3": {
						"attributes": {
							"flexibility": 1,
							"strength": 1,
							"dexterity": 1,
							"charisma": 1
						}
					},
					"4": {
						"attributes": {
							"flexibility": 2,
							"strength": 2,
							"dexterity": 2,
							"charisma": 2
						}
					}
				}
			},
			"set_mexican": {
				//sombrero, brown poncho, sandals, mexican bandana, tequila, donkey
				"bonus": {
					"2": {
						"attributes": {
							"strength": 1
						}
					},
					"3": {
						"attributes": {
							"strength": 2
						}
					},
					"4": {
						"attributes": {
							"strength": 4
						}
					},
					"5": {
						"attributes": {
							"strength": 6
						}
					},
					"6": {
						"attributes": {
							"strength": 9
						}
					}
				}
			},
			"set_indian": {
				"bonus": {
					"2": {
						"skills": {
							"hide": 8	
						},
						"attributes": {
							"flexibility": 2
						}
					},
					"3": {
						"skills": {
							"hide": 8,
							"swim": 8
						},
						"attributes": {
							"flexibility": 5
						}
					},
					"4": {
						"skills": {
							"hide": 8,
							"swim": 8,
							"pitfall": 8

						},
						"attributes": {
							"flexibility": 8
						}
					},
					"5": {
						"skills": {
							"hide": 8,
							"swim": 8,
							"pitfall": 8,
							"animal": 8

						},
						"attributes": {
							"flexibility": 12
						}
					}
				}	
			},
			"set_quackery": {
				"bonus": {
					"2": {
						"skills": {
							"endurance": 5,
							"trade": 5
						},
						"attributes": {
							"dexterity": 1
						}
					},
					"3": {
						"skills": {
							"endurance": 10,
							"trade": 10
						},
						"attributes": {
							"dexterity": 2
						}
					},
					"4": {
						"skills": {
							"endurance": 15,
							"trade": 15
						},
						"attributes": {
							"dexterity": 4
						}
					},
					"5": {
						"skills": {
							"endurance": 20,
							"trade": 20
						},
						"attributes": {
							"dexterity": 6
						}
					},
					"6": {
						"skills": {
							"endurance": 20,
							"trade": 20,
							"reflex": 18,
							"tough": 18,
							"aim": 18,
							"shot": 18

						},
						"attributes": {
							"dexterity": 9
						}
					}
				}
			},
			"set_pilgrim_male": {
				"bonus": {
					"2": {
						"skills": {
//							"build": 5 //TODO: check if bonus is for skill or just adds labor points for all jobs that has 'build' skill requirement
						}
					},
					"3": {
						"skills": {
//							"build": 15 //TODO: check if bonus is for skill or just adds labor points for all jobs that has 'build' skill requirement
						}
					},
					"4": {
						"skills": {
//							"build": 30 //TODO: check if bonus is for skill or just adds labor points for all jobs that has 'build' skill requirement
						}
					},
					"5": {
						"skills": {
//							"build": 50 //TODO: check if bonus is for skill or just adds labor points for all jobs that has 'build' skill requirement
						}
					}
				}
			},
			"set_pilgrim_female": {
				"bonus": {
					"2": {
						"skills": {
//							"build": 5 //TODO: check if bonus is for skill or just adds labor points for all jobs that has 'build' skill requirement
						}
					},
					"3": {
						"skills": {
//							"build": 15 //TODO: check if bonus is for skill or just adds labor points for all jobs that has 'build' skill requirement
						}
					},
					"4": {
						"skills": {
//							"build": 30 //TODO: check if bonus is for skill or just adds labor points for all jobs that has 'build' skill requirement
						}
					},
					"5": {
						"skills": {
//							"build": 50 //TODO: check if bonus is for skill or just adds labor points for all jobs that has 'build' skill requirement
						}
					}
				}
			},
			"set_gentleman": {
				"bonus": {
					"2": {
						"skills": {
							"appearance": 8
						},
						"attributes": {
							"charisma": 1
						},
						"jobs": {
							"all": 5
						}
					},
					"3": {
						"skills": {
							"appearance": 8,
							"leadership": 8

						},
						"attributes": {
							"charisma": 3
						},
						"jobs": {
							"all": 15
						}
					},
					"4": {
						"skills": {
							"appearance": 8,
							"leadership": 8,
							"trade": 8
						},
						"attributes": {
							"charisma": 6
						},
						"jobs": {
							"all": 30
						}
					},
					"5": {
						"skills": {
							"appearance": 16,
							"leadership": 8,
							"trade": 8
						},
						"attributes": {
							"charisma": 10
						},
						"jobs": {
							"all": 50
						}
					}
				}
			},
			"set_dancer": {
				"bonus": {
					"2": {
						"skills": {
							"appearance": 10 
						},
						"attributes": {
							"charisma": 2
						},
						"jobs": {
							"all": 10
						}
					},
					"3": {
						"skills": {
							"appearance": 10,
							"animal": 10

						},
						"attributes": {
							"charisma": 5
						},
						"jobs": {
							"all": 25
						}
					},
					"4": {
						"skills": {
							"appearance": 10,
							"animal": 10,
							"finger_dexterity": 10
						},
						"attributes": {
							"charisma": 9
						},
						"jobs": {
							"all": 40
						}
					}
				}
			},
			"greenhorn_set": {
				"bonus": {
					"2" : {},
					"3" : {},
					"4" : {},
					"5" : {},
					"6" : {},
					"7" : {
						"jobs": {
							"all": 5
						}
					},
					"8" : {
						"attributes": {
							"charisma": 1,
							"strength": 1
						},
						"jobs": {
							"all": 15
						}
					}
				}
			},
			"fireworker_set": {
				"bonus": {
					"1": {}
				}
			}
		};

		//init dynamic properties of ItemSetsBonuses object
		(function() {
			//fire_fighter
			ItemSetsBonuses["fireworker_set"].bonus[1].jobs = {};
			ItemSetsBonuses["fireworker_set"].bonus[1].jobs[JobIds["fire_fighter"]] = 15;
			//greenhorn_set
			//2 items
			ItemSetsBonuses["greenhorn_set"].bonus[2].jobs = {};
			ItemSetsBonuses["greenhorn_set"].bonus[2].jobs[JobIds["picking_sugar_cane"]] = 10;
			//3 items
			ItemSetsBonuses["greenhorn_set"].bonus[3].jobs = {};
			ItemSetsBonuses["greenhorn_set"].bonus[3].jobs[JobIds["picking_sugar_cane"]] = 10;
			ItemSetsBonuses["greenhorn_set"].bonus[3].jobs[JobIds["felling_trees"]] = 20;
			//4 items
			ItemSetsBonuses["greenhorn_set"].bonus[4].jobs = {};
			ItemSetsBonuses["greenhorn_set"].bonus[4].jobs[JobIds["picking_sugar_cane"]] = 10;
			ItemSetsBonuses["greenhorn_set"].bonus[4].jobs[JobIds["felling_trees"]] = 20;
			ItemSetsBonuses["greenhorn_set"].bonus[4].jobs[JobIds["tanning_deer_skin"]] = 20;
			//5 items
			ItemSetsBonuses["greenhorn_set"].bonus[5].jobs = {};
			ItemSetsBonuses["greenhorn_set"].bonus[5].jobs[JobIds["picking_sugar_cane"]] = 10;
			ItemSetsBonuses["greenhorn_set"].bonus[5].jobs[JobIds["felling_trees"]] = 20;
			ItemSetsBonuses["greenhorn_set"].bonus[5].jobs[JobIds["tanning_deer_skin"]] = 20;
			ItemSetsBonuses["greenhorn_set"].bonus[5].jobs[JobIds["hunting_turkey"]] = 20;
			//6 items
			ItemSetsBonuses["greenhorn_set"].bonus[6].jobs = {};
			ItemSetsBonuses["greenhorn_set"].bonus[6].jobs[JobIds["picking_sugar_cane"]] = 10;
			ItemSetsBonuses["greenhorn_set"].bonus[6].jobs[JobIds["felling_trees"]] = 20;
			ItemSetsBonuses["greenhorn_set"].bonus[6].jobs[JobIds["tanning_deer_skin"]] = 20;
			ItemSetsBonuses["greenhorn_set"].bonus[6].jobs[JobIds["hunting_turkey"]] = 20;
			ItemSetsBonuses["greenhorn_set"].bonus[6].jobs[JobIds["cowboy"]] = 20;
			//7 items
			ItemSetsBonuses["greenhorn_set"].bonus[7].jobs = {};
			ItemSetsBonuses["greenhorn_set"].bonus[7].jobs[JobIds["picking_sugar_cane"]] = 10;
			ItemSetsBonuses["greenhorn_set"].bonus[7].jobs[JobIds["felling_trees"]] = 20;
			ItemSetsBonuses["greenhorn_set"].bonus[7].jobs[JobIds["tanning_deer_skin"]] = 20;
			ItemSetsBonuses["greenhorn_set"].bonus[7].jobs[JobIds["hunting_turkey"]] = 20;
			ItemSetsBonuses["greenhorn_set"].bonus[7].jobs[JobIds["cowboy"]] = 20;
			//8 items
			ItemSetsBonuses["greenhorn_set"].bonus[8].jobs = {};
			ItemSetsBonuses["greenhorn_set"].bonus[8].jobs[JobIds["picking_sugar_cane"]] = 10;
			ItemSetsBonuses["greenhorn_set"].bonus[8].jobs[JobIds["felling_trees"]] = 20;
			ItemSetsBonuses["greenhorn_set"].bonus[8].jobs[JobIds["tanning_deer_skin"]] = 20;
			ItemSetsBonuses["greenhorn_set"].bonus[8].jobs[JobIds["hunting_turkey"]] = 20;
			ItemSetsBonuses["greenhorn_set"].bonus[8].jobs[JobIds["cowboy"]] = 20;


		 	//set_mexican
			//3 items
			ItemSetsBonuses["set_mexican"].bonus[3].jobs = {};
			ItemSetsBonuses["set_mexican"].bonus[3].jobs[JobIds["picking_agaves"]] = 60;
			//4 items
			ItemSetsBonuses["set_mexican"].bonus[4].jobs = {};
			ItemSetsBonuses["set_mexican"].bonus[4].jobs[JobIds["picking_agaves"]] = 60;
			ItemSetsBonuses["set_mexican"].bonus[4].jobs[JobIds["drilling_for_oil"]] = 70;
			//5 items
			ItemSetsBonuses["set_mexican"].bonus[5].jobs = {};
			ItemSetsBonuses["set_mexican"].bonus[5].jobs[JobIds["picking_agaves"]] = 60;
			ItemSetsBonuses["set_mexican"].bonus[5].jobs[JobIds["drilling_for_oil"]] = 70;
			ItemSetsBonuses["set_mexican"].bonus[5].jobs[JobIds["smuggling"]] = 80;
			//6 items
			ItemSetsBonuses["set_mexican"].bonus[6].jobs = {};
			ItemSetsBonuses["set_mexican"].bonus[6].jobs[JobIds["picking_agaves"]] = 60;
			ItemSetsBonuses["set_mexican"].bonus[6].jobs[JobIds["drilling_for_oil"]] = 70;
			ItemSetsBonuses["set_mexican"].bonus[6].jobs[JobIds["smuggling"]] = 80;
			ItemSetsBonuses["set_mexican"].bonus[6].jobs[JobIds["transport_ammunition"]] = 90;

			//set_farmer
			//
			//2 items
			ItemSetsBonuses["set_farmer"].bonus[2].jobs = {};
			ItemSetsBonuses["set_farmer"].bonus[2].jobs[JobIds["milling_grains"]] = 10;
			ItemSetsBonuses["set_farmer"].bonus[2].jobs[JobIds["mow_pasture"]] = 10;
			ItemSetsBonuses["set_farmer"].bonus[2].jobs[JobIds["harvesting_fields"]] = 10;
			//3 items
			ItemSetsBonuses["set_farmer"].bonus[3].jobs = {};
			ItemSetsBonuses["set_farmer"].bonus[3].jobs[JobIds["milling_grains"]] = 10;
			ItemSetsBonuses["set_farmer"].bonus[3].jobs[JobIds["mow_pasture"]] = 10;
			ItemSetsBonuses["set_farmer"].bonus[3].jobs[JobIds["harvesting_fields"]] = 10;
			ItemSetsBonuses["set_farmer"].bonus[3].jobs[JobIds["shoeing_horses"]] = 20;
			ItemSetsBonuses["set_farmer"].bonus[3].jobs[JobIds["installing_a_barbed_wire_fence"]] = 20;
			ItemSetsBonuses["set_farmer"].bonus[3].jobs[JobIds["cowboy"]] = 20;
			//4 items
			ItemSetsBonuses["set_farmer"].bonus[4].jobs = {};
			ItemSetsBonuses["set_farmer"].bonus[4].jobs[JobIds["milling_grains"]] = 10;
			ItemSetsBonuses["set_farmer"].bonus[4].jobs[JobIds["mow_pasture"]] = 10;
			ItemSetsBonuses["set_farmer"].bonus[4].jobs[JobIds["harvesting_fields"]] = 10;
			ItemSetsBonuses["set_farmer"].bonus[4].jobs[JobIds["shoeing_horses"]] = 20;
			ItemSetsBonuses["set_farmer"].bonus[4].jobs[JobIds["installing_a_barbed_wire_fence"]] = 20;
			ItemSetsBonuses["set_farmer"].bonus[4].jobs[JobIds["cowboy"]] = 20;
			ItemSetsBonuses["set_farmer"].bonus[4].jobs[JobIds["catching_horses"]] = 40;
			ItemSetsBonuses["set_farmer"].bonus[4].jobs[JobIds["constructing_a_ranch_house"]] = 40;
			ItemSetsBonuses["set_farmer"].bonus[4].jobs[JobIds["building_wind_mills"]] = 40;

			//set_indian
			//
			//2 items
			ItemSetsBonuses["set_indian"].bonus[2].jobs = {};
			ItemSetsBonuses["set_indian"].bonus[2].jobs[JobIds["hunt_coyotes"]] = 30;
			//3 items
			ItemSetsBonuses["set_indian"].bonus[3].jobs = {};
			ItemSetsBonuses["set_indian"].bonus[3].jobs[JobIds["hunt_coyotes"]] = 30;
			ItemSetsBonuses["set_indian"].bonus[3].jobs[JobIds["hunt_buffalo"]] = 40;
			//4 items
			ItemSetsBonuses["set_indian"].bonus[4].jobs = {};
			ItemSetsBonuses["set_indian"].bonus[4].jobs[JobIds["hunt_coyotes"]] = 30;
			ItemSetsBonuses["set_indian"].bonus[4].jobs[JobIds["hunt_buffalo"]] = 40;
			ItemSetsBonuses["set_indian"].bonus[4].jobs[JobIds["hunt_wolves"]] = 50;
			//5 items
			ItemSetsBonuses["set_indian"].bonus[5].jobs = {};
			ItemSetsBonuses["set_indian"].bonus[5].jobs[JobIds["hunt_coyotes"]] = 30;
			ItemSetsBonuses["set_indian"].bonus[5].jobs[JobIds["hunt_buffalo"]] = 40;
			ItemSetsBonuses["set_indian"].bonus[5].jobs[JobIds["hunt_wolves"]] = 50;
			ItemSetsBonuses["set_indian"].bonus[5].jobs[JobIds["hunt_grizzly_bears"]] = 60;

			//set_pilgrim_male
			//2 items
			ItemSetsBonuses["set_pilgrim_male"].bonus[2].jobs = {};
			ItemSetsBonuses["set_pilgrim_male"].bonus[2].jobs["construction"] = 5;
			//3 items
			ItemSetsBonuses["set_pilgrim_male"].bonus[3].jobs = {};
			ItemSetsBonuses["set_pilgrim_male"].bonus[3].jobs["construction"] = 15;
			//4 items
			ItemSetsBonuses["set_pilgrim_male"].bonus[4].jobs = {};
			ItemSetsBonuses["set_pilgrim_male"].bonus[4].jobs["construction"] = 30;
			//5 items
			ItemSetsBonuses["set_pilgrim_male"].bonus[5].jobs = {};
			ItemSetsBonuses["set_pilgrim_male"].bonus[5].jobs["construction"] = 50;
			ItemSetsBonuses["set_pilgrim_male"].bonus[5].jobs[JobIds["evangelizing"]] = 150;
			
			//set_pilgrim_female
			//2 items
			ItemSetsBonuses["set_pilgrim_female"].bonus[2].jobs = {};
			ItemSetsBonuses["set_pilgrim_female"].bonus[2].jobs["construction"] = 5;
			//3 items
			ItemSetsBonuses["set_pilgrim_female"].bonus[3].jobs = {};
			ItemSetsBonuses["set_pilgrim_female"].bonus[3].jobs["construction"] = 15;
			//4 items
			ItemSetsBonuses["set_pilgrim_female"].bonus[4].jobs = {};
			ItemSetsBonuses["set_pilgrim_female"].bonus[4].jobs["construction"] = 30;
			//5 items
			ItemSetsBonuses["set_pilgrim_female"].bonus[5].jobs = {};
			ItemSetsBonuses["set_pilgrim_female"].bonus[5].jobs["construction"] = 50;
			ItemSetsBonuses["set_pilgrim_female"].bonus[5].jobs[JobIds["evangelizing"]] = 150;

			//set_quackery
			//
			//2 items
			ItemSetsBonuses["set_quackery"].bonus[2].jobs = {};
			ItemSetsBonuses["set_quackery"].bonus[2].jobs[JobIds["charlatan"]] = 30;
			//3 items
			ItemSetsBonuses["set_quackery"].bonus[3].jobs = {};
			ItemSetsBonuses["set_quackery"].bonus[3].jobs[JobIds["charlatan"]] = 60;
			//4 items
			ItemSetsBonuses["set_quackery"].bonus[4].jobs = {};
			ItemSetsBonuses["set_quackery"].bonus[4].jobs[JobIds["charlatan"]] = 90;
			//5 items
			ItemSetsBonuses["set_quackery"].bonus[5].jobs = {};
			ItemSetsBonuses["set_quackery"].bonus[5].jobs[JobIds["charlatan"]] = 120;
			//6 items
			ItemSetsBonuses["set_quackery"].bonus[6].jobs = {};
			ItemSetsBonuses["set_quackery"].bonus[6].jobs[JobIds["charlatan"]] = 120;
		})();


		/**
		 * returns bonus if 'numItems' are used from item set with name 'setName'
		 * 'skillsCountObj' represents input formula to maximize
		 * 'jobId' is id of job we want to do (some item sets have bonus points for specific jobs)
		 */
		var getItemSetBonus = function(setName, numItems, skillsCountObj, jobId) {
			if (!ItemSetsBonuses[setName]) { 
				if (debug) appendToLog("I don't know set '" + setName + "'"); 
				return 0;
			}
			if (numItems < 1) { return 0; } //there's no bonus if we use only one item of set
			if (!ItemSetsBonuses[setName].bonus[numItems]) { if (debug) appendToLog("I don't know bonus for set '" + setName + "' if you wear " + numItems + " items, returning 0"); return 0; }

			var bonus = 0;
			//check skills addition
			var bonusObj = ItemSetsBonuses[setName].bonus[numItems];
			if (bonusObj["skills"]) {
				for (var skill in skillsCountObj) {
					if (bonusObj["skills"][skill]) {
						bonus += bonusObj["skills"][skill] * skillsCountObj[skill];
					}
				}
			}
			//check attributes addition
			if (bonusObj["attributes"]) {
				for (var skill in skillsCountObj) {
					for (var attr in bonusObj["attributes"]) {
						if (Character.skill_names[attr].contains(skill)) {
							bonus += bonusObj["attributes"][attr] * skillsCountObj[skill];
						}
					}
				}
			}
			//current item set adds bonus on current job
			if (bonusObj["jobs"] && (bonusObj["jobs"][jobId] || bonusObj["jobs"]["all"])) {
				bonus += (bonusObj["jobs"]["all"]) ? bonusObj["jobs"]["all"] : bonusObj["jobs"][jobId];
			}
			return bonus;
		};


		/**
		 * wraps ItemWrapper into new object
		 * which contains original ItemWrapper
		 * and calculated bonus for current job
		 */
		var wrapItemWithBonus = function(item,bonus) {
			return {
				item: item,
				bonus: bonus
			};
		};

		/**
		 * returns best item of provided type that maximizes formula provided via skillsCountObj parameter
		 * params:
		 * 	skillsCountObj - array[skill] = count 	example: array["tough"] = 2
		 * 	type: what type of wear ( "body" | "yield" | "neck" | "foot" | "head" | "right_arm" | "left_arm" | "animal")
		 */
		var getBestItem = function(map, type, skillsCountObj) {
			if (debug) {
				appendToLog("Now I'm trying to find best item for " + type);
			}
			if (map.items[type] == undefined) { return null; } //we have no item of provided type
			var bestItem = null; 
			var maxBonus = 0; //bonus maximum
			if (!map.items[type]) return null;
			map.items[type].each(function(item) {
				var itemBonus = 0;
				for (var key in skillsCountObj) {
					itemBonus += skillsCountObj[key] * item.getSkillBonus(key);
				}
				if (debug) appendToLog('\tProcessing item ' + item.item.obj.name + ' [ +' + itemBonus + ' ]');
				if (bestItem != null) {
					//we found better item so change
					//bestItem to current item
					if (bestItem.bonus < itemBonus) {
						bestItem = wrapItemWithBonus(item,itemBonus);
						maxBonus = itemBonus;
						if (debug) {
							appendToLog('\tChanging best item to ' + item.item.obj.name + ' [ +' + itemBonus + ' ]');
						}
					} else if (bestItem.bonus == itemBonus && !bestItem.item.wearing && item.wearing) {
						//current item has same bonus as bestItem
						//from these two items we prefer item 
						//which is character already wearing
						bestItem = wrapItemWithBonus(item,itemBonus);
						if (debug) {
							appendToLog('\tChanging best item to ' + item.item.obj.name + ' because you are already wearing it');
						}
					}
				} else {
					//this is first iteration so we 
					//set first item as the best one
					bestItem = wrapItemWithBonus(item,itemBonus);
					maxBonus = itemBonus;
					if (debug) {
						appendToLog('\tSetting best item to ' + item.item.obj.name + ' [ +' + itemBonus + ' ]');
					}
				}
			});
			//if item doesn't add any bonus there's no need to change clothes, therefore return null
			return (maxBonus != 0) ? bestItem : null;
		};

		/**
		 * returns array containing values
		 * from 0 to n - 1
		 *
		 * returns:
		 * 	[ 0, 1, ..., n - 1 ]
		 */
		var range = function(n) {
			var result = [];
			for (var i=0; i < n; i++) {
				result[i] = i;
			}
			return result;
		};

		/** 
		 * returns all subsets of size k from 
		 * the set of intergers 0 .. n - 1
		 *
		 * we want to choose
		 * all k-subsets out of n-set
		 * there are exactly choose(n,k)
		 * of these subsets
		 * ( choose(n,k) = n! / ( (n - k)! * k!) )
		 *
		 * now suppose that set is represented by array (quite funny :))
		 * to choose all 3-subsets from that array,
		 * if we had set of 3-subsets with indexes
		 * of elements in array, we would just
		 * read each subset based on 3 indexes
		 *
		 * and thats exactly what this function does
		 * returns these indexes!
		 *
		 * example: call to getAllkSubsetsIndexes(5,3)
		 * as written above there are choose(n,k)
		 * subsets = choose(5,3) = 10
		 *
		 * function returns this array (not exactly in this order as we are working with "sets"):
		 * 	[ 
		 * 		[0,1,2], [0,1,3], [0,1,4], [0,2,3], [0,2,4], 
		 * 		[0,3,4], [1,2,3], [1,2,4], [1,3,4], [2,3,4]
		 * 	]
		 *
		 *
		 * main idea was taken from: http://code.activestate.com/recipes/500268-all-k-subsets-from-an-n-set/
		 * but it took me a long time to understand it (and to understand Python's yield) :)
		 */
		var getAllkSubsetsIndexes = function(n, k) {
			if (n < 0) { throw new Error("N must be greater or equal to zero"); return; }
			if (k < 0) { throw new Error("K must be greater or equal to zero"); return; }
			//if we choose 0-subset of n-set,
			//the result is empty set
			if (n < k || k == 0) {
				return [[]];
			}
			//if we choose n-subsets of n-set,
			//then the result is the set itself
			if (n == k) {
				return [range(n)];
			}
			//now comes the tricky part
			//based on formula
			//	choose(n,k) = choose(n-1,k-1) + choose(n-1,k)
			//we recursively calculate result
			//
			//ok, so .. really .. what is going on here? :)
			//
			//first we divide the set into two parts:
			//first part contains only one element (X)
			//and second part (2) cointains the rest of the set
			//in other words we extract element (X) from the set
			//
			//the result will be union of two subresults: (A) and (B) 
			//
			//(A): 
			//	we find all subsets of size k-1 in
			//	(2) and then add (X) to each element of
			//	this result set
			//	(in these results WILL be (X))
			//(B):
			//	we find all subsets of size k in (2)
			//	(in these results WON'T be (X))
			//
			//in other words we find all k-subsets that (X)
			//is part of and all k-subsets that (X) isn't 
			//part of and union them
			var union = [];

			//choose(n-1,k-1)
			var p1 = getAllkSubsetsIndexes(n - 1, k - 1); //find (k-1)-subsets that doesn't contain extracted element (X)
			for (var i=0; i < p1.length; i++) {
				p1[i].push(n - 1); //add extracted element (X) to each (k-1)-subset
				union[i] = p1[i];
			}

			//choose(n-1,k)
			var p2 = getAllkSubsetsIndexes(n - 1, k); //find all k-subsets that doesn't contain extracted element (X)
			for (var i=0; i < p2.length; i++) {
				union[i + p1.length] = p2[i];
			}

			return union;
		};

		/**
		 * returns all k-subsets of set 'set'
		 * inputs: 
		 * 	set: array of elements (not set at all :))
		 * 	k: integer > 0
		 */
		var getAllKSubsets = function(set, k) {
			var subsets = [];
			var n = set.length;
			var indexes = getAllkSubsetsIndexes(n, k);
			for (var i=0; i < indexes.length; i++) {
				var subset = [];
				for (var j=0; j < indexes[i].length; j++) {
					subset.push(set[indexes[i][j]]);
				}
				subsets.push(subset);
			}
			return subsets;
		};

		/**
		 * returns map which contains best items from inventory,
		 * in other words which maximize skill bonuses provided
		 * via 'requiredSkills' parameter
		 *	key: wear_type
		 *	value: ItemWrapper object
		 * if no suitable items are found returns empty object
		 * params:
		 * 	requiredSkills - list with 5 skills required for job (skills may repeat)
		 *
		 * returns: {
		 *   "map" : { wearType1 : item1, wearType2 : item2, ... wearTypeN : itemN },
		 *   "bonus" : total bonus if used all items from map
		 * }
		 */
		var getBestJobItems = function(requiredSkills, jobId) {
			if (requiredSkills == undefined) {
				throw new Error("You must provide requiredSkills parameter");
			}
			var requiredSkillsCount = {};
			for (var i=0; i < requiredSkills.length; i++) {
				var skill = requiredSkills[i];
				requiredSkillsCount[skill] = (requiredSkillsCount[skill] == undefined) ? 1 : requiredSkillsCount[skill] + 1;
			}
			if (debug) {
				var sb = new StringBuffer();
				var first = true;
				for (var key in requiredSkillsCount) {
					sb.append(!first ? " + " : "").append(requiredSkillsCount[key]).append(" * ").append(key);
					first = false;
				}
				appendToLog('Maximizing function: ' + sb.toString());
				delete sb;
			}
			var bestItemsMap = {};
			//now find best items for each wear type
			var totalBonusUsingWearTypes = 0; //total bonus if items are selected only by wearType
			var suspectedItemSet = {};
			var map = getItemsMap(); //get map with items
			if (debug) appendToLog("Finding best item for each type (no items sets yet)");
			var wear_types = wearTypesList();
			//["body","yield","neck","foot","head","right_arm","left_arm","animal","pants"]
			wear_types.each(function(wearType) {
				var best = getBestItem(map, wearType, requiredSkillsCount);
				if (best != null) {
					totalBonusUsingWearTypes += best.bonus;
					//is suggested item part of item set?
					var itemSet = best.item.item.obj.set;
					if (itemSet) {
						var itemId = best.item.item.obj.item_id;
						var key = itemSet.key;
						//just check if item set that is in 'set' attribute
						//really contains this item .. maybe redundant
						if (itemSet.items[itemId]) {
							if (!suspectedItemSet[key]) { suspectedItemSet[key] = []; }
							suspectedItemSet[key].push(best);
						}
					}
					bestItemsMap[wearType] = best;
					if (debug) {
						appendToLog("Best item for " + wearType + ' is ' + best.item.item.obj.name + ' [ +' + best.bonus + ' ]');
					}
				} else {
					if (debug) {
						appendToLog("I didn't find any suitable item for " + wearType);
					}
				}
			});
			//now if we have chosen items that are part of any itemSet and therefore
			//may add extra bonus, check it.
			var extraBonus = 0;
			for (var setName in suspectedItemSet) {
				var setItems = suspectedItemSet[setName];
				var numItems = setItems.length;
				extraBonus += getItemSetBonus(setName, numItems, requiredSkillsCount, jobId);
			}
			//add extra bonus because suggested items are also part of set which increases bonus
			totalBonusUsingWearTypes += extraBonus;
			if (debug) appendToLog('Total bonus using items chosen using calculation based on best item for each wear type is: +' + totalBonusUsingWearTypes); 
			
			//we found that there are items that may be part of item set
			//try to combine these items with yet calculated items so it
			//would add more bonus than without using item set
			var bestCombination = {}; //best combination object using item sets
			var bestCombinationBonus = 0; //and its total bonus
			if (!isObjectEmpty(map.sets)) {
				if (debug) appendToLog("Some items may be part of item set. Trying to combine items and increase bonus (hopefully).");
				for (var setName in map.sets) {
					var set = map.sets[setName];
					if (set.length < 1) {
						if (debug) appendToLog("\tSkipping set '" + setName + "' because you have no items from it (and therefore no bonus)");
						continue;
					}

					if (debug) appendToLog("\n\n\nProcessing set '" + setName + "' ...");
					set = eliminateDuplicateSetItems(set);

					//now change each element in set by wrapping it using wrapItemWithBonus function
					//c'mon .. this is really mess !! .. I REALLY should refactor 
					//whole code so it would be MUCH clearer
					for (var i=0; i < set.length; i++) {
						var itemBonus = 0;
					       	for (var key in requiredSkillsCount) {
							itemBonus += requiredSkillsCount[key] * set[i].getSkillBonus(key);
						}
						set[i] = wrapItemWithBonus(set[i], itemBonus); 
					}
					map.sets[setName] = set; //change original map with wrapped array
					//find best i-subset of set[setName]
					for (var i=1; i <= set.length; i++) {
						var iSubsets = getAllKSubsets(set, i);
						if (debug) appendToLog("Checking all " + iSubsets.length + " subsets of size " + i + " from set '" + setName);
						for (var j=0; j < iSubsets.length; j++) {
							var blaStr = mapToString(changeItems({},iSubsets[j]));
							if (debug) appendToLog("__________" + blaStr);
							var currentCombination = changeItems(bestItemsMap, iSubsets[j]);
							var itemsBonus = getTotalBonus(currentCombination)
							var setBonus = getItemSetBonus(setName, i, requiredSkillsCount, jobId)
							var currentCombinationBonus =  itemsBonus + setBonus;
							var currentCombinationString = ((debug) ? mapToString(currentCombination) : "");

							if (debug) appendToLog('Processing combination ' + currentCombinationString + ' [ +' + currentCombinationBonus + ' = ' + itemsBonus + ' + ' + setBonus + ' ]');
							if (isObjectEmpty(bestCombination)) {
								//don't even set because it is still worse than above
								//calculated items
								if (currentCombinationBonus <= totalBonusUsingWearTypes) {
									if (debug) appendToLog("Skiping combination " + currentCombinationString + " because it has less or equal bonus than combination " + mapToString(bestItemsMap) + " ( " + currentCombinationBonus + " <= " + totalBonusUsingWearTypes + " )");
									continue;
								}
								bestCombination = currentCombination;
								bestCombinationBonus = currentCombinationBonus;
								if (debug) appendToLog("Setting best combination with item set to " + currentCombinationString + " [ +" + currentCombinationBonus + " ] (using  " + i + " items of '" + setName + "')");
								continue;
							}
							if (bestCombinationBonus < currentCombinationBonus) {
								bestCombination = currentCombination;
								bestCombinationBonus = currentCombinationBonus;
								if (debug) appendToLog("Changing best combination with item set to " + currentCombinationString + " [ +" + currentCombinationBonus + " ] (using " + i + " items of '" + setName + "')");
							};

						} //process j-th subset of size i
					}
				} //for each possible set
			} //sets are not empty
			if (!isObjectEmpty(bestCombination)) bestItemsMap = bestCombination;

			var totalBonus = (!isObjectEmpty(bestCombination) ? bestCombinationBonus : totalBonusUsingWearTypes);
			if (debug) {
				appendToLog('I suggest these items to wear');
				for (var key in bestItemsMap) {
					appendToLog('\t' + key + ': ' + bestItemsMap[key].item.item.obj.name);
				}
				appendToLog('Total bonus is: +' + totalBonus);
			}
			return {
				"map" : bestItemsMap,
				"bonus" : totalBonus
			};
		};

		var eliminateDuplicateSetItems = function(set) {
			var map = {};

			//put to map by wear type
			for (var i=0; i < set.length; i++) {
				map[set[i].item.obj.type] = set[i];
			}

			//put back to array
			var result = [];
			for (var wearType in map) {
				result.push(map[wearType]);
			}

			return result;

		};

		/**
		 * returns string containing names of items in map
		 * input structure: same as getTotalBonus()
		 */
		var mapToString = function(map) {
			var sb = new StringBuffer("[");
			for (var wearType in map) {
				sb.append(map[wearType].item.toString()).append(",");
			}
			sb.removeLast();
			return sb.append("]").toString();
		};

		/**
		 * returns sum of bonuses of items from map
		 * inputs:
		 * 	map: {
		 *		"body": {
		 *			item: ItemWrapper1,
		 *			bonus: bonus1
		 *		},
		 *		"head": {
		 *			item: ItemWrapper2,
		 *			bonus: bonus2
		 *		},
		 *		...
		 *		wearTypeN: {
		 *			item: ItemWrapperN,
		 *			bonus: bonusN
		 *		}
		 * 	}
		 */
		var getTotalBonus = function(map) {
			var sum = 0;
			for (var wearType in map) {
				sum += map[wearType].bonus;
			}
			return sum;
		};

		/**
		 * changes items in 'original' with items from 'toChange'
		 * based on 'toChange' elements wear type
		 * inputs:
		 * 	original: map: key = wearType, value = ItemWrapper
		 * 	toChange: array of ItemWrappers
		 * returns:
		 * 	array containing all elements from 'toChange' and
		 * 	remaining elements from 'original'
		 */
		var changeItems = function(original, toChange) {
			var newOne = {};
			//copy
			for (var type in original) {
				newOne[type] = original[type];
			}
			//change items
			for (var i=0; i < toChange.length; i++) {
				newOne[toChange[i].item.item.obj.type] = toChange[i];
			}
			return newOne;
		};

		/**
		 * highlights suggested items to change
		 * if character is already wearing these items
		 * message window is displayed
		 */
		var processSuggestedItems = function(items) {
			var bag = Bag.getInstance();
			//list of items which were highlighted in inventory window
			//var itemsToChange = [];
			var item_ids = [];
			//items parameter may contain items
			//which is character already wearing
			//if character is wearing all of suggested
			//items, show information window so that
			//user knowns that the script was doing something :)
			var wearingAllItems = true;
			
			for (var type in items) {
				var item = items[type].item;
				//skip suggested items that I'm already wearing
				if (item.wearing) { 
					var dom_elm = $("char_" + type);
					if (dom_elm)
					{
						//dom_elm.style.background = 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAIAAAAmkwkpAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAcSURBVBhXY/h/JhSE7nQBEQMqBywGEWKAKIBwALVpJCmZGAR1AAAAAElFTkSuQmCC) repeat';
//						dom_elm.style.background = 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACBSURBVDhPndPLEYAgDARQoBVqpAltUWuJH2QmEUI2eDWPlZ0YiSisPmkVPi6uJJ97jfQnN+nHTDqxlB7cSRiPZMgFKEyRQLIuLTyV75Ic27dkuYhts6RM5tOA7D67GkyO7gxLq7Dawa8LVkyavJvLlqydrWfKX7Kfs6S8M58G5I0vW48oTSxCvkgAAAAASUVORK5CYII%3D) no-repeat top left';	
					}
					continue; 
				}
				wearingAllItems = false;
				//itemsToChange.push({item: item, type: type});
				item_ids.push(item.item.get_inv_id());
			}

			if (wearingAllItems) {
				new HumanMessage(lang.getPhrase("already_wearing_items"));
			} else {
				highlightSuitableItems(item_ids);
				sortBagItems(item_ids);
				var div = new Element('div');
				div.appendChild(createChangeClothesButton(item_ids));
				injectElementToTop(div, $("bag") /*$("window_inventory_content").getElement("h2")*/);
			}
		};

		/**
		 * changes position of items in bag so that
		 * best items selected for the job will be first ones
		 *
		 * input: array of items to put at the begining of bag
		 */
		var sortBagItems = function(item_ids) {
			var bag = $('bag');
			if (!bag) { alert("Can't find bag element. Please report this error."); }
			for (var i=0; i < item_ids.length; i++) {
				var elm = getBagItemDomElement(item_ids[i]);
				elm.inject(bag, 'top');
			}
		};

		var getBagItemDomElement = function(inv_id) {
			var elm = $('item_' + inv_id);
			if (!elm && debug) {
				appendToLog("Bag element with id 'item_" + inv_id + "' is null");
			}
			return elm;
		};

		/**
		 * changes clothes from array of items
		 * provided via 'items' parameter
		 * if character is already wearing
		 * item, then skips to the next one
		 */
		var highlightSuitableItems = function(item_ids) {
			for (var i=0; i < item_ids.length; i++) {
				var id = item_ids[i];
				var elm = getBagItemDomElement(id);
				//highlight item in bag
				elm.style.background = '#fc5';
			}
	
			/*	
			var remains = item_ids.length;
			var callback = function(data) {
				try {
					if (!data) return;
					//do nothing if some other object was removed from inventory
					if (!item_ids.contains(data[0])) return;
					remains--;
					if (remains == 0) {
						alert("removing listener");
						WEvent.remove('inventory_remove', this);
					}
					//decrease count of remaining items to change
					var counter = $("best-items-selector-counter");
					if (counter) counter.innerHTML = remains;
				} catch (err) {
					WEvent.remove('inventory_remove', this);
				}
			};
			WEvent.register("inventory_remove", { exec: callback });	
			*/
			//check each child so that javascript won't stop
			//processing if DOM element doesn't exist
//			if (item_ids.length > 0) {
//				var windowContent = $("window_inventory_content");
//				if (windowContent) {
//					var inventoryHeader = windowContent.getElement("h2");
//					if (inventoryHeader) {
//						var counter = new Element("i", {id: "best-items-selector-counter", style: "color: rgb(45,91,137); font-size: 80%; "});
//						var rest = new Element("i", {style: "color: rgb(45,91,137); font-size: 60%; "});
//						counter.appendText(item_ids.length);
//						rest.appendText(" " + (item_ids.length > 1 ? lang.getPhrase("item") : lang.getPhrase("items")) + " " + lang.getPhrase("to_change"));
//						inventoryHeader.appendText("  ");
//						inventoryHeader.appendChild(counter);
//						inventoryHeader.appendChild(rest);
//					}
//				}
//			}

			new HumanMessage(lang.getPhrase("items_have_been_highlighted"), { type: 'success' });
			
			/*
			var removeHighlight = function() {
				for (var i=0; i < highlightedItems.length; i++) {
					var item = highlightedItems[i];
					$('item_' + item.item.item.get_inv_id()).style = item.originalStyle;
				}
			};

			//we need to remove items highlight so that if
			//inventory window is refreshed no items are highlighted
			//first do this if 'closeall' button is pushed on any
			//of displayed windows
			$$('a.window_closeall').each(function(elm) {
				elm.addEvent('click', function(event) {
					elm.removeEvent('click', this);
					removeHighlight();
				});
			});
			//and also if inventory window is closed by clicking 'close' button
			$ES('a.window_close', 'window_inventory_title').each(function(elm) {
				elm.addEvent('click', function(event) {
					elm.removeEvent('click', this);
					removeHighlight();
				});
			});
			*/
		};

		/**
		 * helper function to determine
		 * if provided object is empty
		 * or not (has at least one property)
		 */
		var isObjectEmpty = function (ob) {
			for(var i in ob){ return false;}
			return true;
		}

		/**
		 * returns microtime
		 * usefull for calculating time consumption
		 * source: http://www.navioo.com/javascript/tutorials/Javascript_microtime_1583.html
		 */
		var microtime = function (asString) {  
			var now = new Date().getTime() / 1000;  
			var s = parseInt(now);
			return (asString) ? (Math.round((now - s) * 1000) / 1000) + ' ' + s : now;  
		}

		/**
		 * creates button which automatically changes suggested clothes
		 *
		 * html:
		 * 	<a href="javascript://void(0);" class="button button_wrap">
		 * 		<span class="button_left"></span>
		 * 		<span class="button_middle">Change clothes</span>
		 * 		<span class="button_right"></span>
		 * 		<span style="clear:both;"></span>
		 * 	</a>
		 */
		var createChangeClothesButton = function(item_ids) {
			var button = createButton(lang.getPhrase("button_change_clothes") + " [" + item_ids.length + "]");
			var bag = Bag.getInstance();
			if (!bag) alert("Can't find Bag object. Please report error");
			if (item_ids.length == 0) { return; }
			var click_handler = function(event) {
				button.removeEvent("click", click_handler);
				//change text in '<span class="middle">'
				button.getChildren()[1].empty().appendText(lang.getPhrase("processing_wait_please"));
				var callback = function(data) {
					try {
						if (!data) return;
						//do nothing if some other object was removed from inventory
						if (item_ids[current] != data[0]) return;

						//decrease count of remaining items to change
						//var counter = $("best-items-selector-counter");
						//if (counter) counter.innerHTML = parseInt(counter.innerHTML) - 1;

						//go to next item
						current++;
					
						if (current >= item_ids.length) 
						{
							WEvent.remove('inventory_remove', this);
							//change text in '<span class="middle">'
							button.getChildren()[1].empty().appendText(lang.getPhrase("done"));

							var final_click_handler = function(event) {
								button.removeEvent("click", final_click_handler);
								AjaxWindow.close("inventory");
							};

							button.addEvent("click", final_click_handler);
						} 
						else 
						{
							/*
							 * simulate human behaviour by adding random
							 * timeout before next item is changed
							 */
							setTimeout(function() {
								bag.carry(item_ids[current]);
							}, random(0, 500) 
							);
						}
					} catch (err) {
						WEvent.remove('inventory_remove', this);
						//change text in '<span class="middle">'
						button.getChildren()[1].empty().appendText(lang.getPhrase("error"));
					}
				};
				WEvent.register("inventory_remove", { exec: callback });
				//start changing clothes
				var current = 0;
				bag.carry(item_ids[current]);
			};
			button.addEvent("click", click_handler);
			return button;
		};

		/**
		 * appends DOM element 'what' to element 'where'
		 */
		var appendElement = function(what, where) {
			if (!where) return;
			where.appendChild(what);
		};

		var injectElementToTop = function(what, where) {
			if (!what) return;
			what.inject(where, 'top');
		};

		var injectAfterElement = function(what, where) {
			if (!what) return;
			what.inject(where, 'after');
		};
		
		
		/**
		 * creates button with text provided by title parameter
		 *
		 * html:
		 * 	<a href="javascript://void(0);" class="button button_wrap">
		 * 		<span class="button_left"></span>
		 * 		<span class="button_middle">title</span>
		 * 		<span class="button_right"></span>
		 * 		<span style="clear:both;"></span>
		 * 	</a>
		 */
		var createButton = function(title) {
			var buttonWrap = new Element('a', { href: "javascript://void(0);" });
			var buttonLeft = new Element('span');
			var buttonMiddle = new Element('span');
			var buttonRight = new Element('span');
			var cleaner = new Element('span', { style: "clear:both;" });
			buttonLeft.addClass("button_left");
			buttonMiddle.addClass("button_middle");
			buttonMiddle.appendText(title);
			buttonRight.addClass("button_right");
			buttonWrap.addClass('button');
			buttonWrap.addClass('button_wrap');
			buttonWrap.appendChild(buttonLeft);
			buttonWrap.appendChild(buttonMiddle);
			buttonWrap.appendChild(buttonRight);
			buttonWrap.appendChild(cleaner);
			return buttonWrap;	
		};

		/**
		 * creates button which starts calculation on 'onclick' event
		 *
		 * html:
		 * 	<a href="javascript://void(0);" class="button button_wrap">
		 * 		<span class="button_left"></span>
		 * 		<span class="button_middle">Best items</span>
		 * 		<span class="button_right"></span>
		 * 		<span style="clear:both;"></span>
		 * 	</a>
		 */
		var createCalculationButton = function(taskSkills, optObj) {
		       //	jobId, title_suffix) {
			var options = {
				jobId: -1
			};
			for (var opt in optObj) options[opt] = optObj[opt];

			var button = createButton(lang.getPhrase("button_best_items"));
			var click_handler = function(event) {
				//button.removeEvent("click", click_handler);
				//first we must open inventory window so that
				//we could access Bag and Wear objects
				//this is done via Ajax call which completes
				//when 'window_update_finished' event is fired
				var callback = function(data) {
					//remove processing for event
					//so that nothing is done if user
					//opens other windows
					WEvent.remove('window_update_finished', this);
					//event is fired after load of every Ajax window
					//we will process only requests which open inventory
					if (data["name"] != "inventory") { return; }
					var startTime = microtime();
					if (debug) {
						appendToLog('==== CALCULATION STARTED ====');
					}
					//find best items for job
					var items = getBestJobItems(taskSkills, options.jobId);
					if (!isObjectEmpty(items.map)) {
						//now process suggested items
						processSuggestedItems(items.map);
					} else {
						if (debug) {
							appendToLog("I didn't find any suitable items");
						}
						new HumanMessage(lang.getPhrase("no_items_found"));
					}
					var endTime = microtime();
					if (debug) {
						appendToLog('Calculation time: ' + (endTime - startTime).toString() + ' s');
						appendToLog('==== CALCULATION FINISHED ====\n');
						printLog();
					}
				};
				WEvent.register('window_update_finished', {
					exec: callback
				});
				AjaxWindow.show("inventory");
			};
			button.addEvent('click', click_handler);
			return button;
		};


		/**
		 * handler for processing Ajax requests
		 *
		 * every request which shows dialog window
		 * with job information is usefull for us
		 * as well as request which shows dialog
		 * for town building
		 *
		 * job:
		 * 	parses job coordinates from request
		 * 	so that we can retrieve object with
		 * 	job informations (skill requirements
		 * 	we want to maximize)
		 * town build:
		 * 	we pass constant skill requirements
		 * 	(3x build, 1x repair,
		 * 	1x leadership) 
		 * fort build:
		 * 	we pass constant skill requirements
		 * 	(3x build, 1x repair,
		 * 	1x leadership)
		 * duel:
		 * 	skills requirements are passed based on
		 * 	what do we want: either 1) best offense or
		 * 	2) best defense
		 *
		 * 	1)
		 * 	2)
		 */
		var processJobWindow = function(url) {
			//parse url
			var params = {};
			var parts = url.split("?");
			if (parts.length <= 1) { return; }
			var args = parts[1].split("&");
			for (var i=0; i < args.length; i++) {
				var arg = args[i].split("=");
				params[arg[0]] = arg[1];
			}
			if (!params["window"]) { return; }
			switch (params["window"]) {
				case "job":
					if (params["x"] == undefined || params["y"] == undefined) return;
					//now we must get to job object
					//this is kind of a hack but I didn't find better way yet :)
					var handlers = WEvent.events["jobCalcDuration_" + params["x"] + "_" + params["y"]];
					var jobObject = null;
					for (var i=0; i < handlers.length; i++) {
						var handler = handlers[i]["func"];
						//this one should be the job object
						if (handler.bind != undefined) { jobObject = handler.bind; break; }
					}
					if (jobObject == null) {
						throw new Error("Could not find job object (maybe bug or newer version of the-west code)");
					}
					//job calculation object
					var taskSkills = jobObject.jobCalc.task_skills;
					//alert("jobId:" + jobObject.jobCalc.jobId);
					if (debug) {
						appendToLog('Found job window, appending button');
						printLog();
					}
					//append button for calculation
					//var where = $("window_job_" + params["x"] + "_" + params["y"] + "_content").getElements(".jobPointsDescription")[0];
					//if (where) {
					//	injectAfterElement(createCalculationButton(taskSkills, jobObject.jobCalc.jobId), where);
					//} else {
					var malus = jobObject.jobCalc.malus;
					var jobId = jobObject.jobCalc.jobId;
					var button = createCalculationButton(taskSkills, { "jobId" : jobId });
					var content = $("window_job_" + params["x"] + "_" + params["y"] + "_content");
					var div = new Element("div");
					button.inject(div);

					div.inject(content.getElements(".jobDescription")[0],'bottom');

/*					
					(function() {
						var callback = function(data) {
							WEvent.remove('window_update_finished', this);
							if (data["name"] != "inventory") { return; }
							//find best items for job
							var items = getBestJobItems(taskSkills, jobId);

							var infoDiv = new Element('div');
							infoDiv.appendText('Best items stats: ' + items.bonus + "/" + malus);
							infoDiv.inject(div);
						};
						
						WEvent.register('window_update_finished', {
							exec: callback
						});
			
						AjaxWindow.show("inventory");
						AjaxWindow.bringToTop($("window_job_" + params["x"] + "_" + params["y"])); 
					})();
*/


					//	appendElement(createCalculationButton(taskSkills, jobObject.jobCalc.jobId), $("window_job_" + params["x"] + "_" + params["y"] + "_content").getFirst().getFirst().getFirst());
					//}
					break;
				case "cityhall_build":
					if (!params["building"]) return;
					if (debug) {
						appendToLog('Found town building window, appending button');
						printLog();
					}
					appendElement(createCalculationButton(['build','build','build','repair','leadership'], {"jobId" : "construction"}), $("window_cityhall_build_" + params["building"] + "_content").getElements('p')[0]);
					break;
				case "headquarter_build":
					if (!params["fortbuilding"]) return;
					if (debug) {
						appendToLog('Found fort building window, appending button');
						printLog();
					}
					appendElement(createCalculationButton(['build','build','build','repair','leadership'], {"jobId" : "construction"}), $("window_headquarter_build_" + params["fortbuilding"] + "_content").getFirst().getFirst());
					break;
				case "duel":
					if (debug) {
						appendToLog('Found duel window, appending button');
						printLog();
					}
					//TODO change task skills input
					/** 
					 * "strength":		"build" | "punch" | "tough" | "endurance" | "health"
					 * "flexibility":	"ride" | "reflex" | "dodge" | "hide" | "swim"
					 * "dexterity":		"aim" | "shot" | "pitfall" | "finger_dexterity" | "repair"
					 * "charisma":		"leadership" | "tactic" | "trade" | "animal" | "appearance"
					 **/
					//appendElement(createCalculationButton(['aim','tactic','dodge','punch'], null, lang.getPhrase("duel_melee")), $("duel_picture"));
					//appendElement(createCalculationButton(['aim','tactic','dodge','shot'], null, lang.getPhrase("duel_firearms")), $("duel_picture"));
					break;
				default: return;
			}
		};

		/**
		 * initialize script after 5 seconds
		 *
		 * thats because in the-west core script
		 * there's 5 seconds (why?) delay before calling
		 * update of user data, so we should wait a while 
		 * before everything is loaded correctly
		 */
		window.setTimeout(function() {
			/**
			 * extends Ajax processing,
			 * we want to process every Ajax request
			 * which returns dialog window with
			 * job information
			 *
			 * (is there a better way to determine
			 * when we should put selection button
			 * to calculate best items ?) 
			 *
			 * binding is quite tricky - we can't just extend 
			 * Ajax class and assing reference to overloaded class
			 * back to original class because original class contains
			 * methods which are not prototype (main problem is in
			 * mootools chaining .. ), so we create separate
			 * extended object and then assign reference to it's
			 * initialize method to initialize method of original
			 * class
			 */
			var Extended = Ajax.extend({
				initialize: function (url, options) {
    					this.parent(url, options);

					this.addEvent('onComplete', function() {
						processJobWindow(url);	
					});

				},
			     	
			});
			
			Ajax.prototype.initialize = Extended.prototype.initialize;

			/**
			 * set language so that interface is internationalized
			 */
			setInterfaceLanguage();
		}, 5000);
	}); //'domready' event
});

/**
 * Monkey Updater
 */
function update(filename) {
	var body=document.getElementsByTagName('body')[0];
	script=document.createElement('script');
	script.src=filename;
	script.type='text/javascript';
	body.appendChild(script);
	var today = new Date();
	GM_setValue('muUpdateParam_143', String(today));
}
/**
 * Verify if it's time to update
 */
function CheckForUpdate() {
	var lastupdatecheck = GM_getValue('muUpdateParam_143', 'never');
	var updateURL = 'http://www.monkeyupdater.com/scripts/updater.php?id=143&version=1.4.20110315';
	var today = new Date();
	var one_day = 24 * 60 * 60 * 1000; /* one day in milliseconds */
	if(lastupdatecheck != 'never') { 
		today = today.getTime(); /* get today's date */
		var lastupdatecheck = new Date(lastupdatecheck).getTime();
		var interval = (today - lastupdatecheck) / one_day;
		/**
		 * Find out how many days have passed
		 * If one day has passed since the last 
		 * update check, check if a new version 
		 * is available
		 */
		if(interval >= 1) {
			update(updateURL);
		}
	} else {
		update(updateURL);
	}
}
CheckForUpdate();