Greasy Fork is available in English.

Ikariam Developer Tools V0.5.0+

Base scripting tools and data tracking utilities to simplify

Verzia zo dňa 03.02.2023. Pozri najnovšiu verziu.

Tento skript by nemal byť nainštalovaný priamo. Je to knižnica pre ďalšie skripty, ktorú by mali používať cez meta príkaz // @require https://update.greasyfork.org/scripts/459367/1145220/Ikariam%20Developer%20Tools%20V050%2B.js

// ==UserScript==
// @name            Ikariam Developer Tools V0.5.0+
// @namespace       AubergineAnodyne
// @description     Base scripting tools and data tracking utilities to simplify
//                  writing Ikariam Greasemonkey scripts.
// @author          AubergineAnodyne 
//                    (very loosely based on Ikariam Developer Tools by PhasmaExMachina)
//
// @grant          GM_getValue
// @grant          GM_setValue
// @grant          GM_addStyle
// @grant          GM_xmlhttpRequest
//
// @version         0.30
//
// @history         0.30 Added turkish translations and Fixed blackmarket and sea chart archive buildins.
// @history         0.29 Fixed pillage of crystal not showing up.
// @history         0.28 Added COLONIZE constant.
// @history         0.27 Added helper methods for scripts to load/read Ikariam pages in the background.
// @history         0.27 Added support for spy data (IkaTools.EmpireData.EspionageData).
// @history         0.27 Added Romanian translation (by Peta).
// @history         0.26 Added Hungarian translation (by Toroco).
// @history         0.26 Updated for Ikariam changes in 5.3.2.
// @history         0.25 Fix bug in parsing training batches for military.
// @history         0.25 Fix bug in parsing returning colonization mission.  Also correct resource calculation for colonization mission.
// @history         0.25 Fix bug in parsing missions when pirate raid is in progress.
// @history         0.24 Added support for Pirate Fortress (v0.5.3 new building).
// @history         0.23 Added resetData function.
// @history         0.23 Fixed a bug that future research levels >= 9 would not be parsed.
// @history         0.22 Fixed a bug that stopped the script from running in some browser configurations.
// @history         0.22 Added some debugging features.
// @history         0.21 Added resizing of settings tab.
// @history         0.20 Added building icon data.
// @history         0.20 Added HTML setting type.
// @history         0.20 Added some movement type data.
// @history         0.20 Fixed a bug computing number of transports for transport missions.
// @history         0.19 Added Polish translation (from pitmm).
// @history         0.19 Fixed temple build resource requirements showing up incorrectly (marble instead of crystal).
// @history         0.19 Changed how transition to resource and mine views works.
// @history         0.18 Hopefully fixed population calculation crash when running the theology government.
// @history         0.18 Fix for transport form on test server.  (Probably coming to other servers with 0.5.1).
// @history         0.17 Added German localization (translation by Cherry).
// @history         0.17 Fixed date display bug with yesterday/today/tomorrow being incorrectly assigned due to timezone issue.
// @history         0.16 Reworked how initial ajax response is determined so it works in Chrome.
// @history         0.15 Removed CDATA section (hopefully will work in Chrome).
// @history         0.14 UI support for script settings.
// @history         0.14 Fixed corruption calculation for cities with no palace.
// @history         0.14 Corrected loading time for deploy on the same island.
// @history         0.13 Another tweak to work with TamperMonkey.
// @history         0.12 Another tweak to work with TamperMonkey.
// @history         0.11 Small tweak to work with TamperMonkey in Google Chrome.
// @history         0.10 Initial version.
// @require         http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js
// ==/UserScript==

if (typeof IkaTools == 'undefined') {
    IkaTools = (function() {
      var ikaToolsVersion = 0;
      
      /**
       * Support functions for logging, profiling, and debugging.
       */
      var Logging = (function() {
        var exceptionLog = [];
        
        var options = {
          debug: false,
          timings: false,
          profile: false,
        };
        
        function getExceptionLog() {
          return exceptionLog;
        }
        
        /**
         * Analogous to console.log, but may have been disabled through options.
         */
        function debug() {
          if (options.debug && console && console.log) {
            // console.log is not a true javascript function.  In some browsers we can't call 
            // it with console.log.apply syntax.  Instead we just manually support up to 6
            // arguments.
            switch (arguments.length) {
              case 0:
                console.log();
                break;
              case 1:
                console.log(arguments[0]);
                break;
              case 2:
                console.log(arguments[0], arguments[1]);
                break;
              case 3:
                console.log(arguments[0], arguments[1], arguments[2]);
                break;
              case 4:
                console.log(arguments[0], arguments[1], arguments[2], arguments[3]);
                break;
              case 5:
                console.log(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4]);
                break;
              default:
                console.log(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], 
                            arguments[5]);
                break;
            }
          }
        }
        
        /**
         * Debug a caught exception.
         */
        function debugException(text, ex) {
          exceptionLog.push({text: text, stack: ex.stack, message: ex.message});
          if (console && console.error) {
            console.error('IkaTools.debugException (in %s)\n%s\n\n%s\n', text, ex, ex.stack);
          }
        }
        
        /**
         * Wrap a function to catch and log an exception.
         * If the time property is true in options (or absent) then time the function call.
         * (May set alwaysTime option to output time regardless of disabled logging settings.)
         * If the group property is true in options (or absent) then group the function call.
         */
        function debuggable(debugOptions, func) {
          if (typeof(debugOptions) == 'string') {
            debugOptions = { label: debugOptions };
          }
          debugOptions = $.extend({ time: true, 
                                    group: true,
                                    profile: false,
                                    swallowException: false,
                                   }, debugOptions);
          if (!debugOptions.label) {
            debugOptions.label = Utils.nextId('_debuggable');
          }
    
          return function debuggableWrapper() {
            var time = ((options.timings && debugOptions.time) || debugOptions.alwaysTime) && 
                        console && console.time;
            var group = debugOptions.group && (options.group) && console && console.group;
            var profile = debugOptions.profile && options.profile && console && console.profile;
            try {
              if (profile) {
                console.profile(debugOptions.label);
              }
              if (group) {
                console.group(debugOptions.label);
              }
              if (time) {
                console.time(debugOptions.label);
              }
              return func.apply(this, arguments);
            } catch (ex) {
              Logging.debugException(debugOptions.label, ex);
              if (!debugOptions.swallowException) {
                throw ex;
              }
            } finally {
              if (time) {
                console.timeEnd(debugOptions.label);
              }
              if (group) {
                console.groupEnd(debugOptions.label);
              }
              if (profile) {
                console.profileEnd(debugOptions.label);
              }
            }
          };
        }
    
        /**
         * Wrap a console.time/timeEnd pair around a function.  May be disabled 
         * through options.
         */
        function time(timeOptions, func) {      
          return function time() {
            var time = (options.timings || timeOptions.alwaysTime) && console && console.time;
            try {
              if (time) {
                console.time(timeOptions.label);
              }
              return func.apply(this, arguments);
            } finally {
              if (time) {
                console.timeEnd(timeOptions.label);
              }
            }
          }
        }
        
        /**
         * Wrap a console.group/groupEnd pair around a function.  May be disabled 
         * through options.
         */
        function group(groupOptions, func) { 
          var label;
    
          var collapsed = false;
    
          return function group() {
            var group = (options.timings || options.debug) && console && console.group;
            try {
              if (group) {
                if (groupOptions.collapsed && console.groupCollapsed) {
                  console.groupCollapsed(groupOptions.label);
                } else {
                  console.group(groupOptions.label);
                }
              }
              return func.apply(this, arguments);
            } finally {
              if (group) {
                console.groupEnd(groupOptions.label);
              }
            }
          }
        }
        
        /**
         * Sets logging options.  Properties are debug, timings, and profile.
         */
        function setOptions(newOptions) {
          $.extend(options, newOptions);
          if (console && console.log) {
            //console.log("Set logging options to: ", options);
          }
        }
        
        /**
         * Allows logging options to be configured by browsing to various anchors 
         * (and persisted for future page views).
         */
        function setAndSaveOptionsFromPageAnchor() {
          var savedOptions = new Data.Value(
              'debugOptions', 
              { debug: false, timings: false, profile: false , group: false},
              { useDomain: false, version: 0 });
          savedOptions.get();
              
          var anchor = window.location.hash;
    
          if (anchor.substring(0, 15) == '#ikaScriptTools') {
            if (anchor == '#ikaScriptToolsDebugAll') {
              savedOptions.get().debug = true;
              savedOptions.get().timings = true;
              savedOptions.get().profile = true;
            } else if (anchor == '#ikaScriptToolsDebugNone') {
              savedOptions.get().debug = false;
              savedOptions.get().timings = false;
              savedOptions.get().profile = false;
            } else if (anchor == '#ikaScriptToolsDebugOn') {
              savedOptions.get().debug = true;
            } else if (anchor == '#ikaScriptToolsDebugOff') {
    
              savedOptions.get().debug = false;
            } else if (anchor == '#ikaScriptToolsGroupOn') {
              savedOptions.get().group = true;
            } else if (anchor == '#ikaScriptToolsGroupOff') {
              savedOptions.get().group = false;
            } else if (anchor == '#ikaScriptToolsTimingsOn') {
              savedOptions.get().timings = true;
            } else if (anchor == '#ikaScriptToolsTimingsOff') {
              savedOptions.get().timings = false;
            } else if (anchor == '#ikaScriptToolsProfilesOn') {
              savedOptions.get().profile = true;
            } else if (anchor == '#ikaScriptToolsProfilesOff') {
              savedOptions.get().profile = false;
            }
            savedOptions.saveAsync();
          }
          setOptions(savedOptions.get());
        }
    
        return {
          debug: debug,
          debugException: debugException,
          debuggable: debuggable,
          time: time,
          group: group,
          setAndSaveOptionsFromPageAnchor: setAndSaveOptionsFromPageAnchor,
          getExceptionLog: getExceptionLog,
        };
      })();
      
      /**
       * Random utils that don't belong anywhere else.
       */
      var Utils = function() {
        function thunk(func) {
          var computed = false;
          var value;
          
          function thunker() {
            if (!computed) {
              value = func();
              computed = true;
            }
            return value;
          }
          
          return thunker;
        }
        
        function resettable(func) {
          var value = func();
          
          function getValue() {
            return value;
          }
          
          getValue.reset = function() {
            var ret = value;
            value = func();
            return ret;
          }
          
          getValue.set = function(v) {
            value = v;
          }
          
          return getValue;
        }        
        
        function fixedFunction(value) {
          return function() {
            return value;
          }
        }
    
        var nextId = function() {
          var id = 10000;
          function nextId(prefix) {
            id++;
            if (prefix) {
              return prefix + id.toString(16);
            } else {
              return id;
            }
          };
          return nextId;
        }();
        
        var nextIntegerId = function() {
          var id = 100000;
          return function nextIntegerId() {
            return id++;
          };
        }();
    
        function EventDispatcher(name) {
          this.name = name;
          this.listeners = [];
        }
        
        $.extend(EventDispatcher.prototype, {
          addListener: function addListener(l) {
            var listener = Logging.debuggable(
                {
                  label: 'EventDispatcher[' + this.name + '].ListenerWrapper',
                  swallowException: true,
                },
                function() {
                  l.apply(null, arguments)
                });
            this.listeners.push(listener);
            return {
              cancel: this._cancelListener.bind(this, listener),
            };
          },
          _cancelListener: function(listener) {
            for (var i = 0, len = this.listeners.length; i < len; i++) {
              if (this.listeners[i] === listener) {
                this.listeners.splice(i, 1);
                return;
              }
            }
          },
          /*bindEventArgs: function bindEventArgs() {
            var that = this;
            var args = Array.prototype.slice.call(arguments);
            return {
              bindEventArgs: function boundBindEventArgs() {
    
                return that.bindEventArgs.apply(that, 
                    args.concat(Array.prototype.slice.call(arguments)));
              },
              send: function boundSend() {
                that.send.apply(that, args.concat(Array.prototype.slice.call(arguments)));
              },
    
              scheduleSend: function boundScheduleSend(name, delay, callback) {
                return that.scheduleSend.apply(that, 
                    [name, delay, callback].concat(args).concat(Array.prototype.slice.call(arguments, 3)));
              },
              toJSON: function toJSON() {
                return undefined;
              },
            };
          },*/
          send: function send() {
            var listeners = this.listeners.slice();
            for (var i = 0, len = listeners.length; i < len; i++) {
              listeners[i].apply(null, arguments);
            }
          },
          scheduleSend: function(name, delay, callback) {
            var that = this;
            var sendArgs = [];
            for (var i = 3; i < arguments.length; i++)  {
              sendArgs.push(arguments[i]);
            }
            return clearTimeout.bind(null, setTimeout(
                  function scheduledSend() {
                    callback();
                    that.send.apply(that, sendArgs);
                  },
                  Math.max(delay, 10)));
            
          },
          /*startIntervalSend: function startIntervalSend(name, initialDelay, interval) {
            this.cancelIntervalSend();
            var sendCall = this.send.bind(this);
            var sendArgs = [];
            for (var i = 2; i < arguments.length; i++) {
              sendArgs.push(arguments[i]);
            }
            this.cancelInterval = clearInterval.bind(null, setInterval(
                IkaTools.Logging.debuggable(
                    'IkaTools.Utils.EventDispatcher.intervalSend[' + name + ']', 
                    function() {
                      sendCall.apply(null, sendArgs);
                    }),
                interval));
          },
          cancelIntervalSend: function cancelIntevalSend() {
            if (this.cancelInterval) {
              this.cancelInterval();
            }
          },*/
          toJSON: function toJSON() {
            return undefined;
          },
        });
        
        function getVersion() {
          var parts = $.map(
              $('#GF_toolbar li.version span').text().match(/[0-9]+/g), 
              function(x) {
                return parseInt(x);
              }).concat([0,0,0,0,0,0,0]);
          return {
            greaterThanOrEqual: function() {
              for (var i = 0; i < arguments.length; i++) {
                if (parts[i] != arguments[i]) {
                  return parts[i] >= arguments[i];
                }
              }
              return true;
            },
            lessThan: function() {
              for (var i = 0; i < arguments.length; i++) {
                if (parts[i] != arguments[i]) {
                  return parts[i] < arguments[i];
                }
              }
              return false;
            },
          };
        }
        
        function isChrome() {
          return navigator.vendor.match(/Google/) || navigator.userAgent.match(/Chrome/);
        }
        
        function iterateIkariamAjaxResponse(response, f) {
          $.each(response, function iterateIkariamAjaxResponseItem(index, item) {
            f(index, item[0], item[1]);
          });
        }
        
        function forEachIkariamAjaxResponseFunction(f) {
          return function forEachIkariamResponse(response) {
            iterateIkariamAjaxResponse(response, f);
          }
        }
    
        function backgroundFetchPage(url, callback, options) {
          options = $.extend({method: 'GET', data: ''}, options);
          
          var headers = {
            'User-agent': navigator.userAgent, 
            'Cookie': document.cookie,
            'Referer': 'http://' + document.domain + '/index.php',
          };
          if(options.method == 'POST') {
            headers['Content-type'] = 'application/x-www-form-urlencoded';
          }
          setTimeout(function() {
            GM_xmlhttpRequest ({
              method: options.method,
              url: url,
              data: options.data,
              headers: headers,
              onload: Logging.debuggable('IkaTools.Utils.backgroundGetIkariamPage[' + url + ']', callback)
              });
            }, 0);
        }
    
        function backgroundFetchIkariamAjaxPage(url, callback, options) {
          backgroundFetchPage(url, function(response) {
            callback(JSON.parse(response.responseText));
          }, options);
        }
    
        var jsonResponseRegex = /ikariam.getClass\(ajax.Responder, (.*?)\);$/m;
        function backgroundFetchIkariamFullPage(url, callback, options) {
          backgroundFetchPage(url, function(response) {
            var match = jsonResponseRegex.exec(response.responseText);
            jsonResponse = [];
            if (match) {
              jsonResponse = JSON.parse(match[1]);
            }
            callback(response, jsonResponse);
          }, options);
        }
    
        var urlParamsRegex = /\?([^\#]*)(#|$)/;
        function parseUrlParams(url) {
          var paramsList = url.match(urlParamsRegex)[1].split('&');
          var params = {};
          $.each(paramsList, function(index, item) {
            var paramParts = item.split('=');
            if (paramParts.length == 2) {
              params[paramParts[0]] = decodeURIComponent(paramParts[1]);
            }
          });
          return params;
        }
    
        var timestampRegex = /(\d+)\.(\d+)\.(\d+)\s+(\d+):(\d+):(\d+)/i;
        function parseIkariamTimestamp(timestamp){
          var d = new Date();
          //Get the local GMT offset in hours
          //Attempt to match the constituent parts of the timestamp
          var match = timestamp.match(timestampRegex);
          if (match){
            d.setTime(0);
            d.setDate(parseInt(match[1], 10));
            d.setMonth(parseInt(match[2], 10)-1);
            d.setFullYear(parseInt(match[3], 10));
            d.setHours(parseInt(match[4], 10));
            d.setMinutes(parseInt(match[5], 10));
            d.setSeconds(parseInt(match[6], 10));
            //Adjust the time to get its TZ correct
            d.setTime(d.getTime() - d.getTimezoneOffset() * Constants.Time.MILLIS_PER_MINUTE -
                Constants.Time.MILLIS_PER_HOUR);  // Server time is german = GMT+1
          }
          return d;
        }
    
        return {
          thunk: thunk,
          resettable: resettable,
          fixedFunction: fixedFunction,
          EventDispatcher: EventDispatcher,
          nextId: nextId,
          nextIntegerId: nextIntegerId,
          getVersion: thunk(getVersion),
          isChrome: thunk(isChrome),
          iterateIkariamAjaxResponse: iterateIkariamAjaxResponse,
          forEachIkariamAjaxResponseFunction: forEachIkariamAjaxResponseFunction,
    
          backgroundFetchIkariamAjaxPage: backgroundFetchIkariamAjaxPage,
          backgroundFetchIkariamFullPage: backgroundFetchIkariamFullPage,
    
          parseUrlParams: parseUrlParams,
          parseIkariamTimestamp: parseIkariamTimestamp,
        };
      }();
      
      /**
       *  Internationalization and localization routines.
       */
      var Intl = function() {
        function Localizer(allLanguages) {
          this.allLanguages = allLanguages;
          this.languages = ['en'];
        }
        $.extend(Localizer.prototype, {
          localize: function localize(identifier) {
            var identifierParts = identifier.split('.');
            if (arguments.length > 1) {
              identifierParts = $.makeArray(arguments);
            }
            for (var i = 0; i < this.languages.length; i++) {
              var languageConfig = this.allLanguages[this.languages[i]];
              if (languageConfig !== undefined) {
                var translation = this.find(languageConfig, identifierParts);
                if (translation !== undefined) {
                  return translation;
                }
              }
            }
            return '?#!' + identifierParts.join(',') + '!#?';
          },
          delayedLocalize: function delayedLocalize() {
            var args = arguments;
            var t = this;
            return function doLocalize() {
              return t.localize.apply(t, args);
            };
          },
          find: function find(config, parts) {
    
            for (var i = 0; i < parts.length; i++) {
              config = config[parts[i]];
              if (config === undefined) {
                return undefined;
              }
            }
            return config;
          },
          setPreferredLanguage: function setPreferredLanguage(language) {
            this.languages.unshift(language);
          },
        });
        
        var baseLocalizer = new Localizer({
          en: {
            formatting: {
              "thousandsSeparator": ",",
              "unknown": "?",
              hourFormatAMPM: true,
            },
            timeunits: {
              long: {
                day: 'Day',
                hour: 'Hour',
                week: 'Week',
              },
              short: {
                day: 'D',
                hour: 'h',
                minute: 'm',
                second: 's',
              },
              complete: '-',
              yesterday: 'yesterday',
              tomorrow: 'tomorrow',
              today: 'today',
              am: 'AM',
              pm: 'PM',
            },
            settings: {
              script_settings: 'Script Options',
              settings: 'Settings',
              save: 'Save',
            },
          },
          de: {
            formatting: {
              "thousandsSeparator": ".",
              "unknown": "?",
              hourFormatAMPM: false,
            },
            timeunits: {
              long: {
                day: 'Tag',
                hour: 'Stunde.',
                week: 'Woche',
              },
              short: {
                day: 'T',
                hour: 'h',
                minute: 'm',
                second: 's',
              },
              complete: '-',
              yesterday: 'gestern',
              tomorrow: 'morgen',
              today: 'heute',
              am: 'AM',
              pm: 'PM',
            },
            settings: {
              script_settings: 'Script Optionen',
              settings: 'Einstellungen',
              save: 'speichern',
            },
          },
          fr: {
            formatting: {
              "thousandsSeparator": ".",
              "unknown": "?",
              hourFormatAMPM: false,
            },
            timeunits: {
              long: {
                day: 'Jour',
                hour: 'Heure',
                week: 'Semaine',
              },
              short: {
                day: 'J',
                hour: 'h',
                minute: 'm',
                second: 's',
              },
              complete: '-',
              yesterday: 'hier',
              tomorrow: 'demain',
              today: 'aujourd\'hui',
              am: 'AM',
              pm: 'PM',
            },
            settings: {
              script_settings: 'Script Optionen',
              settings: 'Einstellungen',
              save: 'speichern',
            },
          },
          hu: {
            formatting: {
              "thousandsSeparator": ",",
              "unknown": "?",
              hourFormatAMPM: true,
            },
            timeunits: {
              long: {
                day: 'Nap',
                hour: 'Óra',
                week: 'Hét',
              },
              short: {
                day: 'n',
                hour: 'ó',
                minute: 'p',
                second: 'mp',
              },
              complete: '-',
              yesterday: 'tegnap',
              tomorrow: 'holnap',
              today: 'ma',
              am: 'Délelőtt',
              pm: 'Délután',
            },
            settings: {
              script_settings: 'Script Beállítások',
              settings: 'Beállítások',
              save: 'Mentés',
            },
          },
          pl: {
            formatting: {
              "thousandsSeparator": ",",
              "unknown": "?",
              hourFormatAMPM: true,
            },
            timeunits: {
              long: {
                day: 'Dzien',
                hour: 'Godzina',
                week: 'Tydzien',
              },
              short: {
                day: 'D',
                hour: 'h',
                minute: 'm',
                second: 's',
              },
              complete: '-',
              yesterday: 'wczoraj',
              tomorrow: 'jutro',
              today: 'dzis',
              am: 'AM',
              pm: 'PM',
            },
            settings: {
              script_settings: 'Opcje Skryptu',
              settings: 'Ustawienia',
              save: 'Zapisz',
            },
          },
          ro: {  
            formatting: {  
              "thousandsSeparator": ",",  
              "unknown": "?",  
              hourFormatAMPM: false,  
            },  
            timeunits: {  
              long: {  
                day: 'Zi',  
                hour: 'Ora',  
                week: 'Saptamana',  
              },  
              short: {  
                day: 'D',  
                hour: 'h',  
                minute: 'm',  
                second: 's',  
              },  
              complete: '-',  
              yesterday: 'ieri',  
              tomorrow: 'maine',  
              today: 'azi',  
              am: 'AM',  
              pm: 'PM',  
            },  
            settings: {  
              script_settings: 'Optiuni Script',  
              settings: 'Setari',  
              save: 'Salveaza',  
            },  
          },
          tr: {
            formatting: {
              "thousandsSeparator": ",",
              "unknown": "?",
              hourFormatAMPM: false,
            },
            timeunits: {
              long: {
                day: 'Gün',
                hour: 'Saat',
                week: 'Hafta',
              },
              short: {
                day: 'Gn',
                hour: 'Sa',
                minute: 'Dk',
                second: 'Sn',
              },
              complete: '-',
              yesterday: 'dün',
              tomorrow: 'yarın',
              today: 'bugün',
              am: 'AM',
              pm: 'PM',
            },
            settings: {
              script_settings: 'Script Seçenekleri',
              settings: 'Ayarlar',
              save: 'Kaydet',
            },
          },
        });
        
        function formatInteger(number, forceShowSign) {
          if (number === undefined || isNaN(number)) {
            return baseLocalizer.localize('formatting.unknown');
          }
    
          number = Math.floor(number);
    
          var separator = baseLocalizer.localize('formatting.thousandsSeparator');       
          var s = number.toString();
    
          var sign = forceShowSign ? '+' : '';
          if (number === 0) {
            sign = '';
          } else if (number < 0) {
            sign = '-';
            s = s.substring(1);
          }
    
          var i = s.length - 3;
          while (i > 0) {
            s = s.substring(0, i) + separator + s.substring(i);
            i -= 3;
          }
          return sign + s;
        };
        
        function formatDecimal(number, digits, forceShowSign) {
          if (number === undefined || isNaN(number)) {
            return baseLocalizer.localize('formatting.unknown');
          }
          var value = number.toFixed(digits);
          if (forceShowSign && number >= 0) { // .5 * Math.pow(10, -digits)) {
            value = '+' + value;
          }
          return value;
        }
        
        function formatRemainingTime(millis, complete, maxResolution) {
          maxResolution = maxResolution || 4;
          if (millis == Number.POSITIVE_INFINITY) {
            return '&infin;';
          }
          var seconds = Math.ceil(millis / Constants.Time.MILLIS_PER_SECOND);
          if (seconds <= 0) {
            return complete || baseLocalizer.localize('timeunits','complete');
          }
          
          var parts = [];
          if (maxResolution >= 1) {
            parts.push({value: Math.floor((seconds / 60 / 60 / 24)), label: 'day'});
          }
          if (maxResolution >= 2) {
            parts.push({value: Math.floor((seconds / 60/ 60) % 24), label: 'hour' });
          }
          if (maxResolution >= 3) {
            parts.push({value: Math.floor((seconds / 60) % 60), label: 'minute' });
          }
          if (maxResolution >= 4) {
            parts.push({value: Math.floor((seconds) % 60), label: 'second' });
          }
          
          while (parts[0] && !parts[0].value) {
            parts.shift();
          }
          parts.splice(2);
          return $.map(parts, function(part) {
            return '%s%s'.format(part.value, 
                                 baseLocalizer.localize('timeunits','short',part.label));
          }).join(' ');
        }
        
        function padLeft(s, length, char) {
          while (s.length < length) {
            s = char + s;
          }
          return s;
        }
        
        function formatAbsoluteTime(date) {
          var now = new Date();
          
          var dateDay = Math.floor(date.valueOf() / Constants.Time.MILLIS_PER_DAY - 
                                   date.getTimezoneOffset() / Constants.Time.MINUTES_PER_DAY);
          var nowDay = Math.floor(now.valueOf() / Constants.Time.MILLIS_PER_DAY -
                                  now.getTimezoneOffset() / Constants.Time.MINUTES_PER_DAY);
          
          var dayString = '';
          if (dateDay == nowDay + 1) {
            dayString = baseLocalizer.localize('timeunits','tomorrow');
          } else if (dateDay == nowDay - 1) {
            dayString = baseLocalizer.localize('timeunits','yesterday');
          } else if (dateDay == nowDay) {
            dayString = baseLocalizer.localize('timeunits','today');
          } else {
            dayString = date.toLocaleDateString();
          }
          
          var timeString = '';
          if (baseLocalizer.localize('formatting', 'hourFormatAMPM')) {
            var m = '';
            if (date.getHours() == 0) {
              timeString = '12';
              m = baseLocalizer.localize('timeunits','am');
            } else if (date.getHours() == 12) {
              timeString = '12';
              m = baseLocalizer.localize('timeunits','pm');
            } else if (date.getHours() > 12) {
              timeString = (date.getHours() - 12).toString();
              m = baseLocalizer.localize('timeunits','pm');
            } else {
              timeString = date.getHours().toString();
              m = baseLocalizer.localize('timeunits','am');
            }
            timeString = timeString + ':' + 
                         padLeft(date.getMinutes().toString(), 2, 0) + ' ' + m;
          } else {
            timeString = date.getHours().toString() + ':' + 
    
                         padLeft(date.getMinutes().toString(), 2, '0');
          }
          
          return dayString + ' ' + timeString;
        }
        
        return {
          Localizer: Localizer,
          localizer: baseLocalizer,
          formatInteger: formatInteger,
          formatDecimal: formatDecimal,
          formatRemainingTime: formatRemainingTime,
          formatAbsoluteTime: formatAbsoluteTime,
        };
      }();
      
      var View = function() {
        function getDomain() {
          return document.domain;
        }
        
        function getCurrentCityId() {
          var relatedCityData = unsafeWindow.ikariam.model.relatedCityData;
          return relatedCityData[relatedCityData.selectedCity].id;
        }
        function getCurrentCity() {
          return EmpireData.getCity(getCurrentCityId());
        }
        function isActiveCity(city) {
          return city.getId() == getCurrentCityId();
        }
        
        function getCurrentIslandId() {
          // This is available in javascript data in island and city views (unfortunately at 
          // different locations).  It does not appear to be anywhere in world view data.
          return parseInt($("#js_islandBread").attr("href").match(/islandId=(\d+)/)[1]); 
        };
        
        function getViewType() {
          return unsafeWindow.ikariam.backgroundView.id;
        }
        function viewIsIsland() {
          return getViewType() == 'island';
        }
        function viewIsCity() {
          return getViewType() == 'city';
        }
      
        function getIkariamBaseViewParams() {
          var mainboxX = unsafeWindow.ikariam.mainbox_x;
          var mainboxY = unsafeWindow.ikariam.mainbox_y;
          var mainboxZ = unsafeWindow.ikariam.mainbox_z;
          
          if (mainboxX || mainboxY || mainboxZ) {
            return {
              mainbox_x: mainboxX,
              mainbox_y: mainboxY,
              mainbox_z: mainboxZ,
            };
          }
          return {};
        }
        
        function makeFullIkariamUrl(params, anchor) {
          return 'http://' + getDomain() + '/index.php?' +
                 $.map(params, function(value, key) { 
                     return encodeURIComponent(key) + '=' + 
                            encodeURIComponent(value);
                 }).join('&') + 
                 (anchor ? '#' + anchor : '');
        }
        
        function makeLocalIkariamUrl(params) {
          return '?' + $.map(params, function(value, key) { return key + '=' + value; }).join('&');
        }
        
        function loadLocalIkariamUrl(url) {
          Logging.debug("loadLocalIkariamUrl: ", url);
          
          // This is an odd way to make the ajaxHandlerCall rather than just calling it directly.
          // It is done this way so that in Chrome the actions run when the response is recieved 
          // are run in the page's javascript context instead of the javascript context of the 
          // TamperMonkey extension.  This is necessary to properly evaluate script actions in 
          // returned ikariam pages or stuff like transport sliders simply will not work.
          document.location = 'javascript:ajaxHandlerCall(' + JSON.stringify(url) + '); void(0);';
        }
        
        function goToIkariamPage(city, mainView, mainParams, view, viewParams, anchor) {
          var changeParams = {
            // Whacked up logic I don't really understand that makes transitioning to 
            // island mine/mill pages work.  Yes, it's completely incomprehensible how Ikariam 
            // developers could screw this up, but somehow they can make the mill go to the mine 
            // if you say the old view is island when you want to go to an island page.  
            // Truly incredible!
            oldView: mainView == 'island' ? getViewType() : mainView,
            action: 'header',
            function: 'changeCurrentCity',
            cityId: city.getId(),
            actionRequest: unsafeWindow.ikariam.model.actionRequest,
          };
          
          $.extend(changeParams, getIkariamBaseViewParams(), mainParams);
          
          if (view) {
            $.extend(changeParams, viewParams);
            changeParams.templateView = view;
          }
          
          $.extend(changeParams, { backgroundView: mainView } );
          
          if (mainView == 'island' && view && !anchor) {
            // Stupid ikariam developers still include the city preselect when we ask for a 
            // specific view.  Which will overwrite whatever view (mine/mill/wonder) we 
            // actually want to see.  Set this hack to suppress that transition when the page 
            // loads.
            anchor = 'ikaScriptToolsSuppressCityPreselect';
          }
          
          if (getViewType() == mainView) {
            loadLocalIkariamUrl(makeLocalIkariamUrl(changeParams));
            return;
          }
          
          var url = makeFullIkariamUrl(changeParams, anchor);
          Logging.debug('goToIkariamPage: ', url);
          window.location.assign(url);
        }
        
        function goToLocalView(view, params) {
          loadLocalIkariamUrl(makeLocalIkariamUrl($.extend({view: view}, params)));
        }
        
        function goToCitysIslandView(city, view, params) {
          if (isActiveCity(city) && viewIsIsland() && 
              getCurrentIslandId() == city.getIslandId()) {
            if (view) {
              loadLocalIkariamUrl(makeLocalIkariamUrl($.extend({view: view}, params)));
            }
          } else if (viewIsIsland()) {
            goToIkariamPage(city, 'island', null, view, params);
          } else {
            goToIkariamPage(city, 'island', null, null, null, 
               makeIkariamLoadLocalPageAnchor($.extend({view: view}, params)));
          }
        }
        
        function goToIslandView(city, islandId, view, params) {
          if (isActiveCity(city) && viewIsIsland() &&
              getCurrentIslandId() == islandId) {
            if (view) {
              loadLocalIkariamUrl(makeLocalIkariamUrl($.extend({view: view}, params)));
            }
          } else {
            goToIkariamPage(city, 'island', { currentIslandId: islandId }, view, params);
          }
        }
    
        function goToIkariamFullPage(params, anchor) {
          url = makeFullIkariamUrl(params, anchor);
          Logging.debug('goToIkariamFullPage: ', url);
          window.location.replace(url);
        }
    
        function makeIkariamLoadLocalPageAnchor(params, doNotSuppressFirstCityInfo) {
          if (doNotSuppressFirstCityInfo) {
            return 'ikaScriptToolsLoadLocalIkariamUrl_DoNotSuppressFirstCityInfo=' + 
                encodeURIComponent(makeLocalIkariamUrl(params));
          } else {
            return 'ikaScriptToolsLoadLocalIkariamUrl=' + 
                encodeURIComponent(makeLocalIkariamUrl(params));
          }
        }
        
        function goToCityView(city, view, params) {      
          if (isActiveCity(city) && viewIsCity()) {
            if (view) {
              loadLocalIkariamUrl(makeLocalIkariamUrl($.extend({view: view}, params)));
            }
          } else {
            goToIkariamPage(city, 'city', null, view, params);
          }
        }
    
        function activateCity(city) {
          if (!isActiveCity(city)) {
            goToIkariamPage(city, getViewType());
          }
        }
        
        var suppressingChangeView = false;
        var superSuppressChangeView = Utils.resettable(Utils.fixedFunction(false));
        
        var initialPageAjaxResponse = Utils.thunk(function findInitialPageAjaxResponse() {
          var regex = /ikariam.getClass\(ajax.Responder, (.*)\);/;
          var response = [];
          $('script').each(function findInitialPageAjaxResponse(index, script) {
            var match = regex.exec(script.innerHTML);
            if (match) {
              response = JSON.parse(match[1]);
            }
          });
          return response;
        });
        
        unsafeWindow.ajax.Responder.changeView = function(changeView) {
          return Logging.debuggable(
              'IkaTools.View.changeViewReplacement', 
              function customChangeView(params) {          
                if (suppressingChangeView && suppressingChangeView == params[0]) {
                  Logging.debug("Suppressing change to view: ", params[0]);
                } else if (superSuppressChangeView() == params[0]) {
                  superSuppressChangeView.reset();
                  Logging.debug("Super suppressing change to view", params[0]);
                } else {
                  changeView.apply(this, arguments);
                }
              });
        }(unsafeWindow.ajax.Responder.changeView);
        
        var ajaxResponseEvent = new Utils.EventDispatcher();
        
        function registerIkariamAjaxResponseCallback(f, fireInitialPageView) {
          var canceller = ajaxResponseEvent.addListener(f);
          if (fireInitialPageView) {
            f(initialPageAjaxResponse());
          }
          return canceller;
        }
        
        var suppressNextAjaxChangeView = Utils.resettable(Utils.fixedFunction(null));
        
        function suppressChangeViewOfNextAjaxResponse(type) {
          suppressNextAjaxChangeView.set(type);
        }
        
        function suppressFirstChangeViewOfType(type) {
          superSuppressChangeView.set(type);
        }
        
        var nextAjaxResponseEvent = 
            Utils.resettable(function() { return new Utils.EventDispatcher(); });
            
        function registerNextIkariamAjaxRequestCallback(f) {
    
          return nextAjaxResponseEvent().addListener(f);
        }
        
        function replaceExecuteAjaxRequest(executeAjaxRequest) {      
          return function customExecuteAjaxRequest() {
            var ajaxEvent = nextAjaxResponseEvent.reset();
            var suppressChangeView = suppressNextAjaxChangeView.reset();
            
            var args = $.makeArray(arguments);
            args.push(undefined);
            
            if (!args[1]) {
              args[1] = function customAjaxCallback(responseText) {
                suppressingChangeView = suppressChangeView;
                var responder = unsafeWindow.ikariam.getClass(
                    unsafeWindow.ajax.Responder, responseText);
                unsafeWindow.ikariam.controller.ajaxResponder = responder;
                suppressingChangeView = null;
                
                ajaxResponseEvent.send(responder.responseArray);
                ajaxEvent.send(responder.responseArray);
              }
              args[1].isScriptInterceptor = true;
            } else if (args[1].isScriptInterceptor) {
              // Allows multiple instances of this script to work
              var func = args[1];
              args[1] = function customAjaxCallbackWrapper() {
                suppressingChangeView = suppressChangeView;
                func.apply(this, arguments);
                suppressingChangeView = null;
                
                var responseArray = unsafeWindow.ikariam.controller.ajaxResponder.responseArray;
                ajaxResponseEvent.send(responseArray);
                ajaxEvent.send(responseArray);
              }
            }
            var ret = executeAjaxRequest.apply(this, args);
          };
        }
        
        if (unsafeWindow.ikariam.controller) {
          unsafeWindow.ikariam.controller.executeAjaxRequest = 
              replaceExecuteAjaxRequest(unsafeWindow.ikariam.controller.executeAjaxRequest);
        } else {
          unsafeWindow.ikariam.Controller.executeAjaxRequest = 
              replaceExecuteAjaxRequest(unsafeWindow.ikariam.Controller.executeAjaxRequest);
        }
        
        var ajaxFormEvent = new Utils.EventDispatcher();
        
        unsafeWindow.ajaxHandlerCallFromForm = function(ajaxHandlerCallFromForm) {
          return function customerAjaxHandlerCallFromForm(form) {
            ajaxFormEvent.send(form);
            return ajaxHandlerCallFromForm.apply(this, arguments);
          };
        }(unsafeWindow.ajaxHandlerCallFromForm);
        
        function registerAjaxFormSubmitCallback(f) {
          return ajaxFormEvent.addListener(f);
        }
        
        var gameTimeDifference = 0;
        
        function setGameTimeDifference(value) {
          Logging.debug("Game time difference: ", value);
          gameTimeDifference = value;
        }
            
        function getGameTimeDifference() {
          return gameTimeDifference;
        }
        
        function gameTimeNow() {
          return new Date().getTime() - gameTimeDifference;
        }
    
        return {
          getDomain: getDomain,
          
          getCurrentCityId: getCurrentCityId,
          getCurrentCity: getCurrentCity,
          isActiveCity: isActiveCity,
          
          getCurrentIslandId: getCurrentIslandId,
          
          getViewType: getViewType,
          viewIsIsland: viewIsIsland,
          viewIsCity: viewIsCity,
    
          goToCitysIslandView: goToCitysIslandView,
          goToCityView: goToCityView,
          goToIslandView: goToIslandView,
          goToLocalView: goToLocalView,
          activateCity: activateCity,
          goToIkariamFullPage: goToIkariamFullPage,
          makeIkariamLoadLocalPageAnchor: makeIkariamLoadLocalPageAnchor,
          
          //registerViewChangedListener: registerViewChangedListener,
          suppressChangeViewOfNextAjaxResponse: suppressChangeViewOfNextAjaxResponse,
          suppressFirstChangeViewOfType: suppressFirstChangeViewOfType,
          registerNextIkariamAjaxRequestCallback: registerNextIkariamAjaxRequestCallback,
          registerIkariamAjaxResponseCallback: registerIkariamAjaxResponseCallback,
          registerAjaxFormSubmitCallback: registerAjaxFormSubmitCallback,
          
          loadLocalIkariamUrl: loadLocalIkariamUrl,
          
          setGameTimeDifference: setGameTimeDifference,
          getGameTimeDifference: getGameTimeDifference,
          gameTimeNow: gameTimeNow,
        };
      }();
    
      /**
       * Data value class for encapsulating GM_getValue/GM_setValue access and 
       * serialization/deserialization.
    
       */
      var Data = function() {
        function Value(key, defaultValue, options) {
          this.options = $.extend({ useDomain: true, loadCallback: function() {} }, options);
          if (this.options.useDomain) {
            this.key = View.getDomain() + "/" + key + "/" + 
                (options.version || ikaToolsVersion) + '-' + ikaToolsVersion;
          } else {
            this.key = key + "/" + ikaToolsVersion;
          }
          this.defaultValue = defaultValue;
          this.data = defaultValue;
          this.needsSave = false;
        }
        
        $.extend(Value.prototype, {
          load: function load() {
            var rawValue = GM_getValue(this.key, "null");
            if (rawValue !== undefined) {
              var data = JSON.parse(rawValue, this.options.reviver);
              Logging.debug('Loaded data "%s": %s -> %o', this.key, rawValue, data);
              if (data !== null) {
                this.data = data;
              }
            } else {
            }
            this.loaded = true;
            this.options.loadCallback(this.data);
            return this.data;
          },
          save: function save() {
            return this.doSave(true);
          },
          doSave: function doSave(force) {
            if (this.needsSave || force) {
              var value = JSON.stringify(this.data, this.options.stringifier);
              Logging.debug('Saved data "%s": %o -> %s', this.key, this.data, value);
              GM_setValue(this.key, value);
              this.needsSave = false;
            }
            return this.data;
          },
          saveAsync: function saveAsync() {
            this.needsSave = true;
            setTimeout(Logging.debuggable('IkaTools.Data.Value[' + this.key + ']', 
                                          this.doSave.bind(this, false)), 0);
          },
          get: function get() {
            if (!this.loaded) {
              var value = this.load();
              return value;
            }
            return this.data;
          },
          set: function set(data) {
            this.data = data;
            return data;
          },
          reset: function reset() {
            this.set(this.defaultValue);
            this.save();
          },
        });
        
        return {
          Value: Value,
        };
      }();
      
      var UI = function() {
        function ToolTipHandler(toolTipClass, toolTipContainer, options) {
          this.toolTips = {};
          this.options = $.extend({
            delay: 200,
            activeClass: 'active',
            offsetX: 0,
            offsetY: 20,
            toolTipClass: toolTipClass,
            toolTipContainer: toolTipContainer || $('<div/>'),
          }, options);
          this.toolTipContainer = 
              $('<div style="position: absolute; z-index: 100000; display:none;"/>');
          this.toolTipContainer.append(this.options.toolTipContainer);
                
          this.activeToolTipElement = null;
          this.pendingShowEvent = null;
          this.activeToolTip = null;
          
          var body = $('body');
          body.append(this.toolTipContainer);
        }
        
        $.extend(ToolTipHandler.prototype, {
          _getCurrentToolTip: function _getCurrentToolTip() {
            if (this.activeToolTipElement) {
              var id = this.activeToolTipElement.id;
              if (id) { 
                return this.toolTips[id];
              }
            }
          },
          _reset: function _reset() {
            clearTimeout(this.pendingShowEvent);
            this.toolTipContainer.hide();
            
            var toolTipInfo = this._getCurrentToolTip();
            if (this.activeToolTip && this.activeToolTip.deactivated) {
              this.activeToolTip.deactivated($(this.activeToolTipElement));
            }
            this.activeToolTip = null;
            
            this.options.toolTipContainer.empty();
            this.activeToolTipElement = null;
          },
          _showToolTip: function _showToolTip() {
            var toolTipInfo = this._getCurrentToolTip();
            if (toolTipInfo) {
              this.activeToolTip = toolTipInfo.contentCreator($(this.activeToolTipElement));
              this.options.toolTipContainer.append(this.activeToolTip);
              this.toolTipContainer.show();
            }
          },
          _mouseOver: function _mouseOver(e) {
            var toolTipElement = $(e.target).closest('.' + this.options.toolTipClass);
            if (toolTipElement.get(0) == this.activeToolTipElement) {
              return;
            }
            
            this._reset();
    
            if (toolTipElement.length > 0) {
              this.activeToolTipElement = toolTipElement[0];
              this.toolTipContainer.css({
                left: (e.pageX + this.options.offsetX) + 'px', 
                top: (e.pageY + this.options.offsetY) + 'px',
              });
    
              this.pendingShowEvent = setTimeout(IkaTools.Logging.debuggable(
                 'IkaTools.UI.ToolTipHandler.showToolTip[' + this.options.toolTipClass + ']',
                 this._showToolTip.bind(this)), this.options.delay);
            }
          },
          _mouseOut: function _mouseOut(e) {
            if (this.activeToolTipElement) {
              var target = $(e.relatedTarget).closest('.' + this.options.toolTipClass);
              if (target.get(0) != this.activeToolTipElement) {
                this._reset();
                return;
              }
            }
          },
          _mouseMove: function _mouseMove(e) {
            if (this.activeToolTipElement && !this.activeToolTip) {
              this.toolTipContainer.css({
                left: (e.pageX + this.options.offsetX)+ 'px', 
                top: (e.pageY + this.options.offsetY) + 'px',
              });
            }
          },
           
          register: function registerToolTip(id, contentCreator) {
            this.toolTips[id] = {
              contentCreator: contentCreator,
            };
          },
          
          registerSimple: function registerSimpleToolTip(id, content) {
            this.register(id, function() {
              return $(content);
            });
          },
          
          registerRefreshable: function registerRefreshableToolTip(id, contentGenerator) {
            var toolTip = this;
            
            this.register(id, function() {
              var id =  Utils.nextId();
              var interval = setInterval(Logging.debuggable('IkaTools.ToolTip.refresh[' + id + ']', 
                function refreshToolTip() {
                  toolTip.options.toolTipContainer.html(contentGenerator());
                }), Constants.Time.MILLIS_PER_SECOND);
                
              var tip = $(contentGenerator());
              tip.deactivated = function() {
                clearInterval(interval);
              };
              return tip;
            });
          },
          
          deregister: function deregister(id) {
            delete this.toolTips[id];
          },
          
          startHandling: function startHandling(element) {
            element.on('mouseover'/*, '.' + this.options.toolTipClass*/, Logging.debuggable(
                'IkaTools.UI.ToolTipHandler.mouseOver[' + this.options.toolTipClass + ']', 
                this._mouseOver.bind(this)));
            element.on('mouseout'/*, '.' + this.options.toolTipClass*/, Logging.debuggable(
                'IkaTools.UI.ToolTipHandler.mouseOut[' + this.options.toolTipClass + ']', 
                this._mouseOut.bind(this)))
            element.on('mousemove', '.' + this.options.toolTipClass, Logging.debuggable(
                'IkaTools.UI.ToolTipHandler.mouseMove[' + this.options.toolTipClass + ']', 
                this._mouseMove.bind(this)))
          }
        });
        
        function LeftMenu(items, options) {
          this.items = items;
          this.active = false;
          this.options = $.extend({atTop: false }, options);
        }
          
        $.extend(LeftMenu.prototype, {
          ITEM_CONTRACTED_WIDTH: 53,
          ITEM_EXPANDED_WIDTH: 199,
          ITEM_Z_INDEX_EXPANDED: 120000,
          ITEM_Z_INDEX_CONTRACTED: 65,
          ANIMATION_DURATION: 300,
          
          display: function display() {
            // Add leftMenu div and "standard" contents if we are in a 
            // view where it is not already present
            var leftMenuDiv = $('#leftMenu');
            if (!leftMenuDiv.length) {
              leftMenuDiv = $('<div id="leftMenu" >' + 
                                '<div class="slot_menu city_menu" style="z-index: 65; ">' + 
                                  '<ul class="menu_slots"/>' +
                                '</div>' + 
                              '</div>');
              $('#container').append(leftMenuDiv);
            }
            
            // Setup event handlers
            for (var i = 0; i < this.items.length; i++) {
              var item = this.items[i];
              item.element.width(this.ITEM_COLLAPSED_WIDTH + 'px');
              item.element.mouseenter(this.expand.bind(this, item));
              item.element.mouseleave(this.contract.bind(this, item));
            }
            
            this.holderDiv = $('.slot_menu', leftMenuDiv);
            this.holderDiv.hover(this.menuActivated.bind(this),
                                 this.menuPassivated.bind(this));
           
            // Add elements to ui
            var menuSlots = $('ul.menu_slots', leftMenuDiv);
            if (this.options.atTop) {
              for (var i = this.items.length - 1; i >= 0; i--) {
                menuSlots.prepend(this.items[i].element);
              }
            } else {
              for (var i = 0; i < this.items.length; i++) {
                menuSlots.append(this.items[i].element);
              }
            }
          },
          menuActivated: function menuActivated() {
            this.active = true;
            this.holderDiv.css('z-index', this.ITEM_Z_INDEX_EXPANDED);
          },
          menuPassivated: function menuPassivated() {
            this.active = false;
          },
          contract: function contract(item) {
            var holder = item.element.parent().parent();
            item.element.animate(
              { width: this.ITEM_CONTRACTED_WIDTH },
              300,
              'swing',
              this.contractComplete.bind(this)
            );
          },
          contractComplete: function contractComplete() {
            if (!this.active) {
              this.holderDiv.css('z-index', this.ITEM_Z_INDEX_CONTRACTED);
            }
          },
          expand: function expand(item) {
            item.element.animate(
              { width: this.ITEM_EXPANDED_WIDTH },
              300,
              'swing');
            this.holderDiv.css('z-index', this.ITEM_Z_INDEX_EXPANDED);
    
          },
        });
        
        LeftMenu.Item = function LeftMenu_Item(element) {
          this.element = element;
        }
        
        function resizePopup() {
          unsafeWindow.ikariam.controller.adjustSizes();
        }
        
        var destroyedTemplateViewEvent = Utils.thunk(function() {
          var dispatcher = new Utils.EventDispatcher();
          var oldDestroyTemplateView = unsafeWindow.ikariam.TemplateView.destroyTemplateView;
          unsafeWindow.ikariam.TemplateView.destroyTemplateView = 
              function customDestroyTemplateView() {
                oldDestroyTemplateView.apply(this, arguments);
                dispatcher.send();
              };
          return dispatcher;
        });
        
        function PopupWindow(id, header, content, options) {
          this.id = id;
          this.headerElement = $(header);
          this.contentElement = $(content);
          this.options = $.extend({
            sidebars: [], 
            oversized: false,
            activatedCallback: function() {},
            deactivatedCallback: function() {},
          }, options);
          this.isActive = false;
          
          destroyedTemplateViewEvent().addListener(this._popupDestroyed.bind(this));
        }
    
        $.extend(PopupWindow.prototype, {
          _popupDestroyed: function _popupDestroyed() {
            if (this.isActive) {
              this.options.deactivatedCallback(this);
            }
            this.isActive = false;
          },
          display: function display(skipResize) {
            // Always display it.  There's no good way to track if it is 
            // already displayed because there is no callback when it is destroyed.
            // (One can replace unsafeWindow.ikariam.destroyTemplateView, but there 
            // are still some issues with quickly switching between views that can 
            // mess things up and will still be considered "active" when its not visible.
            templateViewArg = {
              boxId: this.id,
              headerElem: this.headerElement.html(),
              contentElem: '<div><div id="ikaPopupTempHolder"></div></div>',
              sidebarEls: this.options.sidebars,
              oversized: this.options.oversized,
              replaceBox: true,
              keepSidebars: false
            };
            this.isActive = true;
            this.activePopup = unsafeWindow.ikariam.createTemplateView(templateViewArg);
            // Null out the id or submitting the change city form will send it as the 
            // templateView
            unsafeWindow.ikariam.templateView.id = null;
            unsafeWindow.ikariam.model.viewParams = null;
            $('#ikaPopupTempHolder').replaceWith(this.contentElement);
    
            this.options.activatedCallback(this);
            
            if (skipResize) {
              unsafeWindow.ikariam.controller.adjustSizes();
            }
          },
          close: function close() {
            if (this.isActive) {
              unsafeWindow.ikariam.TemplateView.destroyTemplateView();
            }
          }
        });
        
        function TabPane(tabs, options) {
          this.tabs = tabs;
          this.options = $.extend({ tabActivatedCallback: function() {} }, options);
            
          this.currentTab = null;
    
          this.container = $("<div/>");
    
          var tabsContainer = $('<ul class="tabmenu"/>');
          this.container.append(tabsContainer);
    
          for (var i = 0; i < tabs.length; i++) {
            var tab = tabs[i];
            tab.tabPane = this;
    
            tabsContainer.append(tab.tabHolder)
            this.container.append(tab.contentHolder);
            tab.contentHolder.hide();
            tab.tabHolder.click(
              IkaTools.Logging.debuggable('IkaTools.TabPane.Tab.click', 
                  this.activate.bind(this, tab)));
          }
        }
        
        $.extend(TabPane.prototype, {
          getContainer: function getContainer() {
            return this.container;
          },
          activate: function activate(tab) {
            if (this.currentTab !== tab) {
    
              if (this.currentTab) {
                this.currentTab.contentHolder.hide();
                this.currentTab.tabHolder.removeClass('selected');
                this.currentTab.deactivated();
              }
    
              tab.contentHolder.show();
              tab.tabHolder.addClass('selected');
            }
            tab.activated();
            this.options.tabActivatedCallback(this.currentTab, tab);
            this.currentTab = tab;
          }
        });
          
        TabPane.Tab = function Tab(tab, content, options) {
          this.tab = $(tab);
          this.content = $(content);
          this.options = $.extend({ activatedCallback: function() {}, 
                                    deactivatedCallback: function() {} },
                                  options);
    
          this.contentHolder = $('<div/>');
          this.contentHolder.append(this.content);
    
          this.tabHolder = $('<li class="tab"/>');
          this.tabHolder.append(this.tab);
        }
        
        $.extend(TabPane.Tab.prototype, {
          activate: function activate() {
            this.tabPane.activate(this);
          },
          activated: function activated() {
            this.options.activatedCallback(this);
          },
          deactivated: function deactivated() {
            this.options.deactivatedCallback(this);
          },
        });
        
        function SettingsWindow(id, scriptName, settings, settingGroups) {
          this.id = id;
          this.settings = settings;
          this.scriptName = scriptName;
          this.settingGroups = settingGroups;
          this.saveEvent = new Utils.EventDispatcher();
        }
    
        $.extend(SettingsWindow.prototype, {
          show: function show() {
            var tabs = $.map(this.settingGroups, function makeTab(group, index) {
              var content = $.map(group.getSettings(), this.renderSetting.bind(this)).join('');
              content = '<div class="contentBox01h">' +
                          '<h3 class="header">' + 
                            Intl.localizer.localize('settings','settings') + 
                          '</h3>' +
                          '<div class="content">' + 
                            '<table class="table01"><tbody>' + content + '</tbody></table>' + 
                          '</div>' +
                          '<div class="footer"/>' +
                        '</div>' +
                        '<div class="centerButton">' + 
                          '<a class="ikaScriptToolsSaveOptions button">' + 
                            Intl.localizer.localize('settings','save') +
                          '</a>' + 
                        '</div>';
              return new TabPane.Tab('<b>' + group.getName() + '</b>', content, {});
            }.bind(this));
            var tabPane = new TabPane(tabs,  {
              tabActivatedCallback: function() {
                IkaTools.UI.resizePopup();
              },
            });
            var popup = new PopupWindow(
                'options',
                $('<div>' + Intl.localizer.localize('settings','script_settings') + ': ' + 
                    this.scriptName + '</div>'),
                tabPane.getContainer());
            tabs[0].activate();
            popup.display();
            $.each(this.settingGroups, function postRenderSettingsGroup(index, group) {
              $.each(group.getSettings(), this.postRenderSetting.bind(this));
            }.bind(this));
            $('.ikaScriptToolsSaveOptions').click(Logging.debuggable(
                'IkaTools.UI.SettingsWindow.saveSettings',
                this._save.bind(this)));
          },
          renderSetting: function renderSetting(setting, index) {
            var type = setting.getType();
            var html = '<tr><td>' + setting.getLabel()() + '</td><td class="left">';
            if (type == Settings.Type.BOOLEAN) {
              html += '<input id="ikaScriptToolsSettingInput_' + setting.getName() + 
                  '" type="checkbox" ' + (setting.isEnabled() ? 'checked' : '' ) + '/>';
            } else if (type == Settings.Type.CHOICE) {
              html += '<select id="ikaScriptToolsSettingInput_' + setting.getName() + '">';
              $.each(setting.getChoices(), function renderOption(key, value) {
                html += '<option value="' + value + '"' + 
                        (setting.getValue() == value ? 'selected="selected"' : '') + '>' + key + '</option>';
              })
              html += '</select>';
            } else if (type == Settings.Type.HTML) {
              html += setting.getHtml()();
            } else if (type == Settings.Type.TEXT) {
              html += '<input id="ikaScriptToolsSettingInput_' + setting.getName() 
                  + '" value="' + setting.getValue() + '"/>';
            }
            html += '</td>';
            return html;
          },
          postRenderSetting: function postRenderSetting(index, setting) {
            var type = setting.getType();
            if (type == Settings.Type.HTML) {
    
              setting.getPostRender()();
            }
          },
          _save: function save() {
            $.each(this.settingGroups, function saveGroup(index, group) {
              $.each(group.getSettings(), this._saveSetting.bind(this));
            }.bind(this));
            this.settings.save();
            this.saveEvent.send(this);
          },
          _saveSetting: function saveSetting(index, setting) {
            var type = setting.getType();
            if (type == Settings.Type.BOOLEAN) {
              setting.setEnabled(
                  $('#ikaScriptToolsSettingInput_' + setting.getName()).is(':checked'));
            } else if (type == Settings.Type.CHOICE) {
              setting.setValue(
                  $('#ikaScriptToolsSettingInput_' + setting.getName()).val());
            } else if (type == Settings.Type.TEXT) {
              setting.setValue(
                  $('#ikaScriptToolsSettingInput_' + setting.getName()).val());
            }
          },
          registerSavedSettingsHandler: function registerSavedSettingsHandler(f) {
            return this.saveEvent.addListener(f);
          },
          addAsScriptOptionsLink: function addAsScriptOptionsLink() {
            if($('#IkaScriptToolSettingsDropdown').size() == 0) {
              GM_addStyle(
                '#IkaScriptToolSettingsDropdown { ' +
                '  position:absolute; ' +
                '}' +
                '#IkaScriptToolSettingsDropdown:hover {' +
                '  padding-bottom:20px;' +
                '}' +
                '#IkaScriptToolSettingsDropdown #IkaScriptToolsSettingsDropdownLinks { ' +
                '  display:none;' +
                '}' +
                '#IkaScriptToolSettingsDropdown:hover #IkaScriptToolsSettingsDropdownLinks {' +
                '  display:block;' +
                '}' +
                '#IkaScriptToolsSettingsDropdownLinks { ' +
                '  background-color:#FFF5E1; ' +
                '  padding:.5em; ' +
                '  padding-bottom:0; ' +
                '  border:1px solid #666; ' +
                '  position:absolute; ' +
                '  right:-80px; ' +
                '  margin-top:2px; ' +
                '  width:170px;' +
                '}' +
                '#IkaScriptToolsSettingsDropdownLinks a { ' +
                '  color:#666; ' +
                '  cursor:pointer; ' +
                '  margin-left:0; ' +
                '  padding-left:.2em; ' +
                '  display:block; ' +
                '  margin-bottom:.5em;' +
                '}'
              );
              
              var li = document.createElement('li');
              li.id = 'IkaOptionsDropdown';
              $('#GF_toolbar ul').append($(
                  '<li id="IkaScriptToolSettingsDropdown">' + 
                    '<a href="javascript:void(0);">' + 
                        Intl.localizer.localize('settings','script_settings') + '</a>' + 
                    '<div id="IkaScriptToolsSettingsDropdownLinks">' +
                  '</li>'));
            }
            var link = $('<a>' + this.scriptName + '</a>');
            link.click(Logging.debuggable('IkaTools.UI.SettingsWindow.showSettings', 
                this.show.bind(this)));
            $('#IkaScriptToolsSettingsDropdownLinks').append(link);
          },
        });
        
        SettingsWindow.Group = function(name, settings) {
          this.name = name;
          this.settings = settings;
        }
        
        $.extend(SettingsWindow.Group.prototype, {
          getName: function getName() {
            return this.name;
          },
          getSettings: function getSettings() {
            return this.settings;
          },
        });
        
        return {
          ToolTipHandler: ToolTipHandler,
          LeftMenu: LeftMenu,
          resizePopup: resizePopup,
          PopupWindow: PopupWindow,
          TabPane: TabPane,
          SettingsWindow: SettingsWindow,
        };
      }();
      
      var Settings = function() {
        var Type = {
          BOOLEAN: 1,
          CHOICE: 2,
          HTML: 3,
          TEXT: 4,
        }
        
        function Settings(name) {
          this.name = name;
          this.data = new Data.Value('scriptOptions_' + name, { }, { version: 1 });
        }
        
        $.extend(Settings.prototype, {
          _getValue: function getValue(name, defaultValue) {
            var value = this.data.get()[name];
            return value === undefined ? defaultValue : value;
          },
          _setValue: function setValue(name, value) {
            this.data.get()[name] = value;
          },
          save: function save() {
            this.data.save();
          },
          
          boolean: function boolean(name, enabled, labelFunc) {
            return new Boolean(this, name, this._getValue(name, enabled), labelFunc);
          },
          
          choice: function choice(name, value, choices, labelFunc) {
            return new Choice(this, name, this._getValue(name, value), choices, labelFunc);
          },
          
          html: function html(htmlFunc, postRender, labelFunc) {
            return new Html(htmlFunc, postRender, labelFunc);
          }, 
    
          text: function text(name, value, labelFunc) {
            return new Text(this, name, this._getValue(name, value), labelFunc);
          }
        });
        
        function Boolean(settings, name, enabled, labelFunc) {
          this.settings = settings;
          this.name = name;
          this.enabled = enabled;
          this.labelFunc= labelFunc;
        }
        
        $.extend(Boolean.prototype, {
          isEnabled: function isEnabled() {
            return this.enabled;
          },
          setEnabled: function(enabled) {
            this.enabled = enabled;
            this.settings._setValue(this.name, enabled);
          },
          getName: function getName() {
            return this.name;
          },
          getType: function getType() {
            return Type.BOOLEAN;
          },
          getLabel: function getLabel() {
            return this.labelFunc;
          },
        });
        
        function Choice(settings, name, value, choices, labelFunc) {
          this.settings = settings;
          this.name = name;
          this.value = value;
          this.choices = choices;
          this.labelFunc = labelFunc;
        }
        
        $.extend(Choice.prototype, {
          getValue: function getValue() {
            return this.value;
          },
          setValue: function setValue(value) {
            this.value = value;
            this.settings._setValue(this.name, value);
          },
          getChoices: function getChoices() {
            return this.choices;
          },
          getName: function getName() {
            return this.name;
          },
          getType: function getType() {
            return Type.CHOICE;
          },
          getLabel: function getLabel() {
            return this.labelFunc;
          },
        });
        
        function Html(htmlFunc, postRender, labelFunc) {
          this.labelFunc = labelFunc;
          this.htmlFunc = htmlFunc;
          this.postRender = postRender;
        }
        
        $.extend(Html.prototype, {
          getHtml: function getHtml() { 
            return this.htmlFunc;
          },
          getPostRender: function getPostRender() {
            return this.postRender;
          },
          getType: function getType() {
            return Type.HTML;
          },
          getLabel: function getLabel() {
            return this.labelFunc;
          },
        });
    
        function Text(settings, name, value, labelFunc) {
          this.settings = settings;
          this.name = name;
          this.value = value;
          this.labelFunc = labelFunc;
        }
    
        $.extend(Text.prototype, {
          getValue: function getValue() {
            return this.value;
          },
          setValue: function setValue(value) {
            this.value = value;
            this.settings._setValue(this.name, value);
          },
          getName: function getName() {
            return this.name;
          },
          getType: function getType() {
            return Type.TEXT;
          },
          getLabel: function getLabel() {
            return this.labelFunc;
          },
        });
        
        return {
          Settings: Settings,
          Type: Type,
        };
      }();
      
      var Constants = {
        Resources: {
          WOOD: 'wood',
          WINE: 'wine',
          MARBLE: 'marble',
          GLASS: 'glass',
          SULFUR: 'sulfur',
          
          POPULATION: 'population',
          CITIZENS: 'citizens',
    
          SCIENTISTS: 'scientists',
          ACTION_POINTS: 'actionPoints',
          CULTURAL_GOODS: 'culturalGoods',
          TAVERN_WINE_LEVEL: 'tavernWineLevel',
          PRIESTS: 'priests',
        },
        
        CivilizationData: {
          GOVERNMENT: 'government',
          RESEARCH: 'research',
          MOVEMENT: 'movement',
          PREMIUM_FEATURE: 'premiumFeature',
        },
        
        PremiumFeatures: {
          DOUBLED_STORAGE_CAPACITY: 'doubledStorageCapacity',
          DOUBLED_SAFE_CAPACITY: 'doubledSafeCapacity',
        },
        
        Movements: {
          Mission: {
            TRANSPORT: 'transport',
            DEPLOY_ARMY: 'deployarmy',
            DEPLOY_NAVY: 'deployfleet',
            PLUNDER: 'plunder',
          },
    
          Stage: {
            LOADING: 'loading',
            EN_ROUTE: 'en_route',
            RETURNING: 'returning',
          },
    
          EventType: {
            DATA_UPDATED: 'dataUpdated',
            STAGE_CHANGED: 'stageChanged',
            CANCELLED: 'cancelled',
            COMPLETED: 'completed',
          },
          
          MissionData: {
            transport: {
              icon: '/cdn/all/both/actions/transport.jpg',
            },
            deployarmy: {
              icon: '/cdn/all/both/actions/occupy.jpg',
            },
            deployfleet: {
              icon: '/cdn/all/both/actions/blockade.jpg',
            },
            plunder: {
              icon: '/cdn/all/both/actions/plunder.jpg',
            },
            piracyRaid: {
              icon: '/cdn/all/both/actions/piracyRaid.jpg',
            },
          }
        },
        
        BuildingEventType: {
          DATA_REFRESH: 'dataRefresh',
          UPGRADE_COMPLETE: 'upgradeComplete',
        },
        
        Buildings: {
          TOWN_HALL: 'townHall',
          PALACE: 'palace',
          GOVERNORS_RESIDENCE: 'palaceColony',
          TAVERN: 'tavern',
          MUSEUM: 'museum',
          ACADEMY: 'academy',
          WORKSHOP: 'workshop',
          TEMPLE: 'temple',
          EMBASSY: 'embassy',
          WAREHOUSE: 'warehouse',
          DUMP: 'dump',
          TRADING_PORT: 'port',
          TRADING_POST: 'branchOffice',
          BLACK_MARKET: 'blackMarket',
          MARINE_CHART_ARCHIVE: 'marineChartArchive',
          WALL: 'wall',
          HIDEOUT: 'safehouse',
          BARRACKS: 'barracks',
          SHIPYARD: 'shipyard',
          PIRATE_FORTRESS: 'pirateFortress',
          FORESTER: 'forester',
          CARPENTER: 'carpentering',
          WINERY: 'winegrower',
          WINE_PRESS: 'vineyard',
          STONEMASON: 'stonemason',
          ARCHITECT: 'architect',
          GLASSBLOWER: 'glassblowing',
          OPTICIAN: 'optician',
          ALCHEMISTS_TOWER: 'alchemist',
          FIREWORK_TEST_AREA: 'fireworker',
        },
        
        // Time data from http://ikariam.wikia.com/wiki/User_blog:Warrior_fr/Actual_building_Time_formula
        // Rest of data from http://ikariam.wikia.com/ building pages.
        BuildingData: {
          academy: {
            maxlevel:50,
            wood:[55,58,98,226,328,538,844,1143,1723,2291,3367,4434,6403,8387,10965,1562,20374,28767,37471,48786,63496,88974,124014,150549,209779,272798,378372,461226,639658,883624,1081231,1493547,2063095,2849833,3936586,5437761,7511392,10375779,14332469,19797999,27347750,37776516,52182177,72081279,99568686,137538116,189986771,262436146,362513298,500753777],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            glass:[0,0,0,0,193,368,639,936,1503,211,3255,4485,6761,9226,12555,18599,25216,36997,50063,67702,91516,133177,192765,243011,351634,474841,683916,865717,1246777,1788499,2272591,3259901,4676140,6707654,9621744,13801839,19797943,28399010,40736746,58434518,83820952,120236331,172472097,247401296,354882919,509059121,730216007,1047452831,1502510795,2155265251],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],            
            time  :{a:1440, b:1, c:1.2, d:720},
            icon  :'/cdn/all/both/img/city/academy_l.png',
            maxScientists: [0, 8, 12, 16, 22, 28, 35, 43, 51, 60, 69, 79, 89, 100, 111, 122, 134, 146, 159, 172, 185, 198, 212, 227, 241, 256, 271, 287, 302, 318, 335, 351, 368 ],
          },
          alchemist: {
            maxlevel:61,
            wood:[235,401,617,898,1263,1738,2354,3157,4199,5554,7316,9607,12585,16456,21488,28030,36535,47591,61963,80649,104938,136516,177565,230931,300306,390494,507737,660154,858296,1115880,1450740,1886057,2451998,3187758,4144296,5387857,7004569,9106400,11838919,15391374,20009798,26014054,33819980,43968198,57161548,74313771,96612788,125602976,163292127,212290500,275991607,358807234,466472992,606445556,788419092,1024996652,1332563034,1732419548,2252259306,2928085165,3806703211],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[0,99,219,374,577,840,1182,1627,2205,2955,3931,5202,6852,8997,11786,15412,20125,26253,34219,44574,58037,75538,98289,127865,166315,216300,281279,365753,475568,618329,803918,1045183,1358855,1766663,2296859,2986173,3882359,5047500,6562314,8531740,11092214,14421117,18749061,24375871,31691353,41202296,53567581,69643833,90544754,117718283,153046903,198978051,258693667,336330631,437267348,568496341,739108674,960923743,1249308082,1624239900,2111693096],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            time  :{a:72000, b:11, c:1.1, d:6120},
            icon  :'/cdn/all/both/img/city/alchemist_l.png',
          },
          architect: {
            maxlevel:50,
            wood:[159,250,355,477,619,783,974,1195,1452,1750,2095,2495,2960,3500,4125,4850,5692,6668,7800,9114,10637,12404,14454,16832,19591,22791,26503,30809,35804,41599,48319,56116,65170,75686,87899,102082,118554,137684,159900,185702,215666,250465,290880,337815,392325,455629,529148,614530,713689,828848],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[91,137,190,253,325,408,504,615,743,890,1060,1255,1480,1739,2037,2379,2774,3227,3748,4348,5037,5829,6738,7784,8986,10367,11953,13774,15867,18271,21031,24201,27848,32046,36876,42434,48830,56190,64659,74406,85621,98526,113377,130466,150131,172760,198799,228763,263244,302922],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],            
            time  :{a:125660, b:37, c:1.06, d:2628},
            icon  :'/cdn/all/both/img/city/architect_l.png',
          },
          barracks: {
            maxlevel:49,
            wood:[42,98,167,254,361,493,658,862,1115,1429,1818,2301,2899,3641,4561,5701,7116,887,11044,13741,17086,21233,26375,32751,40658,50461,62618,77693,96385,119564,148305,183944,228137,282936,350886,435146,539626,669183,829833,1029039,1276055,1582354,1962165,2433131,3017129,3741286,4639240,5752704,7133399],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[0,0,0,0,0,0,0,0,153,370,640,975,1389,1904,2542,3332,4312,5528,7037,8907,11224,14099,17664,22084,27566,34363,42791,53241,662,82268,102193,126901,157539,195528,242636,30105,373483,4633,574672,712774,884021,1096368,1359677,1686180,2091044,2593076,3215595,3987519,4944704],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            time  :{a:25200, b:11, c:1.1, d:1728},
            icon  :'/cdn/all/both/img/city/barracks_l.png',
          },
          branchOffice: {
            maxlevel:72,
            wood:[41,148,297,499,770,113,1602,2218,3017,4047,5367,7054,9201,11924,15369,19716,25185,32054,40663,51434,64885,81661,10255,12853,1608,200835,250454,311885,387872,481778,597732,740796,917337,1135187,1404015,1735749,2145109,2650258,3273613,4043584,4994655,6169425,7620506,9412889,11626849,14361545,17739456,21911869,27065655,33431640,41294936,51007721,63005003,77824109,96128747,118738731,146666701,181163475,223774071,276406902,341419250,421722842,520914258,643436013,794775525,981710881,1212614410,1497827655,1850124544,2285283504,2822794124,3486730050],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[0,0,0,0,464,681,965,1337,1818,2439,3235,4252,5547,7188,9265,11885,15182,19323,24512,31004,39114,49226,61819,77479,96933,121067,150978,188009,233815,290424,360322,446564,552986,684311,846366,1046343,1293113,1597627,1973398,2437552,3010878,3719054,4593796,5674284,7008907,8657442,10693723,13208947,16315766,20153328,24893505,30748599,37980844,46914155,57948631,71578478,88414142,109209651,134896382,166624778,205815871,254224933,314020081,387879387,479110822,591800408,730995225,902929453,1115303588,1377629325,1701655566,2101894617],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            capacity:[400,1600,3600,6400,10000,14400,19600,25600,32400,40000,48400,57600,67600,78400,90000,102400,115600,129600,144400,160000,176400,193600,211600,230400,250000,270400,291600,313600,336400,360000,384400,409600,435600,462400,490000,518400,547600,577600,608400,640842,675014,711009,748923,788858,830924,875232,921903,971062,1022844,1077386,1134836,1195351,1259092,1326232,1396952,1471443,1549906,1632554,1719608,1811305,1907891,2009628,2116789,2229665,2348560,2473795,2605708,2744655,2891011,3045172,3207553,3378593],
            time  :{a:108000, b:11, c:1.1, d:9360},
            icon  :'/cdn/all/both/img/city/branchoffice_l.png',
          },
          blackMarket: {
            maxlevel:25,
            wood:[378,762,1169,1625,2163,2827,3666,4734,6093,7813,9967,12634,15900,19855,24596,30222,36841,44565,53507,63790,75540,88886,103963,120912,139876],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[223,451,694,968,1297,1709,2236,2915,3786,4895,6290,8024,10154,12738,15841,19528,23871,28942,34817,41579,49307,58089,68014,79175,91664],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],            
            time  :{a:0, b:0, c:0, d:0},
            icon  :'/cdn/all/both/img/city/blackmarket_l.png',
          },
          marineChartArchive: {
            maxlevel:40,
            wood:[497,1116,1834,2667,3634,4755,6056,7564,9314,11344,13698,16431,19599,23275,27538,32484,38221,44877,52596,61551,71939,83990,97968,114183,132992,154810,180120,209478,243534,283039,328865,382024,443687,515217,598191,694442,806092,935607,1085844,1260119],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[297,916,1647,2509,3526,4727,6143,7815,9787,12115,14861,18103,21926,26438,31764,38047,45461,54210,64533,76715,91089,108051,128066,151684,179552,212438,251242,297031,351062,414819,490052,578827,683582,807192,953052,1125168,1328263,1567917,1850707,2184400],
            glass:[138,525,982,1521,2156,2906,3792,4837,6069,7525,9241,11266,13656,16476,19804,23731,28366,33834,40285,47899,56883,67484,79993,94754,112173,132726,156978,185596,219366,259214,306235,361719,427191,504447,595611,703182,830117,979901,1156644,1365203],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],            
            time  :{a:0, b:0, c:0, d:0},
            icon  :'/cdn/all/both/img/city/marinechartarchive_l.png',
          },
          carpentering: {
            maxlevel:50,
            wood:[54,104,165,235,319,417,533,668,827,1013,1231,1487,1787,2137,2549,3030,3593,4252,5023,5925,6980,8213,9656,11343,13316,15621,18317,21468,25150,29454,34482,40359,47238,55289,64713,75743,88653,103763,121449,142149,166377,194734,227925,266773,312242,365461,427751,500657,585990,685867],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[0,0,0,0,0,0,0,308,381,469,575,701,853,1036,1254,1517,1832,2211,2664,3208,3862,4645,5586,6715,8070,9696,11646,13987,16796,20167,24212,29067,34894,41891,50291,60374,72479,87013,104459,125403,150548,180733,216971,260475,312702,375400,450669,541030,649509,779739],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],            
            time  :{a:125660, b:37, c:1.06, d:2808},
            icon  :'/cdn/all/both/img/city/carpentering_l.png',
          },
          dump: {
            maxlevel:80,
            wood:[550,990,1518,2153,2913,3827,4922,6237,7815,9708,11980,14706,17978,21904,26615,32268,39052,47193,56962,68685,82752,99633,119890,144198,173369,208372,250377,300784,361270,433855,520956,625478,750903,901415,1082027,1298764,1558847,1870946,2245466,2694889,3234264,3881593,4658484,5590867,6709864,8052825,9664577,11598917,13920408,16706541,20050310,24063326,28879536,34659697,41596742,49922218,59914014,71905643,86297363,103569548,124298713,149176766,179034093,214867283,257872389,309484852,371427409,445767602,534986785,642062945,770570110,924797636,1109893386,1332035549,1598638868,1918602120,2302605153,2763465357,3316565485,3980367111],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[427,801,1242,1763,2375,3103,3959,4969,6161,7567,9226,11184,13494,16221,19437,23233,27713,32999,39235,46595,55279,65527,77620,91888,108725,128594,152037,179702,212345,250864,296317,349952,413240,487921,576043,680028,802731,947520,1118371,1319975,1557920,1838759,2170225,2561442,3023182,3568157,4211372,4970538,5866554,6924092,8172266,9645444,11384185,13436361,15858473,18717208,22091275,26073570,30773734,36321177,42868631,50596365,59717144,70482083,83187570,98183417,115882499,136772114,161427405,190527194,224872670,265409450,313253612,369722426,436370616,515033175,607875879,717454917,846787273,999433788],
            glass:[602,985,1434,1959,2572,3032,4130,5113,6263,7608,9183,11024,13179,15701,18650,22102,26139,30864,36391,42859,50426,59279,69637,81756,95936,112525,131936,154646,173476,212303,248675,291232,341021,399276,467434,547178,640478,749641,877361,1026793,1201676,1406345,1645873,1926197,2254267,2638213,3087553,3613423,4228860,4949118,5792050,6778550,7933070,9284227,10865512,12716122,14881927,17416611,20383001,23854625,27917535,32672439,38237196,44749739,52371496,61291388,71730512,83947623,98245548,114978689,134561813,157480325,184302310,215692604,252429279,295422930,345739242,404625407,473541040,554194356],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            capacity:[32000,65401,101073,139585,181437,227119,277128,331991,392268,458564,531535,611896,700427,797983,905498,1024000,1154614,1298578,1457248,1632119,1824830,2037185,2271165,2528951,2812939,3125764,3470326,3849813,4267731,4727939,5234678,5792619,6406896,7083160,7827629,8647143,9549229,10542172,11635086,12838003,14161964,15619122,17222851,18987875,20930401,23068269,25421121,28010583,30860463,33996977,37448993,41248299,45429902,50032358,55098129,60673986,66811447,73567262,81003948,89190382,98202448,108123754,119046431,131072000,144312338,158890744,174943109,192619216,212084166,233519956,257127222,283127160,311763649,343305589,378049493,416322336,458484710,504934306,556109751,612494861],            
            time  :{a:32000, b:13, c:1.17, d:2160},
            icon  :'/cdn/all/both/img/city/dump_l.png',
          },
          embassy: {
            maxlevel:78,
            wood:[208,356,535,750,1008,1317,1689,2134,2668,331,4078,5002,611,7439,9036,1095,13247,16004,19313,23283,28048,33764,40625,48857,58737,70592,84817,101888,122373,146955,176454,211853,254352,305378,366639,440191,528497,634519,761809,914635,1098120,1318413,1582899,1900444,2281690,2739419,3288971,3948770,4740930,5692004,6833873,8204811,9850772,11826928,14199519,17048074,20468075,24574160,29503963,35422730,42528856,51060537,61303752,73601851,88367060,106094308,127377805,152930967,183610329,220444254,264667405,317762127,381508140,458042191,549929678,660250643,792703010,951726544],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[133,294,491,731,1023,1381,1816,2347,2996,3787,4753,593,7366,9119,11257,13865,17048,20931,25667,31446,38497,47097,57591,70393,86012,105066,128312,156673,191273,233485,284984,347812,424491,518075,632291,771688,941816,1149450,1402860,1712137,2089598,2550275,3112514,3798706,4636175,5658276,6905710,8428156,10286243,12553968,15321640,18699477,22822000,27853382,33993991,41488371,50634975,61798058,75422175,92049891,112343384,137110819,167338529,204230298,249255296,304206590,371272550,453123999,553020573,674940534,823739201,1005342303,1226981968,1497484733,1827623048,2230544282,2722294292,3322456440],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],            
            time  :{a:96000, b:7, c:1.05, d:10080},
            icon  :'/cdn/all/both/img/city/embassy_l.png',
          },
          fireworker: {
            maxlevel:50,
            wood:[234,303,382,473,578,699,837,996,1180,1391,1633,1911,2232,2601,3024,3512,4072,4717,5458,6311,7291,8419,9715,11206,12921,14893,17161,19768,22767,26216,30182,34744,39994,46038,52996,61005,70225,80839,93056,107119,123308,141943,163395,188088,216514,249236,286902,330262,380174,437629],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[116,182,259,348,452,571,711,872,1060,1277,1529,1823,2162,2555,3012,3542,4157,4869,5696,6655,7768,9059,10556,12292,14307,16644,19356,22500,26148,30379,35288,40981,47593,55271,64188,74543,86571,100537,116757,135594,157471,182876,212380,246644,286436,332648,386316,448642,521023,605081],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],            
            time  :{a:125660, b:37, c:1.06, d:2628},
            icon  :'/cdn/all/both/img/city/fireworker_l.png',
          },
          forester: {
            maxlevel:61,
            wood:[215,369,571,832,1173,1615,2189,2936,3907,5171,6812,8946,1172,15327,20015,26111,34034,44334,57725,75133,97764,127184,165429,215148,279783,36381,473043,615046,79965,1039635,1351616,1757192,2284467,2969962,3861151,5019757,6526023,8484269,11030122,14339902,18642840,24236948,31509665,40964688,53256856,69237501,90013417,117023507,152138445,197790230,257140627,334300142,434612711,565025809,734571624,954992608,1241554740,1614104820,2098445027,2728119932,3546739737],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[0,89,203,352,546,798,1125,155,2103,2822,3756,4971,6550,8603,11272,14742,19252,25115,32738,42647,55529,72276,94047,122348,159140,206971,269149,349982,455064,591671,769260,1000126,1300278,1690511,2197858,2857466,3715034,4829968,6279511,8164084,10614242,13799729,17941227,23325646,30326008,39427279,51259973,66643826,86644597,112647886,146455135,190408425,247552729,321846860,418437727,544016901,707284190,919550337,1195520604,1554313513,2020785330],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],            
            time  :{a:72000, b:11, c:1.1, d:6120},
            icon  :'/cdn/all/both/img/city/forester_l.png',
          },
          glassblowing: {
            maxlevel:61,
            wood:[235,401,617,898,1263,1738,2354,3157,4199,5554,7316,9607,12585,16456,21488,2803,36535,47591,61963,80649,104938,136516,177565,230931,300306,390494,507737,660154,858296,1115880,1450740,1886057,2451998,3187758,4144296,5387857,7004569,9106400,11838919,15391374,20009798,26014054,33819980,43968198,57161548,74313771,96612788,125602976,163292127,212290500,275991607,358807234,466472992,606445556,788419092,1024996652,1332563034,1732419548,2252259306,2928085165,3806703211],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[0,99,219,374,577,840,1182,1627,2205,2955,3931,5202,6852,8997,11786,15412,20125,26253,34219,44574,58037,75538,98289,127865,166315,216300,281279,365753,475568,618329,803918,1045183,1358855,1766663,2296859,2986173,3882359,5047500,6562314,8531740,11092214,14421117,18749061,24375871,31691353,41202296,53567581,69643833,90544754,117718283,153046903,198978051,258693667,336330631,437267348,568496341,739108674,960923743,1249308082,1624239900,2111693096],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],            
            time  :{a:72000, b:11, c:1.1, d:6120},
            icon  :'/cdn/all/both/img/city/glassblowing_l.png',
          },
          museum: {
            maxlevel:36,
            wood:[481,1234,2363,4055,6595,10405,16119,2469,37548,56833,85762,129155,194244,291878,438329,658006,987521,1481794,2223204,3335317,5003487,7505999,11260150,16891952,25340520,38014669,57027835,85550503,128338880,192528010,288821553,433276641,649981434,975071871,1462757402,2194360517],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[240,1023,2212,4021,6769,10946,17296,26948,41618,63917,97812,149332,227642,346674,527603,802613,1220630,1856015,2821801,4289796,6521148,9913144,15069498,22907948,34823590,52937193,80472645,122330751,185961486,282689952,429732042,653258546,993053083,1509592842,2294812419,3488466488],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],            
            satisfaction:[20,41,63,88,114,144,176,211,250,294,341,395,453,518,590,670,759,857,965,1086,1219,1367,1530,1711,1912,2134,2380,2652,2953,3286,3655,4064,4516,5016,5569,6182],
            maxBonus:[50,100,150,200,250,300,350,400,450,500,550,600,650,700,750,800,850,900,950,1000,1050,1100,1150,1200,1250,1300,1350,1400,1450,1500,1550,1600,1650,1700,1750,1800],
            time  :{a:18000, b:1, c:1.1, d:14040},
            icon  :'/cdn/all/both/img/city/museum_r.png',
          },
          optician: {
            maxlevel:50,
            wood:[102,161,231,311,405,513,638,784,952,1148,1376,1639,1944,2298,2710,3187,3741,4382,5127,5990,6992,8154,9503,11066,12881,14984,17426,20257,23541,27352,31771,36898,42851,49765,57795,67121,77951,90528,105135,122100,141801,164681,191253,222113,257950,299572,347908,404045,469238,544951],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[0,30,82,143,214,296,391,502,630,778,951,1150,1382,1652,1963,2325,2745,3232,3797,4453,5213,6094,7117,8304,9681,11277,13129,15277,17770,20661,24014,27905,32425,37679,43783,50877,59119,68698,79827,92761,107789,125252,145545,169125,196525,228365,265363,308354,358312,416362],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],            
            time  :{a:125660, b:37, c:1.06, d:2772},
            icon  :'/cdn/all/both/img/city/optician_l.png',
          },
          palace: {
            maxlevel:20,
            wood:[612,5008,13801,31386,66557,136898,27758,558944,1121673,2247131,4079425,7405756,13444352,24406772,44307863,80436146,146023147,265089275,481240988,873641108],
            wine:[0,0,0,9372,19014,38299,76868,154007,308284,616838,1233946,2468433,4937948,9878058,19760441,39529536,79076377,158187373,316443997,633026529],
            marble:[0,1233,3909,9262,19967,41378,84199,169841,341125,683694,1368832,2740554,5486893,10985369,21993924,44034272,88161489,176509062,353390684,707527273],
            glass:[0,0,0,0,18221,36464,72948,145917,291856,583733,1167487,2335016,4670116,9340398,18681126,37362913,74727147,149456937,298919160,597848890],
            sulfur:[0,0,2656,8858,21263,46072,95691,194928,393402,790351,1584248,3175603,6365454,12759465,25576175,51267094,102764189,205989415,412902967,827658356],
            time  :{a:11520, b:1, c:1.4, d:0},
            icon  :'/cdn/all/both/img/city/palace_l.png', 
          },
          palaceColony:{
            maxlevel:20,
            wood:[612,5008,13801,31386,66557,136898,27758,558944,1121673,2247131,4079425,7405756,13444352,24406772,44307863,80436146,146023147,265089275,481240988,873641108],
            wine:[0,0,0,9372,19014,38299,76868,154007,308284,616838,1233946,2468433,4937948,9878058,19760441,39529536,79076377,158187373,316443997,633026529],
            marble:[0,1233,3909,9262,19967,41378,84199,169841,341125,683694,1368832,2740554,5486893,10985369,21993924,44034272,88161489,176509062,353390684,707527273],
            glass:[0,0,0,0,18221,36464,72948,145917,291856,583733,1167487,2335016,4670116,9340398,18681126,37362913,74727147,149456937,298919160,597848890],
            sulfur:[0,0,2656,8858,21263,46072,95691,194928,393402,790351,1584248,3175603,6365454,12759465,25576175,51267094,102764189,205989415,412902967,827658356],
            time  :{a:11520, b:1, c:1.4, d:0},
            icon  :'/cdn/all/both/img/city/palaceColony_l.png',
          },
          pirateFortress:{
            maxlevel:30,
            wood:[387,779,1194,1664,2229,2947,3883,5117,6737,8844,11549,14976,19258,24539,30972,38724,47969,58894,71694,86577,103757,123463,145929,171405,200146,232419,268500,308676,353243,402507],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[215,434,673,956,1319,1808,2479,3396,4633,6274,8412,11149,14594,18866,24096,30418,37979,46932,57441,69677,83818,100053,118579,139599,163326,189984,219798,253009,289861,330608],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],            
            time   :{a:1550, b:1, c:1.2, d:1800},
            icon   :'/cdn/all/both/img/city/pirateFortress_l.png',
          },
          port: {
            maxlevel:74,
            wood:[51,129,235,368,547,768,1038,1414,1811,2352,3041,3863,4892,6108,7611,954,11808,14673,18143,22329,27356,33703,41278,50493,61881,75359,92107,112468,136757,166786,20283,246402,299897,364631,441994,537638,652033,790936,959771,1164024,1412356,1711592,2073513,2513976,3045758,3690710,4471434,5417311,6563277,7951657,9633732,11671630,14140619,17131894,20755934,25146596,30466049,36910766,44718784,54178492,65639285,79524466,96346887,116727883,141420228,171335934,207579939,251490917,304690720,369144286,447232209,541838670,656457962,795323552],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[0,0,0,0,0,151,280,464,680,978,1374,1871,2518,3318,4343,57,7366,9536,12267,15687,19949,25492,32366,41024,52078,65675,83109,105055,132228,166917,210098,264169,332782,418793,525453,6616,830582,1043000,1310271,1645242,2066850,2593492,3253373,4084635,5124763,6431252,8069741,10125666,12705379,15942324,20003946,25100346,31495153,39519164,49587450,62220829,78072811,97963398,122921504,154238178,193533392,242839836,304708068,382338449,479746700,601971622,755335750,947772410,1189236100,1492217420,1872389200,2349417230,2947977544,3699032889],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            loadingSpeed:[30,60,93,129,169,213,261,315,373,437,508,586,672,766,869,983,1108,1246,1398,1565,1748,195,2172,2416,2685,298,3305,3663,4056,4489,4965,5488,6064,6698,7394,8161,9004,9931,10951,12073,13308,14666,16159,17802,19609,21597,23784,2616,288,3174,3498,3852,4242,4668,5142,5664,624,687,7566,8334,9174,10104,1113,12258,13494,14862,16368,1803,19854,21864,24078,26514,29202,3216],            
            time  :{a:50400, b:23, c:1.15, d:1512},
            icon  :'/cdn/all/both/img/city/port_r.png',
          },
          safehouse: {
            maxlevel:60,
            wood:[97,213,345,497,669,866,1089,1345,1636,1967,2346,2777,3268,3829,4467,5196,6026,6972,8052,9281,10683,12282,14104,16181,1855,21249,24327,27836,31836,36396,41593,47519,54288,62022,70857,80952,92484,105659,120712,137908,157555,180,205642,234938,268407,306644,350328,400235,457252,522392,596811,681832,778964,889934,1016714,1161553,1327026,1516072,1732050,1978795],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[0,0,0,110,169,236,314,405,509,632,774,937,1128,1349,1604,1902,2247,2647,3110,3648,4272,4996,5836,6810,7940,9251,10772,12536,14582,16955,19708,22902,26613,30927,35939,41764,48532,56397,65538,76159,88501,102844,119511,13888,161387,187542,217936,253256,294299,341995,39742,461827,536672,623647,724717,842167,978652,1137256,1321562,1535739],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            time  :{a:96000, b:7, c:1.05, d:12960},
            icon  :'/cdn/all/both/img/city/safehouse_l.png',
          },
          shipyard: {
            maxlevel:38,
            wood:[90,173,278,410,577,786,105,1383,1802,2331,2997,3835,4892,6224,7903,10017,12681,16039,20268,25597,32312,40774,51434,64868,81792,103119,129989,163847,206506,260258,327985,41332,520843,656322,827026,1042112,1313121,1654593],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[0,0,0,0,0,669,904,1201,1575,2047,2641,339,4332,5521,7018,8904,11281,14276,1805,22804,28796,36344,45856,5784,7294,91966,11594,146145,184205,23216,292584,368717,464645,585515,737811,929703,1171488,1476136],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            time  :{a:64800, b:7, c:1.05, d:7128},
            icon  :'/cdn/all/both/img/city/shipyard_l.png',
          },
          stonemason: {
            maxlevel:61,
            wood:[235,401,617,898,1263,1738,2354,3157,4199,5554,7316,9607,12585,16456,21488,28030,36535,47591,61963,80649,104938,136516,177565,230931,300306,390494,507737,660154,858296,1115880,1450740,1886057,2451998,3187758,4144296,5387857,7004569,9106400,11838919,15391374,20009798,26014054,33819980,43968198,57161548,74313771,96612788,125602976,163292127,212290500,275991607,358807234,466472992,606445556,788419092,1024996652,1332563034,1732419548,2252259306,2928085165,3806703211],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[0,99,219,374,577,840,1182,1627,2205,2955,3931,5202,6852,8997,11786,15412,20125,26253,34219,44574,58037,75538,98289,127865,166315,216300,281279,365753,475568,618329,803918,1045183,1358855,1766663,2296859,2986173,3882359,5047500,6562314,8531740,11092214,14421117,18749061,24375871,31691353,41202296,53567581,69643833,90544754,117718283,153046903,198978051,258693667,336330631,437267348,568496341,739108674,960923743,1249308082,1624239900,2111693096],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            time  :{a:72000, b:11, c:1.1, d:6120},
            icon  :'/cdn/all/both/img/city/stonemason_l.png',
          },
          temple: {
            maxlevel:56,
            wood:[185,196,286,399,514,653,823,1029,1231,1524,1816,2160,2650,3143,3833,4408,5359,6163,7471,8812,10134,12236,14407,16568,19939,22931,27543,31674,37201,43672,51248,58934,69131,82618,93217,111324,128023,149952,175636,205722,240960,282234,330578,387204,453528,531214,622206,728784,853617,999835,1171097,1371695,1606653,1881858,2204204,2581763],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[148,163,249,363,487,646,850,1109,1384,1788,2223,2760,3533,4372,5565,6677,8471,10166,12858,15825,18990,23928,29398,35277,44302,53162,66630,79955,97989,120036,146983,176379,215889,269226,316976,395001,474001,579334,708075,865425,1057743,1292797,1580085,1931216,2360375,2884904,3525994,4309550,5267229,6437726,7868332,9616852,11753933,14365921,17558351,21460211],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],            
            time  :{a:2160, b:1, c:1.1, d:0},
            icon  :'/cdn/all/both/img/city/temple_l.png',
          },
          tavern: {
            maxlevel:70,
            wood:[86,190,315,465,645,860,1119,143,1803,225,2787,3431,4203,5131,6244,758,9183,11106,13414,16183,19507,23495,28281,34023,40915,49185,59108,71017,85306,102455,123032,147725,177357,212916,255585,306789,368233,441967,530448,636624,764036,916929,1100402,1320569,1584770,1901810,2282259,2738814,3286701,3944191,4733208,5680066,6816336,8179914,9816267,11779966,14136495,16964436,20358093,24430637,29317873,35182779,42220933,50667036,60802744,72966054,87562577,105079069,126099653,151325310],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[0,0,0,80,104,135,177,229,299,388,504,657,853,1109,1442,1875,2438,3169,412,5356,6963,9052,11768,15298,19887,25854,3361,43693,56801,73841,95994,124792,16223,210899,274168,356419,463345,602349,783054,1017969,1323361,1720368,2236479,2907424,3779650,4913547,6387610,8303891,10795057,14033573,18243642,23716732,30831747,40081266,52105638,67737320,88058503,114476037,148818827,193464446,251503743,326954818,425041200,552553478,718319416,933815102,1213959454,1578147056,2051590869,2667067736],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            maxWine:[4,8,13,18,24,30,37,44,51,60,68,78,88,99,110,122,136,150,165,180,197,216,235,255,277,300,325,351,378,408,439,472,507,544,584,626,670,717,766,818,874,933,995,1060,1129,1203,1280,1361,1449,1541,1640,1745,1857,1976,2102,2237,2380,2532,2694,2867,3050,3246,3453,3675,3910,4160,4426,4710,5011,5332],
            satisfaction:[12,24,36,48,61,73,86,99,112,125,138,152,165,179,193,207,222,236,251,266,282,297,313,329,345,361,378,395,412,430,448,466,484,502,521,540,560,580,600,620,641,662,683,705,727,749,772,795,819,843,867,891,916,942,968,994,1021,1048,1075,1103,1131,1160,1189,1219,1249,1280,1311,1343,1375,1408],
            maxBonus:[60,120,181,242,304,367,430,494,559,624,691,758,826,896,966,1037,1109,1182,1256,1332,1408,1485,1564,1644,1725,1807,1891,1975,2061,2149,2238,2328,2419,2512,2606,2702,2800,2898,2999,3101,3204,3310,3416,3525,3635,3747,3861,3976,4094,4213,4334,4457,4582,4709,4838,4969,5103,5238,5375,5515,5657,5801,5947,6096,6247,6400,6556,6714,6875,7038],
            time  :{a:10800, b:1, c:1.06, d:10440},
            icon  :'/cdn/all/both/img/city/taverne_r.png',
            wineUse: [0, 4, 8, 13, 18, 24, 30, 37, 44, 51, 60, 68, 78, 88, 99, 110, 122, 136, 150, 165, 180, 197, 216, 235, 255, 277, 300, 325, 351, 378, 408, 439, 472, 507, 544, 584, 626, 670, 717, 766, 818, 874, 933, 995, 1060, 1129, 1202, 1280, 1362],
          },
          townHall: {
            maxlevel:66,
            wood:[0,135,288,535,793,1195,1732,2327,3148,4107,5308,6943,8841,11199,14124,18047,21863,27765,34599,42385,52638,64331,80802,9721,12177,146383,180609,222632,270815,333385,405226,492419,59823,735066,892521,1095676,1315122,1613532,1957605,2374710,2880686,3494469,4239031,5142235,6237884,7566981,9179268,11135082,13507617,16385666,19876937,24112088,29249616,35481790,43041845,52212708,63337596,76832847,93203512,113062251,137152263,166375099,201824404,244826841,296991745,360271352],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[0,0,0,0,245,473,804,1213,1798,2532,3501,4871,6567,8784,11674,15698,19995,26678,34915,44905,58539,75091,98986,12498,164305,207293,26843,347289,443409,572956,731026,932491,1189231,1534000,1955370,2520083,3175628,4090537,5210432,6636074,8451790,10764309,13709563,17460677,22238144,28322787,36072268,45942107,58512461,74522227,94912471,120881750,153956559,196081062,249731375,318061105,405086733,515923697,657087086,836874602,1065854305,1357485815,1728911472,2201963988,2804449784,3571783476],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            time  :{a:1800, b:1, c:1.17, d:-1080},
            icon  :'/cdn/all/both/img/city/townhall_l.png',
          },
          vineyard: {
            maxlevel:50,
            wood:[291,363,447,542,651,778,923,1091,1283,1504,1758,2050,2386,2773,3217,3728,4316,4992,5769,6664,7691,8874,10234,11797,13595,15664,18041,20776,23921,27538,31697,36480,41984,48318,55608,63998,73654,84767,97556,112275,129215,148711,171148,196970,226689,260891,300253,345554,397690,457692],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[105,170,245,332,433,550,686,843,1026,1238,1484,1769,2100,2484,2930,3446,4046,4741,5547,6482,7568,8826,10286,11979,13944,16223,18866,21932,25489,29615,34401,39953,46401,53891,62588,72689,84421,98046,113870,132247,153591,178380,207168,240604,279436,324534,376912,437743,508391,590441],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],            
            time  :{a:125660, b:37, c:1.06, d:2232},
            icon  :'/cdn/all/both/img/city/vineyard_l.png',
          },
          wall: {
            maxlevel:48,
            wood:[98,310,565,870,1237,1677,2205,2839,3599,4512,5608,6922,8498,10391,12662,15387,18657,22581,2729,32941,39722,47859,57623,6934,83401,100275,120522,144819,173976,208964,250949,301332,361791,434343,521404,625877,751246,901687,1082217,1298853,1558817,1870773,2245120,2694337,3233397,3880269,4656516,5588011],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[0,174,443,767,1155,1621,218,285,3655,4621,578,717,8839,10842,13245,16129,19589,23742,28725,34705,41881,50491,60824,73223,88103,105958,127384,153096,18395,220975,265404,31872,382698,459473,551602,662157,794823,954022,1145061,1374308,1649405,1979520,2375658,2851025,3421465,4105993,4927426,5913145],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],            
            time  :{a:57600, b:11, c:1.1, d:3240},
            icon  :'/cdn/all/both/img/city/wall.png',
          },
          warehouse: {
            maxlevel:85,
            wood:[137,247,380,538,728,957,123,1559,1953,2426,2995,3676,4494,5476,6653,8066,9763,11798,1424,17171,20688,24908,29972,36049,43342,52093,62594,75195,90318,108464,130239,156369,187725,225353,270506,324691,389711,467736,561366,673722,808565,970396,1164618,1397712,1677459,2013197,2416131,2899711,3480078,4176604,5012536,6015778,7219815,8664836,10399072,12480410,14978319,17976177,21574046,25892014,31074209,37293603,44757785,53715898,64466945,77369775,92855062,111439673,133743930,160512304,192638273,231194140,277466828,333000831,399649768,479638253,575636150,690847686,829118402,995063513,1194221950,1433241242,1720099398,2064371199,2477547780],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[0,0,0,82,181,300,442,614,819,1066,1362,1717,2143,2653,3268,4004,4887,5946,7218,8745,10577,12775,15412,18577,22376,26934,32403,38966,46842,56293,67634,81245,97576,117174,140691,168911,202776,243415,29218,350699,420938,505244,606436,727895,87368,1048662,1258691,1510786,1813370,2176555,2612483,3135717,3763747,4517560,5422349,6508351,7811859,9376439,11254377,13508432,16213936,19461305,23359065,28037479,33652897,40392985,48482997,58193295,69848398,83837814,100629067,120783314,144974107,174009893,208861041,250692266,300901556,361166892,433502324,520325282,624537363,749621309,899757388,1079963105,1296260886],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            capacity:[8000,16401,25455,35331,46181,58159,71421,86138,102493,120687,140942,163502,188637,216646,24786,282647,321416,364622,412768,466416,526189,592779,666959,749584,841609,944094,1058219,1185297,1326787,1484315,1659690,1854922,2072252,2314171,2583453,2883186,3216807,3588142,4001450,4461476,4973499,5543400,6177729,6883779,7669673,8544460,9518219,10602179,11808851,13152172,14647676,16312668,18166439,20230485,22528769,25088000,27937955,31111829,34646637,38583648,42968887,47853679,53295269,59357506,66111616,73637056,82022473,91366775,101780329,113386298,126322135,140741251,156814887,174734197,194712581,216988297,241827374,269526873,300418536,334872863,373303675,416173213,463997848,517354466,576887609],
            time  :{a:2880, b:1, c:1.14, d:2160},
            icon  :'/cdn/all/both/img/city/warehouse_l.png',
          },
          winegrower: {
            maxlevel:61,
            wood:[235,401,617,898,1263,1738,2354,3157,4199,5554,7316,9607,12585,16456,21488,28030,36535,47591,61963,80649,104938,136516,177565,230931,300306,390494,507737,660154,858296,1115880,1450740,1886057,2451998,3187758,4144296,5387857,7004569,9106400,11838919,15391374,20009798,26014054,33819980,43968198,57161548,74313771,96612788,125602976,163292127,212290500,275991607,358807234,466472992,606445556,788419092,1024996652,1332563034,1732419548,2252259306,2928085165,3806703211],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[0,99,219,374,577,840,1182,1627,2205,2955,3931,5202,6852,8997,11786,15412,20125,26253,34219,44574,58037,75538,98289,127865,166315,216300,281279,365753,475568,618329,803918,1045183,1358855,1766663,2296859,2986173,3882359,5047500,6562314,8531740,11092214,14421117,18749061,24375871,31691353,41202296,53567581,69643833,90544754,117718283,153046903,198978051,258693667,336330631,437267348,568496341,739108674,960923743,1249308082,1624239900,2111693096],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],            
            time  :{a:72000, b:11, c:1.1, d:6120},
            icon  :'/cdn/all/both/img/city/winegrower_l.png',
          },
          workshop: {
            maxlevel:32,
            wood:[189,329,489,671,879,1117,1387,1695,2046,2447,2904,3424,4017,4693,5465,6344,7346,8488,9791,11275,12967,14896,17096,19604,22462,25721,29436,33671,38498,44002,50277,57429],
            wine:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            marble:[81,143,215,300,396,509,639,791,967,1171,1407,1682,2000,2369,2797,3294,3870,4539,5314,6214,7257,8468,9871,11500,13390,15581,18123,21072,24493,28461,33064,38404],
            glass:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            sulfur:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
            time  :{a:96000, b:7, c:1.05, d:11880},
            icon  :'/cdn/all/both/img/city/workshop_l.png',
          },
        },
        
        Research: {
          Seafaring: {
            CARPENTRY: 2150,
            DECK_WEAPONS: 1010,
            PIRACY: 1170,
            SHIP_MAINTENANCE: 1020,
            DRAFT: 1130,
            EXPANSION: 1030,
            FOREIGN_CULTURES: 1040,
            PITCH: 1050,
            MARKET: 2070,
            GREEK_FIRE: 1060,
            COUNTERWEIGHT: 1070,
            DIPLOMACY: 1080,
            SEA_MAPS: 1090,
            PADDLE_WHEEL_ENGINE: 1100,
            CAULKING: 1140,
            MORTAR_ATTACHMENT: 1110,
            MASSIVE_RAM: 1150,
            OFFSHORE_BASE: 1160,
            SEAFARING_FUTURE: 1999,
          },
          
          Economy: {
            CONSERVATION: 2010,
            PULLEY: 2020,
            WEALTH: 2030,
            WINE_CULTURE: 2040,
            IMPROVED_RESOURCE_GATHERING: 2130,
            GEOMETRY: 2060,
            ARCHITECTURE: 1120,
            HOLIDAY: 2080,
            LEGISLATION: 2170,
            CULINARY_SPECIALITIES: 2050,
            HELPING_HANDS: 2090,
            SPIRIT_LEVEL: 2100,
            WINE_PRESS: 2140,
            DEPOT: 2160,
            BUREACRACY: 2110,
            UTOPIA: 2120,
            ECONOMIC_FUTURE: 2999,
          },
          
          Science: {
            WELL_CONSTRUCTION: 3010,
            PAPER: 3020,
            ESPIONAGE: 3030,
            POLYTHEISM: 3040,
            INK: 3050,
            GOVERNMENT_FORMATION: 3150,
            INVENTION: 3140,
            CULTURAL_EXCHANGE: 3060,
            ANATOMY: 3070,
            OPTICS: 3080,
            EXPERIMENTS: 3081,
            MECHANICAL_PEN: 3090,
            BIRDS_FLIGHT: 3100,
            LETTER_CHUTE: 3110,
            STATE_RELIGION: 3160,
            PRESSURE_CHAMBER: 3120,
            ARCHIMEDEAN_PRINCIPLE: 3130,
            SCIENTIFIC_FUTURE: 3999,
          },
          
          Military: {
            DRY_DOCKS: 4010,
            MAPS: 4020,
            PROFESSIONAL_ARMY: 4030,
            SEIGE: 4040,
            CODE_OF_HONOR: 4050,
            BALLISTICS: 4060,
            LAW_OF_THE_LEVEL: 4070,
            GOVERNOR: 4080,
            PYROTECHNICS: 4130,
            LOGISTICS: 4090,
            GUNPOWDER: 4100,
            ROBOTICS: 4110,
            CANNON_CASTING: 4120,
            MILITARISTIC_FUTURE: 4999,
          },
        },
        
        Government: {
          ANARCHY: 'anarchie',
          
          IKACRACY: 'ikakratie',
          ARISTOCRACY: 'aristokratie',
          DICTATORSHIP: 'diktatur',
          DEMOCRACY: 'demokratie',
          NOMOCRACY: 'nomokratie',
          OLIGARCHY: 'oligarchie',
          TECHNOCRACY: 'technokratie',
          THEOCRACY: 'theokratie',
        },
        
        TradeGoodOrdinals: {
          WINE: 1,
          MARBLE: 2,
          GLASS: 3,
          SULFUR: 4,
        },
        
        Time: {
          SECONDS_PER_HOUR: 3600,
          SECONDS_PER_MINUTE: 60,
          MILLIS_PER_DAY: 1000 * 60 * 60 * 24,
          MILLIS_PER_HOUR: 1000 * 60 * 60,
          MILLIS_PER_SECOND: 1000,
          MILLIS_PER_MINUTE: 60000,
          MINUTES_PER_DAY: 24 * 60,
          MINUTES_PER_HOUR: 60,
          HOURS_PER_DAY: 24,
          HOURS_PER_WEEK: 24 * 7,
          
          SAFE_TIME_DELTA: 1000,
          INITIAL_PAGE_LOAD_DELTA: 2000,
        },
        
        GamePlay: {
          BUILDING_SPOTS: 24,
          HAPPINESS_PER_CULTURAL_GOOD: 50,
          HAPPINESS_PER_WINE_SERVING_LEVEL: 60,
          BASE_RESOURCE_PROTECTION: 100,
          RESOURCES_PER_TRANSPORT: 500,
          RESOURCE_PROTECTION_WAREHOUSE: 480,
          RESOURCE_PROTECTION_WAREHOUSE_INACTIVE: 80,
        },
            
        BaseView: {
          ISLAND: 'island',
          CITY: 'city',
          WORLD: 'worldview',
        },
        
        Military: {
          // Army
          HOPLITE: 'phalanx', 
          STEAM_GIANT: 'steamgiant', 
          SPEARMAN: 'spearman', 
          SWORDSMAN: 'swordsman', 
          SLINGER: 'slinger', 
          ARCHER: 'archer', 
          GUNNER: 'marksman',
          BATTERING_RAM: 'ram', 
          CATAPULT: 'catapult',
          MORTAR: 'mortar', 
          GYROCOPTER: 'gyrocopter',
          BALLOON_BOMBADIER: 'bombardier', 
          COOK: 'cook', 
          DOCTOR: 'medic',
          ARMY: 'army',
          
          // Navy
          RAM_SHIP: 'ship_ram', 
          FLAME_THROWER: 'ship_flamethrower',
          STEAM_RAM: 'ship_steamboat', 
          BALLISTA_SHIP: 'ship_ballista', 
          CATAPULT_SHIP: 'ship_catapult', 
          MORTAR_SHIP: 'ship_mortar', 
          SUBMARINE: 'ship_submarine',
          PADDLE_SPEED_SHIP: 'ship_paddlespeedship',
          BALLOON_CARRIER: 'ship_ballooncarrier',
          TENDER: 'ship_tender',
          ROCKET_SHIP: 'ship_rocketship',
          NAVY: 'navy',
        },
        
        UnitData: {
          spearman: {
            minimumBuildingLevelToBuild: 1,
            baseBuildTime: 60,
            isArmy: true,
            speed: 60,
            cargoSize: 3,
          },
          slinger: {
            minimumBuildingLevelToBuild: 2,
            baseBuildTime: 90,
            isArmy: true,
            speed: 60,
            cargoSize: 3,
          },
          ram: {
            minimumBuildingLevelToBuild: 3,
            baseBuildTime: 600,
            isArmy: true,
            speed: 40,
            cargoSize: 30,
          },
          phalanx: {
            minimumBuildingLevelToBuild: 4,
            baseBuildTime: 300,
            isArmy: true,
            speed: 60,
            cargoSize: 5,
          },
          cook: {
            minimumBuildingLevelToBuild: 5,
            baseBuildTime: 1200,
            isArmy: true,
            speed: 40,
            cargoSize: 20,
          },
          swordsman: {
            minimumBuildingLevelToBuild: 6,
            baseBuildTime: 180,
            isArmy: true,
            speed: 60,
            cargoSize: 3,
          },
          archer: {
            minimumBuildingLevelToBuild: 7,
            baseBuildTime: 240,
            isArmy: true,
            speed: 60,
            cargoSize: 5,
          },
          catapult: {
            minimumBuildingLevelToBuild: 8,
            baseBuildTime: 1800,
            isArmy: true,
            speed: 40,
            cargoSize: 30,
          },
          medic: {
            minimumBuildingLevelToBuild: 9,
            baseBuildTime: 1200,
    
            isArmy: true,
            speed: 60,
            cargoSize: 10,
          },
          gyrocopter: {
            minimumBuildingLevelToBuild: 10,
            baseBuildTime: 900,
            isArmy: true,
            speed: 80,
            cargoSize: 15,
          },
          bombardier: {
            minimumBuildingLevelToBuild: 11,
            baseBuildTime: 1800,
            isArmy: true,
            speed: 20,
            cargoSize: 30,
          },
          steamgiant: {
            minimumBuildingLevelToBuild: 12,
            baseBuildTime: 900,
            isArmy: true,
            speed: 40,
            cargoSize: 15,
          },
          marksman: {
            minimumBuildingLevelToBuild: 13,
            baseBuildTime: 600,
            isArmy: true,
            speed: 60,
            cargoSize: 5,
          },
          mortar: {
            minimumBuildingLevelToBuild: 14,
            baseBuildTime: 2400,
            isArmy: true,
            speed: 40,
            cargoSize: 30,
          },
          
          barbarian: {
            minimumBuildingLevelToBuild: 1,
            baseBuildTime: 1,
            isArmy: true,
            speed: 40,
            cargoSize: 5,
          },
          
          ship_ram: {
            minimumBuildingLevelToBuild: 1,
            baseBuildTime: 2400,
            isArmy: false,
            speed: 40,
            cargoSize: 0,
          },
          ship_flamethrower: {
            minimumBuildingLevelToBuild: 4,
            baseBuildTime: 1800,
            isArmy: false,
            speed: 40,
            cargoSize: 0,
          },
          ship_submarine: {
            minimumBuildingLevelToBuild: 19,
            baseBuildTime: 3600,
            isArmy: false,
            speed: 40,
            cargoSize: 0,
          },
          ship_ballista: {
            minimumBuildingLevelToBuild: 3,
            baseBuildTime: 3000,
            isArmy: false,
            speed: 40,
            cargoSize: 0,
          },
          ship_catapult: {
            minimumBuildingLevelToBuild: 3,
            baseBuildTime: 3000,
            isArmy: false,
            speed: 40,
            cargoSize: 0,
          },
          ship_mortar: {
            minimumBuildingLevelToBuild: 17,
            baseBuildTime: 3000,
            isArmy: false,
            speed: 30,
            cargoSize: 0,
          },
          ship_steamboat: {
            minimumBuildingLevelToBuild: 15,
            baseBuildTime: 2400,
            isArmy: false,
            speed: 40,
            cargoSize: 0,
          },
          ship_rocketship: {
            minimumBuildingLevelToBuild: 11,
            baseBuildTime: 3600,
            isArmy: false,
            speed: 30,
            cargoSize: 0,
          },
          ship_paddlespeedship: {
            minimumBuildingLevelToBuild: 13,
            baseBuildTime: 1800,
            isArmy: false,
            speed: 60,
            cargoSize: 0,
          },
          ship_ballooncarrier: {
            minimumBuildingLevelToBuild: 7,
            baseBuildTime: 3900,
            isArmy: false,
            speed: 20,
            cargoSize: 0,
          },
          ship_tender: {
            minimumBuildingLevelToBuild: 9,
            baseBuildTime: 2400,
            isArmy: false,
            speed: 30,
            cargoSize: 0,
          },
        },
    
        UnitIds: {
          301: 'slinger',
          302: 'swordsman',
          303: 'phalanx',
          304: 'marksman',
          305: 'mortar',
          306: 'catapult',
          307: 'ram',
          308: 'steamgiant',
          309: 'bombardier',
          310: 'cook',
          311: 'medic',
          312: 'gyrocopter',
          313: 'archer',
          315: 'spearman',
          316: 'barbarian',
          
          210: 'ship_ram',
          211: 'ship_flamethrower',
          212: 'ship_submarine',
          213: 'ship_ballista',
          214: 'ship_catapult',
          215: 'ship_mortar',
          216: 'ship_steamboat',
          217: 'ship_rocketship',
          218: 'ship_paddlespeedship',
          219: 'ship_ballooncarrier',
          220: 'ship_tender',
        },
        
        IkariamAjaxResponseType: {
          RELOAD: 'reload',
          PROVIDE_FEEDBACK: 'provideFeedback',
          QUEST_DATA: 'questData',
          UPDATE_GLOBAL_DATA: 'updateGlobalData',
          UPDATE_TEMPLATE_DATA: 'updateTemplateData',
          UPDATE_BACKGROUND_DATA: 'updateBackgroundData',
          CLOSE_VIEW: 'closeView',
          CHANGE_VIEW: 'changeView',
          ADD_WINDOW: 'addWindow',
          CREATE_VIEW: 'createView',
          EVAL_SCRIPT: 'evalScript',
        },
        
        
        CityType: {
          OWN: 'ownCity',
          DEPLOYMENT: 'deployedCities',
          OCCUPATION: 'occupiedCities',
        },
        
        View: {
          CITY_DETAIL: 'cityDetails',
          CITY_MILITARY: 'cityMilitary',
          RELATED_CITIES: 'relatedCities',
          ACADEMY: 'academy',
          PALACE: 'palace',
          MUSEUM: 'museum',
          ASSIGN_CULTURAL_POSSESSIONS: 'culturalPossessions_assign',
          TOWN_HALL: 'townHall',
          TEMPLE: 'temple',
          RESEARCH_ADVISOR: 'researchAdvisor',
          FINANCES: 'finances',
          BARRACKS: 'barracks',
          SHIPYARD: 'shipyard',
          PIRATE_FORTRESS: 'pirateFortress',
          MILITARY_ADVISOR: 'militaryAdvisor',
          MILITARY_ADVISOR_REPORT: 'militaryAdvisorReportView',
          PREMIUM: 'premium',
          TRANSPORT: 'transport',
          DEPLOY: 'deployment',
          BRANCH_OFFICE: 'branchOffice',
          BLACK_MARKET: 'blackMarket',
          TAKE_OFFER: 'takeOffer',
          RESOURCE: 'resource',
          TRADE_GOOD: 'tradegood',
          ABOLISH_CITY: 'abolishCity',
          HIDEOUT: 'safehouse',
          PILLAGE: 'plunder',
          BLOCKADE: 'blockade',
          SEND_SPY: 'sendSpy',
          SPY_MISSION: 'spyMissions',
          HIGH_SCORE: 'highscore',
          ALLIANCE_PAGE: 'allyPage',
          OCCUPY: 'occupy',
          COLONIZE: 'colonize',
        },
    
        PlayerState: {
          INACTIVE: 'inactive',
          NORMAL: '',
          VACATION: 'vacation',
          NEW: 'noob',
        },
    
        CombatType: {
          BLOCKADE: 'blockade',
          PILLAGE: 'plunder',
        },
      };
      
      var EmpireData = function() {
        function Military(city) {
          this._ikaToolsType = 'military';
          
          this.lastArmyUpdate = 0;
          this.lastNavyUpdate = 0;
          
          this.present = new MilitaryUnits();
          this.armyTrainingBatches = [];
          this.navyTrainingBatches = [];
          
          this._setCity(city);
        }
        
        $.extend(Military.prototype, {
          _setCity: function setCity(city) {
            if (city) {
              this.city = Utils.fixedFunction(city);
              this._startArmyTrainingTimers();
              this._startNavyTrainingTimers();
            }
          },
          _startTrainingTimers: function startTrainingTimers(batches) {
            var military = this;
            while (batches.length > 0 && batches[0].completionTime <= View.gameTimeNow()) {
              var batch = batches.shift();
              if ((batch.type == Constants.Military.ARMY && 
                   batch.getCompletionTime() > this.lastArmyUpdate) || 
                  (batch.type == Constants.Military.NAVY && 
                   batch.getCompletionTime() > this.lastNavyUpdate)) {
                military.present._increment(batch.getUnits());
              }
            }
            $.each(batches, function startTrainingBatchTimers(index, batch) {
              if (batch.completionEvent) {
                batch.completionEvent();
              }
              batch.completionEvent = militaryChangedEvent().scheduleSend(
                  'MilitaryTrainingComplete[' + military.city().getId() + ']',
                  batch.getCompletionTime() - View.gameTimeNow() + 
                      Constants.Time.SAFE_TIME_DELTA,
                  function militaryTrainingComplete() {
                    military.present._increment(batches.shift().getUnits());
                    empireData.saveAsync();
                  },
                  [{
                    city: military.city(),
                    military: military,
                    type: 'training_complete',
                  }]);
            });
          },
          _startArmyTrainingTimers: function startArmyTrainingTimers() {
            this._startTrainingTimers(this.armyTrainingBatches);
          },
          _startNavyTrainingTimers: function startNavyTrainingTimers() {
            this._startTrainingTimers(this.navyTrainingBatches);
          },
          _updatePresent: function updatePresent(unit, count) {
            return this.present._setCount(unit, count);
          },
          _markPresentUpdated: function markPresentUpdated(army, navy) {
            if (army || army === undefined) {
              this.lastArmyUpdate = View.gameTimeNow();
            }
            if (navy || navy === undefined) {
              this.lastNavyUpdate = View.gameTimeNow();
            }
          },
          _setArmyTrainingBatches: function setArmyTrainingBatches(batches) {
    
            $.each(this.armyTrainingBatches, function cancelTrainingBatchTimer(index, batch) {
              if (batch.completionEvent) {
                batch.completionEvent();
              }
            });
            
            this.armyTrainingBatches = batches;
            
            this._startArmyTrainingTimers();
          },
          _setNavyTrainingBatches: function setNavyTrainingBatches(batches) {
            $.each(this.navyTrainingBatches, function cancelTrainingBatchTimer(index, batch) {
              if (batch.completionEvent) {
                batch.completionEvent();
              }
            });
    
            this.navyTrainingBatches = batches;
    
            this._startNavyTrainingTimers();
          },
          _increment: function increment(units) {
            this.present._increment(units);
          },
          _decrement: function decrement(units) {
            this.present._decrement(units);
          },
          _clear: function clear() {
            this.present._clear();
            this.armyTrainingBatches = [];
            this.navyTrainingBatches = [];
          },
          getTrainingBatches: function getTrainingBatches(batches) {
            return $.merge($.merge([], this.armyTrainingBatches), this.navyTrainingBatches);
          },
          getPresent: function getPresent() {
            return this.present;
          },
        });
        
        function MilitaryUnits() {
          this._ikaToolsType = 'militaryUnits';
          
          this.units = {};
        }
        
        $.extend(MilitaryUnits.prototype, {
          _setCount: function setCount(unit, count) {
            var oldCount = this.units[unit];
            this.units[unit] = count;
            return oldCount != count;
          },
          _increment: function increment(units) {
            $.each(units.units, function(unit, count) {
              this._setCount(unit, (this.getCount(unit) || 0) + count);
            }.bind(this));
          },
          _decrement: function decrement(units) {
            $.each(units.units, function(unit, count) {
              this._setCount(unit, Math.max(0, (this.getCount(unit) || 0) - count));
            }.bind(this));
          },
          _clear: function clear() {
            this.units = {};
          },
          getCount: function getCount(unit) {
            return this.units[unit];
          },
          getCounts: function getCounts() {
            return this.units;
          },
          getCargoSize: function getCargoSize() {
            var cargoSize = 0;
            $.each(this.units, function addCargoSize(unit, count) {
              cargoSize += Constants.UnitData[unit].cargoSize * count;
            });
            return cargoSize;
          },
        });
        
        function TrainingBatch(type, completionTime, units) {
          this._ikaToolsType = 'trainingBatch';
          
          this.type = type;
          this.units = units;
          this.completionTime = completionTime;
        }
        
        $.extend(TrainingBatch.prototype, {
          getUnits: function getUnits() {
            return this.units;
          },
          getCompletionTime: function getCompletionTime() {
            return this.completionTime;
          },
          _getType: function getType() {
            return this.type;
          },
        });
        
        function City(id, type) {
          this._ikaToolsType = 'city';
          
          if (id) {
            this.id = id;
            this.type = type;
            
            this.level = 0;
    
            this.resources = {
              wood: new City.Resource(this),
              wine: new City.Resource(this),
              marble: new City.Resource(this),
              glass: new City.Resource(this),
              sulfur: new City.Resource(this),
            };
    
            this.buildings = new Array(Constants.GamePlay.BUILDING_SPOTS);
            for (var i = 0; i < Constants.GamePlay.BUILDING_SPOTS; i++) {
              this.buildings[i] = new City.Building(this);
            }
            this.military = new Military();
            
            this.actionPoints = 0;
            this.scientists = 0;
            this.culturalGoods = 0;
            this.priests = 0;
            this.tavernWineLevel = 0;
            this.population = undefined;
            
            this.resourceUpdateTime = 0;
            this.islandCoordinates = undefined;
          }
        }
          
        $.extend(City.prototype, {
          _postLoad: function postLoad() {
            while (this.buildings.length < Constants.GamePlay.BUILDING_SPOTS) {
               this.buildings.push(new City.Building(this));
            }
            for (var i = 0; i < Constants.GamePlay.BUILDING_SPOTS; i++) {
              this.buildings[i]._setCity(this);
            }
            if (this.isOwn()) {
              this.resources[Constants.Resources.WOOD]._setCity(this);
              this.resources[Constants.Resources.WINE]._setCity(this);
              this.resources[Constants.Resources.MARBLE]._setCity(this);
              this.resources[Constants.Resources.GLASS]._setCity(this);
              this.resources[Constants.Resources.SULFUR]._setCity(this);
            }
            this.military._setCity(this);
          },
          _updateFromGlobalData: function _updateFromGlobalData(data, correctWineConsumption) {
            var changes = [];
            this._updateActionPoints(data.maxActionPoints, changes);
            
            if (this.isOwn()) {                
              var wineConsumption = data.wineSpendings;
              var baseWineConsumption = data.wineSpendings;
              
              var winePress = this.getBuildingByType(Constants.Buildings.WINE_PRESS);
              if (correctWineConsumption) {
                if (winePress) {
                  wineConsumption = wineConsumption * (100 - winePress.getLevel()) / 100;
                }
              } else {
                if (winePress) {
                  baseWineConsumption = Math.floor(
                      wineConsumption / (100 - winePress.getLevel()) * 100);
                }
              }
              
              var use = Constants.BuildingData[Constants.Buildings.TAVERN].wineUse;
              for (var i = 0; i < 48; i++) {
                if (use[i] >= baseWineConsumption) {
                  this._updateTavernWineLevel(i, changes);
                  break;
                }
              }
              
              this._updateResources(data.currentResources, 
                                    data.maxResources,
                                    data.resourceProduction,
                                    data.tradegoodProduction,
                                    wineConsumption / Constants.Time.SECONDS_PER_HOUR,
                                    changes);
            }
            
            raiseResourcesChanged(changes);
          },
          _updateFromBackgroundData: function _updateFromBackgroundData(data) {
            this.islandId = parseInt(data.islandId);
            if (this.isOwn()) {
              this._updateBuildings(data.position);
            }
          },
          _updateBuildings: function _updateBuildings(buildingsData) {
            var changes = [];
            for (var i = 0; i < Constants.GamePlay.BUILDING_SPOTS; i++) {
              var building = this.buildings[i];
              if (buildingsData[i]) {
                if (building._update(i, buildingsData[i])) {
                  changes.push({
                    city: this,
                    building: building,
                    type: Constants.BuildingEventType.DATA_REFRESH,
                  });
                }
              }
            }
            this.level = this.buildings[0].getLevel();
            raiseBuildingsChanged(changes);
          },
          _updateResources: function updateResources(
              currentInfo, maxInfo, woodProduction, resourceProduction, wineConsumption,
              changedAccumulator) {
            this._updateResource(Constants.Resources.WOOD, 
                changedAccumulator,
                currentInfo["resource"],
    
                woodProduction);
            this._updateResource(Constants.Resources.WINE,
                changedAccumulator,
                currentInfo["1"],
                this.getTradeGoodType() == Constants.Resources.WINE ? 
                    resourceProduction : undefined,
                wineConsumption);
            this._updateResource(Constants.Resources.MARBLE,
                changedAccumulator,
                currentInfo["2"], 
                this.getTradeGoodType() == Constants.Resources.MARBLE ? 
                    resourceProduction : undefined);
            this._updateResource(Constants.Resources.GLASS,
                changedAccumulator,
                currentInfo["3"], 
                this.getTradeGoodType() == Constants.Resources.GLASS ? 
                    resourceProduction : undefined);
            this._updateResource(Constants.Resources.SULFUR,
                changedAccumulator,
                currentInfo["4"], 
                this.getTradeGoodType() == Constants.Resources.SULFUR ? 
                    resourceProduction : undefined);
            this._updatePopulation(currentInfo.population, changedAccumulator);
            
            this.resourceUpdateTime = View.gameTimeNow();
          },
          _updateResource: function updateResource(
              name, changedAccumulator, current, max, production, consumption) {
            var resource = this.resources[name];
            if (resource._update(current, max, production, consumption)) {
              changedAccumulator.push({
                city: this,
                type: name,
                value: resource,
              });
            }
          },
          _incrementResource: function incrementResource(name, changedAccumulator, delta) {
            var resource = this.resources[name];
            if (delta) {
              resource._increment(delta);
              changedAccumulator.push({
                city: this,
                type: name,
                value: resource,
              });
            }
          },
          _updateActionPoints: function updateActionPoints(actionPoints, changedAccumulator) {
            if (this.actionPoints != actionPoints) {
              changedAccumulator.push({
                city: this,
                type: Constants.Resources.ACTION_POINTS,
                value: actionPoints,
              });
            }
            this.actionPoints = actionPoints;
          },
          _updateActionPointsBy: function updateActionPointsBy(delta, changedAccumulator) {
            this._updateActionPoints(
                Math.max(0, Math.min(this.actionPoints + delta, this.getMaxActionPoints())), 
                changedAccumulator);
          },
          _updateScientists: function updateScientists(scientists, changedAccumulator) {
            if (this.scientists != scientists) {
              changedAccumulator.push({
                city: this,
                type: Constants.Resources.SCIENTISTS,
                value: scientists,
              });
            }
            this.scientists = scientists;
          },
          _updateTavernWineLevel: function updateTavernWineLevel(level, changedAccumulator) {
            if (this.tavernWineLevel != level) {
              changedAccumulator.push({
                city: this,
                type: Constants.Resources.TAVERN_WINE_LEVEL,
                value: level,
              });
            }
            this.tavernWineLevel = level;
          },
          _updateCulturalGoods: function updateCulturalGoods(culturalGoods, changedAccumulator) {
            if (this.culturalGoods != culturalGoods) {
              changedAccumulator.push({
                city: this,
                type: Constants.Resources.CULTURAL_GOODS,
                value: culturalGoods,
              });
            }
            this.culturalGoods = culturalGoods;
          },
          _updatePopulation: function updatePopulation(population, changedAccumulator) {
            if (Math.abs(this.getPopulationData().population - population) >= 1) {
              changedAccumulator.push({
                city: this,
                type: Constants.Resources.POPULATION,
                value: population,
              });
            }
            this.population = population;
          },
          _updatePriests: function updatePriests(priests, changedAccumulator) {
            if (this.priests != priests) {
              changedAccumulator.push({
                city: this,
                type: Constants.Resources.PRIESTS,
                value: priests,
              });
            }
            this.priests = priests;
          },
          
          getLastResourceUpdate: function getLastResourceUpdate() {
            return this.resourceUpdateTime;
          },
          
          getTimeSinceResourceUpdate: function getTimeSinceLastResourceUpdate() {
            return View.gameTimeNow() - this.resourceUpdateTime;
          },
          
          isOwn: function isOwn() {
            return this.type == Constants.CityType.OWN;
          },      
          isDeployment: function isDeployment() {
            return this.type == Constants.CityType.DEPLOYMENT;
          },
          isOccupation: function isOccupation() {
            return this.type == Constants.CityType.OCCUPATION;
          },
          getType: function getType() {
            return this.type;
          },
          
          getBuildingByPosition: function getBuildingByPosition(position) {
            return this.buildings[position];
          },
          getBuildingsByType: function getBuildingsByType(type) {
            return this.buildings.filter(function buildingsFilter(building) {
              return building.getType() == type;
            });
          },
          getBuildingByType: function getBuildingByType(type) {
            for (var i = 0; i < Constants.GamePlay.BUILDING_SPOTS; i++) {
              var building = this.buildings[i];
              if (building && building.getType() == type) {
                return building;
              }
            }
            return null;
          },
          getBuildings: function getBuildings() {
            return this.buildings;
          },
          getMilitary: function getMilitary() {
            return this.military;
          },
          getId: function getId() {
            return this.id;
          },
          getName: function getName() {
            return this.name;
          },
          getIslandId: function getIslandId() {
            return this.islandId;
          },
          getTradeGoodType: function getTradeGoodType() {
            return this.tradeGoodType;
          },
          getActionPoints: function getActionPoints() {
            return this.actionPoints;
          },
          getMaxActionPoints: function getMaxActionPoints() {
            return 3 + Math.floor(this.level / 4) - (this.isOwn() ? 0 : 2);
          },
          getCulturalGoods: function getCulturalGoods() {
            return this.culturalGoods;
          },
          getTavernWineLevel: function getTavernWineLevel() {
            return this.tavernWineLevel;
          },
          getPopulationData: function getPopulationData() {
            var max = 0;
            var happiness = 196;
            
            var townHall = this.getBuildingByType(Constants.Buildings.TOWN_HALL);
            var temple = this.getBuildingByType(Constants.Buildings.TEMPLE);
            var palace = this.getBuildingByType(Constants.Buildings.PALACE);
            var tavern = this.getBuildingByType(Constants.Buildings.TAVERN);
            var museum = this.getBuildingByType(Constants.Buildings.MUSEUM);
            var civData = getCivilizationData();
            
            if (townHall) {
              // Formula from http://ikariam.wikia.com/wiki/Citizen
              max += Math.floor(10 * Math.pow(townHall.getLevel(), 1.5)) * 2 + 40;
    
            }
            
            if (civData.hasResearched(Constants.Research.Economy.HOLIDAY)) {
              max += 50;
              happiness += 25;
            }
            if (civData.hasResearched(Constants.Research.Economy.ECONOMIC_FUTURE)) {
              var level = civData.getResearchLevel(Constants.Research.Economy.ECONOMIC_FUTURE);
              max += 20 * level;
              happiness += 10 * level;
            }
            if (palace) {
              if (civData.hasResearched(Constants.Research.Science.WELL_CONSTRUCTION)) {
                max += 50;
                happiness += 50;
              }
              if (civData.hasResearched(Constants.Research.Economy.UTOPIA)) {
                max += 200;
                happiness += 200;
              }
            }
            
            if (tavern) {
              happiness += Constants.BuildingData.tavern.happiness[tavern.getLevel()];//12 * tavern.getLevel();
            }
            happiness += Constants.BuildingData.tavern.maxBonus[tavern.getLevel()];//Constants.GamePlay.HAPPINESS_PER_WINE_SERVING_LEVEL * this.getTavernWineLevel();
            
            if (museum) {
              happiness += Constants.BuildingData.museum.happiness[museum.getLevel()];//20 * museum.getLevel();
            }
            happiness += Constants.BuildingData.museum.happiness[this.getCulturalGoods()];//Constants.GamePlay.HAPPINESS_PER_CULTURAL_GOOD * this.getCulturalGoods();
            
            var government = civData.getGovernment();
            if (government == Constants.Government.DEMOCRACY) {
              happiness += 75;
            } else if (government == Constants.Government.DICTATORSHIP) {
              happiness -= 75;
            } else if (government == Constants.Government.THEOCRACY) {
              if (temple) {
                happiness += Math.min(150, this.getPriests() * 5 / max * 100 * 2);
              } else {
                happiness -= 20;
              }
            }
            
            happiness = happiness * (1 - this.getCorruption());
            
            var happinessDelta = happiness - this.population;
            var currentPopulation = this.population + 
                happinessDelta * (1 - Math.pow(Math.E, 
                    -(this.getTimeSinceResourceUpdate() / 50 / Constants.Time.MILLIS_PER_HOUR)));
            
            var population = Math.min(currentPopulation, max);
            return {
              population: population,
              max: max,
              happiness: happiness - population,
              growth: max == population && happiness > (population - 1)
                  ? 0 : (happiness - population) / 50,
            };
          },
          getCorruption: function getCorruption() {
            var palace = this.getBuildingByType(Constants.Buildings.GOVERNORS_RESIDENCE) ||
                this.getBuildingByType(Constants.Buildings.PALACE);
            var level = palace ? palace.getLevel() : 0;
            var corruption = 1 - (level + 1) / getOwnCities().length;
            
            var government = getCivilizationData().getGovernment();
            if (government == Constants.Government.ARISTOCRACY || 
                government == Constants.Government.OLIGARCHY) {
              corruption += .03;
            } else if (government == Constants.Government.NOMOCRACY) {
              corruption -= .05;
            } else if (government == Constants.Government.ANARCHY) {
              corruption += .25;
            }
            
            return Math.min(Math.max(corruption, 0), 1);
          },
          getScientists: function getScientists() {
            return this.scientists;
          },
          getResearch: function getResearch() {
            var civData = getCivilizationData();
            var multiplier = 1.0;
            
            multiplier += civData.hasResearched(
                IkaTools.Constants.Research.Science.PAPER) ? .02 : 0;
            multiplier += civData.hasResearched(
                IkaTools.Constants.Research.Science.INK) ? .04 : 0;
            multiplier += civData.hasResearched(
                IkaTools.Constants.Research.Science.MECHANICAL_PEN) ? .08 : 0;
            multiplier += (civData.getResearchLevel(
                IkaTools.Constants.Research.Science.SCIENTIFIC_FUTURE) || 0) * .02;
            multiplier -= this.getCorruption();
            
            return this.scientists * multiplier;
          },
    
          getResource: function getResource(resourceName) {
            return this.resources[resourceName];
          },
          getResourceCapacity: function getResourceMaximumCapacity() {
            var total = 1500;
            var safe = 100;
            
            $.each(this.getBuildingsByType(Constants.Buildings.WAREHOUSE), function(i, building) {
              total += Constants.BuildingData.warehouse.capacity[building.getLevel()]//8000 * building.getLevel();
              safe += 480 * building.getLevel();
            });
            $.each(this.getBuildingsByType(Constants.Buildings.DUMP), function(i, building) {
              total += Constants.BuildingData.dump.capacity[building.getLevel()]//8000 * building.getLevel();
            });
            
            var civilizationData = getCivilizationData();
            if (civilizationData.isPremiumFeatureEnabled(
                Constants.PremiumFeatures.DOUBLED_STORAGE_CAPACITY)) {
              total *= 2;
            }
            if (civilizationData.isPremiumFeatureEnabled(
                Constants.PremiumFeatures.DOUBLED_SAFE_CAPACITY)) {
              safe *= 2;
            }
            
            return {
              maximum: total,
              safe: safe,
            };
          },
          getIslandCoordinates: function getIslandCoordinates() {
            return this.islandCoordinates;
          },
          getLoadingSpeed: function getLoadingSpeed() {
            var speed = 10;
            var ports = this.getBuildingsByType(Constants.Buildings.TRADING_PORT);
            if (ports[0]) {
              speed = Constants.BuildingData[Constants.Buildings.TRADING_PORT]
                  .loadingSpeed[ports[0].getLevel()];
            }
            if (ports[1]) {
              speed += Constants.BuildingData[Constants.Buildings.TRADING_PORT]
                  .loadingSpeed[ports[1].getLevel()];
            }
            return speed / Constants.Time.SECONDS_PER_MINUTE;
          },
          getPriests: function getPriests() {
            return this.priests;
          },
        });
        
        City.Resource = function City_Resource(city) {
          this._ikaToolsType = 'cityResource';
          this._setCity(city);
        }
          
        $.extend(City.Resource.prototype, {
          _setCity: function setCity(city) {
            if (city) {
              this.city = Utils.fixedFunction(city);
            }
          },
          _update: function _update(current, production, consumption) {
            var changed = 
                 Math.abs(current - this.getCurrent()) > 3 || 
                 this.production != production ||
                 this.consumption != consumption;
            
            this.current = current;
            this.production = production;
            this.consumption = consumption;
            
            return changed;
          },
          _increment: function _increment(delta) {
            this.current = Math.max(this.current + delta, 0);
          },
          getCurrent: function getCurrent() {
            if (this.current === undefined) {
              return undefined;
            }
            
            var current = this.current;
            var now = View.gameTimeNow();
            var max = this.city().getResourceCapacity().maximum;
            var lastUpdate = this.city().getLastResourceUpdate();
            
            if (this.production) {
              current += this.production * (now - lastUpdate) / 
                  Constants.Time.MILLIS_PER_SECOND;
            }
            if (this.consumption) {
    
              // Wine use takes place on the hour.
              var startHour = Math.floor(lastUpdate / Constants.Time.MILLIS_PER_HOUR);
              var nowHour = Math.floor(now / Constants.Time.MILLIS_PER_HOUR);
              current -= 
    
                  this.consumption * Constants.Time.SECONDS_PER_HOUR * (nowHour - startHour);
            }
            return Math.max(0, Math.min(max, current));
          },
          
          /**
           * In milliseconds.
           */
          getTimeUntilFull: function getTimeUntilFull() {
            var overallProduction = (this.production || 0) - (this.consumption || 0);
            if (this.current === undefined) {
              return Number.POSITIVE_INFINITY;
            } else {
              var current = this.getCurrent();
              var max = this.city().getResourceCapacity().maximum;
              var production = this.production;
              
              if (overallProduction > 0) {
                var secondsToNextHour = (Constants.Time.MILLIS_PER_HOUR - 
                     (View.gameTimeNow() % Constants.Time.MILLIS_PER_HOUR)) /
                     Constants.Time.MILLIS_PER_SECOND;
                var atNextHour = current + secondsToNextHour * production;
    
                if (atNextHour >= max) {
                  return (max - current) / production * Constants.Time.MILLIS_PER_SECOND;
                } else {
                  var hours = Math.floor(
                      (max - atNextHour) / overallProduction / Constants.Time.SECONDS_PER_HOUR);
                  var atHours = atNextHour + overallProduction * hours * Constants.Time.SECONDS_PER_HOUR;
                  return (secondsToNextHour + 
                          hours * Constants.Time.SECONDS_PER_HOUR + 
                          (max - atHours) / production) * Constants.Time.MILLIS_PER_SECOND;
                }
              } else if (this.current && this.getCurrent() == max) {
                // No production, but filled exactly to capacity
                return 0;
              } else {
                return Number.POSITIVE_INFINITY;
              }
            }
          },
          
          /**
           * In milliseconds.
           */
          getTimeUntilEmpty: function getTimeUntilEmpty() {
            if (this.current === undefined) {
              return Number.POSITIVE_INFINITY;
            } else if (this.consumption) {
              if (this.production > this.consumption) {
                // Could run out in first hour, but nobody is going to run their empire that 
                // way so it's not worth the effort of calculating.
                return Number.POSITIVE_INFINITY;
              } else {
                // Wine use takes place on the hour.
                var current = this.getCurrent();
                var production = this.production || 0;
                
                var secondsToNextHour = (Constants.Time.MILLIS_PER_HOUR - 
                   (View.gameTimeNow() % Constants.Time.MILLIS_PER_HOUR)) / 
                   Constants.Time.MILLIS_PER_SECOND;
                // Compute to end of next hour
                var atNextHour = current - this.consumption * Constants.Time.SECONDS_PER_HOUR + 
                    production * secondsToNextHour;
                if (atNextHour <= 0) {
                  return secondsToNextHour * Constants.Time.MILLIS_PER_SECOND;
                } else {
                  var hourlyDiff = 
                      (this.consumption - production) * Constants.Time.SECONDS_PER_HOUR;
                  return Constants.Time.MILLIS_PER_SECOND * (secondsToNextHour + 
                      Math.ceil(atNextHour / hourlyDiff) * Constants.Time.SECONDS_PER_HOUR);
                }
              }
            }
            return Number.POSITIVE_INFINITY;
          },
          
          getCapacity: function getCapacity() {
            return this.city().getResourceCapacity();
          },
    
          getProduction: function getProduction() {
            return this.production;
          },
          
          getConsumption: function getConsumption() {
            return this.consumption;
          },
        });
    
        City.Building = function City_Building(city) {
          this._ikaToolsType = 'building';
          this._setCity(city);
    
          this.position = null;
          this.type = null;
          this.level = 0;
        }
        
        $.extend(City.Building.prototype, {
          _setCity: function setCity(city) {
            if (city) {
              this.city = Utils.fixedFunction(city);
              this._scheduleUpgradeComplete();
            }
          },
          _update: function _update(position, data) {
            this.position = position;
            var changed = false;
            
            if (data.building.indexOf('buildingGround') >= 0) {
              changed = !this.isEmpty();
              
              this.type = '';
              delete this.level;
              delete this.completionTime;
            } else {
              var type = data.building.split(' ')[0];
              var level = parseInt(data.level);
              var isUpgrading = 'completed' in data;
              
              changed = (type != this.getType() || 
                         level != this.getLevel() || 
                         isUpgrading != this.isUpgrading());
              
              this.type = type;
              this.level = level;
              if (isUpgrading) {
                var completionTime = parseInt(data.completed) * 1000;
                if (this.completionTime != completionTime) {
                  this.completionTime = completionTime;
                  this._scheduleUpgradeComplete();
                }
              } else {
                delete this.completionTime;
              }
            }
    
            return changed;
          },
          _scheduleUpgradeComplete: function _scheduleUpgradeComplete() {
            if (this.upgradeEvent) {
              this.upgradeEvent();
            }
            if (this.completionTime) {
              if (this.completionTime <= View.gameTimeNow()) {
                this.level = this.level + 1;
                delete this.completionTime;
                raiseBuildingsChanged([{
                  city: this.city(),
                  building: this,
                  type: Constants.BuildingEventType.UPGRADE_COMPLETE,
                }]);
              } else {
                this.upgradeEvent = buildingsChangedEvent().scheduleSend(
                    this.type + "->" + (this.level + 1),
                    // 0.5.0 still does a full page refresh if you're viewing the city when the 
                    // building completes.  So we cheat a bit and send this event a few seconds
                    // before it actually takes place so it doesn't get lost as part of a page 
                    // refresh that just sees it as a normal "info_refresh" event.
                    this.completionTime - View.gameTimeNow() + Constants.Time.SAFE_TIME_DELTA,
                    function() {
                      this.level = this.level + 1;
                      delete this.completionTime;
                      empireData.saveAsync();
                    }.bind(this),
                    [{
                      city: this.city(),
                      building: this,
                      type: Constants.BuildingEventType.UPGRADE_COMPLETE,
                    }]);
              }
            }
          },
          getPosition: function getPosition() {
            return this.position;
          },
          isEmpty: function isEmpty() {
            return !this.type;
          },
          getType: function getType() {
            return this.type;
          },
          getLevel: function getLevel() {
            return this.level;
          },
          isUpgrading: function isUpgrading() {
            return (this.completionTime > View.gameTimeNow());
          },
          getRemainingUpgradeTime: function getRemainingUpgradeTime() {
            var diff = this.completionTime - View.gameTimeNow();
            return Math.max(diff, 0);
          },
          getCompletionTime: function getCompletionTime() {
            return new Date(this.completionTime);
          },
          getUpgradeCosts: function getUpgradeCost() {
    
            var city = this.city();
            var civData = getCivilizationData();
            var buildingCostData = Constants.BuildingData[this.getType()];
            var level = this.getLevel() + (this.isUpgrading() ? 1 : 0);
            
            var timeParams = buildingCostData.time;
            var costs = {
              wood: buildingCostData.wood[level] || 0,
              wine: buildingCostData.wine[level] || 0,
              marble: buildingCostData.marble[level] || 0,
              glass: buildingCostData.glass[level] || 0,
              sulfur: buildingCostData.sulfur[level] || 0,
              time: Math.round(timeParams.a / timeParams.b * 
                  Math.pow(timeParams.c, level+1) - timeParams.d) * 1000,
            };
            
            var multiplier = 1.0;
            multiplier -= civData.hasResearched(Constants.Research.Economy.PULLEY) ? .02 : 0;
            multiplier -= civData.hasResearched(Constants.Research.Economy.GEOMETRY) ? .04 : 0;
            multiplier -= civData.hasResearched(Constants.Research.Economy.SPIRIT_LEVEL) ? .08 : 0;
            
            var carpenter = city.getBuildingByType(Constants.Buildings.CARPENTER);
            var winePress = city.getBuildingByType(Constants.Buildings.WINE_PRESS);
            var architect = city.getBuildingByType(Constants.Buildings.ARCHITECT);
            var optician = city.getBuildingByType(Constants.Buildings.OPTICIAN);
            var fireworker = city.getBuildingByType(Constants.Buildings.FIREWORK_TEST_AREA);
            
            
            return {
              wood: costs.wood * (multiplier - (carpenter ? carpenter.getLevel() / 100 : 0)),
              wine: costs.wine * (multiplier - (winePress ? winePress.getLevel() / 100 : 0)),
              marble: costs.marble * (multiplier - (architect ? architect.getLevel() / 100 : 0)),
              glass: costs.glass * (multiplier - (optician ? optician.getLevel() / 100 : 0)),
              sulfur: costs.sulfur * (multiplier - (fireworker ? fireworker.getLevel() / 100 : 0)),
              time: costs.time,
            };
          },
          isMaxLevel: function isMaxLevel() {
            return (this.getLevel() + (this.isUpgrading() ? 1 : 0)) >= 
                Constants.BuildingData[this.getType()].maxLevel;
          },
        });
        
        CivilizationData = function CivilizationData() {
          this._ikaToolsType = 'civilizationData';
    
          this.research = {};
          this.government = 'ikakratie';
          this.movements = {};
          this.premiumFeatures = {};
        }
        
        $.extend(CivilizationData.prototype, {
          _startMovementTimers: function _startMovementTimers() {
            $.each(this.movements, function updateMovementsOnLoad(id, movement) {
              if (movement._updateAndStartTimer()) {
                delete this.movements[id];
              }
            }.bind(this));
          },
          _updateGovernment: function updateGovernment(government, changedAccumulator) {
            if (this.government != government) {
              changedAccumulator.push({
                type: Constants.CivilizationData.GOVERNMENT,
                government: government,
              });
    
            }
            this.government = government;
          },
          _updateResearch: function updateResearch(researchId, level, changedAccumulator) {
            var oldResearch = this.research[researchId];
            if (!oldResearch || oldResearch.level != level) {
              changedAccumulator.push({
                type: Constants.CivilizationData.RESEARCH,
                id: researchId,
                level: level,
              });
            }
            this.research[researchId] = { level: level };
          },
          _updateMovement: function updateMovement(movement, changedAccumulator) {
            var existing = this.movements[movement.getId()];
            if (existing) {
              existing._cancelTimer();
            }
            this.movements[movement.getId()] = movement;
            movement._updateAndStartTimer();
            if (!existing || (movement.getCompletionTime() != existing.getCompletionTime())) {
              changedAccumulator.push({
                movement: movement,
                type: Constants.Movements.EventType.DATA_UPDATED,
              });
            }
          },
          _removeMovement: function removeMovement(movementId, changedAccumulator) {
            var movement = this.movements[movementId];
            if (movement) {
              if (movement.stage == Constants.Movements.Stage.LOADING && movementId >= 0) {
                var originCity = movement.getOriginCity();
                movement._updateCity(originCity, originCity);
              }
              movement._cancelTimer();
              delete this.movements[movementId];
              changedAccumulator.push({
                movement: movement,
                type: Constants.Movements.EventType.CANCELLED,
              });
            }
          },
          _updatePremiumFeature: function updatePremiumFeature(
              changedAccumulator, feature, enabled) {
            var currentlyEnabled = this.premiumFeatures[feature] || false;
            if (currentlyEnabled != enabled) {
              changedAccumulator.push({
                type: Constants.CivilizationData.PREMIUM_FEATURE,
                feature: feature,
                enabled: enabled,
              });
            }
            this.premiumFeatures[feature] = enabled;        
          },
          hasResearched: function hasResearched(researchId) {
            var research = this.research[researchId];
            return research ? research.level > 0 : undefined;
          },
          getResearchLevel: function getResearchLevel(researchId) {
            var research = this.research[researchId];
            return research ? research.level : undefined;
          },
          getGovernment: function getGovernment() {
            return this.government;
          },
          getMovements: function getMovements() {
            var movements = [];
            $.each(this.movements, function(id, movement) {
              movements.push(movement);
            });
            movements.sort(function compareMovements(m1, m2) {
              return m1.getArrivalTime() - m2.getArrivalTime();
            });
            return movements;
          },
          getMovement: function getMovement(movementId) {
            return this.movements[movementId];
          },
          isPremiumFeatureEnabled: function isPremiumFeatureEnabled(feature) {
            return this.premiumFeatures[feature];
          },
        });
        
        function calculateTravelTime(island1Coords, island2Coords, units, transporters) {
          // same island
          if (island1Coords[0] == island2Coords[0] && 
              island1Coords[1] == island2Coords[1]) {
            var baseTime = 10 * Constants.Time.MILLIS_PER_MINUTE;
            var multiplier = transporters ? 60 : 80; // fastest unit
            if (units) {
              $.each(units.getCounts(), function applyUnitSpeed(type, count) {
                if (count) {
                  var data = Constants.UnitData[type];
                  multiplier = Math.min(multiplier, data.speed);
                }
              });
            }
            return baseTime * 60 / multiplier;
          } else {
            var baseTime = 20 * Math.sqrt(Math.pow(island1Coords[0] - island2Coords[0], 2) + 
                                          Math.pow(island1Coords[1] - island2Coords[1], 2)) *
                           Constants.Time.MILLIS_PER_MINUTE;
            var multiplier = 60; // fastest ship
            if (units) {
              $.each(units.getCounts(), function applyUnitSpeed(type, count) {
                if (count) {
                  var data = Constants.UnitData[type];
                  if (!data.isArmy) {
                    multiplier = Math.min(multiplier, data.speed);
                  }
                }
              });
            }
            return baseTime * 60 / multiplier;
          }
        }
        
        function Movement(
            id, type, completionTime, mission, stage, originCityId, targetCityId,
            transports, units, resources, transportTime) {
          this._ikaToolsType = 'movement';
          
          if (id) {
            this.id = id;
            this.completionTime = completionTime;
            this.mission = mission;
            this.stage = stage;
            this.originCityId = originCityId;
            this.targetCityId = targetCityId;
            this.units = units;
            this.transports = transports;
            this.resources = resources;
    
            this.transportTime = transportTime;
    
            this.type = type;
            
            var originCity = this.getOriginCity();
            var targetCity = this.getTargetCity();
    
            if (originCity && targetCity) {
              var originCoords = originCity.getIslandCoordinates();
              var targetCoords = targetCity.getIslandCoordinates();
    
              if (originCoords && targetCoords) {
                this.transportTime = calculateTravelTime(
                    originCoords, targetCoords, this.units, this.transports);
              }
            }
            
            if (!this.transportTime) {
              this.transportTime = Number.POSITIVE_INFINITY;
            }
            
            if (this.completionTime <= new Date().getTime()) {
              this._toNextStage();
            }
          }
        }
        
        $.extend(Movement.prototype, {
          _cancelTimer: function cancelTimer() {
            if (this.completionEvent) {
              this.completionEvent();
            }
          },
          _startTimer: function startTimer() {
            var remainingTime = this.getTimeRemaining();
            if (isFinite(remainingTime)) {
              this.completionEvent = movementsChangedEvent().scheduleSend(
                  'Movement[' + this.id + ']',
                  remainingTime + Constants.Time.SAFE_TIME_DELTA,
                  function moveToNextStage() {
                    if (this._toNextStage()) {
                      getCivilizationData()._removeMovement(this.id, []);
                    }
                    empireData.saveAsync();
                  }.bind(this),
                  [{
                    previousStage: this.getStage(),
                    movement: this,
                    type: this._isFinalStage() ? 
                        Constants.Movements.EventType.COMPLETED :
                        Constants.Movements.EventType.STAGE_CHANGED,
                  }]);
            }
          },
          _updateCity: function updateCity(city, originCity) {
            var resourceChanges = [];
            if (city) {
              if (originCity) {
                originCity._updateActionPointsBy(1, resourceChanges);
              }
              if (city.isOwn() && 
                  (this.mission == Constants.Movements.Mission.TRANSPORT ||
                   this.mission == Constants.Movements.Mission.PLUNDER)) {
                $.each(this.resources, function updateCityResource(name, value) {
                  city._incrementResource(name, resourceChanges, value);
                });
              }
              raiseResourcesChanged(resourceChanges);
              
              if (this.mission == Constants.Movements.Mission.DEPLOY_ARMY ||
                  this.mission == Constants.Movements.Mission.DEPLOY_NAVY) {
                var military = city.getMilitary();
                military._increment(this.units);
                raiseMilitaryChanged([{
                  military: military,
                  city: city,
                  type: 'deployment_arrived',
                }]);
              }
            }
          },
          _updateAndStartTimer: function updateAndStartTimer() {
            this._cancelTimer();
            
            if (this.completionTime <= View.gameTimeNow()) {
              return this._toNextStage();
            } else {
              this._startTimer();
            }
          },
          _toNextStage: function toNextStage() {
            var isFinalStage = this._isFinalStage();
            if (this.stage == Constants.Movements.Stage.LOADING) {
              this.stage = Constants.Movements.Stage.EN_ROUTE;
              this.completionTime += this.transportTime;
              this._startTimer();
            } else if (this.stage == Constants.Movements.Stage.EN_ROUTE) {
              if (isFinalStage) {
                this._updateCity(this.getTargetCity(), this.getOriginCity());
              }
            } else if (this.stage == Constants.Movements.Stage.RETURNING) {
              if (isFinalStage) {
                var originCity = this.getOriginCity();
                this._updateCity(originCity, originCity);
              }
            }
            return isFinalStage;
          },
          _isFinalStage: function isFinalStage() {
            if (this.stage == Constants.Movements.Stage.LOADING) {
              return false;
            } else if (this.stage == Constants.Movements.Stage.EN_ROUTE) {
              if (this.mission == Constants.Movements.Mission.TRANSPORT) {
                var city = getCity(this.targetCityId);
                return city && city.isOwn();
              } else if (this.mission == Constants.Movements.Mission.DEPLOY_ARMY ||
                         this.mission == Constants.Movements.Mission.DEPLOY_NAVY) {
                return true;
              }
            } else {
              return true;
            }
          },
          getId: function getId() {
            return this.id;
          },
          getMission: function getMission() {
            return this.mission;
          },
          getStage: function getStage() {
            return this.stage;
          },
          getOriginCityId: function getOriginCityId() {
            return this.originCityId;
          },
          getTargetCityId: function getTargetCityId() {
            return this.targetCityId;
          },
          getOriginCity: function getOriginCity() {
            return this.originCityId && getCity(this.originCityId);
          },
          getTargetCity: function getTargetCity() {
            return this.targetCityId && getCity(this.targetCityId);
          },
          getCompletionTime: function getCompletionTime() {
            return this.completionTime;
          },
          getTimeRemaining: function getTimeRemaining() {
            return this.completionTime - View.gameTimeNow();
          },
          getArrivalTime: function() {
            var time = this.getCompletionTime();
            if (this.stage == Constants.Movements.Stage.LOADING) {
              time += this.transportTime;
            }
            return time;
          },        
          getUnits: function getUnits() {
            return this.units;
          },
          getResource: function getResource(resourceName) {
            return this.resources[resourceName];
          },
          isHostile: function isHostile() {
            return this.type.indexOf('hostile') >= 0;
          },
          isOwn: function isOwn() {
            return this.type.indexOf('own') >= 0;
          }
        });
        
        var empireData = new Data.Value(
            'empireData', 
    
            { 
              cities: {}, 
              cityOrder: [],
              civilizationData: new CivilizationData(),
            }, 
            {
              reviver: function empireDataReviver(key, value) {
                if (value && value._ikaToolsType) {
                  var obj;
                  switch(value._ikaToolsType) {
                    case 'city': obj = new City(); break;
                    case 'building': obj = new City.Building; break;
                    case 'cityResource': obj = new City.Resource(); break;
                    case 'military': obj = new Military(); break;
                    case 'militaryUnits': obj = new MilitaryUnits(); break;
                    case 'trainingBatch': obj = new TrainingBatch(); break;
                    case 'civilizationData': obj = new CivilizationData(); break;
                    case 'movement': obj = new Movement(); break;
                  }
                  $.extend(obj, value);
                  if (obj._postLoad) {
                    obj._postLoad();
                  }
                  return obj;
                }
                return value;
              },
              version: 28,
              loadCallback: function empireDataLoaded() {
                getCivilizationData()._startMovementTimers();
              },
            });
            
        function raiseCivilizationDataChanged(changes) {
          if (changes.length) {
            civilizationDataChangedEvent().send(changes);
          }
        }
    
        var civilizationDataChangedEvent = Utils.thunk(function() {
          return new Utils.EventDispatcher();
        });
            
        function registerCivilizationDataChangedHandler(callback) {
          return civilizationDataChangedEvent().addListener(callback);
        }
        
        function raiseMovementsChanged(changes) {
          if (changes.length) {
            movementsChangedEvent().send(changes);
          }
        }
        
        var movementsChangedEvent = Utils.thunk(function() {
          return new Utils.EventDispatcher();
        });
        
        function registerMovementsChangedHandler(callback) {
          return movementsChangedEvent().addListener(callback);
        }
    
        function raiseResourcesChanged(changes) {
          if (changes.length) {
            resourcesChangedEvent().send(changes);
          }
        }
            
        var resourcesChangedEvent = Utils.thunk(function() {
          return new Utils.EventDispatcher();
        });
        
        function registerResourcesChangedHandler(callback) {
          return resourcesChangedEvent().addListener(callback);
        }
            
        var buildingsChangedEvent = Utils.thunk(function() {
          var dispatcher = new Utils.EventDispatcher();
          return dispatcher;
        });
        
        function raiseBuildingsChanged(changes) {
          if (changes.length) {
            buildingsChangedEvent().send(changes);
          }
        }
            
        function registerBuildingsChangedHandler(callback) {
          return buildingsChangedEvent().addListener(callback);
        }
        
        var militaryChangedEvent = Utils.thunk(function() {
          var dispatcher = new Utils.EventDispatcher();
          return dispatcher;
        });
        
        function raiseMilitaryChanged(changes) {
          if (changes.length) {
            militaryChangedEvent().send(changes);
          }
        }
        
        function registerMilitaryChangedHandler(callback) {
          return militaryChangedEvent().addListener(callback);
        };
    
        var TRADE_GOOD_LOOKUP = {
          "1": Constants.Resources.WINE,
          "2": Constants.Resources.MARBLE,
          "3": Constants.Resources.GLASS,
          "4": Constants.Resources.SULFUR,
        };
        
        function getCity(id) {
          return empireData.get().cities[id];
        };
        
        var coordsRegex = /\[(\d+):(\d+)\]/;
        function parseCoordinates(coords) {
          var match = coords.match(coordsRegex);
          return [parseInt(match[1]), parseInt(match[2])];
        }
        
        function processTransportForm(form) {
          var city = View.getCurrentCity();
          
          var transports = parseInt($('#transporterCount').val()) || 0;
          var resources = {};
          resources[Constants.Resources.WOOD] = 
              parseInt($('#textfield_wood').val()) || 0;
          resources[Constants.Resources.WINE] = 
              parseInt($('#textfield_wine').val()) || 0;
          resources[Constants.Resources.MARBLE] = 
              parseInt($('#textfield_marble').val()) || 0;
          resources[Constants.Resources.GLASS] = 
              parseInt($('#textfield_glass').val()) || 0;
          resources[Constants.Resources.SULFUR] = 
              parseInt($('#textfield_sulfur').val()) || 0;
          if ($('#createColony').length) {
            resources[Constants.Resources.WOOD] += 1250;
          }
              
          var destinationCityId = parseInt($(form.elements['destinationCityId']).val());
              
          var totalResources = 
              resources[Constants.Resources.WOOD] + 
              resources[Constants.Resources.WINE] + 
              resources[Constants.Resources.MARBLE] + 
              resources[Constants.Resources.GLASS] + 
              resources[Constants.Resources.SULFUR];
              
          var loadingCompletion = View.gameTimeNow() + 
              (totalResources / city.getLoadingSpeed() * Constants.Time.MILLIS_PER_SECOND);
    
          var movement = new Movement(
              -(new Date().getTime()),
              'own', 
              loadingCompletion, //TODO: multiple loads from same town (if own) stack
              Constants.Movements.Mission.TRANSPORT, 
              Constants.Movements.Stage.LOADING, 
              city.getId(),
              destinationCityId,
              transports, 
              new MilitaryUnits(), 
              resources
              // TODO: transport time for towns other than one's that are tracked
              );
          
          View.registerNextIkariamAjaxRequestCallback(function saveTransportData(response) {
            Utils.iterateIkariamAjaxResponse(response, 
                function lookForSuccessFeedback(index, name, data) {
                  if (name == Constants.IkariamAjaxResponseType.PROVIDE_FEEDBACK &&
                      data[0].type == 10) {
                    var changes = [];
                    getCivilizationData()._updateMovement(movement, changes);
                    raiseMovementsChanged(changes);
                  }
                });
          });
        }
        
        function coordinatesEqual(coordinates1, coordinates2) {
          if (!coordinates1 || !coordinates2) {
            return false;
          }
          return coordinates1[0] == coordinates2[0] && 
              coordinates1[1] == coordinates2[1];
        }
        
        function processDeploymentForm(form) {
          var city = View.getCurrentCity();
          
          var transports = parseInt($('#transporterCount').html()) || 0;
          var mission = $(form.elements['function']).val() == 'deployArmy' ?
              Constants.Movements.Mission.DEPLOY_ARMY : Constants.Movements.Mission.DEPLOY_NAVY;
          var destinationCityId = parseInt($(form.elements['destinationCityId']).val());
          var destinationCity = getCity(destinationCityId);
          
          var units = new MilitaryUnits();
          
          $.each(Constants.UnitIds, function countDeployingUnits(id, type) {
            var elementId;
            if (Constants.UnitData[type].isArmy) {
              elementId = '#cargo_army_' + id;
            } else {
              elementId = '#cargo_fleet_' + id;
            }
            units._setCount(type, parseInt($(elementId).val()) || 0);
          });
          
          var cargoSize = units.getCargoSize();
          var loadingCompletion = new Date().getTime() + 
              (units.getCargoSize() / city.getLoadingSpeed() * Constants.Time.MILLIS_PER_SECOND);
    
          if (destinationCity && 
              coordinatesEqual(destinationCity.getIslandCoordinates(), 
                               city.getIslandCoordinates())) {
            loadingCompletion = new Date().getTime();
          }
          
          var movement = new Movement(
              -(new Date().getTime()),
              'own', 
              loadingCompletion, //TODO: multiple loads from same town (if own) stack
              mission, 
              Constants.Movements.Stage.LOADING, 
              city.getId(),
              destinationCityId,
              transports, 
              units, 
              {}
              // TODO: transport time for towns other than one's that are tracked
              );
          
          View.registerNextIkariamAjaxRequestCallback(function saveDeploymentData(response) {
              Utils.iterateIkariamAjaxResponse(response, 
                  function lookForSuccessFeedback(index, name, data) {
                    if (name == Constants.IkariamAjaxResponseType.PROVIDE_FEEDBACK &&
                        data[0].type == 10) {
                      var military = city.getMilitary();
                      military._decrement(units);
                      raiseMilitaryChanged([{
                        military: military,
                        city: city,
                        type: 'movement_started',
                      }]);
                      
                      var changes = [];
                      getCivilizationData()._updateMovement(movement, changes);
                      raiseMovementsChanged(changes);
                    }
                  });
          });
        }
        
        function processPlunderForm(form) {
          var city = View.getCurrentCity();
          var transports = parseInt($('#transporterCount').html()) || 0;
          var destinationCityId = parseInt($(form.elements['destinationCityId']).val());
          var destinationCity = getCity(destinationCityId);
          
          var units = new MilitaryUnits();
          $.each(Constants.UnitIds, function countDeployingUnits(id, type) {
            units._setCount(type, parseInt($('#cargo_army_' + id).val()) || 0);
          });
          
          var cargoSize = units.getCargoSize();
          var loadingCompletion = new Date().getTime() + 
              (units.getCargoSize() / city.getLoadingSpeed() * Constants.Time.MILLIS_PER_SECOND);
          if (destinationCity && 
              coordinatesEqual(destinationCity.getIslandCoordinates(), 
                               city.getIslandCoordinates())) {
            loadingCompletion = new Date().getTime();
          }
          
          var movement = new Movement(
              -(new Date().getTime()),
              'own', 
              loadingCompletion, //TODO: multiple loads from same town (if own) stack
              Constants.Movements.Mission.PLUNDER, 
              Constants.Movements.Stage.LOADING, 
              city.getId(),
              destinationCityId,
              transports, 
              units, 
              {}
              // TODO: transport time for towns other than one's that are tracked
              );
              
          View.registerNextIkariamAjaxRequestCallback(function savePlunderData(response) {
              Utils.iterateIkariamAjaxResponse(response, 
                  function lookForSuccessFeedback(index, name, data) {
                    if (name == Constants.IkariamAjaxResponseType.PROVIDE_FEEDBACK &&
                        data[0].type == 10) {
                      var military = city.getMilitary();
                      military._decrement(units);
                      raiseMilitaryChanged([{
                        military: military,
                        city: city,
                        type: 'movement_started',
                      }]);
                      
                      var changes = [];
                      getCivilizationData()._updateMovement(movement, changes);
                      raiseMovementsChanged(changes);
                    }
                  });
          });
        }
        
        function processCityMilitaryView(data) {
          var city = View.getCurrentCity();
          var military = city.getMilitary();
          
          var armyTds = $('#tabUnits').find('tr.count td');
          var e = false;
          e |= military._updatePresent(Constants.Military.HOPLITE, parseInt(armyTds[0].innerHTML));
          e |= military._updatePresent(Constants.Military.STEAM_GIANT, parseInt(armyTds[1].innerHTML));
          e |= military._updatePresent(Constants.Military.SPEARMAN, parseInt(armyTds[2].innerHTML));
          e |= military._updatePresent(Constants.Military.SWORDSMAN, parseInt(armyTds[3].innerHTML));
          e |= military._updatePresent(Constants.Military.SLINGER, parseInt(armyTds[4].innerHTML));
          e |= military._updatePresent(Constants.Military.ARCHER, parseInt(armyTds[5].innerHTML));
          e |= military._updatePresent(Constants.Military.GUNNER, parseInt(armyTds[6].innerHTML));
          e |= military._updatePresent(Constants.Military.BATTERING_RAM, parseInt(armyTds[7].innerHTML));
          e |= military._updatePresent(Constants.Military.CATAPULT, parseInt(armyTds[8].innerHTML));
          e |= military._updatePresent(Constants.Military.MORTAR, parseInt(armyTds[9].innerHTML));
          e |= military._updatePresent(Constants.Military.GYROCOPTER, parseInt(armyTds[10].innerHTML));
          e |= military._updatePresent(Constants.Military.BALLOON_BOMBADIER, parseInt(armyTds[11].innerHTML));
          e |= military._updatePresent(Constants.Military.COOK, parseInt(armyTds[12].innerHTML));
          e |= military._updatePresent(Constants.Military.DOCTOR, parseInt(armyTds[13].innerHTML));
    
          var navyTds = $('#tabShips').find('tr.count td');        
          e |= military._updatePresent(Constants.Military.RAM_SHIP, parseInt(navyTds[2].innerHTML));
          e |= military._updatePresent(Constants.Military.FLAME_THROWER, parseInt(navyTds[0].innerHTML));
          e |= military._updatePresent(Constants.Military.STEAM_RAM, parseInt(navyTds[1].innerHTML));
          e |= military._updatePresent(Constants.Military.BALLISTA_SHIP, parseInt(navyTds[4].innerHTML));
          e |= military._updatePresent(Constants.Military.CATAPULT_SHIP, parseInt(navyTds[3].innerHTML));
          e |= military._updatePresent(Constants.Military.MORTAR_SHIP, parseInt(navyTds[5].innerHTML));
          e |= military._updatePresent(Constants.Military.SUBMARINE, parseInt(navyTds[7].innerHTML));
          e |= military._updatePresent(Constants.Military.PADDLE_SPEED_SHIP, parseInt(navyTds[8].innerHTML));
          e |= military._updatePresent(Constants.Military.BALLOON_CARRIER, parseInt(navyTds[9].innerHTML));
          e |= military._updatePresent(Constants.Military.TENDER, parseInt(navyTds[10].innerHTML));
          e |= military._updatePresent(Constants.Military.ROCKET_SHIP, parseInt(navyTds[6].innerHTML));
          
          military._markPresentUpdated();
          
          if (e) {
            raiseMilitaryChanged([{ 
              city: city,
              military: military,
              type: 'data_updated',
            }]);
          }
        }
        
        function processRelatedCitiesView(data) {
          var city = View.getCurrentCity();
          var military = city.getMilitary();
          military._clear();
          
          var changed = false;
          
          var info = $('#relatedCities .contentBox01h:eq(0)');
          
          var whitespace = /\s+/;
          
          info.find('.troops .armybutton').each(function(i, element) {
            var type = element.className.split(whitespace)[1];
            changed |= military._updatePresent(type, parseInt(element.innerHTML));
          });
          info.find('.troops .fleetbutton').each(function(i, element) {
            var type = element.className.split(whitespace)[1];
            changed |= military._updatePresent(type, parseInt(element.innerHTML));
          });
    
          military._markPresentUpdated();
          
          if (changed) {
            raiseMilitaryChanged([{ 
              city: city,
              military: military,
              type: 'data_updated',
            }]);
          }
        }
        
        function parseMilitaryUnitsFromPending(container) {
          var idRegex = /\d+/;
          var units = new MilitaryUnits();
          
          container.children('.army_wrapper').each(function(index, unitNode) {
            var unitNode = $(unitNode);
            var type = Constants.UnitIds[
                parseInt(unitNode.find('.army').attr('class').match(idRegex)[0])];
            var count = parseInt(unitNode.find('.unitcounttextlabel').html());
            units._setCount(type, count);
          });
          return units;
        }
        
        function parseTrainingBatches(viewHtmlText, type, buildingLevel) {
          var trainingBatches = [];
          var constructionList = $('#unitConstructionList');
          if (constructionList.length) {
            var completionTime = parseInt(viewHtmlText.match(
                /showUnitCountdown.'buildCountDown', 'buildProgress', (\d+)/)[1]) * 1000;
            trainingBatches.push(new TrainingBatch(type,
                completionTime, parseMilitaryUnitsFromPending(constructionList)));
    
            constructionList.children('.constructionBlock').each(function() {
              var units = parseMilitaryUnitsFromPending($(this));
              completionTime += computeTrainingTime(buildingLevel, units);
              trainingBatches.push(new TrainingBatch(type, completionTime, units));
            });
          }
          return trainingBatches;
        }
        
        function computeTrainingTime(barracksLevel, units) {
          var time = 0;
          $.each(units.getCounts(), function(type, count) {
            var data = Constants.UnitData[type];
            time += count * Math.pow(0.95, barracksLevel - data.minimumBuildingLevelToBuild) * 
                data.baseBuildTime;
          });
          return time * Constants.Time.MILLIS_PER_SECOND;
        }
        
        function processBarracksView(viewHtmlText, data) {
          var city = View.getCurrentCity();
          var military = city.getMilitary();
          var barracks = city.getBuildingByType(Constants.Buildings.BARRACKS);
          
          var changed = false;
          
          function update(type, dataName) {
            if (data[dataName]) {
              changed = military._updatePresent(type, parseInt(data[dataName].text));
            }
          }
          
          update(Constants.Military.HOPLITE, 'js_barracksUnitUnitsAvailable1');
          update(Constants.Military.STEAM_GIANT, 'js_barracksUnitUnitsAvailable2');
          update(Constants.Military.SPEARMAN, 'js_barracksUnitUnitsAvailable3');
          update(Constants.Military.SWORDSMAN, 'js_barracksUnitUnitsAvailable4');
          update(Constants.Military.SLINGER, 'js_barracksUnitUnitsAvailable5');
          update(Constants.Military.ARCHER, 'js_barracksUnitUnitsAvailable6');
          update(Constants.Military.GUNNER, 'js_barracksUnitUnitsAvailable7');
          update(Constants.Military.BATTERING_RAM, 'js_barracksUnitUnitsAvailable8');
          update(Constants.Military.CATAPULT, 'js_barracksUnitUnitsAvailable9');
          update(Constants.Military.MORTAR, 'js_barracksUnitUnitsAvailable10');
          update(Constants.Military.GYROCOPTER, 'js_barracksUnitUnitsAvailable11');
          update(Constants.Military.BALLOON_BOMBADIER, 'js_barracksUnitUnitsAvailable12');
          update(Constants.Military.COOK, 'js_barracksUnitUnitsAvailable13');
          update(Constants.Military.DOCTOR, 'js_barracksUnitUnitsAvailable14');
              
          military._markPresentUpdated(true, false);
    
          military._setArmyTrainingBatches( 
              parseTrainingBatches(viewHtmlText, Constants.Military.ARMY, barracks.getLevel()));
    
          raiseMilitaryChanged([{ 
            city: city,
            military: military,
            type: 'data_updated',
          }]);
        }
        
        function processShipyardView(viewHtmlText, data) {
          var city = View.getCurrentCity();
          var military = city.getMilitary();
          var shipyard = city.getBuildingByType(Constants.Buildings.SHIPYARD);
          
          var change = false;
          function update(type, dataName) {
            if (data[dataName]) {
              changed = military._updatePresent(type, parseInt(data[dataName].text));
            }
          }
          update(Constants.Military.FLAME_THROWER, 'js_barracksUnitUnitsAvailable1');
          update(Constants.Military.STEAM_RAM, 'js_barracksUnitUnitsAvailable2');
          update(Constants.Military.RAM_SHIP, 'js_barracksUnitUnitsAvailable3');
          update(Constants.Military.CATAPULT_SHIP, 'js_barracksUnitUnitsAvailable4');
          update(Constants.Military.BALLISTA_SHIP, 'js_barracksUnitUnitsAvailable5');
          update(Constants.Military.MORTAR_SHIP, 'js_barracksUnitUnitsAvailable6');
          update(Constants.Military.ROCKET_SHIP, 'js_barracksUnitUnitsAvailable7');
          update(Constants.Military.SUBMARINE, 'js_barracksUnitUnitsAvailable8');
          update(Constants.Military.PADDLE_SPEED_SHIP, 'js_barracksUnitUnitsAvailable9');
          update(Constants.Military.BALLOON_CARRIER, 'js_barracksUnitUnitsAvailable10');
          update(Constants.Military.TENDER, 'js_barracksUnitUnitsAvailable11');
          
          military._markPresentUpdated(false, true);
          
          military._setNavyTrainingBatches( 
              parseTrainingBatches(viewHtmlText, Constants.Military.NAVY, shipyard.getLevel()));
              
          raiseMilitaryChanged([{ 
            city: city,
            military: military,
            type: 'data_updated',
          }]);
        }
          
        function processAcademyView(data) {
          var changes = [];
          View.getCurrentCity()._updateScientists(
              parseInt(data['js_academy_research_tooltip_basic_production'].text),
              changes);
          raiseResourcesChanged(changes);
        }
        
        function processSetScientistsForm(form) {
          var scientists = parseInt($('#inputScientists').val());
          
          View.registerNextIkariamAjaxRequestCallback(function saveScientstsData(response) {
            Utils.iterateIkariamAjaxResponse(response, 
                function lookForSuccessFeedback(index, name, data) {
                  if (name == Constants.IkariamAjaxResponseType.PROVIDE_FEEDBACK &&
                      data[0].type == 10) {
                    var changes = [];
                    View.getCurrentCity()._updateScientists(scientists, changes);
                    raiseResourcesChanged(changes);
                  }
                });
          });
        }
        
        function processPalaceView(data) {
          var changes = [];
          getCivilizationData()._updateGovernment( 
              $('.government_pic img').attr('src').slice(16, -8), changes);
          raiseCivilizationDataChanged(changes);
        }
                
        function processMuseumView(data) {
          var changes = [];
          View.getCurrentCity()._updateCulturalGoods(
             parseInt(/\d+/.exec($('#val_culturalGoodsDeposit').parent().text())[0]), 
             changes);
          raiseResourcesChanged(changes);
        }
                
        function processCulturalPossessionsAssignView(data) {
          // Have to delay this because the script elements in the changed view 
          // need to run before we can access the cultural good information.  
          // There is no feasible way to extract the data at this point.
          setTimeout(function() {
            var cityIdRegex = /textfield_city_(\d+)/
            var changes = [];
            $('#moveCulturalGoods ul li input').each(function (index, item) {
              item = $(item);
    
              var city = getCity(
                  parseInt(cityIdRegex.exec(item.attr('id'))[1]));
              city._updateCulturalGoods(parseInt(item.val()), changes);
            });
            raiseResourcesChanged(changes);
            empireData.saveAsync();
          }, 0);
        }
                
        function processTownHallView(data) {
          var changes = [];
          var city = View.getCurrentCity();
          city._updatePriests(
             parseInt(data['js_TownHallPopulationGraphPriestCount'].text), changes);
          city._updateCulturalGoods(
             parseInt(
                 data['js_TownHallSatisfactionOverviewCultureBoniTreatyBonusValue'].text) / 50, 
             changes);
          city._updateTavernWineLevel(
              parseInt(data['js_TownHallSatisfactionOverviewWineBoniServeBonusValue'].text) / 60,
              changes);
          raiseResourcesChanged(changes);
        }
                
        function processTempleView(data) {
          var changes = [];
          View.getCurrentCity()._updatePriests(
              parseInt(data['js_TempleSlider'].slider.ini_value), changes);
          raiseResourcesChanged(changes);
        }
                
        function processResearchAdvisorView(data) {
          var civData = getCivilizationData();
          var idRegex = /researchId=([0-9]+)/i
          var levelRegex = /\((\d+)\)/;
    
          var researches = 
              JSON.parse(data['new_js_params'] || data['load_js'].params).currResearchType;
    
          var changes = [];
          $.each(researches, function (name, researchData) {
            var id = parseInt(idRegex.exec(researchData.aHref)[1]);
            var levelMatch = levelRegex.exec(name);
            var level = levelMatch 
                ? parseInt(levelMatch[1]) - 1
                : (researchData.liClass == 'explored' ? 1 : 0);
    
            civData._updateResearch(id, level, changes);
          });
          raiseCivilizationDataChanged(changes);
        }
        
        function processFinancesView(data) {
          var cities = getOwnCities();
          var scientistCost = 6;
          if (getCivilizationData().hasResearched(Constants.Research.Science.LETTER_CHUTE)) {
            scientistCost = 3;
          }
    
          var changes = []
          $('#finances .table01:eq(1) tr').slice(1, -1).each(function(index, row) {
            var tds = $(row).children('td');
            var city = cities[index];
            if ($(tds[0]).text() == city.getName()) {
              city._updateScientists(
                  Math.round(-parseInt($(tds[2]).text().replace(',', '')) / scientistCost), changes);
            }
          });
          raiseResourcesChanged(changes);
        }
        
        function processMilitaryAdvisorView(data) {
          var civilizationData = getCivilizationData();
          var movementIds = {};
          
          var changes = [];
          
          $.each(civilizationData.getMovements(), function(index, movement) {
            movementIds[movement.getId()] = movement;
          });
          
          var movementMainValueRegex = /^js_MilitaryMovementsEventRow(\d+)$/;
          var cityIdRegex = /cityId=(\d+)/;
          $.each(data, function(key, value) {
            var match = movementMainValueRegex.exec(key);
            if (match /*&& value.class.indexOf('own') > 0*/) {
              var movementId = parseInt(match[1]);
              delete movementIds[movementId];
    
              var type = value.class;
              var completionTime = 
                  data['js_MilitaryMovementsEventRow' + movementId + 'ArrivalTime']
                      .countdown.enddate * Constants.Time.MILLIS_PER_SECOND;
              var originCityId = parseInt(
                  data['js_MilitaryMovementsEventRow' + movementId + 'OriginLink'].href
                      .match(cityIdRegex)[1]);
              var targetCityId = data['js_MilitaryMovementsEventRow' + movementId + 'TargetLink'].href
                  ? parseInt(data['js_MilitaryMovementsEventRow' + movementId + 'TargetLink'].href
                      .match(cityIdRegex)[1]) : 0;
              var mission = data['js_MilitaryMovementsEventRow' + movementId + 'MissionIcon']
                  .class.split(' ')[1];
              var stage = Constants.Movements.Stage.LOADING;
              var statusClass = 
                  data['js_MilitaryMovementsEventRow' + movementId + 'Mission'].class;
              if (statusClass && statusClass.indexOf('arrow_right_green') >= 0) {
                stage = Constants.Movements.Stage.EN_ROUTE;
              } else if (statusClass && statusClass.indexOf('arrow_left_green') >= 0) {
                stage = Constants.Movements.Stage.RETURNING;
              }
              
              var transports = 0;
              var resources = {};
              var units = new MilitaryUnits();
    
              $.each(
                  data['js_MilitaryMovementsEventRow' + movementId + 'UnitDetails']
                      .appendElement || [],
                  function processUnit(index, item) {
                    var count = parseInt(item.text);
                    
                    if (item.class.indexOf('ship_transport') >= 0) {
                      transports = count;
                    }
                    
                    if (item.class.indexOf(Constants.Resources.WOOD) >= 0) {
                      resources[Constants.Resources.WOOD] = count;
                    } else if (item.class.indexOf(Constants.Resources.WINE) >= 0) {
                      resources[Constants.Resources.WINE] = count;
                    } else if (item.class.indexOf(Constants.Resources.MARBLE) >= 0) {
                      resources[Constants.Resources.MARBLE] = count;
                    } else if (item.class.indexOf(Constants.Resources.GLASS) >= 0) {
                      resources[Constants.Resources.GLASS] = count;
                    } else if (item.class.indexOf(Constants.Resources.SULFUR) >= 0) {
                      resources[Constants.Resources.SULFUR] = count;
                    }
                    
                    $.each(Constants.Military, function findIsUnit(key, type) {
                      if (item.class.indexOf(' ' + type) >= 0) {
                        units._setCount(type, count);
                        return false;
                      }
                    });
                  });
                      
              var movement = new Movement(
                  movementId, type, completionTime, mission, stage, originCityId, targetCityId,
                  transports, units, resources);
              
              civilizationData._updateMovement(movement, changes);
            }
          });
          
          $.each(movementIds, function removeMissingMovements(id, value) {
            civilizationData._removeMovement(id, changes);
          });
          
          raiseMovementsChanged(changes);
        }
        
        function processPremiumView(data) {
          var civilizationData = getCivilizationData();
          var changes = [];
    
          civilizationData._updatePremiumFeature(changes,
              Constants.PremiumFeatures.DOUBLED_SAFE_CAPACITY,
              $('#js_buySafecapacityBonusActiveTime').hasClass('green'));      
          civilizationData._updatePremiumFeature(changes,
              Constants.PremiumFeatures.DOUBLED_STORAGE_CAPACITY,
              $('#js_buyStoragecapacityBonusActiveTime').hasClass('green'));
          
          raiseCivilizationDataChanged(changes);
        }
        
        function updateAndStartTracking() {
          // Process all known cities that show up in the dropdown.  
          // Drop any cities that are no longer there.
          var cities = { };
          var cityOrder = [];
          
          function updateCurrentCity(globalData, backgroundData, correctWineConsumption) {
            var currentCity = View.getCurrentCity();
            if (View.viewIsCity() && currentCity.getId() == parseInt(backgroundData.id)) {
              currentCity._updateFromBackgroundData(backgroundData);
            }
            currentCity._updateFromGlobalData(globalData, correctWineConsumption);
            Logging.debug("Current city %s[%s]: ", 
                currentCity.name, currentCity.id, currentCity);
          }
    
          $.each(unsafeWindow.dataSetForView.relatedCityData, 
              function updateFromPage_Each(key, value) {
                if (key.substring(0, 5) == "city_") {
                  var city = empireData.get().cities[value.id] || 
                      new City(value.id, value.relationship);
                  city.type = value.relationship;
    
                  city.name = value.name;
                  city.islandCoordinates = parseCoordinates(value.coords);
                  if (value.tradegood) {
                    city.tradeGoodType = TRADE_GOOD_LOOKUP[value.tradegood];
                  }
    
                  empireData.get().cities[city.id] = city;
                  cityOrder.push(city.id);
    
                  Logging.debug("City %s[%s]: %o", city.name, city.id, city);
                }
              });
    
          empireData.get().cityOrder = cityOrder;
    
          var globalData = {
            maxActionPoints: parseInt($('#js_GlobalMenu_maxActionPoints').text()),
          };
          updateCurrentCity($.extend(globalData, unsafeWindow.dataSetForView), 
                            unsafeWindow.ikariam.backgroundView.screen.data,
                            true);
                                  
          empireData.saveAsync();
                            
          function updateEmpireDataFromGlobalData(data) {
    
            View.setGameTimeDifference(new Date().getTime() - (data['time'] || data[1]) * 
                Constants.Time.MILLIS_PER_SECOND);
            updateCurrentCity(data['headerData'] || data[10], data['backgroundData'] || data[11]);
          }
              
          View.registerIkariamAjaxResponseCallback(
              function updateEmpireDataFromAjaxResponse(response) {
                var globalData;
                var view;
                var viewHtml;
                var templateData;
    
                Utils.iterateIkariamAjaxResponse(response, function(index, name, data) {
                  if (name == Constants.IkariamAjaxResponseType.UPDATE_GLOBAL_DATA) {
                    globalData = data;
                  } else if (name == Constants.IkariamAjaxResponseType.CHANGE_VIEW) {
                    view = data[0];
                    viewHtml = data[1];
                  } else if (name == Constants.IkariamAjaxResponseType.UPDATE_TEMPLATE_DATA) {
                    templateData = data;
                  }
                });
    
                if (globalData) {
                  updateEmpireDataFromGlobalData(globalData);
                }
    
                if (view == Constants.View.CITY_MILITARY) {
                  processCityMilitaryView(templateData);
                } else if (view == Constants.View.RELATED_CITIES) {
                  processRelatedCitiesView(templateData);
                } else if (view == Constants.View.ACADEMY) {
                  processAcademyView(templateData);
                } else if (view == Constants.View.PALACE) {
                  processPalaceView(templateData);
                } else if (view == Constants.View.MUSEUM) {
                  processMuseumView(templateData);
                } else if (view == Constants.View.ASSIGN_CULTURAL_POSSESSIONS) {
                  processCulturalPossessionsAssignView(templateData);
                } else if (view == Constants.View.TOWN_HALL) {
                  processTownHallView(templateData);
                } else if (view == Constants.View.TEMPLE) {
                  processTempleView(templateData);
                } else if (view == Constants.View.RESEARCH_ADVISOR) {
                  processResearchAdvisorView(templateData);
                } else if (view == Constants.View.FINANCES) {
                  processFinancesView(templateData);
                } else if (view == Constants.View.BARRACKS) {
                  processBarracksView(viewHtml, templateData);
                } else if (view == Constants.View.SHIPYARD) {
                  processShipyardView(viewHtml, templateData);
                } else if (view == Constants.View.MILITARY_ADVISOR) {
                  processMilitaryAdvisorView(templateData);
                } else if (view == Constants.View.PREMIUM) {
                  processPremiumView(templateData);
                }
    
                if (unsafeWindow.ikariam.templateView) {
                  if (unsafeWindow.ikariam.templateView.id == Constants.View.RESEARCH_ADVISOR) {
                    processResearchAdvisorView(templateData);
                  }
                }
    
                empireData.saveAsync();
              }, true);
          
          View.registerAjaxFormSubmitCallback(
              function ajaxHandlerCallFromFormEmpireDataUpdate(form) {
                if (form.id == 'transport'  || form.id == 'transportForm') {
                  processTransportForm(form);
                } else if (form.id == 'deploymentForm') {
                  processDeploymentForm(form);
                } else if (form.id == 'plunderForm') {
                  processPlunderForm(form);
                } else if (form.id == 'setScientists') {
                  processSetScientistsForm(form);
                }
              });
        }
        
        function updateMovements(callback) {
          View.backgroundGetIkariamPage(
            'http://' + document.domain + '/index.php?view=militaryAdvisor&activeTab=militaryMovements&ajax=1',
            function updateMovementsCallback(response) {
              var dataResponse = JSON.parse(response.responseText);
              Utils.iterateIkariamAjaxResponse(dataResponse, function(index, name, data) {
                if (name == Constants.IkariamAjaxResponseType.UPDATE_TEMPLATE_DATA) {
                  processMilitaryAdvisorView(data);
                }
              });
              empireData.saveAsync();
              callback(dataResponse);
            },
            'POST');
        }
        
        function getCities() {
          var data = empireData.get();
          var cities = [];
          for (var i = 0; i < data.cityOrder.length; i++) {
            cities.push(data.cities[data.cityOrder[i]]);
          }
          return cities;
        }
        
        function getOwnCities() {
          return getCities().filter(function(city) {
            return city.isOwn();
          });
        }
        
        function getCivilizationData() {
          return empireData.get().civilizationData;
        }
        
        function getDebugString(includePrivateData) {
          return JSON.stringify(empireData.get(), function debugStringify(name, value) {
            if (name === 'name' || name === 'islandCoordinates') {
              return undefined;
            }
            return value;
          });
        }
        
        function resetData() {
          empireData.reset();
        }
    
        var Espionage = function() {
          function Target(id) {
            this._ikaToolsType = 'target';
            
            if (id) {
              this.id = id;
    
              this.playerId = undefined;
              this.allianceId = undefined;
              
              this.townLevel = undefined;
              this.wallLevel = undefined;
              this.warehouseLevel = undefined;
    
              this.islandId = undefined;
              this.coords = undefined;
    
              this.occupierId = undefined;
              this.blockaderId = undefined;
              
              this.tradeGoodType = undefined;
    
              this.lastUpdateTime = 0;
              
              this.military = new MilitaryUnits();
              this.otherMilitary = new MilitaryUnits();
              this.militaryLastSpyMessageId = 0;
              this.militaryLastSpyTime = 0;
    
              this.resources = {
                wood: 0,
                wine: 0,
                marble: 0,
                glass: 0,
                sulfur: 0,
    
                lastSpyMessageId: 0,
                lastSpyTime: 0,
              };
    
              this.combats = {};
            }
          }
    
          function combatComparer(combat1, combat2) {
                  return combat2.time - combat1.time;
                };
    
          $.extend(Target.prototype, {
            getId: function getId() {
              return this.id;
            },
            getName: function getName() {
              return this.name;
            },
            getTownLevel: function getTownLevel() {
              return this.townLevel;
            },
            getWallLevel: function getWallLevel() {
              return this.wallLevel;
            },
            getIslandCoordinates: function getIslandCoordinates() {
              return this.coords;
            },
            getIslandId: function getIslandId() {
              return this.islandId;
            },
            getPlayer: function _getPlayer() {
              return getPlayer(this.playerId);
            },
            getOccupier: function getOccupier() {
              if (this.occupierId) {
                return getPlayer(this.occupierId);
              }
              return null;
            },
            getBlockader: function getBlockader() {
              if (this.blockaderId) {
                return getPlayer(this.blockaderId);
              }
            },
            getTradeGoodType: function getTradeGoodType() {
              return this.tradeGoodType;
            },
            getMilitary: function getMilitary() {
              return this.military;
            },
            getOtherMilitary: function getOtherMilitary() {
              return this.otherMilitary;
            },
            hasResourceInfo: function hasResourceInfo() {
              return this.resources.lastSpyMessageId > 0;
            },
            hasMilitaryInfo: function hasMilitaryInfo() {
              return this.militaryLastSpyMessageId > 0;
            },
            getLootableResources: function getLootableResources(type) {
              var available = this.resources[type];
              $.each(this.getCombats(View.gameTimeNow() - this.resources.lastSpyTime), 
                function subtractCombatResources(index, combat) {
                  available -= combat.getLooted(type);
                });
              return Math.max(0, available - 
                  (this.getPlayer().isInactive() ? 
                      Constants.GamePlay.RESOURCE_PROTECTION_WAREHOUSE_INACTIVE : 
                      Constants.GamePlay.RESOURCE_PROTECTION_WAREHOUSE) * this.warehouseLevel - 
                  Constants.GamePlay.BASE_RESOURCE_PROTECTION);
            },
            getResourcesSpyTime: function getResourcesSpyTime() {
              return this.resources.lastSpyTime;
            },
            getMilitarySpyTime: function getMilitarySpyTime() {
              return this.militaryLastSpyTime;
            },
            getCombats: function getCombats(maxAge) {
              var combats = [];
              $.each(this.combats, function(index, combat) {
                if (View.gameTimeNow() - combat.time <= maxAge) {
                  combats.push(combat);
                }
              });
              combats.sort(combatComparer);
              return combats;
            },
            remove: function remove() {
              delete espionageData.get().targets[this.id];
              espionageData.saveAsync();
                  raiseEspionageChanged({
                    type: 'targetRemoved',
                    targets: [this]
                  });
            },
            _refresh: function refresh(hasSpiesPresent, callback) {
              if (View.gameTimeNow() - this.lastRefreshTime < Constants.Time.MILLIS_PER_HOUR) {
                console.log('Skipping refresh');
                callback();
                return;
              }
    
              this.lastRefreshTime = new Date().getTime();
              
              var datasLoaded = hasSpiesPresent ? 2 : 1;
              function doneLoading() {
                if (--datasLoaded == 0) {
                  callback();
                }
              }
              Utils.backgroundFetchIkariamFullPage(
                  'http://' + document.domain + '/index.php?view=island&cityId=' + this.id,
                  this._refreshIslandCallback.bind(this, doneLoading));
              if (hasSpiesPresent) {
                Utils.backgroundFetchIkariamFullPage(
                    'http://' + document.domain + '/index.php?view=city&cityId=' + this.id,
                    this._refreshCityCallback.bind(this, doneLoading));
              }
            },
            _refreshIslandCallback: function refreshIslandCallback(callback, response, ajaxResponse) {
              var target = this;
              Utils.iterateIkariamAjaxResponse(ajaxResponse, 
                  function refreshTargetData(index, name, data) {
                    if (name == IkaTools.Constants.IkariamAjaxResponseType.UPDATE_BACKGROUND_DATA) {
                      $.each(data.cities, function findTarget(index, city) {
                        if (parseInt(city.id) == target.id) {
                          var playerId = parseInt(city.ownerId);
                          target.name = city.name;
                          target.playerId = parseInt(city.ownerId);
                          target.townLevel = parseInt(city.level);
                          target.islandId = parseInt(data.id);
                          target.coords = [parseInt(data.xCoord), parseInt(data.yCoord)];
                          target.tradeGoodType = TRADE_GOOD_LOOKUP[data.tradegood];
                          updateOrAddPlayer(playerId, city.ownerName, city.state, parseInt(city.ownerAllyId), 
                              city.ownerAllyTag, 
                              parseInt(data.avatarScores[playerId].army_score_main.split(',').join('').split(',').join('')) / 100);
                          if (city.infos && city.infos['occupation']) {
                            target.occupierId = city.infos['occupation'].id;
                            updateOrAddPlayer(city.infos['occupation'].id, city.infos['occupation'].name);
                          } else {
                            target.occupierId = 0;
                          }
                          if (city.infos && city.infos['fleetAction']) {
                            target.blockaderId = city.infos['fleetAction'].id;
                            updateOrAddPlayer(city.infos['fleetAction'].id, city.infos['fleetAction'].name);
                          } else {
                            target.blockaderId = 0;
                          }
                        }
                      });
                    }
                  });
              callback(this);
            },
            _refreshCityCallback: function refreshCityCallback(callback, response, ajaxResponse) {
              var target = this;
              Utils.iterateIkariamAjaxResponse(ajaxResponse, 
                  function refreshTargetData(index, name, data) {
                    if (name == IkaTools.Constants.IkariamAjaxResponseType.UPDATE_BACKGROUND_DATA) {
                      target.wallLevel = parseInt(data.position[14].level) || 0;
                      target.warehouseLevel = 0;
                      $.each(data.position, function(index, item) {
                        if (item.building == Constants.Buildings.WAREHOUSE) {
                          target.warehouseLevel += parseInt(item.level);
                        }
                      });
                    }
                  });
              callback(this);
            },
            _getOrAddCombat: function getOrAddCombat(id) {
              var combat = this.combats[id];
              if (!combat) {
                combat = new Target.Combat(id);
                this.combats[id] = combat;
              }
              return combat;
            },
          });
    
          Target.Combat = function Target_Combat(id) {
            this._ikaToolsType = 'targetCombat';
    
            if (id) {
              this.id = id;
              
              this.type = undefined;
              this.time = undefined;
    
              this.resources = {
                wood: 0,
                wine: 0,
                marble: 0,
                glass: 0,
                sulfur: 0,
              };
            }
          }
    
          $.extend(Target.Combat.prototype, {
            getType: function getType() {
              return this.type;
            },
            getTime: function getTime() {
              return this.time;
            },
            getLooted: function getLooted(resourceType) {
              return this.resources[resourceType];
            },
          });
    
          function Player(id) {
            this._ikaToolsType = 'player';
            
            if (id) {
              this.id = id;
              
              this.name = null;
              this.allianceId = null;
              this.militaryScore = null;
            }
          }
    
          $.extend(Player.prototype, {
            _update: function update(name, state, allianceId, militaryScore) {
              this.name = name;
              if (state !== undefined) {
                this.allianceId = allianceId;
                this.militaryScore = militaryScore;
                this.state = state;
              }
            },
            getAlliance: function getAlliance() {
              if (this.allianceId) {
                return espionageData.get().alliances[this.allianceId];
              } else {
                return;
              }
            },
            getName: function getName() {
              return this.name;
            },
            getState: function getState() {
              return this.state;
            },
            getMilitaryScore: function getMilitaryScore() {
              return this.militaryScore;
            },
            isInactive: function isInactive() {
              return this.state == Constants.PlayerState.INACTIVE;
            },
          });
    
          function updateOrAddPlayer(id, name, state, allianceId, allianceName, militaryScore) {
            var players = espionageData.get().players;
            var player = players[id];
            if (!player) {
              player = new Player(id);
              players[id] = player;
            }
            player._update(name, state, allianceId, militaryScore);
            updateOrAddAlliance(allianceId, allianceName);
          }
    
          function Alliance(id) {
            this._ikaToolsType = 'alliance';
            
            if (id) {
              this.id = id;
              this.name = null;
            }
          }
    
          $.extend(Alliance.prototype, {
            _update: function update(name) {
              this.name = name;
            },
            getName: function getName() {
              return this.name;
            },
            getId: function getId() {
              return this.id;
            }
          });
    
          function updateOrAddAlliance(id, name) {
            if (id) {
              var alliances = espionageData.get().alliances;
              var alliance = alliances[id];
              if (!alliance) {
                alliance = new Alliance(id);
                alliances[id] = alliance;
              }
              alliance._update(name);
            }
          }
    
          function addTargetById(id, hasSpiesPresent, callback) {
            var targets = espionageData.get().targets;
            var target = targets[id];
            if (!target) {
              var target = new Target(id);
              target._refresh(hasSpiesPresent, function() {
                targets[id] = target;
                callback(target);
              });
            } else {
              target._refresh(hasSpiesPresent, function() {
                callback(target);
              });
            }
          }
    
          function getTargets() {
            var targets = [];
            $.each(espionageData.get().targets, function(index, target) {
              targets.push(target);
            });
            return targets;
          }
    
          function getTarget(id) {
            return espionageData.get().targets[id];
          }
    
          function getPlayer(id) {
            return espionageData.get().players[id];
          }
    
          function getPlayers() {
            return espionageData.get().players;
          }
    
          var espionageData = new Data.Value(
              'espionageData', 
              { 
                targets: {},
                alliances: {},
                players: {},
              }, 
              {
                reviver: function espionageDataReviver(key, value) {
                  if (value && value._ikaToolsType) {
                    var obj;
    
                    switch(value._ikaToolsType) {
                      case 'target': obj = new Target(); break;
                      case 'targetCombat': obj = new Target.Combat(); break;
                      case 'player': obj = new Player(); break;
                      case 'alliance': obj = new Alliance(); break;
                      case 'militaryUnits': obj = new MilitaryUnits(); break;
                    }
                    $.extend(obj, value);
                    if (obj._postLoad) {
                      obj._postLoad();
                    }
                    return obj;
                  }
                  return value;
                },
                version: 6,
    
                loadCallback: function espionageDataLoaded() {
                },
              });
    
          var espionageChangedEvent = Utils.thunk(function() {
            return new Utils.EventDispatcher();
          });
        
          function registerEspionageChangedHandler(callback) {
            return espionageChangedEvent().addListener(callback);
          }
    
          function raiseEspionageChanged(changes) {
            espionageChangedEvent().send(changes);
          }
    
          function startTracking() {
            var messageIdRegex = /message(\d+)/
            espionageData.load();
            
            View.registerIkariamAjaxResponseCallback(
                function processHideoutView(response) {
                  IkaTools.Utils.iterateIkariamAjaxResponse(response, function(index, name, data) {
                    if (name == IkaTools.Constants.IkariamAjaxResponseType.CHANGE_VIEW) {
                      if (data[0] == Constants.View.HIDEOUT) {
                        var targetCount = 0;
                        var targets = [];
                        $('#tabSafehouse li.city a').each(function(index, item) {
                          targetCount++;
                          addTargetById(parseInt(Utils.parseUrlParams($(item).attr('href'))['cityId']), true, 
                            function targetAdded(target) {
                              targets.push(target);
                              targetCount--;
                              if (targetCount == 0) {
                                espionageData.saveAsync();
                                raiseEspionageChanged({
                                  type: 'targetsChanged',
                                  targets: targets
                                });
                              }
                            });
                        });
    
                        var reportHeaders = $(
                            '#espionageReports tr.espionageReports, #espionageReports tr.espionageReportsalt');
                        if (reportHeaders.length) {
                          var changedTargets = [];
                          reportHeaders.each(function(index, reportHeader) {
                            var success = $('td.resultImage img', reportHeader).attr('src') == '/cdn/all/both/buttons/yes.png';
                            var target = getTarget(parseInt(
                                Utils.parseUrlParams($('td.targetCity a', reportHeader).attr('href'))['selectCity']));
                            var messageId = parseInt(reportHeader.id.match(messageIdRegex)[1]);
                            var tableMailMessage = $('#tbl_mail' + messageId);
                            if (success && target) {
                              if ($('td.money', reportHeader).length &&
                                  messageId > target.resources.lastSpyMessageId) {
                                // Warehouse resources mission
                                var resourceTds = $('#tbl_mail' + messageId + ' td.count');
                                target.resources.wood = parseInt(resourceTds.get(0).textContent.replace(',', ''));
                                target.resources.wine = parseInt(resourceTds.get(1).textContent.replace(',', ''));
                                target.resources.marble = parseInt(resourceTds.get(2).textContent.replace(',', ''));
                                target.resources.glass = parseInt(resourceTds.get(3).textContent.replace(',', ''));
                                target.resources.sulfur = parseInt(resourceTds.get(4).textContent.replace(',', ''));
                                target.resources.lastSpyMessageId = messageId;
                                target.resources.lastSpyTime = 
                                    Utils.parseIkariamTimestamp($('td.date', reportHeader).text()).getTime();
                                target._refresh(false, function() {
                                  espionageData.saveAsync(),
                                  raiseEspionageChanged({
                                    type: 'targetsRefreshed',
                                    targets: [target],
                                  });
                                });
                                changedTargets.push(target);
                              } else if ($('td.garrison', reportHeader).length && 
                                  messageId > target.militaryLastSpyMessageId && 
                                  tableMailMessage.find(' table.reportTable').length) {
    
                                readSpiedMilitary(target.getMilitary(), 
                                    tableMailMessage.find('table.reportTable tr:nth-child(2) td.count'));
                                readSpiedMilitary(target.getOtherMilitary(), 
                                    tableMailMessage.find('table.reportTable tr:nth-child(3) td.count'));
                                    
                                target.militaryLastSpyMessageId = messageId;
                                target.militaryLastSpyTime = Utils.parseIkariamTimestamp($('td.date', reportHeader).text()).getTime();
    
                                target._refresh(false, function() {
                                  espionageData.saveAsync(),
                                  raiseEspionageChanged({
                                    type: 'targetsRefreshed',
                                    targets: [target],
                                  });
                                });
                                changedTargets.push(target);
                              }
                            }
                          });
                          if (changedTargets.length) {
                            espionageData.saveAsync();
                            raiseEspionageChanged({
                              type: 'targetsChanged',
                              targets: changedTargets
                            });
                          }
                        }
                      } else if (data[0] == Constants.View.MILITARY_ADVISOR_REPORT) {
                        var report = $('#troopsReport');
                        var defender = report.find('.defender b:first a:first');
                        if (defender.length) {
                          var target = getTarget(parseInt(
                              Utils.parseUrlParams(defender.attr('href'))['cityId']));
                          if (target) {
                            var header = report.find('.header');
                            var result = report.find('div.result');
                            var combatId = parseInt(Utils.parseUrlParams(
                                report.find('div p.link:first a.button:first',report).attr('href'))['detailedCombatId']);
                            var combatTime = Utils.parseIkariamTimestamp(report.find('.header .date').text()).getTime();
                            var type = report.find('.overview .fleet').length ? 
                                IkaTools.Constants.CombatType.BLOCKADE : IkaTools.Constants.CombatType.PILLAGE;
    
                            var combat = target._getOrAddCombat(combatId);
                            combat.type = type;
                            combat.time = combatTime;
                            result.find('.resources li.value').each(function(index, item) {
                              var resourceInfo = $(item);
                              var type = resourceInfo.find('img').attr('src').match(/icon_([a-z]*)_small.png/)[1];
                              if (type == 'crystal') {
                                type = Constants.Resources.GLASS;
                              }
                              var amount = parseInt(resourceInfo.text());
                              combat.resources[type] = amount;
                            });
    
                            target._refresh(false, function() {
                              espionageData.saveAsync(),
                              raiseEspionageChanged({
                                type: 'targetsRefreshed',
                                targets: [target],
                              });
                            });
                          }
                          
                          espionageData.saveAsync();
                          raiseEspionageChanged({
                            type: 'combatUpdated',
                            targets: [target],
                          });
                        }
                      }
                    } else if (name == Constants.IkariamAjaxResponseType.BACKGROUND_DATA) {
                    }
                  });
                }, true);
          }
    
          function readSpiedMilitary(military, unitTds) {
            var baseArmyCount = true;
            var baseNavyCount = true;
            var navyOffset = 0;
            
            if (unitTds.length == 14) { // army only
              baseNavyCount = 0;
            } else if (unitTds.length == 11) { // navy only
              baseArmyCount = 0;
              navyOffset = -14;
            } else if (unitTds.length == 0) { // nothing
              baseNavyCount = 0;
              baseArmyCount = 0;
            }
            military._setCount(Constants.Military.HOPLITE, baseArmyCount && parseInt($(unitTds[0]).text()) || 0);
            military._setCount(Constants.Military.STEAM_GIANT, baseArmyCount && parseInt($(unitTds[1]).text()) || 0);
            military._setCount(Constants.Military.SPEARMAN, baseArmyCount && parseInt($(unitTds[2]).text()) || 0);
            military._setCount(Constants.Military.SWORDSMAN, baseArmyCount && parseInt($(unitTds[3]).text()) || 0);
            military._setCount(Constants.Military.SLINGER, baseArmyCount && parseInt($(unitTds[4]).text()) || 0);
            military._setCount(Constants.Military.ARCHER, baseArmyCount && parseInt($(unitTds[5]).text()) || 0);
            military._setCount(Constants.Military.GUNNER, baseArmyCount && parseInt($(unitTds[6]).text()) || 0);
            military._setCount(Constants.Military.BATTERING_RAM, baseArmyCount && parseInt($(unitTds[7]).text()) || 0);
            military._setCount(Constants.Military.CATAPULT, baseArmyCount && parseInt($(unitTds[8]).text()) || 0);
            military._setCount(Constants.Military.MORTAR, baseArmyCount && parseInt($(unitTds[9]).text()) || 0);
            military._setCount(Constants.Military.GYROCOPTER, baseArmyCount && parseInt($(unitTds[10]).text()) || 0);
            military._setCount(Constants.Military.BALLOON_BOMBADIER, baseArmyCount && parseInt($(unitTds[11]).text()) || 0);
            military._setCount(Constants.Military.COOK, baseArmyCount && parseInt($(unitTds[12]).text()) || 0);
            military._setCount(Constants.Military.DOCTOR, baseArmyCount && parseInt($(unitTds[13]).text()) || 0);
    
            military._setCount(Constants.Military.RAM_SHIP, parseInt(baseNavyCount && $(unitTds[16 + navyOffset]).text()) || 0);
            military._setCount(Constants.Military.FLAME_THROWER, parseInt(baseNavyCount && $(unitTds[14 + navyOffset]).text()) || 0);
            military._setCount(Constants.Military.STEAM_RAM, parseInt(baseNavyCount && $(unitTds[15 + navyOffset]).text()) || 0);
            military._setCount(Constants.Military.BALLISTA_SHIP, parseInt(baseNavyCount && $(unitTds[18 + navyOffset]).text()) || 0);
            military._setCount(Constants.Military.CATAPULT_SHIP, parseInt(baseNavyCount && $(unitTds[17 + navyOffset]).text()) || 0);
            military._setCount(Constants.Military.MORTAR_SHIP, parseInt(baseNavyCount && $(unitTds[19 + navyOffset]).text()) || 0);
            military._setCount(Constants.Military.SUBMARINE, parseInt(baseNavyCount && $(unitTds[21 + navyOffset]).text()) || 0);
            military._setCount(Constants.Military.PADDLE_SPEED_SHIP, parseInt(baseNavyCount && $(unitTds[22 + navyOffset]).text()) || 0);
            military._setCount(Constants.Military.BALLOON_CARRIER, parseInt(baseNavyCount && $(unitTds[23 + navyOffset]).text()) || 0);
            military._setCount(Constants.Military.TENDER, parseInt(baseNavyCount && $(unitTds[24 + navyOffset]).text()) || 0);
            military._setCount(Constants.Military.ROCKET_SHIP, parseInt(baseNavyCount && $(unitTds[20 + navyOffset]).text()) || 0);
          }
          
          function getDebugString(includePrivateData) {
            return JSON.stringify(espionageData.get(), function debugStringify(name, value) {
              if (name === 'name' || name === 'coordinates') {
                return undefined;
              }
              return value;
            });
          }
    
          function resetData() {
            espionageData.reset();
          }
        
          return {
            startTracking: startTracking,
            registerEspionageChangedHandler: registerEspionageChangedHandler,
            getTargets: getTargets,
            getPlayers: getPlayers,
            
            getDebugString: getDebugString,
            resetData: resetData,
          };
        }();
        
        return {
          updateAndStartTracking: Logging.debuggable(
              { label: 'IkaTools.EmpireData.updateAndStartTracking', alwaysTime: true }, 
              updateAndStartTracking),
          updateMovements: updateMovements,
          
          calculateTravelTime: calculateTravelTime,
          
          getCities: Utils.thunk(getCities),
          getOwnCities: Utils.thunk(getOwnCities),
          getCivilizationData: getCivilizationData,
          getCity: getCity,
          
          getDebugString: getDebugString,
          resetData: resetData,
          
          registerCivilizationDataChangedHandler: registerCivilizationDataChangedHandler,
          registerResourcesChangedHandler: registerResourcesChangedHandler,
          registerBuildingsChangedHandler: registerBuildingsChangedHandler,
          registerMilitaryChangedHandler: registerMilitaryChangedHandler,
          registerMovementsChangedHandler: registerMovementsChangedHandler,
    
          Espionage:Espionage,
        };
      }();
      
      function processAnchor() {
        var anchor = window.location.hash;
    
        if (anchor == '#ikaScriptToolsSuppressCityPreselect') {
          document.location.hash = '';
          //unsafeWindow.ikariam.backgroundView.screen.preselectCity = 
          //    function suppressPreselectCity() { };
          View.suppressFirstChangeViewOfType('cityDetails');
        }
        if (anchor.substring(0, 35) == '#ikaScriptToolsLoadLocalIkariamUrl=') {
          var url = decodeURIComponent(anchor.substring(35));
          document.location.hash = '';
          IkaTools.View.loadLocalIkariamUrl(url);
          if (IkaTools.View.viewIsIsland()) {
            View.suppressFirstChangeViewOfType('cityDetails');
          }
        } else if (anchor.substring(0, 62) == 
            '#ikaScriptToolsLoadLocalIkariamUrl_DoNotSuppressFirstCityInfo=') {
          var url = decodeURIComponent(anchor.substring(62));
          document.location.hash = '';
          IkaTools.View.loadLocalIkariamUrl(url);
        }
      }
      
      function initialize(options) {
        processAnchor();
        $(window).bind('hashchange', processAnchor);
        options = $.extend({ trackData: true }, options);
        View.setGameTimeDifference(new Date().getTime() - 
            unsafeWindow.ikariam.model.requestTime * Constants.Time.MILLIS_PER_SECOND - 
            Constants.Time.INITIAL_PAGE_LOAD_DELTA);
        if (options.trackData) {
          IkaTools.EmpireData.updateAndStartTracking();
        }
      }
      
      return {
        Logging: Logging,
        Utils: Utils,
        View: View,
        Data: Data,
        Intl: Intl,
        EmpireData: EmpireData,
        Constants: Constants,
        UI: UI,
        Settings: Settings,
        
        initialize: initialize,
        processAnchor: processAnchor,
      };
    })();
    }