Source: aiclient.js

/**
 * @file Manages AI and Colony logic
 * @author Matthew Morrow
 * @copyright 2014
 */

/**
 * MatthewMMorrow root namespace
 * @namespace mmm
 */
(function(mmm){
	var CLICKS_PER_SECOND = 4;
	
	/**
	 * Represents the artificial intelligence
	 * @class
	 * @param {mmm.Colony} colony - The parent colony
	 */
	mmm.AI = function(colony){
		var me = this;
		this.colony = colony;
		this.aicode = ko.observable("Alpha.prototype.execute = function(colony){\n\t//Add your code here\n\tthis.click(1);\n}");
		this.code = ko.computed(function(){
			var code = "importScripts('http://myprojects.localhost/aicolony/js/aisandbox.js');\n"
			code += "var Alpha = function(){};\n"
			code += "Alpha.prototype = new mmm.AI();\n"
			code += me.aicode() + ";\n";
			code += "var alpha00 = new Alpha();"
			return code;
		});
		this.clearCode = function(){
			this.aicode("Alpha.prototype.execute = function(colony){\n\t//Add your code here\n\tthis.click(1);\n}");
			this.loaded(false);
		}

		this.rate = ko.observable(10);
		this.loaded = ko.observable(false);
		this.running = ko.observable(false);
		
		this.port = null;
		this.load = function(){
			var blob;
			try {
				blob = new Blob([me.code()], {type: 'application/javascript'});
			} catch (e) { // Backwards-compatibility
				window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder;
				blob = new BlobBuilder();
				blob.append(me.code());
				blob = blob.getBlob();
			}
			
			me.port = new Worker(URL.createObjectURL(blob));		
			me.port.addEventListener("message", function(event){
				var message = event.data;
				switch(message.command){
					case "click":
					case "none":
					case "build":
					case "upgrade":
					case "recycle":
						me.colony[message.command](message.data);
						me.run();
						break;
				}
			},false);
			me.loaded(true);
		}
		
		this.run = function(){
			if(me.running()){
				me.next();
			}
		};
		
		this.next = function(){
			setTimeout(function(){
				var colony = ko.toJS(me.colony);
				removeAllFunctions(colony);
				me.sendCommand("execute", colony);
			}, (1000 / me.rate()));
		};
		
		this.go = function(){
			me.running(true);
			this.run();
		}
		
		this.stop = function(){
			me.running(false);
		}
		
		var removeAllFunctions = function(obj){
			for(var key in obj){
				if(key === 'ai' || key === 'colony'){
					delete obj[key];
				} else if(typeof obj[key] === 'function'){
					delete obj[key];
				} else if (obj[key] !== null && typeof obj[key] === 'object'){
					removeAllFunctions(obj[key]);
				}
			}
		}
		
		/**
		 * Whether or not to show the help message
		 */
		this.show_help = ko.observable(false);
		/**
		 * Shows the help message
		 */
		this.help = function(){
			me.show_help(true);
		}
		
		this.sendCommand = function(command, data){
			if(me.port != null){
				me.port.postMessage({
					command: command,
					data: data
				});
			}
		};
	};
	
	/**
	 * Represents the colony state.
	 * @class
	 */
	mmm.Colony = function(){
		/** @region Intelligence **/{		
		var me = this;
		/** 
		 * AI function
		 * @member {mmm.AI} 
		 */
		this.ai = new mmm.AI(me);
		
		//** seconds timer */
		var timer = null;
		this.mode = ko.observable("ai");
		this.mode.subscribe(function(){
			if(me.mode()=='ai'){
				clearInterval(timer);
			} else {
				me.ai.stop();
				timer = setInterval(function(){
					if(me.autoclick()){
						me.click(1);
					} else {
						me.none(1);
					}
				},1000);
			}
		});
		this.autoclick = ko.observable(false);
		this.devmode = ko.observable(false);
		}/** @endregion Intelligence **/

		
		/** @region Status **/{
		/** 
		 * Total turns (seconds) elapsed
		 * @member {number}
		 */
		this.turns = ko.observable(0);
		/** 
		 * Current number of available common ore
		 * @member {number}
		 */
		this.store_common = ko.observable(250000);
		/** 
		 * Current number of available rare ore
		 * @member {number}
		 */
		this.store_rare = ko.observable(100);
		var store_rare_subscription = this.store_rare.subscribe(function(newValue){
			if(newValue >= 1){
				me.unlocked_designations.push('??');
				store_rare_subscription.dispose();
			}
		});
		/** 
		 * Current array of designation codes that have been unlocked
		 * @member {String[]}
		 */
		this.unlocked_designations = ko.observableArray(["CC","??"]);
		/** 
		 * Whether or not rare ore has been unlocked
		 * @member {boolean}
		 */
		this.unlocked_rare = ko.computed(function(){
			return me.unlocked_designations.indexOf('RR') > -1;
		});
		}/** @endregion Status **/

		
		/** @region Sequence **/{
		/**
		 * @todo Make sequence private
		 */
		this.sequence_last_command = ko.observable(null);
		this.sequence_last_count = ko.observable(0);
		this.sequence_last = ko.observable("");
		this.sequence = ko.computed(function(){
			return me.sequence_last() + (me.sequence_last_command() ? me.sequence_last_command() + me.sequence_last_count(): "");
		});
		this.loadSequence = function(sequence){
			var re = /(\D\d*)/g;
			var matches = sequence.match(re);
			for(var i = 0; i < matches.length; ++i){
				var command = matches[i][0];
				var num = '1';
				if(matches[i].length > 1){
					num = matches[i].substring(1);
				}
				switch(command){
					case "c": //click
						me.click(parseInt(num));
						break;
					case "n": //none
						me.none(parseInt(num));
						break;
					case "u": //upgrade
						me.upgrade(num);
						break;
					case "b": //build
						me.build(num);
						break;
					case "r": //recycle
						me.recycle(num);
						break;
					case "h": //human click
						me.click_human(parseInt(num));
						break;
				}
			}
		};
		this.addSequence = function(command, number, iscount){
			if(iscount && command == this.sequence_last_command()){
				this.sequence_last_count(this.sequence_last_count() + number);
			} else {
				this.sequence_last(this.sequence());
				this.sequence_last_command(command);
				this.sequence_last_count(number);
			}
		}
		this.clearSequence = function(){
			this.sequence_last_command(null);
			this.sequence_last_count(0);
			this.sequence_last("");
		};
		}/** @endregion Sequence **/

		
		/** @region Units **/{
		/** 
		 * Object dictionary of all units
		 * @type {Object.<string, mmm.Unit>}
		 */
		this.units = mmm.Unit.setupUnits(this);
		/** 
		 * Object dictionary of all unit upgrades
		 * @type {Object.<string, mmm.UnitUpgrade>}
		 */
		this.unit_upgrades = mmm.UnitUpgrade.setupUnitUpgrades(this);
		/** 
		 * Object dictionary of all research upgrades
		 * @type {Object.<string, mmm.ResearchUpgrade>}
		 */
		this.upgrades = mmm.ResearchUpgrade.setupResearchUpgrades(this);
		/** 
		 * Object dictionary of all buildings
		 * @type {Object.<string, mmm.Building>}
		 */
		this.buildings = mmm.Building.setupBuildings(this);
		}/** @endregion Units **/
		
		
		/** @region Rates **/{
		/**
		 * Current rate of recycling
		 */
		this.rate_recycle = ko.computed({read:function(){
			var count = 1;
			for(var key in me.upgrades){
				if(me.upgrades[key].designation == 'RRECYCLE'){
					count = count + me.upgrades[key].count();
				}
			}
			return count * .2;
		},deferEvaluation:true});
		
		/** 
		 * Current rate of common ore production excluding clicks and exchanges
		 * @member {number}
		 */
		this.rate_common_simple = ko.computed({read:function(){
			var rate = 0;
			for(var key in me.units){
				if(me.units[key].designation != 'USER' && me.units[key].designation != 'EXCHANGE'){
					rate = rate + (me.units[key].rate_common() * me.units[key].count());
				}
			}
			for(var key in me.buildings){
				if(me.buildings[key].designation != 'USER' && me.buildings[key].designation != 'EXCHANGE'){
					rate = rate + (me.buildings[key].rate_common() * me.buildings[key].count());
				}
			}
			return rate;
		},deferEvaluation:true});		
		
		/** 
		 * Current rate of common ore production of clicks
		 * @member {number}
		 */
		this.rate_common_user = ko.computed({read:function(){
			var rate = 0;
			for(var key in me.units){
				if(me.units[key].designation == 'USER'){
					rate = rate + (me.units[key].rate_common() * CLICKS_PER_SECOND);
				}
			}
			for(var key in me.buildings){
				if(me.buildings[key].designation == 'USER'){
					rate = rate + (me.buildings[key].rate_common() * CLICKS_PER_SECOND);
				}
			}
			return rate;
		},deferEvaluation:true});

		/** 
		 * Current rate of common ore production of exchanges
		 * @member {number}
		 */
		this.rate_common_exchange = ko.computed({read:function(){
			var rate = 0;
			for(var key in me.units){
				if(me.units[key].designation == 'EXCHANGE'){
					rate = rate + (me.units[key].rate_common());
				}
			}
			for(var key in me.buildings){
				if(me.buildings[key].designation == 'EXCHANGE'){
					rate = rate + (me.buildings[key].rate_common());
				}
			}
			return rate;
		},deferEvaluation:true});

		/** 
		 * Current rate of common ore production excluding clicks and including exchanges
		 * @member {number}
		 */
		this.rate_common = ko.computed({read:function(){
			return me.rate_common_simple() + me.rate_common_exchange();
		},deferEvaluation:true});
		
		/** 
		 * Current rate of common ore production including clicks and exchanges
		 * @member {number}
		 */
		this.rate_common_click = ko.computed({read:function(){
			return me.rate_common_simple() + me.rate_common_exchange() + me.rate_common_user();
		},deferEvaluation:true});
		
		/** 
		 * Current rate of rare ore production excluding clicks and exchanges
		 * @member {number}
		 */
		this.rate_rare_simple = ko.computed({read:function(){
			var rate = 0;
			for(var key in me.units){
				if(me.units[key].designation != 'USER' && me.units[key].designation != 'EXCHANGE'){
					rate = rate + (me.units[key].rate_rare() * me.units[key].count());
				}
			}
			for(var key in me.buildings){
				if(me.buildings[key].designation != 'USER' && me.buildings[key].designation != 'EXCHANGE'){
					rate = rate + (me.buildings[key].rate_rare() * me.buildings[key].count());
				}
			}
			return rate;
		},deferEvaluation:true});		
		
		/** 
		 * Current rate of rare ore production of clicks
		 * @member {number}
		 */
		this.rate_rare_user = ko.computed({read:function(){
			var rate = 0;
			for(var key in me.units){
				if(me.units[key].designation == 'USER'){
					rate = rate + (me.units[key].rate_rare() * CLICKS_PER_SECOND);
				}
			}
			for(var key in me.buildings){
				if(me.buildings[key].designation == 'USER'){
					rate = rate + (me.buildings[key].rate_rare() * CLICKS_PER_SECOND);
				}
			}
			return rate;
		},deferEvaluation:true});

		/** 
		 * Current rate of rare ore production of exchanges
		 * @member {number}
		 */
		this.rate_rare_exchange = ko.computed({read:function(){
			var rate = 0;
			for(var key in me.units){
				if(me.units[key].designation == 'EXCHANGE'){
					rate = rate + (me.units[key].rate_rare());
				}
			}
			for(var key in me.buildings){
				if(me.buildings[key].designation == 'EXCHANGE'){
					rate = rate + (me.buildings[key].rate_rare());
				}
			}
			return rate;
		},deferEvaluation:true});

		/** 
		 * Current rate of rare ore production excluding clicks and including exchanges
		 * @member {number}
		 */
		this.rate_rare = ko.computed({read:function(){
			return me.rate_rare_simple() + me.rate_rare_exchange();
		},deferEvaluation:true});
		
		/** 
		 * Current rate of rare ore production including clicks and exchanges
		 * @member {number}
		 */
		this.rate_rare_click = ko.computed({read:function(){
			return me.rate_rare_simple() + me.rate_rare_exchange() + me.rate_rare_user();
		},deferEvaluation:true});
		}/** @endregion Rates **/

		
		/** @region Scores **/{
		/**
		 * Current score total
		 * @member {number}
		 */
		this.score = ko.computed({read:function(){
			return me.score_common() + me.score_rare() + me.score_units() + me.score_unit_upgrades() + me.score_upgrades() + me.score_buildings();
		},deferEvaluation:true});
		
		/**
		 * Current score total over number of seconds
		 * @member {number}
		 */
		this.score_seconds = ko.computed({read:function(){
			return me.score() / me.turns();
		},deferEvaluation:true});
		
		this.score_content = ko.computed({read:function(){
			var content = "<h2>Scores</h2>";
			if(me.unlocked_rare()){
				content = content + "<p><label>Common: <span>"+me.score_common()+"</span></label></p>";
				content = content + "<p><label>Rare: <span>"+me.score_rare()+"</span></label></p>";
			} else {
				content = content + "<p><label>Ore: <span>"+(me.score_common() + me.score_rare())+"</span></label></p>";
			}
			content = content + "<p><label>Units: <span>"+me.score_units()+"</span></label></p>";
			content = content + "<p><label>Unit Upgrades: <span>"+me.score_unit_upgrades()+"</span></label></p>";
			content = content + "<p><label>Research: <span>"+me.score_upgrades()+"</span></label></p>";
			content = content + "<p><label>Buildings: <span>"+me.score_buildings()+"</span></label></p>";
			return content;
		},deferEvaluation:true});
		 
		/** 
		 * Current score of common ore in storage
		 * @member {number}
		 */
		this.score_common = ko.computed({read:function(){
			return Math.max((Math.log(me.store_common()) / Math.log(2)) - 32, 0);
		},deferEvaluation:true});
		
		/** 
		 * Current score of rare ore in storage
		 * @member {number}
		 */
		this.score_rare = ko.computed({read:function(){
			return Math.max((Math.log(me.store_common()) / Math.log(2)) - 22, 0);
		},deferEvaluation:true});
		
		/** 
		 * Current count of items of a type
		 * @member {number}
		 * @param {string} type - Type of items to count
		 */
		this.countItems = function(type){
			var count = 0;
			for(var key in me[type]){
				count = count + me[type][key].count();
			}
			return count;
		}
		/** 
		 * Current score of units
		 * @member {number}
		 */
		this.score_units = ko.computed({read:function(){
			return me.countItems("units") - 1;
		},deferEvaluation:true});

		/** 
		 * Current score of unit upgrades
		 * @member {number}
		 */
		this.score_unit_upgrades = ko.computed({read:function(){
			return me.countItems("unit_upgrades");
		},deferEvaluation:true});

		/** 
		 * Current score of research upgrades
		 * @member {number}
		 */
		this.score_upgrades = ko.computed({read:function(){
			return me.countItems("upgrades");
		},deferEvaluation:true});

		/** 
		 * Current score of buildings
		 * @member {number}
		 */
		this.score_buildings = ko.computed({read:function(){
			return me.countItems("buildings");
		},deferEvaluation:true});
		}/** @endregion Scores **/
		
		
		/** @region Acheivements **/{
		this.show_win = ko.observable(false);
		
		this.hasAllItems = function(type){
			for(var key in me[type]){
				if(me[type][key].count() == 0){
					return false;
				}
			}
			return true;
		};
		}/** @endregion Acheivements **/
		
		
		/** @region Commands **/{
		/** 
		 * Spends a second clicking
		 * @param {number} count - Number of seconds to spend clicking
		 */
		this.click = function(count){
			if(typeof count == 'undefined') {count = 0;}
			this.addSequence('c', count, true);
			this.use(count, true);
		};
		
		/** 
		 * Spends a second doing nothing
		 * @param {number} count - Number of seconds to spend doing nothing
		 */
		this.none = function(count){
			if(typeof count == 'undefined') {count = 0;}
			this.addSequence('n', count, true);
			this.use(count, false);
		}
		
		/** 
		 * Performs a human click
		 * @param {number} count - Number of clicks
		 */
		this.click_human = function(count){
			if(typeof count === 'undefined') {
				count = 1;
			}
			this.addSequence('h', count, true);
			this.units['1'].use(count);
		}
		
		/** 
		 * Spends a second building a unit or building
		 * @param {string} id - ID of unit or building to build
		 */
		this.build = function(id){
			this.addSequence('b', id, false);
			if(this.units.hasOwnProperty(id)){
				this.units[id].purchase(1);
			} else if(this.buildings.hasOwnProperty(id)){
				this.buildings[id].purchase(1);
			} else {
				//Not valid
			}
			this.use(1, false);
		};
		
		/** 
		 * Spends a second buying unit or research upgrades
		 * @param {string} id - ID of unit upgrade or research to buy
		 */
		this.upgrade = function(id){
			this.addSequence('u', id, false);
			if(this.upgrades.hasOwnProperty(id)){
				this.upgrades[id].purchase(1);
			} else if(this.unit_upgrades.hasOwnProperty(id)){
				this.unit_upgrades[id].purchase(1);
			} else {
				//Not valid
			}
			this.use(1, false);
		};
		
		/** 
		 * Spends a second recycling a unit or building
		 * @param {string} id - ID of unit or building to recycle
		 */
		this.recycle = function(id){
			this.addSequence('r', id, false);
			if(this.units.hasOwnProperty(id)){
				this.units[id].recycle(1);
			} else if(this.buildings.hasOwnProperty(id)){
				this.buildings[id].recycle(1);
			} else {
				//Not valid
			}
			this.use(1, false);
		};
		
		/** 
		 * Uses all items over the elapsed seconds
		 * @param {number} count - Number of seconds to calculate
		 * @param {boolean} click - Whether or not to include clicking for this second
		 */
		this.use = function(count, click){
			if(click){
				this.store_common(Math.max(this.store_common() + this.rate_common_click() * count,0));
				this.store_rare(Math.max(this.store_rare() + this.rate_rare_click() * count,0));
			} else {
				this.store_common(Math.max(this.store_common() + this.rate_common() * count,0));
				this.store_rare(Math.max(this.store_rare() + this.rate_rare() * count,0));
			}
			this.turns(this.turns() + count);
		};
		}/** @endregion Commands **/
	};
	
	/**
	 * Base class representing an item for purchase
	 * @class
	 * @param {mmm.Colony} colony - The parent colony
	 * @param {string} id - ID of the item
	 * @param {string} name - Name of the item
	 * @param {string} description - Description of the item
	 * @param {string} designation - Designation which when unlocked, unlocks the item
	 * @param {number|function} price_common - Number or function returning the price in common ore
	 * @param {number|function} price_rare - Number or function returning the price in rare ore
	 * @param {number|function} rate_common - Number or function returning the production rate in common ore
	 * @param {number|function} rate_rare - Number or function returning the production rate in rare ore
	 * @param {boolean} unique - Whether or not there can only be at most one of the item
	 */
	mmm.Item = function(colony, id, name, description, designation, price_common, price_rare, rate_common, rate_rare, unique, is_upgrade){
		var me = this;
		/** 
		 * Parent colony
		 * @member {mmm.Colony}
		 */
		this.colony = colony;
		/** 
		 * ID of the item
		 * @member {string}
		 */
		this.id = id;
		/** 
		 * Name of the item
		 * @member {string}
		 */
		this.name = name;
		/** 
		 * Description of the item
		 * @member {string}
		 */
		this.description = description;
		/** 
		 * Designation which when unlocked, unlocks the item
		 * @member {string}
		 */
		this.designation = designation;
		
		/** 
		 * Production rate of common ore
		 * @member {number} rate_common
		 */
		 if(typeof rate_common == 'function'){
			this.rate_common = ko.computed({read:rate_common,deferEvaluation:true,owner:me});
		}else {
			this.rate_common = ko.observable(rate_common);
		}
		/** 
		 * Production rate of rare ore
		 * @member {number} rate_common
		 */
		if(typeof rate_rare == 'function'){
			this.rate_rare = ko.computed({read:rate_rare,deferEvaluation:true,owner:me});
		}else {
			this.rate_rare = ko.observable(rate_rare);
		}
		/** 
		 * Price in common ore
		 * @member {number} rate_common
		 */
		if(typeof price_common == 'function'){
			this.price_common = ko.computed({read:price_common,deferEvaluation:true,owner:me});
		}else {
			this.price_common = ko.observable(price_common);
		}
		/** 
		 * Price in rare ore
		 * @member {number} rate_common
		 */
		if(typeof price_rare == 'function'){
			this.price_rare = ko.computed({read:price_rare,deferEvaluation:true,owner:me});
		}else {
			this.price_rare = ko.observable(price_rare);
		}
		/** 
		 * Whether or not there can only be one of this item
		 * @member {boolean}
		 */
		this.unique = unique;
		
		/** 
		 * Whether or not the item is an upgrade
		 * @member {boolean}
		 */
		this.is_upgrade = is_upgrade;
		
		/** 
		 * Current number of this unit already purchased
		 * @member {number}
		 */
		this.count = ko.observable(0);
		
		/** 
		 * Whether or not an item has already been purchased
		 * @member {boolean}
		 */
		this.purchased = ko.computed({read:function(){
			return me.count() > 0;
		},deferEvaluation:true});
		
		/** 
		 * Whether or not an item can be purchased because it unlocked, affordable and available
		 * @member {boolean}
		 */
		this.purchasable = ko.computed({read:function(){
			return me.unlocked() && me.affordable() && me.available();
		},deferEvaluation:true});
		
		/**
		 * Actions to take when purchase was successful
		 * @abstract
		 */
		this.purchaseComplete = function(){
			throw new Error('purchaseComplete not implemented');
		};
		
		/** 
		 * Purchases the item, paying the purchase price
		 */
		this.purchase = function(count){
			while(count > 0){
				if(this.purchasable()){
					this.colony.store_common(this.colony.store_common() - Math.ceil(this.price_common()));
					this.colony.store_rare(this.colony.store_rare() - Math.ceil(this.price_rare()));
					this.count(this.count() + 1);
					this.purchaseComplete();
				} else {
					//Something isn't right
					break;
				}
				count = count - 1;
			}
		}
		
		/** 
		 * Whether or not an item can be purchased because it's designation has been unlocked
		 * @member {boolean}
		 */
		this.unlocked = ko.computed(function(){
			return colony.unlocked_designations.indexOf(me.designation) > -1;
		});
		
		/** 
		 * Whether or not an item can be purchased because enough ore is in storage to afford the price
		 * @member {boolean}
		 */
		this.affordable = ko.computed({read:function(){
			return me.colony.store_common() >= me.price_common() && me.colony.store_rare() >= me.price_rare();
		},deferEvaluation: true});
		
		/** 
		 * Whether or not an item can be purchased because it is unique and already purchased
		 * @member {boolean}
		 */
		this.available = ko.computed({read:function(){
			return !(me.unique && me.count() > 0);
		},deferEvaluation:true});
		
		/** 
		 * Whether or not the item can be recycled (sold)
		 * @member {boolean}
		 */
		this.recyclable = ko.computed({read:function(){
			return me.count() > 0 && !me.unique;
		},deferEvaluation:true});
		/** 
		 * Recycles the item, refunding some of the purchase price
		 */
		this.recycle = function(count){
			while(count > 0){
				if(this.recyclable()){
					this.colony.store_common(this.colony.store_common() + Math.ceil(this.price_common() * this.colony.rate_recycle()));
					this.colony.store_rare(this.colony.store_rare() + Math.ceil(this.price_rare() * this.colony.rate_recycle()));
					this.count(this.count() - 1);
					if(colony.unlocked_designations.indexOf('RECYCLE') == -1){
						colony.unlocked_designations.push('RECYCLE')
					}
					if(colony.unlocked_designations.indexOf('RR') > -1){
						colony.unlocked_designations.push('RRECYCLE')
					}
					this.recycleComplete();
				} else {
					//Something isn't right
					break;
				}
				count = count - 1;
			}
		};
		
		/**
		 * Actions to take when recycle was successful
		 * @abstract
		 */
		this.recycleComplete = function(){
			throw new Error('recycleComplete not implemented');
		};
		
		/**
		 * 
		 */
		this.show_details = ko.observable(true);
		
		this.show_details_active = ko.computed({
			read: function(){return me.show_details() ? 0 : false;},
			write: function(newValue){me.show_details(parseInt(newValue, 0) == 0);}
		});
		
		/** 
		 * Seconds each item costs when the user is clicking
		 * @member {number}
		 */
		this.time_each = ko.computed({read:function(){
			if(me.colony.rate_common() <= 0 && me.price_common() > 0){
				return Infinity;
			}
			if(me.colony.rate_rare() <= 0 && me.price_rare() > 0){
				return Infinity;
			}

			var time_common = 0;
			var time_rare = 0;
			if(me.colony.rate_common() > 0){
				time_common = me.price_common() / me.colony.rate_common();
			}
			if(me.colony.rate_rare() > 0){
				time_rare = me.price_rare() / me.colony.rate_rare();
			}
			
			return Math.max(time_common, time_rare);
		},deferEvaluation: true} );
		
		/** 
		 * Seconds each item costs when the user is clicking
		 * @member {number}
		 */
		this.time_each_click = ko.computed({read:function(){
			if(me.colony.rate_common_click() <= 0 && me.price_common() > 0){
				return Infinity;
			}
			if(me.colony.rate_rare_click() <= 0 && me.price_rare() > 0){
				return Infinity;
			}

			var time_common = 0;
			var time_rare = 0;
			if(me.colony.rate_common_click() > 0){
				time_common = me.price_common() / me.colony.rate_common_click();
			}
			if(me.colony.rate_rare_click() > 0){
				time_rare = me.price_rare() / me.colony.rate_rare_click();
			}
			
			return Math.max(time_common, time_rare);
		},deferEvaluation: true} );
		
		/** 
		 * Seconds until item is affordable when the user is not clicking
		 * @member {number}
		 */
		this.time_next = ko.computed({read:function(){
			if(me.colony.rate_common() <= 0 && me.price_common() > me.colony.store_common()){
				return Infinity;
			}
			if(me.colony.rate_rare() <= 0 && me.price_rare() > me.colony.store_rare()){
				return Infinity;
			}

			var time_common = 0;
			var time_rare = 0;
			if(me.colony.rate_common() > 0){
				time_common = (me.price_common() - me.colony.store_common()) / me.colony.rate_common();
			}
			if(me.colony.rate_rare() > 0){
				time_rare = (me.price_rare() - me.colony.store_rare()) / me.colony.rate_rare();
			}
			
			return Math.max(Math.max(time_common, time_rare),0);
		},deferEvaluation: true});
		
		/** 
		 * Seconds until item is affordable when the user is clicking
		 * @member {number}
		 */
		this.time_next_click = ko.computed({read:function(){
			if(me.colony.rate_common_click() <= 0 && me.price_common() > me.colony.store_common()){
				return Infinity;
			}
			if(me.colony.rate_rare_click() <= 0 && me.price_rare() > me.colony.store_rare()){
				return Infinity;
			}

			var time_common = 0;
			var time_rare = 0;
			if(me.colony.rate_common_click() > 0){
				time_common = (me.price_common() - me.colony.store_common()) / me.colony.rate_common_click();
			}
			if(me.colony.rate_rare_click() > 0){
				time_rare = (me.price_rare() - me.colony.store_rare()) / me.colony.rate_rare_click();
			}
			
			return Math.max(Math.max(time_common, time_rare),0);
		},deferEvaluation: true});

		/** 
		 * Rate of item common ore production over total colony common ore production
		 * @member {number}
		 */
		this.rate_common_gain = ko.computed({read:function(){
			if(me.colony.rate_common() > 0) {
				return me.rate_common() / colony.rate_common();
			}  else if (me.rate_common() > 0){
				return Infinity;
			}
			return 0;
		},deferEvaluation: true});
		
		/** 
		 * Rate of item rare ore production over total colony rare ore production
		 * @member {number}
		 */
		this.rate_rare_gain = ko.computed({read:function(){
			if(me.colony.rate_rare() > 0){
				return me.rate_rare() / colony.rate_rare();
			} else if (me.rate_rare() > 0){
				return Infinity;
			}
			return 0;
		},deferEvaluation: true});
	};

	/**
	 * Represents a unit for purchase
	 * @class
	 * @extends mmm.Item
	 * @param {mmm.Colony} colony - The parent colony
	 * @param {string} id - ID of the unit
	 * @param {string} name - Name of the unit
	 * @param {string} description - Description of the unit
	 * @param {string} designation - Designation which when unlocked, unlocks the unit
	 * @param {number|function} price_common - Number or function returning the price in common ore
	 * @param {number|function} price_rare - Number or function returning the price in rare ore
	 * @param {number|function} rate_common - Number or function returning the production rate in common ore
	 * @param {number|function} rate_rare - Number or function returning the production rate in rare ore
	 * @param {string} type_mining - Mining type of the unit
	 * @param {string} type_mining - Hauling type of the unit
	 * @param {string} type_mining - Model of the unit
	 */
	mmm.Unit = function(colony, id, name, description, designation, price_common, price_rare, rate_common, rate_rare, type_mining, type_hauling, model){
		var me = this;
		
		//Base Constructor
		var rate_common_func = function(){ return me.rate_common_current(); };
		var rate_rare_func = function(){ return me.rate_rare_current(); };
		mmm.Item.call(this,colony, id, name, description, designation, price_common, price_rare, rate_common_func,rate_rare_func, false, false);
		
		//Subclass Members
		/** 
		 * Base rate of common ore production
		 * @member {number}
		 */
		this.rate_common_base = ko.observable(rate_common);
		/** 
		 * Base rate of rate ore production
		 * @member {number}
		 */
		this.rate_rare_base = ko.observable(rate_rare);
		/** 
		 * Mining type, mining group for upgrades
		 * @member {number}
		 */		
		this.type_mining = type_mining;
		/** 
		 * Hauling type, hauling group for upgrades
		 * @member {number}
		 */
		this.type_hauling = type_hauling;
		/** 
		 * Model, combination of designation, mining and hauling types
		 * @member {number}
		 */
		this.model = model;

		/** 
		 * Current rate multiplier, 1 = 100%
		 * @member {number}
		 */		
		this.multiplier = ko.observable(1);
		
		/** 
		 * Current rate of common ore production
		 * @member {number}
		 */
		this.rate_common_current = ko.computed(function(){
			return me.rate_common_base() * me.multiplier();
		});
		
		/** 
		 * Current rate of rare ore production
		 * @member {number}
		 */
		this.rate_rare_current = ko.computed(function(){
			return me.rate_rare_base() * me.multiplier();
		});

		/**
		 * Increases unit price when purchase was successful
		 */		
		this.purchaseComplete = function(){
			this.price_common(this.price_common() * 1.15);
			this.price_rare(this.price_rare() * 1.15);
		}

		/**
		 * Does nothing when recycle was successful
		 */		
		this.recycleComplete = function(){
			
		};
		
		/** 
		 * Uses unit over the elapsed seconds
		 * @param {number} turns - Number of seconds to calculate
		 */
		this.use = function(turns){
			colony.store_common(colony.store_common() + this.rate_common() * this.count() * turns);
			colony.store_rare(colony.store_rare() + this.rate_rare() * this.count() * turns);
		};
	};
	mmm.Unit.prototype = Object.create(mmm.Item.prototype);

	/**
	 * Represents a building for purchase
	 * @class
	 * @extends mmm.Item
	 * @param {mmm.Colony} colony - The parent colony
	 * @param {string} id - ID of the building
	 * @param {string} name - Name of the building
	 * @param {string} description - Description of the building
	 * @param {string} designation - Designation which when unlocked, unlocks the building
	 * @param {number|function} price_common - Number or function returning the price in common ore
	 * @param {number|function} price_rare - Number or function returning the price in rare ore
	 * @param {number|function} rate_common - Number or function returning the production rate in common ore
	 * @param {number|function} rate_rare - Number or function returning the production rate in rare ore
	 * @param {boolean} unique - Whether or not there can only be at most one of the building
	 */
	mmm.Building = function(colony, id, name, description, designation, price_common, price_rare, rate_common, rate_rare, unique){
		var me = this;
		
		//Base Constructor
		mmm.Item.call(this,colony, id, name, description, designation, price_common, price_rare, rate_common,rate_rare, unique, false);
		
		//Constructor Properties	
		/**
		 * Increases unit price when purchase was successful
		 */		
		this.purchaseComplete = function(){
			this.price_common(this.price_common() * 1.15);
			this.price_rare(this.price_rare() * 1.15);
		};
		
		/**
		 * Does nothing when recycle was successful
		 */		
		this.recycleComplete = function(){
			
		};
		
		/** 
		 * Uses building over the elapsed seconds
		 * @param {number} turns - Number of seconds to calculate
		 */
		this.use = function(turns){
			while(turns > 0){
				colony.store_common(colony.store_common() + this.rate_common() * this.count());
				colony.store_rare(colony.store_rare() + this.rate_rare() * this.count());
				turns = turns - 1;
			}
		};
	};
	mmm.Building.prototype = Object.create(mmm.Item.prototype);

	/**
	 * Represents a research upgrade for purchase
	 * @class
	 * @extends mmm.Item
	 * @param {mmm.Colony} colony - The parent colony
	 * @param {string} id - ID of the research
	 * @param {string} name - Name of the research
	 * @param {string} description - Description of the research
	 * @param {string} designation - Designation which when unlocked, unlocks the research
	 * @param {number|function} price_common - Number or function returning the price in common ore
	 * @param {number|function} price_rare - Number or function returning the price in rare ore
	 */
	mmm.ResearchUpgrade = function(colony, id, name, description, designation, price_common, price_rare){
		var me = this;
		
		//Base Constructor
		var rate_common_func = function(){ return 0; };
		var rate_rare_func = function(){ return 0; };
		mmm.Item.call(this,colony, id, name, description, designation, price_common, price_rare, rate_common_func,rate_rare_func, true, true);
	};
	mmm.ResearchUpgrade.prototype = Object.create(mmm.Item.prototype);
	
	/**
	 * Represents a unit upgrade for purchase
	 * @class
	 * @extends mmm.Item
	 * @param {mmm.Colony} colony - The parent colony
	 * @param {string} id - ID of the upgrade
	 * @param {string} name - Name of the upgrade
	 * @param {string} description - Description of the upgrade
	 * @param {string} designation - Designation which when unlocked, unlocks the upgrade
	 * @param {number|function} price_common - Number or function returning the price in common ore
	 * @param {number|function} price_rare - Number or function returning the price in rare ore
	 * @param {number} multiplier - Multiplier effect on production
	 * @param {string} type - Type of units that are upgraded
	 */	
	mmm.UnitUpgrade = function(colony, id, name, description, designation, price_common, price_rare, multiplier, type){
		var me = this;
		
		/**
		 * Array of units that the upgrade affects
		 * @member {mmm.Unit[]} 
		*/
		this.units = [];
		for(var key in colony.units){
			if(colony.units[key].type_mining === type || colony.units[key].type_hauling === type){
				this.units.push(key);
			}
		}
		
		//Base Constructor
		var rate_common_func = function(){
			var rate = 0;
			for(var idx in me.units){
				var key = me.units[idx];
				var unit = colony.units[key];
				rate = rate + unit.rate_common_base() * unit.count() * multiplier;
			}
			return rate;
		}
		var rate_rare_func = function(){
			var rate = 0;
			for(var idx in me.units){
				var key = me.units[idx];
				var unit = colony.units[key];
				rate = rate + unit.rate_rare_base() * unit.count() * multiplier;
			}
			return rate;
		}
		if(typeof description === 'undefined' || !description){
			description = this.description = "Upgrades all " + type + " units, increasing their effectiveness by " + (multiplier * 100) + "%";
		}
		mmm.Item.call(this,colony, id, name, description, designation, price_common, price_rare, rate_common_func,rate_rare_func, true, true);
		
		//Subclass members
		/**
		 * Additional multiplier effect on production, 1 = 100%
		 * @member {number} 
		 */
		this.multiplier = multiplier;
		/**
		 * Type of units that the upgrade affects ("C" or "R" for common and rare)
		 * @member {string} 
		 */
		this.type = type;
		
		/**
		 * Adds multiplier to units when purchase was successful
		 */
		this.purchaseComplete = function(){
			for(var idx in this.units){
				var key = this.units[idx];
				var unit = colony.units[key];
				unit.multiplier(unit.multiplier() + this.multiplier);
			}
		};
	};
	mmm.UnitUpgrade.prototype = Object.create(mmm.Item.prototype);

	/**
	 * Represents an ore upgrade for purchase
	 * @class
	 * @extends mmm.Item
	 * @param {mmm.Colony} colony - The parent colony
	 * @param {string} id - ID of the upgrade
	 * @param {string} name - Name of the upgrade
	 * @param {string} description - Description of the upgrade
	 * @param {string} designation - Designation which when unlocked, unlocks the upgrade
	 * @param {number|function} price_common - Number or function returning the price in common ore
	 * @param {number|function} price_rare - Number or function returning the price in rare ore
	 * @param {number} multiplier - Multiplier effect on production
	 * @param {string} type - Type of units that are upgraded
	 */	
	mmm.OreUpgrade = function(colony, id, name, description, designation, price_common, price_rare, multiplier, type){
		var me = this;
		
		/**
		 * Array of units that the upgrade affects
		 * @member {mmm.Unit[]} 
		*/
		this.units = [];
		for(var key in colony.units){
			if(colony.units[key].designation.indexOf(type) > -1){
				this.units.push(key);
			}
		}
		
		//Base Constructor
		var rate_common_func = function(){
			var rate = 0;
			for(var idx in this.units){
				var key = this.units[idx];
				var unit = colony.units[key];
				if(type == 'C'){
					rate = rate + unit.rate_common_base() * unit.count() * multiplier;
				}
			}
			return rate;
		}
		var rate_rare_func = function(){
			var rate = 0;
			for(var idx in this.units){
				var key = this.units[idx];
				var unit = colony.units[key];
				rate = rate + unit.rate_rare_base() * unit.count() * multiplier;
				if(type == 'R'){
					rate = rate + unit.rate_common_base() * unit.count() * multiplier;
				}
			}
			return rate;
		}
		if(typeof description === 'undefined' || !description){
			description = this.description = "Upgrades all " + type + " units, increasing their effectiveness by " + (multiplier * 100) + "%";
		}
		mmm.Item.call(this,colony, id, name, description, designation, price_common, price_rare, rate_common_func,rate_rare_func, true, true);

		//Subclass members
		/**
		 * Additional multiplier effect on production, 1 = 100%
		 * @member {number} 
		 */
		this.multiplier = multiplier;
		/**
		 * Type of units that the upgrade affects ("C" or "R" for common and rare)
		 * @member {string} 
		 */
		this.type = type;
		
		/**
		 * Adds multiplier to units when purchase was successful
		 */
		this.purchaseComplete = function(){
			for(var idx in this.units){
				var key = this.units[idx];
				var unit = colony.units[key];
				if(type == 'C') {
					unit.rate_common_base(unit.rate_common_base() * (1 + multiplier));
				} else if (type == 'R'){
					unit.rate_rare_base(unit.rate_rare_base() * (1 + multiplier));
				}
				
				unit.multiplier(unit.multiplier() + this.multiplier);
			}
		};
	};
	mmm.OreUpgrade.prototype = Object.create(mmm.Item.prototype);

	
	mmm.Data = function(){
		this.colony = ko.observable(new mmm.Colony());
	}
}(window.mmm = window.mmm || {}));