Kronos Utils 0.6

By Johan Sundström Last update Jun 24, 2008 — Installed 23,713 times.

the source is over 100KB, syntax highlighting in the browser is too slow

// ==UserScript==
// @name           Kronos Utils
// @namespace      Kronos
// @description    Tons of UI upgrades and features for Ikariam.
// @include        http://ikariam.tld/
// @include        http://s*.ikariam.tld/*
// @include        http://s*.ikariam.com.pt/*
// @exclude        http://board.ikariam.*/
// @exclude        http://*.ikariam.*/index.php?view=renameCity*
// @include        http://ikariam.immortal-nights.com/ikafight/*
// @require        http://ecmanaut.googlecode.com/svn/trunk/lib/gm/wget.js
// @resource woody http://ecmanaut.googlecode.com/svn/trunk/sites/ikariam.org/kronos-utils/header.png
// @resource att-r http://ecmanaut.googlecode.com/svn/trunk/sites/ikariam.org/kronos-utils/arrow-right.png
// @resource att-l http://ecmanaut.googlecode.com/svn/trunk/sites/ikariam.org/kronos-utils/arrow-left.png
// @resource buy-r http://ecmanaut.googlecode.com/svn/trunk/sites/ikariam.org/kronos-utils/arrow-right-buy.png
// @resource buy-l http://ecmanaut.googlecode.com/svn/trunk/sites/ikariam.org/kronos-utils/arrow-left-buy.png
// @resource sel-r http://ecmanaut.googlecode.com/svn/trunk/sites/ikariam.org/kronos-utils/arrow-right-sell.png
// @resource sel-l http://ecmanaut.googlecode.com/svn/trunk/sites/ikariam.org/kronos-utils/arrow-left-sell.png
// @resource trp-r http://ecmanaut.googlecode.com/svn/trunk/sites/ikariam.org/kronos-utils/arrow-right-trp.png
// @resource trp-l http://ecmanaut.googlecode.com/svn/trunk/sites/ikariam.org/kronos-utils/arrow-left-trp.png
// @resource tsp-r http://ecmanaut.googlecode.com/svn/trunk/sites/ikariam.org/kronos-utils/arrow-right-tsp.png
// @resource tsp-l http://ecmanaut.googlecode.com/svn/trunk/sites/ikariam.org/kronos-utils/arrow-left-tsp.png
// @resource col-r http://ecmanaut.googlecode.com/svn/trunk/sites/ikariam.org/kronos-utils/arrow-right-col.png
// @resource col-l http://ecmanaut.googlecode.com/svn/trunk/sites/ikariam.org/kronos-utils/arrow-left-col.png
// @resource   css http://ecmanaut.googlecode.com/svn/trunk/sites/ikariam.org/kronos-utils/kronos.css
// @require        http://ecmanaut.googlecode.com/svn/trunk/sites/ikariam.org/kronos-utils/i18n.js
// @require        http://ecmanaut.googlecode.com/svn/trunk/sites/ikariam.org/kronos-utils/gamedata.js
// @require        http://ecmanaut.googlecode.com/svn/trunk/sites/ikariam.org/kronos-utils/support.js
// @require        http://ecmanaut.googlecode.com/svn/trunk/sites/ikariam.org/kronos-utils/config.js
// @require        http://ecmanaut.googlecode.com/svn/trunk/sites/ikariam.org/kronos-utils/memory.js
// ==/UserScript==

var kronos = this, version = "0.6", lang, scientists, growthDebug = 0;
if (config.get("debug")) unsafeWindow.kronos = kronos;
if (/^http:\/\/ikariam.immortal-nights.com\/ikafight/i.test(location.href))
  augmentIkaFight();
if (location.hostname.match(/^s\d+\./))
  init();
else
  login();

function init() {
  try { upgradeConfig(); }
  catch(e if e instanceof ReferenceError) {
    if (confirm("Kronos Utils requires Greasemonkey 0.8. Click OK for an " +
                "installation tutorial."))
      location.href = "http://corentin.jarnoux.free.fr/kronosutils/" +
        "?topic=2.0#post_requirements";
    return;
  }

  unbreakSliders();

  addEventListener("load", maybeAugmentOverviewTable, false); // wine shortages?
  addEventListener("keyup", cityByNumber, false);
  if (innerWidth > 1003) document.body.style.overflowX = "hidden"; // !scrollbar
  css(GM_getResourceText("css"));
  lang = langs[getLanguage()];

  var view = urlParse("view");
  var action = urlParse("action");

  var help = $X('id("buildingUpgrade")/h3/a[@class="help"]');
  if (help) {
    var building = buildingID(view);
    if (isDefined(building)) {
      linkTo(urlTo("building", building), help);
      var level = $X('id("buildingUpgrade")//div[@class="buildingLevel"]');
      if (level)
        config.setCity(["l", building], number(level));
    }
  }

  try {
    augment(view, action, lang);
  }
  finally {
    processHash();
  }
}

function augment(view, action, lang) {
  switch (view || action) {
    case "tavern": tavernView(); break;
    case "resource":  // fall-through:
    case "tradegood": resourceView(); // fall-through:
    case "takeOffer": scrollWheelable(); break;
    case "premiumTrader": // fall-through:
    case "transport": scrollWheelable(); evenShips(); break;
    case "loginAvatar":// &function=login
    case "CityScreen": // &function=build&id=...&position=4&building=13
    case "city": cityView(); break;
    case "finances": financesView(); break;
    case "buildingDetail": buildingDetailView(); break;
    case "port": portView(); break;
    case "island": islandView(); break;
    case "worldmap_iso": worldmap_isoView(); break;
    case "townHall": townHallView(); break;
    case "culturalPossessions_assign": scrollWheelable(); // fall-through:
    case "museum": museumView(); break;
    case "embassy": embassyView(); break;
    case "fleetGarrisonEdit": // fall-through:
    case "armyGarrisonEdit": dontSubmitZero(); break;
    case "shipyard": shipyardView(); break;
    case "barracks": barracksView(); break;
    case "workshop-army": workshopView("troops"); break;
    case "workshop-fleet": workshopView("ships"); break;
    case "buildingGround": buildingGroundView(); break;
    case "branchOffice": branchOfficeView(); break;
    case "researchOverview": researchOverviewView(); break;
    case "colonize": colonizeView(); break;
    case "blockade": blockadeView(); break;
    case "deployment": deploymentView(); break;
    case "merchantNavy": merchantNavyView(); break;
    case "militaryAdvisorReportView":
      militaryAdvisorReportViewView(); break;
    case "militaryAdvisorCombatReports":
      militaryAdvisorCombatReportsView(); break;
    case "militaryAdvisorMilitaryMovements":
      militaryAdvisorMilitaryMovementsView(); break;
    case "tradeAdvisor": tradeAdvisorView(); break;
    case "researchAdvisor": researchAdvisorView(); break;
    case "diplomacyAdvisor": diplomacyAdvisorView(); break;
    case "plunder": plunderView(); break;
    case "safehouse": safehouseView(); break;
    case "Espionage":
    case "safehouseReports": safehouseReportsView(); break;
    case "academy": academyView(); break;
    case "options": optionsView(); break;
  }

  if (!$("tabz") && $("tearing"))
    cityTabs();

  var upgradeDiv = $("upgradeCountDown");
  var buildDiv = $("buildCountDown");
  projectCompletion(upgradeDiv, "time")
  projectCompletion(buildDiv);
  projectHaveResourcesToUpgrade();

  var queued;
  if ((queued = (location.hash||"").match("#q:(.*)")))
    reallyUpgrade(queued[1]);
  processQueue(!queued);
  document.addEventListener("click", changeQueue, true);

  improveTopPanel();
  if ({ city: 1, island: 1 }[view])
    unsafeWindow.friends = config.getServer("treaties", []);
  fixUpdates();

  if (!config.get("kronosMenu")) return title();
  title();
  if ("militaryAdvisorMilitaryMovements" != urlParse("view")) {
    var langChoice = panelInfo();
    langChoice.title = lang.execTime +": "+ (Date.now() - DEBUT) +"ms";
  }
}

function processHash() {
  if (location.hash) {
    var fn = location.hash.match(/^#call:(.*)/);
    if (fn && (fn = kronos[fn[1]])) setTimeout(fn, 1e3);

    var id = location.hash.match(/^#keep:(.*)/);
    if (id) keep(id[1].split(","));

    var rest = urlParse(null, location.hash.slice(1));
    for each (var where in "before,after,prepend,append,replace".split(",")) {
      if (!rest[where]) continue;
      var opts = {}, is = rest[where], at = is.indexOf(":");
      var path = is.slice(0, at), what = is.slice(at+1);
      if ((opts[where] = $X(path))) {
        var div = document.createElement("div");
        div.innerHTML = what;
        var tmp = document.createRange();
        tmp.selectNodeContents(div);
        opts.tag = tmp.extractContents();
        node(opts);
      }
    }
  }
}

function unbreakSliders() {
  var sliders = unsafeWindow.sliders;
  if (!sliders || !config.get("debug")) return;
  return;
  for (var id in sliders) {
    var slider = sliders[id];
    slider.adjustSliderRange(slider.actualMax + 1e-5);
    slider.valueToThumb = function() {
      this.setValue(Math.floor( (this.actualValue / this.scaleFactor) -
                                config.topConstraint ));
    };
  }
}

function cityByNumber(e) {
  var on = e.target, name = on && on.nodeName;
  if (/^(input|textarea)$/i.test(name||"") &&
      !/radio|checkbox/i.test(on.type||""))
    return; // focused element was a text field of some sort
  var li = $x('id("changeCityForm")//*[contains(@class,"citySelect")]/ul/li');
  var key = integer(String.fromCharCode(e.keyCode));
  if (isNaN(key)) return;
  var n = (key || 10) - 1;
  var id = cityIDs()[n];
  switch ((e.altKey << 1) + e.shiftKey) {
    case 3: return location.search = urlTo("fleet", id);
    case 2: return location.search = urlTo("army", id);
    case 1: return location.search = urlTo("transport", id);
    case 0:
      li = li[n];
      li && click(li);
  }
}

function login() {
  var uni = $("universe");
  if (!uni || location.pathname != "/" || !document.referrer ||
      !document.referrer.indexOf(location.href))
    return;
  var site = /^http:..s(\d+)\.ikariam/.exec(document.referrer);
  if (site)
    uni.selectedIndex = integer(site[1]) - 1;
}

function wineConsumptionTime(total, rate, city) {
  if (city)
    rate = config.getCity(["x", buildingIDs.tavern], 0, city);
  total = integer(total);
  rate = Math.abs(integer(rate));
  if (!rate) return Infinity;
  return Math.floor(3600 * total / rate);
}

function maybeAugmentOverviewTable() {
  function countdown(td, s) {
    var time = secsToDHMS(s, 0);
    if (time != td.textContent)
      td.textContent = time;
    var wait = s < 61 ? 1 : s % 60 + 1;
    setTimeout(countdown, wait * 1e3, td, s - wait);
  }

  function wineLasts(td) {
    if (td.textContent) return;
    var total = $X('preceding-sibling::td[2]', td).textContent;
    var rate = $X('preceding-sibling::td[1]', td).textContent;
    if (rate) {
      var left = wineConsumptionTime(total, rate);
      if (!isFinite(rate)) return;
      countdown(td, left);
    }
  }
  var td = $x('id("overview__table")/table[@class="resources_table"]/tbody/' +
              'tr[count(td) = 19 and not(@class="table_footer")]/td[11]'); // W
  if (td) td.map(wineLasts);
}

function url(query) {
  return (location.search || "").replace(/([#?].*)?$/, query||"");
}

function jsVariable(nameValue) {
  var resourceScript = $X('id("cityResources")//script');
  if (resourceScript) {
    var text = resourceScript.innerHTML;
    text = text.substr(text.indexOf(nameValue+" = "),
                       text.length);
    text = text.substr(nameValue.length+3,
                       text.indexOf(";")-(nameValue.length+3));
    return text;
  }
}

function luxuryType(type) {
  var script = $X('id("cityResources")/script').textContent.replace(/\s+/g," ");
  var what = script.match(/currTradegood.*?value_([^\x22\x27]+)/)[1];
  switch (type) {
    case undefined:
    case 0: return resourceIDs[what];

    case "name":
    case 1: return what;

    case "glass": return what.replace("crystal", "glass");

    case "english":
    case 2:
      what = $X('id("value_'+ what +'")/preceding-sibling::span');
      return what.textContent.replace(/:.*/, "");
  }
}

function goto(href) {
  //console.log("aborted goto %x", href); return;
  location.href = href.match(/\?/) ? href : urlTo(href);
}

function gotoCity(url, id) {
  // console.log("aborted gotoCity(%x, %x)", url, id); return;
  var city = $("citySelect");
  var ids = cityIDs();
  if (isDefined(id)) {
    city.selectedIndex = ids.indexOf(id);
  } else {
    var index = referenceCityID("index");
    city.selectedIndex = index;
    id = ids[index];
  }
  var form = city.form;
  if (isDefined(url)) form.action = url;
  form.elements.namedItem("oldView").value = "city";
  form.elements.namedItem("id").value = id;
  form.submit()
}


// Military stuff:

function militaryAdvisorMilitaryMovementsView() {
  function project(div) {
    var li = $X('ancestor::li', div)
    projectCompletion(div);
    li.style.height = "52px";
  }
  $x('//li/div/div[contains(@id,"CountDown")]').forEach(project);
  tab3('id("tabz")/tbody/tr/td/a');
}

function makeLootTable(table, reports) {
  function changedFilters(e) {
    function filterRow(tr) {
      var values = $x('td[position() > 3 and position() < 12]', tr).map(value);
      var show = visiblep.apply(this, values);
      if (0 && !zzz++) {
        console.log(unsafeWindow.names = names);
        console.log(unsafeWindow.values = values);
        console.log(show);
      }
      tr.className = tr.className.replace(/( not-filtered)?$/, show ?
                                           " not-filtered" : "");
    }
    function value(td, n) {
      var value, time = 6 == n;
      if (time)
        value = td.getAttribute("time");
      else
        value = td.firstChild;
      return integer(value || "0");
      return !value && n > 5 ? -Infinity : value;
    }
    var tests = [], names = [];
    for (var i = 0; i < filters.length; i++) {
      var f = filters[i];
      var time = "vT" == f.id;
      var bash = "vbash" == f.id;
      var v = time ? parseTime(f.value) : integer(f.value || "0");
      if (!v && (time || bash)) v = "Infinity";
      var n = f.id.replace(/^v/, "");
      var op = $("op"+ n).textContent == "≤" ? "<=" : ">=";
      var check = $(f.id.slice(1));
      if (check) {
        if (v) {
          if (!check.checked)
            check.setAttribute("auto", "yep");
          check.checked = true;
        } else if (check.getAttribute("auto")) {
          check.checked = false;
          check.removeAttribute("auto");
        }
      }
      if (!v) f.value = "";
      var expr = "("+ n + op + v +")";
      tests.push(expr);
      names.push(n);
    }
    //console.log("return "+ tests.join(" && ") +";");

    var visiblep = new Function(names, "return "+ tests.join(" && ") +";");
    $x('tr[starts-with(@class,"loot")]', body).forEach(filterRow);
  }

  function listen(input) {
    input.addEventListener("click", expensive(changedFilters), false);
    input.addEventListener("keyup", function(e) { click(e.target); }, false);
  }

  function filterView(e) {
    if (e) {
      var node = e.target;
      var text = $("v"+ node.id);
      if (text) {
        if (node.checked) {
          if (!text.value) {
            text.value = "1";
            text.setAttribute("auto", "yep");
            var changed = true;
          }
        } else {
          if (text.getAttribute("auto")) {
            text.removeAttribute("auto");
            text.value = "";
            changed = true;
          }
        }
        if (changed)
          changedFilters();
      }
    }
    var visible = show.filter(function(x) { return x.checked; });
    //if ((hide.disabled = 0 == visible.length)) return;
    hide.textContent = hideMost + "#loot-report tr.loot." +
      pluck(visible, "id").join(".") + " { display: table-row; }";
  }

  function filter(check) {
    return function() { click(check); };
  }

  function sort(col, key) {
    function move(junk, i, all) {
      var pos = keys[i] % all.length;
      buffer.insertBefore(tr[pos], buffer.firstChild);
    }

    var td = $x('tr[starts-with(@class,"loot")]/td['+ (col+1) +']', body);
    if (!td.length) return;
    var tr = pluck(td, "parentNode");
    var keys = td.map(key);
    var direction = col == 9 ?
      function descending(a, b) { return a < b ? 1 : -1; } :
      function ascending(a, b) { return a > b ? 1 : -1; };
    keys.sort(direction);
    var last = tr[tr.length-1].nextSibling;

    var buffer = document.createDocumentFragment();
    tr.forEach(move);
    body.insertBefore(buffer, last);
  }

  function sortByCity() {
    function key(td, i, all) {
      var a = $X('a[2]', td);
      var id = a ? integer(urlParse("selectCity", a.search)) : 0;
      return id * all.length + i;
    }
    sort(11, key);
  }

  function sortByWhen() {
    function key(td, i, all) {
      var M, D, h, m;
      [D, M, h, m] = trim(td.textContent).split(/\D+/g);
      return integer([M, D, h, m].join("")) * all.length + i;
    }
    sort(2, key);
  }

  function sortByDistance() {
    function key(td, i, all) {
      var t = td.getAttribute("time") || 1000000;
      return t * all.length + i;
    }
    sort(9, key);
  }

  function sortByLoot(col) {
    return function(e) {
      function key(td, i, all) {
        var value = integer(td.firstChild || 0);
        return value * all.length + i;
      }
      sort(col, key);
    };
  }

  function showLoot(report) {
    var tr = report.tr;
    delete report.tr;
    var loot = report.l;
    var has = ["loot"];
    for (var c = 3; c < cols.length; c++) {
      var td = tr.insertCell(c);
      var r = cols[c];
      if ("T" == r && report.c) {
        var t = travelTime(report.c);
        if (t) {
          td.setAttribute("time", Math.round(t)+"");
          td.innerHTML = secsToDHMS(t, 1);
          td.className = "time";
        }
        continue;
      }
      if ("#" == r) {
        var wonToday = hits[report.c] || ""
        td.innerHTML = wonToday;
        if (wonToday > 5)
          td.className = "warn"; // bash alert!
      }
      if (!loot || !loot[r]) continue;
      td.className = "number";
      var got = {}; got[r] = loot[r];
      td.innerHTML = visualResources(got, { size: 0.5 });
      has.push(r);
    }
    tr.className = (tr.className||"").replace(/^.*( non-filtered)?$/,
                                              has.join(" ") + "$1");
  }

  table.id = "loot-report";
  var override = "#container #mainview #troopsOverview #finishedReports ";
  var hideMost = "#loot-report tr.loot { display:none; } " + override +
    "#loot-report tr.loot.not-filtered { display: table-row }";
  var hide = css("", true);
  var body = $X('tbody', table);
  var head = body.insertRow(0);
  var only = body.insertRow(0);
  var cols = [, , , "g", "w", "W", "M", "C", "S", "T", "#"];
  var hits = {}; // indexed on city id, values are attacks today
  var show = [];
  var title = [, , "When",
               "$gold", "$wood", "$wine", "$marble", "$glass", "$sulfur",
               "Time", "#", "City"];
  for (var i = 0; i < 13; i++) {
    var r = cols[i];
    var t = title[i] || "";
    var th = node({ className: r ? "Time" == t ? "": "number" : "",
                    tag: i && i < 12 ? "th" : "td",
                    html: visualResources(t),
                    append: head });
    if (1 == i) th.style.minWidth = "25px";
    if (2 == i) th.style.minWidth = "68px";
    if (11 == i) th.style.width = "400px";
    if (r) { // only show filter for cols with relevant data
      var id = "#" == r ? "bash" : r;
      var op = /[T#]/.test(r) ? "≤" : "≥"; // config.getCity(...+ r, def);
      var val = ""; // config.getCity(...+ r, "");
      var html = <><span id={"op"+ id}>{op}</span><input
                         id={"v"+ id} value={val} type="text"/></>;
      var filter = node({ tag: "th", className: "filter", html: html,
                          append: only });
      only[r] = $X('input', filter);
    } else {
      only.insertCell(i);
    }
    if ("When" == t) clickTo(th, sortByWhen);
    if ("City" == t) clickTo(th, sortByCity);
    if ("Time" == t) { clickTo(th, sortByDistance); continue; }
    if ("#" == t || !r) continue;

    var check = node({ tag: "input", type: "checkbox", id: r, prepend: th });
    show.push(check);

    //var img = $X('img', th);
    clickTo(th, sortByLoot(i), 'not(self::input)');
    //clickTo(check, filterView); -- (preventDefault:s)
    check.addEventListener("click", filterView, false);
    dblClickTo(th, filter(check), "", true);
  }
  var filters = cols.filter(I).map(function(r) { return only[r]; });
  scrollWheelable(filters);
  filters.forEach(listen);

  var yesterday = Date.now() - (24 * 36e5);
  for (var i = 0; r = reports[i]; i++) {
    var recent = r.t > yesterday;
    if (recent && r.w && r.c)
      hits[r.c] = 1 + (hits[r.c] || 0);
  }
  reports.forEach(showLoot);
  unsafeWindow.markAll = safeMarkAll;

  changedFilters();
  filterView();

  // need to restow these a bit not to break the layout:
  var selection = $X('tr[last()]/td[@class="selection"]', body);
  var go = $X('tr[last()]/td[@class="go"]', body);
  go.parentNode.removeChild(go);
  selection.innerHTML += go.innerHTML;
  selection.setAttribute("colspan", "7");
  selection.className += " go";
  go = $X('input[@type="submit"]', selection);
  go.style.marginLeft = "6px";
}

function safeMarkAll(cmd) {
  //console.log("safe %x!", cmd);
  var boxes = $x('id("finishedReports")//input[@type="checkbox" and not(@id)]');
  for (var i = 0; i < boxes.length; i++) {
    var box = boxes[i], tr = $X('ancestor::tr[1]', box);
    if ("none" != getComputedStyle(tr, "").display) {
      if ("checked" == cmd) box.checked = true;
      if ("reverse" == cmd) box.checked = !box.checked;
    }
  }
}

function tradeAdvisorView() {
  pruneTodayDates('id("inboxCity")/tbody/tr/td[@class="date"]');
}

function researchAdvisorView() {
  function learnTech(a) {
    var name = a.textContent.match(/['"]([^'"]+)['"]/);
    if (!name) return;
    name = name[1];

    var tech = techs.filter(function(t) { return t.name == name; });
    if (!tech.length) return;
    tech = tech[0];

    config.setServer(["techs", tech.id], 1);

    a.title = a.textContent;
    a.href = urlTo("research", tech.id);
    a.innerHTML = name.bold() +" — "+ tech.does;
    // tech.does[0].toLowerCase() + tech.does.slice(1);
  }

  var rp = $X('id("breadcrumbs")/following::div[1]/div[@class="content"]/p[2]');
  if (rp) {
    rp = integer(rp.textContent.split("/")[0]);
    config.setServer("techs.points", { count: rp, at: Date.now() });
  }
  updateCurrentResearch();
  var techs = techinfo();
  $x('id("inboxResearch")/tbody/tr/td[@class="text"]/a').forEach(learnTech);
}

function diplomacyAdvisorView() {
  function span(td) { td.setAttribute("colspan", "8"); }
  function showIslandInfo(a) {
    var td = a.parentNode, x, y, t, id = urlParse("id", a.search);
    if ("#" == a.getAttribute("href")) id = 0;
    var t = /\[(\d+):(\d+)\]$/.exec(trim(a.textContent || "")) || "";
    if (t) {
      [t, x, y] = t;
      t = travelTime(x, y);
      t = t && secsToDHMS(t);
    }
    node({ tag: "td", className: "tt", text: t, before: td });
    var r = id ? config.getIsle("r", "x", id) : "x";
    var R = id ? config.getIsle("R", "",  id) : "";
    node({ tag: "td", className: "tradegood " + r, title: R, after: td });
  }

  //[contains(translate(.,"0123456789:",""),"[]")]').
  var body = $X('id("messages")//tbody[tr[count(th) = 6]]');
  var date = $X('tr[1]/th[6]', body);
  var town = $X('tr[1]/th[5]', body);
  node({ tag: "th", className: "tradegood", before: date });
  node({ tag: "th", className: "tt", text: lang.travelTime, before: town });
  $x('tr/td[5]/a', body).forEach(showIslandInfo);
  $x('tr/td[@colspan="6"]', body).forEach(span);
}

function militaryAdvisorCombatReportsView() {
  function read(e) {
    if ((e.keyCode||e.charCode) != "-".charCodeAt()) return;
    removeEventListener("keypress", read, false);
    proceed();
  }

  function proceed() {
    var a = my.unknowns.pop();
    if (a) {
      setTimeout(wget, Math.random()*3e3, a.href, proceed, true, false);
      a.style.opacity = "0.5";
    }
    console.log(my.unknowns.length + 1);
  }

  function fileReport(tr, n) {
    var a = $X('td[contains(@class,"subject")]/a', tr);
    var w = $X('contains(../@class,"won")', a);
    var r = parseInt(urlParse("combatId", a.search));
    var d = $X('td[@class="date"]', tr);
    var t = parseDate(d);
    repId[n] = r;
    if (!allreps[r]) {
      w ? history.won++ : history.lost++;
      newreps[r] = { t: t, w: 0 + w };
      allreps[r] = newreps[r];
    }
    rows[n] = copy(allreps[r]);
    rows[n].tr = tr;
  }

  tab3('id("tabz")/tbody/tr/td/a');

  var my = militaryAdvisorCombatReportsView; my.unknowns = [];
  var table = $X('id("finishedReports")/table[@class="operations"]');
  if (!table) return;
  var history = config.getServer("battles", { won: 0, lost: 0 });
  var allreps = config.getServer("battles.reports", {});
  var reports = $x('tbody/tr[td[contains(@class,"subject")]]', table);
  var newreps = {};
  var cities = {};
  var repId = [];
  var rows = [];
  reports.forEach(fileReport);

  var city = config.getServer("cities", {});
  for (var i = reports.length; --i >= 0;) {
    var a = $X('.//a', reports[i]);
    var r = allreps[repId[i]];

    if (!r.c) my.unknowns.push(a);
    var recent = r.t > Date.now() - (25 * 36e5);
    // we won, we don't know what city, it's the past 24h (+ DST safety margin)
    if (r.w && !r.c && recent) {
      a.style.fontStyle = "italic"; // Warn about it! Read that report, please.
      a.innerHTML = "?: "+ a.innerHTML;
    }

    if (r.c) {
      if (recent)
        var c = cities[r.c] = 1 + (cities[r.c] || 0);
      var name = city[r.c].n;
      var text = a.textContent;
      text = text.slice(0, text.lastIndexOf(name));
      a.textContent = text;
      node({ tag: <a href={urlTo("island", { island: city[r.c].i, city: r.c })}
                     rel={"i" + r.c}>{name}</a>, after: a });
    }
  }
  var header = $X('id("troopsOverview")/div/h3');
  var loot = node({ tag: "a", text: lang.showLoot,
                    style: { marginLeft: "8px" }, append: header });
  clickTo(loot, function() { rm(loot); makeLootTable(table, rows); });
  if (my.unknowns.length)
    addEventListener("keypress", read, false);

  config.setServer("battles", history);
  config.setServer("cities", city);
  //config.setServer("reports", allreps);
  //console.log(history.toSource());
  //console.log(allreps.toSource());
}

function militaryAdvisorReportViewView() {
  var loot = parseResources('//td[@class="winner"]/ul[@class="resources"]/li');
  var a = $X('id("mainview")//a[contains(@href,"selectCity")][last()]');
  var cities = config.getServer("cities", {});
  var city = parseInt(urlParse("selectCity", a.search));
  var island = parseInt(urlParse("id", a.search));
  var reports = config.getServer("battles.reports", {});
  var r = urlParse("combatId");
  var report = reports[r];
  if (report) {
    if (loot) report.l = loot;
    report.c = city;
  }
  if (!cities.hasOwnProperty(city))
    cities[city] = {};
  var c = cities[city];
  c.n = a.textContent;
  c.i = integer(island);
  config.setServer("cities", cities);
  config.setServer("battles.reports", reports);

  var q = urlParse();
  var url = "?view=militaryAdvisorReportView&";
  var panel = $("troopsReport");
  var detail = $X('//a[contains(@href,"'+ url +'")]') ||
    node({ append: $("ergebnis").insertRow(-1).insertCell(0),
           tag: <><a id="detail">Detailed combat report</a>
           &gt;&gt;</> }).detail;
  detail.parentNode.setAttribute("colspan", "7");
  var lastrow = rm(detail.parentNode.parentNode);

  if (q.combatId) {
    var wall = $X('id("ergebnis")//td[img[contains(@src,"wall.gif")]]');
    var defense = $X('following-sibling::td[contains(.,"%")]', wall);
    if (defense && wall) {
      var text = wall.lastChild, txt = text.nodeValue;
      text.nodeValue = txt.replace(":", " "+ wallLevel(defense, city) +":");
    }
    url += "detailedCombatId="+ q.combatId +'#before=id("troopsReport"):'+
      encodeURIComponent(encodeURIComponent(trim(panel.innerHTML)));
  } else return detailedCombatView(); /* {
    return $("ergebnis").appendChild(lastrow);
    url += "combatId="+ q.detailedCombatId +'#after=id("troopsReport"):'+
      encodeURIComponent(encodeURIComponent(trim(panel.innerHTML)));
  } */

  $("ergebnis").appendChild(lastrow);
  detail.href = url;
}

function detailedCombatView() {
  function register(unit, i) {
    if (!unit.A || !unit.D) return;
    var div = ("Resistance" == unit.x ? 1.3 : 1) * wallBonus;
    var att = Math.max(0, Math.floor((a[i] - unit.a) / unit.A));
    var def = Math.max(0, Math.floor((d[i]/div - unit.d) / unit.D));
    att = Math.min(3, att);
    def = Math.min(3, def);
    units[unit.id] = n[i];
    levels[unit.id] = { a: att, d: def };
    att = <img alt={"["+ att +"]"} src={ gfx.sword(att) }/>;
    def = <img alt={"["+ def +"]"} src={ gfx.shield(def) }/>;
    att.@style = def.@style = "margin-right: 2px";
    node({ prepend: attack[i], tag: att });
    node({ prepend: defend[i], tag: def });
  }
  var wallBonus = $X('//td[@colspan="15"][a]/text()[contains(.,"%")]');
  wallBonus = 1 + (!wallBonus ? 0 :
                   integer(wallBonus.textContent.match(/\d+%/)[0]) / 100);
  var c = $X('(//td[@colspan="15"]/a[contains(@href,"selectCity")])[last()]');
  var cid = urlParse("selectCity", c.search);
  var units = config.getCity("U", {}, cid);
  var player = config.getCity("o", null, cid);
  if (!player) return;

  var levels = config.getServer(["players", player, "u"], {});

  var trs = $x('id("result")/tbody/tr[th[@class="defenders"]][1]/' +
               'following-sibling::tr');
  var stats = $x('td/img', trs[0]).map(unitStatsFromImage);
  var counts = $x('td[text()]', trs[1]), n = counts.map(integer);
  var attack = $x('td[text()]', trs[2]), a = attack.map(number);
  var defend = $x('td[text()]', trs[3]), d = defend.map(number);
  stats.forEach(register);
  config.setCity("U", units, cid);
  config.setServer(["players", player, "u"], levels);
}

function deploymentView() {
  plunderView("army");
}

function blockadeView() {
  plunderView("fleet");
}

function augmentIkaFight() {
  var specs = (location.hash||"#").slice(1);
  if (window != top) {
    document.body.style.background = "none";
    if (specs)
      window.name = specs;
    else
      specs = window.name;
  }
  var args = urlParse(null, specs);
  for (var n in args) {
    var td = $X('//td[.="'+ n +'"]');
    if (!td) continue;
    var v, a, d, f, A, D;
    [v, a, d, f, A, D] = args[n].split(".").concat(0, 0, 0, 0, 0);
    var input = $x('following::input', td);
    if (!input) return;
    input[0].value = v;
    if (a) input[a].checked = true;
    if (d) input[integer(d)+3].checked = true;
    if (f) input[7].value = f;
    if (A) input[integer(A)+7].checked = true;
    if (D) input[integer(D)+10].checked = true;
  }
  scrollWheelable($x('//input[@type="text"]'));
}

function plunderView(where) {
  function simulateBattle() {
    function count(input) {
      var unit = readUnit(input);
      var units = integer(input.value);
      if (empty) // pick the maximum available, if no units were selected
        units = integer($X('preceding::div[@class="amount"][1]', input));
      if (!units) return;
      var name = ika[unit.id] || (troops[unit.id] || ships[unit.id]).n;
      var a = config.getServer(["techs", "units", unit.id, "a"], 0);
      var d = config.getServer(["techs", "units", unit.id, "d"], 0);
      if (a || d) { units += "."+ a +"."+ d; }
      stats[name] = units;
      return input;
    }

    var ika = { 308: "Steamgiant", 312: "Gyro", 210: "Ram", 213: "Ballista",
                214: "Catapult", 215: "Mortor", 216: "Paddle-wheel" };
    var send = sending(), stats = {};
    var empty = !sum(send); // none selected?
    send.forEach(count);
    var to = $X('id("mainview")//input[@name="destinationCityId"]');
    if (to) {
      var city = integer(to);
      var player = config.getCity("o", "", city);
      if (player) {
        var nmiLevels = config.getServer(["players", player, "u"], {});
        for (var id in nmiLevels) {
          var l = nmiLevels[id];
          var n = config.getCity(["U", id], 0, city);
          var name = ika[id] || (troops[id] || ships[id]).n;
          stats[name] = (stats[name] || "0.0.0") +"."+ n +"."+ l.a +"."+ l.d;
        }
      }
      var levels = config.getCity("l", {}, city);
      stats["Town Level"] = levels[buildingIDs.townHall] || 0;
      stats["Wall Level"] = levels[buildingIDs.wall] || 0;
    }

    var url = "http://ikariam.immortal-nights.com/ikafight/?battleType=";
    url += ("fleet" == where ? "sea" : "land") +"#"+ makeQuery(stats);

    var form = $("plunderForm") || $("blockadeForm");
    var div = node({ before: form, tag: <div class="contentBox01h" id="f">
      <h3 class="header">ImmortalNights&apos; IkaFight</h3>
      <div class="content" id="ikafight"> </div>
      <div class="footer"> </div>
    </div> }).f;
    node({ append: $("ikafight"), tag: <iframe src={ url }/> });
    return div;
  }

  function ikaFight() {
    if (!ikaFight.loaded)
      ikaFight.loaded = simulateBattle();
    else
      toggle(ikaFight.loaded)
  }

  function readUnit(input) {
    var id = integer(input.id);
    var type = input.id.split("_")[1]; // army | fleet
    var unit = (type == "army" ? troops : ships)[id];
    return unit;
  }

  function sending() {
    return $x('//input[@type="text" and starts-with(@id,"cargo_")]');
  }

  function updateMilitaryScores() {
    function cost(input) {
      var n = integer(input.value);
      var unit = readUnit(input);
      score += n * (2*unit.w + 4*(unit.C||0) + 16*(unit.S||0) + 4*(unit.S||0));
      var level = config.getServer;
      offense += n * (unit.a + level("techs.units."+unit.id+".a", 0) * unit.A);
      defense += n * (unit.d + level("techs.units."+unit.id+".d", 0) * unit.D);
    }
    var offense = 0, defense = 0, score = 0;
    sending().forEach(cost);
    $("militaryscore").textContent = score;
    if (!od) return;
    $("offense").innerHTML = offense;
    $("defense").innerHTML = defense;
  }

  if (config.getServer("techs.units."+ (where == "fleet" ? 210 : 301), 0)) {
    node({ tag: <div class="cost"><div id="offense">0</div></div>,
       prepend: $X('id("upkeepPerHour")/..') });
    node({ tag: <div class="cost"><div id="defense">0</div></div>,
       prepend: $X('id("estimatedTotalCosts")/..') });
    var od = true;
  }
  node({ tag: <div id="militaryscore">0</div>,
       after: $X('(//input[starts-with(@id,"cargo_")])[last()]') });
  onChange($x('//input[@type="text" and starts-with(@id,"cargo_")]'),
           updateMilitaryScores, "value", "watch");

  scrollWheelable();
  dontSubmitZero(2, 'id("selectArmy")//input[@type="submit"]');
  toggler(gfx.swords, ikaFight);
}


function currentResources() {
  var inhab = $("value_inhabitants").textContent.split(/\s+/);
  return {
    p: getFreeWorkers(), P: getPopulation(),
    g: integer($("value_gold")), w: integer($("value_wood")),
    W: integer($("value_wine")), M: integer($("value_marble")),
    C: integer($("value_crystal")), S: integer($("value_sulfur"))
  };
}

function addResources(a, b, onlyIterateA) {
  return opResources(a, b, function(a, b) { return a + b; }, onlyIterateA);
}
function subResources(a, b, onlyIterateA) {
  return opResources(a, b, function(a, b) { return a - b; }, onlyIterateA);
}
function mulResources(a, n, op) {
  return opResources(a, n, function(a) { return (op||Math.round)(a * n); }, 1);
}

function opResources(a, b, op, onlyIterateA) {
  //console.log("a: %x, b: %x", a?a.toSource():a, isObject(b)?b.toSource():b);
  //console.log(op.toSource());
  var o = {}, r, A, B;
  for (r in a) {
    if (isNumber(A = a[r]) && (!isObject(b) || isNumber(B = b[r] || 0)))
      o[r] = op(A, B);
  }
  //if (onlyIterateA) console.log("o: %x", o?o.toSource():o);
  if (onlyIterateA) return o;
  for (r in b) {
    if (o.hasOwnProperty(r)) continue;
    if (isNumber(A = a[r] || 0) && isNumber(B = b[r] || 0))
      o[r] = op(A, B);
  }
  //console.log("o: %x", o?o.toSource():o);
  return o;
}

function parseResources(res) {
  if (isString(res))
    res = $x(res);
  var o = {}, r, id;
  if (res.length)
    for (var i = 0; i < res.length; i++) {
      r = res[i];
      id = resourceIDs[r.className.split(" ")[0]];
      o[id] = integer(r);
    }
  else
    return null;
  return o;
}

function haveResources(needs) {
  var have = currentResources();
  for (var r in needs)
    if (needs[r] > (have[r] || 0))
      return false;
  return true;
}

function reapingPace() {
  var pace = reapingPace.pace;
  if (!pace) {
    // FIXME: This just gives city income; ought to do a pace.G for totals too
    var preciseCityIncome = $("valueWorkCosts") ||
      $X('//li[contains(@class,"incomegold")]/span[@class="value"]');

    var sciCost = 8 - config.getServer("techs.3110", 0); // Letter chute bonus
    var maintenance, gold;
    var computedIncome = getFreeWorkers() * 4 -
      config.getCity(["x", buildingIDs.academy], 0) * sciCost;
    var refCity = referenceCityID();
    var mainCity = mainviewCityID();
    if ((mainCity == refCity) && preciseCityIncome) {
      gold = integer(preciseCityIncome);
      maintenance = computedIncome - gold;
      setMaintenanceCost(maintenance, refCity);
    } else {
      maintenance = maintenanceCost(refCity);
      gold = computedIncome - maintenance;
    }

    reapingPace.pace = pace = {
      g: gold,
      w: secondsToHours(jsVariable("startResourcesDelta"))
    };
    pace[luxuryType()] = secondsToHours(jsVariable("startTradegoodDelta"));

    var wineUse = config.getCity(["x", buildingIDs.tavern], 0);
    if (wineUse)
      pace.W = (pace.W || 0) - wineUse;
  }
  return pace;
}

function buildingClass(id) {
  id = buildingID(id);
  for (var name in buildingIDs)
    if (buildingIDs[name] == id)
      return name;
}

function buildingID(a) {
  if (isNumber(a)) return a;
  var building = isString(a) ? a : a.parentNode.className;
  return buildingIDs[building];
}

function haveBuilding(b) {
  return buildingLevel(b, 0) && ("-" != buildingPosition(b, "-"));
}

function buildingPosition(b, otherwise, cityID) {
  var p = config.getCity(["p", buildingID(b)], "?", cityID);
  return "?" == p ? otherwise : p;
}

function buildingLevel(b, otherwise, saved, city) {
  b = buildingID(b);
  if (!saved && "city" == urlParse("view")) {
    var div = $("position" + buildingPosition(b));
    var a = $X('a[@title]', div);
    if (a && isUndefined(integer(a.title)))
      a = undefined; // a ghost house we set up to visualize the queue
  }
  if (saved || isUndefined(a))
    b = config.getCity(["l", b], undefined, city);
  else
    b = integer(a.title);
  return isUndefined(b) ? otherwise : b;
}

function buildingLevels() {
  var levels = {};
  for (var name in buildingIDs) {
    var id = buildingIDs[name];
    var level = config.getCity(["l", id], 0);
    if (level)
      levels[id] = level;
  }
  return levels;
}

function buildingCapacity(b, l, warehouse) {
  b = buildingClass(b);
  var c = buildingCapacities[b];
  c = c && c[isDefined(l) ? l : buildingLevel(b, 0)];
  return isDefined(warehouse) ? c && c[warehouse] : c;
}

function buildingExpansionNeeds(b, level) {
  level = isDefined(level) ? level : buildingLevel(b);
  var needs = costs[b = buildingID(b)][level];
  var value = {};
  var factor = 1.00;
  if (config.getServer("techs.2020")) factor -= 0.02; // Pulley
  if (config.getServer("techs.2060")) factor -= 0.04; // Geometry
  if (config.getServer("techs.2100")) factor -= 0.08; // Spirit Level
  for (var r in needs)
    if ("t" == r) // no time discount
      value[r] = needs[r];
    else
      value[r] = Math.floor(needs[r] * factor);
  return value;
}

function haveEnoughToUpgrade(b, level, have) {
  var upgrade = buildingExpansionNeeds(b, level);
  have = have || currentResources();
  for (var resource in upgrade)
    if (resource != "t" && have[resource] < upgrade[resource])
      return false;
  return true;
}

function wallLevel(defense, city) {
  var level = 0, defense = number(defense), wall = ["l", buildingIDs.townHall];
  if (defense) {
    var townHall = config.getCity(wall, null, city);
    if (!townHall) return undefined;
    level = Math.sqrt(defense * townHall / 10);
  }
  return config.setCity(wall, Math.round(level), city);
}

function buildingExtraInfo(div, id, name, level) {
  function annotate(msg) {
    node({ tag: "span", className: "ellipsis", text: msg, append: div,
          style: { position: "relative" }});
    div.style.padding = "0 3px 0 5px";
    div.style.width = "auto";
  }

  if (("wall" != name) && !isMyCity()) return;
  var originalLevel = buildingLevel(id, 0, "saved");

  switch (name) {
    case "townHall":
      if (originalLevel != level) {
        var delta = getMaxPopulation(level) - getMaxPopulation(originalLevel);
        if (delta > 0) delta = "+" + delta;
        annotate(delta);
      }
      break;

    case "wall":
      var townSize = buildingLevel("townHall", 0);
      annotate(Math.floor(Math.min(1, level / townSize) * level * 10) + "%");
      break;

    case "tavern":
      var wineMax = buildingCapacity(name, level);
      var wineCur = config.getCity(["x", buildingIDs.tavern], 0);
      if (wineCur != wineMax)
        annotate(wineCur +"/"+ wineMax);
      break;

    case "museum":
      var museum = buildingLevel(name) || 0;
      var culture = config.getCity(["x", buildingIDs.museum], 0);
      if (culture != museum)
        annotate(culture +"/"+ museum);
      break;

    case "academy":
      var seats = buildingCapacity(name, level);
      var working = config.getCity(["x", buildingIDs.academy], 0);
      if (working != seats)
        annotate(working +"/"+ seats);
      break;

    case "warehouse":
      var wood = buildingCapacity("warehouse", "wood", level);
      var rest = buildingCapacity("warehouse", "rest", level);
      if (originalLevel != level && wood && rest)
        annotate(wood +"/"+ rest);
      break;
  }
}

function annotateBuilding(li, level) {
  var a = $X('a', li);
  if (!a) return;
  $x('div[@class="rounded"]', li).forEach(rm);
  var id = buildingID(a);
  if (isNumber(id) && li.id && isUndefined(level)) {
    config.setCity(["l", id], number(a.title));
    config.setCity(["p", id], number(li.id));
  }
  if ("original" == level) {
    level = buildingLevel(id, 0, "saved");
    a.title = a.title.replace(/\d+/, level);
  } else {
    level = level || number(a.title);
  }
  var div = node({ className: "rounded", text: level, append: li });
  if (haveEnoughToUpgrade(a, level)) {
    div.style.backgroundColor = "#FEFCE8";
    div.style.borderColor = config.get("haveEnough");
  } else {
    div.style.borderColor = config.get("notEnough");
  }
  clickTo(div, a.href);
  div.style.visibility = "visible";
  buildingExtraInfo(div, id, buildingClass(id), level);
  div.title = a.title;
}

function showResourceNeeds(needs, parent, div, top, left) {
  if (div)
    rm(div);
  else
    div = node({ className: "rounded resource-list" });
  div.innerHTML = visualResources(needs, { nonegative: true });
  if (parent.id == "position3") { // far right
    div.style.top = top || "";
    div.style.left = "auto";
    div.style.right = "-17px";
    div.style.margin = "0";
  } else if ("position7" == parent.id) { // far left
    div.style.top = top || "";
    div.style.left = "-11px";
    div.style.right = "auto";
    div.style.margin = "0";
  } else {
    div.style.top = top || "";
    div.style.left = "0";
    div.style.right = "auto";
    div.style.margin = "0 0 0 -50%";
  }
  if (isDefined(left))
    div.style.left = left;
  show(div);
  parent.appendChild(div);
  return div;
}

function levelBat() { // Ajout d'un du level sur les batiments.
  function hoverHouse(e) {
    var a = $X('(ancestor-or-self::li)/a[@title and @href]', e.target);
    if (a && a.title.match(/ \d+$/i)) {
      var li = a && a.parentNode;
      var top = $X('div[@class="timetofinish"]', li) ? "73px": "";
      if (top && li.id == "position0") top = "0";
      var div = showResourceNeeds(buildingExpansionNeeds(a), li, hovering, top);
      clickTo(div, urlTo("building", buildingID(a)));
      var enough = haveEnoughToUpgrade(a);
      hovering.style.borderColor =
        config.get(enough ? "haveEnough" :"notEnough");
      hovering.style.backgroundColor = enough ? "#FEFCE8" : "#FDF8C1";
    } else {
      hide(hovering);
      if (hovering.parentNode)
        annotateBuilding(hovering.parentNode, "original");
    }
  }

  var places = $("locations");
  if (places) {
    var hovering = node({ id: "hovering", className: "rounded resource-list",
                          title: lang.popupInfo, append: $('position0') });
    hide(hovering);
    places.addEventListener("mouseover", hoverHouse, false);
    hovering.addEventListener("DOMMouseScroll", function(e) {
      var li = hovering.parentNode;
      var a = $X('a', li), b = buildingID(a);
      var l = Math.min(Math.max(!b, number(a.title) + (e.detail < 0 ? 1 : -1)),
                       costs[b].length - 1);
      a.title = a.title.replace(/\d+/, l);
      annotateBuilding(li, l);
      e.preventDefault();
      hoverHouse({ target: hovering });
    }, false);
  }

  config.setCity("l", []); // clear old broken config
  var all = $x('id("locations")/li[not(contains(@class,"buildingGround"))]');
  all.forEach(function(li) { annotateBuilding(li); });
}

function worldmap_isoView() {
  function randomizableCoords() {
    function changeCoords(e) {
      function edit(n) {
        with ($(n))
          value = e.altKey ? 100 - value : Math.floor(100 * Math.random());
      }
      edit("inputXCoord");
      edit("inputYCoord");
      click($X('id("mapCoordInput")/input[@name="submit"]'));
    }
    clickable({ prepend: $("mapCoordInput") }, changeCoords, "dice");
  }

  function showResources() {
    drawMap();
    var w = unsafeWindow;
    var cx = w.center_x, dim = w.MAXSIZE;
    var cy = w.center_y, mid = w.halfMaxSize;
    for (var i = 0; i < dim; i++)
      for (var j = 0; j < dim; j++) {
        var x = cx + mid - i;
        var y = cy + mid - j;
        var r = resources[x+":"+y] || [];
        var R = r[tradegood];
        var t = $("tradegood_"+ i +"_"+ j);
        t.innerHTML = !R ? "" : <>
          <div class="cities">{R}</div>
          <div class="tradegood wood">
            <div class="cities">{r[wood]}</div>
          </div>
        </>.toXMLString();
      }
    return islands;
  }

  function dropTooltip(x) { x.removeAttribute("title"); }

  function mark(x, y) {
    var v = setMark(x, y);
    travelDistanceBreadcrumbs();
    return v;
  }

  $x('//area[@title]').forEach(dropTooltip);
  $x('id("worldmap")/text()').forEach(rm);
  var resources = {}, tradegood = 0, wood = 1;
  var islands = config.getServer("islands");
  for (var id in islands) {
    var i = islands[id];
    if (i.R)
      resources[i.x+":"+i.y] = [i.R, i.w];
  }
  //var islands = config.get("islands", {});

  var drawMap = unsafeWindow.center_map;
  unsafeWindow.center_map = showResources;

  var setMark = unsafeWindow.mark;
  unsafeWindow.mark = mark;

  randomizableCoords();
}

// island view selected city
function focusCity(city) {
  var a = $("city_" + city);
  var other = $X('//a[starts-with(@id,"city_") and not(@id="city_'+city+'")]');
  if (other) {
    click(other);
    return click(a);
  }
  location.href = "javascript:selectedCity = -1; try { (function() {" +
    a.getAttribute("onclick") + "}).call(document.getElementById('city_" +
    city +"')) } catch(e) {}; void 0";
  setTimeout(function() { a.parentNode.className += " selected"; }, 1e3);
}

function travelDistanceBreadcrumbs(island) {
  var breadcrumbs = $X('id("breadcrumbs")/span[.//text()[contains(translate' +
                       '(.,"0123456789:",""),"[]")]]'), x, y, t, junk;
  if (breadcrumbs) {
    [junk, x, y] = breadcrumbs.textContent.match(/\[(\d+):(\d+)\]/);
    if (island) {
      config.setIsle("x", x = integer(x), island);
      config.setIsle("y", y = integer(y), island);
    }
    //console.log("isle %x at %x:%y", island, x, y);
    if ((t = travelTime(x, y)))
      node({ id: "travel_time", tag: "span", text: " ("+ secsToDHMS(t) +")",
             append: breadcrumbs });
  }
}

function nextIsland(id) {
  var skip = nonIslands[++id];
  return skip ? id + skip : id;
}

function prevIsland(id) {
  id = integer(id);
  var i = 0;
  while (--i >= -60) {
    var skip = nonIslands[i + id];
    if (skip)
      if (skip == -i)
        return id - skip - 1;
      else
        break;
  }
  return --id > 0 ? id : 5721;
}

function showMinimap(i) {
  function createMinimap() {
    return node({ tag: <iframe id="miniMap" src={ u }/>, append: b }).miniMap;
  }

  function toggleMinimap() {
    var m = $("miniMap") || createMinimap();
    m.style.visibility = m.style.visibility ? "" : "visible";
  }

  var me = cityIDs().map(referenceIslandID).join(",");
  var u = base +"minimap.html?s="+ location.hostname +"&t=_top&w=7&h=7&c="+
    i +"&r=0&red="+ me +"&white="+ i;
  var b = $("breadcrumbs");
  toggler(gfx.world, toggleMinimap, u.replace(/([wh])=7/g, "$1=33"));
}

function islandView() {
  function cultureTreatyMassMessage() {
    //toggler(gfx.stamina, toggleClickMode);
  }

  function alliancePresence() {
    var all = {}, main = $("mainview");
    var a = node({ append: main, tag:<table id="alliances"></table> });
    cssToggler("alliances", true, gfx.alliances, <><![CDATA[
#container #mainview #alliances {
  display: none;
}
]]></>);
    a = a.alliances;
    for each (var city in c) {
      var name = $X('string(li[@class="ally"]/a[1])', city) || "-";
      all[name] = (all[name] || []).concat(integer(city.parentNode.id));
    }
    for (name in all) {
      var tr = a.insertRow(0);
      tr.insertCell(0).textContent = all[name].length;
      tr.insertCell(0).textContent = name;
    }
  }

  function nextprev(event) {
    var n = event.charCode || event.keyCode;
    var next = { 37: prevIsland, 39: nextIsland }[n];
    if (next) {
      event.stopPropagation();
      event.preventDefault();
      goto(urlTo("island", next(island)));
    }
  }

  function registerCity(ul) {
    function item(p) {
      var li = $X('li[@class="'+ city[p] +'"]/span/following::text()[1]', ul);
      return li && trim(li.textContent);
    }

    var id = number($X('preceding-sibling::a[contains(@id,"city_")]', ul).id);
    var city = { i: island, n: "name", o: "owner" };
    for (var p in city)
      if (isString(city[p]))
        city[p] = item(p);
    for (p in city)
      config.setCity(p, city[p], id);

    var players = config.getServer("players", {});
    var player = players[city.o] = players[city.o] || {};
    player.c = player.c || [];
    if (-1 == player.c.indexOf(id)) {
      player.c.push(id);
      player.c.sort(function(a, b) { return a - b; });
    }
    config.setServer("players", players);
  }

  function showSpies() {

  }

  var city = urlParse("selectCity");
  if (city)
    setTimeout(focusCity, 200, city);
  levelTown();
  levelResources();
  var island = integer(urlParse("id", $X('id("advCities")/a').search));
  travelDistanceBreadcrumbs(island);

  var c = $x('id("cities")/li[contains(@class,"level") and ' +
             ' not(contains(@class,"level0"))]/ul[@class="cityinfo"]');
  c.forEach(registerCity);
  alliancePresence();

  cssToggler("playernames", false, "http://i297.photobucket.com/albums/mm209/apocalypse33/Avatar/Ava52.png", "#island #container #mainview #cities .textLabel.player-name { display: none; }");

  showSpies();

  showMinimap(island);
  if (island)
    addEventListener("keypress", nextprev, false);
}

// alternative args formats: "isle", "x1, y1, isle", "isle, null, isle",
// "null, null, isle" (for mainview -> isle)
function travelTime(x1, y1, x2, y2) {
  if (arguments.length & 1) { // a city id
    var isle = x1 ? isleForCity(x1) : mainviewIslandID();
    x1 = config.getIsle("x", 0, isle);
    y1 = config.getIsle("y", 0, isle);
    //console.log("isle %x at %x:%y", isle, x1, y1);
    if (!x1 || !y1) return 0;
  }
  if (arguments.length < 4)
    if (arguments.length == 3)
      isle = x2 || mainviewIslandID();
    else
      isle = referenceIslandID();
  if (arguments.length < 4) {
    x2 = config.getIsle("x", 0, isle);
    y2 = config.getIsle("y", 0, isle);
    //console.log("to isle %x at %x:%y", isle, x2, y2);
    if (!x2 || !y2) return 0;
  }
  var dx = x2 - x1, dy = y2 - y1;
  return 60 * 20 * (1 + Math.sqrt(dx*dx + dy*dy));
}

function click(node) {
  var event = node.ownerDocument.createEvent("MouseEvents");
  event.initMouseEvent("click", true, true, node.ownerDocument.defaultView,
                       1, 0, 0, 0, 0, false, false, false, false, 0, node);
  node.dispatchEvent(event);
}


function levelResources() {
  function annotate(what) {
    what = $X('id("islandfeatures")/li['+ what +']');
    if (!what) return;

    var level = number(what.className);
    node({ className: "rounded", text: level, append: what });

    var id = urlParse("id");
    if (id) {
      var res = what.className.split(" ")[0];
      var rid = resourceIDs[res];
      if ("w" == rid) {
        config.setIsle("w", level, id);
      } else {
        config.setIsle("R", level, id);
        config.setIsle("r", rid, id);
      }
    }
  }
  annotate('contains(@class,"wood")');
  annotate('not(contains(@class,"wood")) and not(@id)');
}

function levelTown() {
  function addToFriendList(e) {
    var flName = $("flNewName"), flLink = $("flNewLink");
    if (flName && flLink) {
      var player = e.target;
      flName.value = player.childNodes[1].textContent;
      var city = number(player.parentNode.id);
      var isle = urlParse("id", $X('id("islandfeatures")/li/a').search);
      flLink.value = "http://" + location.hostname + "/index.php?" +
        "view=island&id="+ isle +"&selectCity="+ city;
      location.href = "javascript:void(flToggleFrame(1))";
    }
  }

  function level(li) {
    var level = integer(li.className);
    var city = $X('a[@onclick]/span', li);
    if (!city) return; // new city site
    var name = $X('text()[preceding-sibling::span]', city);
    if (name) {
      var id = integer($X('a', li).id);
      config.setCity(["l", buildingIDs.townHall], level, id);
      name.nodeValue = level +":"+ name.nodeValue;
      var wl = config.getCity(["l", buildingIDs.wall], "", id);
      if ("" != wl && config.get("debug"))
        name.nodeValue = wl +"/"+ name.nodeValue;
      name = name.parentNode;
      name.style.left = Math.round((name.offsetWidth) / -2 + 34) + "px";
    }
    var player = city.cloneNode(true);
    player.innerHTML = '<span class="before"></span>Player name' +
      '<span class="after"></span>';
    name = trim($X('ul/li[@class="owner"]/text()[1]', li).textContent);
    player.childNodes[1].nodeValue = name;
    addClass(player, "player-name");

    city.parentNode.insertBefore(player, city.nextSibling);
    player.style.top = "84px";
    player.style.left = Math.round((player.offsetWidth) / -2 + 34) + "px";

    var msg = $X('ul/li[@class="owner"]/a', li);
    //player.title = msg.title;
    clickTo(player, addToFriendList);
    dblClickTo(player, msg.href);
  }

  $x('//li[starts-with(@class,"cityLocation city level")]').forEach(level);
}

function linkTo(url, node, styles, opts) {
  if (!url.match(/\?/))
    url = urlTo(url);
  if (!url) return;
  if (isString(node))
    node = $X(node, opts && opts.context);
  if (!url)
    return;
  var a = document.createElement("a");
  a.href = url;
  if (node) {
    while (node.lastChild)
      a.insertBefore(node.lastChild, a.firstChild);
    if (node.id)
      a.id = node.id;
    if (node.title)
      a.title = node.title;
    if (node.className)
      a.className = node.className;
    if (node.hasAttribute("style"))
      a.setAttribute("style", node.getAttribute("style"));
  }
  if (styles)
    for (var prop in styles)
      a.style[prop] = styles[prop];
  if (opts) {
    if (opts.saveParent) {
      while (node.lastChild)
        a.appendChild(node.removeChild(node.firstChild));
      return node.appendChild(a);
    }
    if (opts.text)
      a.textContent = opts.text;
  }
  if (node)
    node.parentNode.replaceChild(a, node);
  return a;
}

function urlTo(what, id, opts) {
  function building() {
    var cid = id || c;
    var bid = buildingID(what);
    var l = buildingLevel(bid, undefined, null, cid);
    if (isDefined(l)) {
      var p = buildingPosition(bid, "", cid);
      return url("?view="+ what +"&id="+ cid +"&position="+ p);
    }
    return "";
  }

  function resource(view) {
    if (!opts.city)
      return "?view="+ view +"&type="+ view +"&id=" + i;
    return "?action=header&function=changeCurrentCity&oldView=tradegood" +
           "&view="+ view +"&type="+ view +"&id="+ i +"&cityId="+ opts.city;
  }

  if (isUndefined(opts)) opts = {};
  var c = cityID(), i = islandID(), ci = config.getCity("i", 0, c) || i;
  if (what == "workshop")
    what = "workshop-army";
  switch (what) {
    default:		return url("?view="+ what);
    case "luxe":	return url(resource("tradegood"));
    case "wood":	return url(resource("resource"));

    case "townhall":	case "workshop":case "workshop-fleet":
    case "townHall":	case "port":	case "academy":
    case "shipyard":	case "wall":	case "warehouse":
    case "barracks":	case "museum":	case "branchOffice":
    case "embassy":	case "palace":	case "palaceColony":
    case "safehouse":	case "tavern":	case "workshop-army":
      return building();

    case "culturegoods":
      return urlTo("museum").replace("museum", "culturalPossessions_assign");

    case "library":	what = "researchOverview"; // fall-through:
    case "changeResearch":                         // fall-through:
    case "researchOverview":
      			return urlTo("academy").replace("academy", what);

    case "city":	return !opts || !opts.changeCity ?
                          url("?view=city&id="+ (id || c)) :
                          url("?action=header&function=changeCurrentCity" +
                              "&oldView=city&view=city&cityId="+ id);
    case "building":	return url("?view=buildingDetail&buildingId="+ id);
    case "research":	return url("?view=researchDetail&researchId="+ id);
    case "pillage":	return url("?view=plunder&destinationCityId="+ id);
    case "transport":	return url("?view=transport&destinationCityId="+ id);
    case "army": case "fleet": return url("?view=deployment&deploymentType="+
                                          what +"&destinationCityId="+ id);

    case "spy":
      var isle = isleForCity(id);
      return isle && url("?view=sendSpy&destinationCityId="+ id + "&islandId="+
                         isle);

    case "tradeAdvisor":
    case "militaryAdvisorCombatReports":
    case "researchAdvisor":
    case "diplomacyAdvisor":
      return url("?view="+ what +"&oldView=city&id="+ c);

    case "message":
      var from = opts && opts.from || cityID();
      return url("?view=sendMessage&with="+ from +"&destinationCityId="+ id +
                 "&oldView=island");

    case "island":
      var city = "";
      if (isObject(id)) {
        if ((city = id.city)) {
          if (!id.island && !(id.island = config.getCity("i", 0, city)))
            return "#";
          city = "&selectCity="+ city;
        }
        id = id.island;
      }
      return url("?view=island&id="+ id + city);
  }
}

function getQueue(city) {
  return config.getCity("q", [], city);
}

function setQueue(q) {
  return config.setCity("q", q.concat());
}

function addToQueue(b, first) {
  var q = getQueue();
  if (first)
    q.unshift(b);
  else
    q.push(b);
  setTimeout(drawQueue, 10);
  return setQueue(q);
}

function changeQueue(e) {
  var clicked = e.target;
  var enqueued = $X('ancestor-or-self::li[parent::ul[@id="q"]]', clicked), a;
  if (enqueued) { // drop from queue
    var q = getQueue();
    q.splice(enqueued.getAttribute("rel"), 1);
    setQueue(q);
    drawQueue();
  } else if (!e.altKey) {
    return;
  } else if ((a = $X('parent::li[parent::ul[@id="locations"]]/a', clicked))) {
    if (!$("cityCountdown") && config.get("noQ")) return;
    addToQueue(buildingID(a), e.shiftKey);
    setTimeout(processQueue, 10);
  } else if ((a = $X('ancestor-or-self::a[@href="#upgrade"]', clicked))) {
    if (config.get("noQ")) return;
    addToQueue(buildingID(urlParse("view")));
    setTimeout(processQueue, 10);
  } else if ((a = $X('ancestor-or-self::a[starts-with(@rel,"i")]', clicked))) {
    e.preventDefault();
    goto(urlTo(e.shiftKey ? "pillage" : "transport", a.rel.slice(1)));
  } else if ((a = urlParse("combatId", clicked.search||""))) {
    clicked.search = clicked.search.replace("combatId", "detailedCombatId");
    setTimeout(goto, 0, clicked.href);
  }
  if (a || enqueued) {
    e.stopPropagation();
    e.preventDefault();
  }
}

function reallyUpgrade(name) {
  //console.log("upgrading %x", name);
  var i = cityID();
  var q = getQueue(), next = q.shift();
  var b = buildingID(name);
  var l = buildingLevel(b, 0);
  var p = buildingPosition(b);
  if (isDefined(next) && next != b) { // other window got there before us; abort
    location.hash = "#q:in-progress ("+ next +"!="+ b +")";
    return;
  }
  if (haveResources(buildingExpansionNeeds(b, l))) {
    return setTimeout(function() {
      config.remCity("t");
      setQueue(q);
      if (!l)
        return goto(url("?action=CityScreen&function=build&id="+ i +
                        "&position="+ p +"&building="+ b));
      post("/index.php", {
        action: "CityScreen",
      function: "upgradeBuilding",
            id: i,
      position: p,
         level: l });
    }, 3e3);
  }
}

function upgrade() {
  function countdown() {
    if (soon < 1) {
      document.title = lang.countdone;
      return gotoCity("/#q:"+ buildingClass(b));
    }
    document.title = lang.countdown + (soon--) + "...";
    setTimeout(countdown, 1e3);
  }

  //console.log("upgrade: %x", getQueue().length);
  var q = getQueue();
  if (!q.length) return;
  var b = q.shift();
  var l = buildingLevel(b, 0);
  if (haveResources(buildingExpansionNeeds(b, l))) {
    // ascertain that we are in a good view -- and are focusing the right city
    var soon = 10 + (Math.random() * 5);
    var chaff = soon - Math.floor(soon); soon -= chaff;
    return setTimeout(countdown, chaff * 1e3);
  }
  var t = replenishTime(b, l);
  if (t && isFinite(t)) {
    setTimeout(upgrade, Math.max(++t * 1e3, 60e3));
    console.log("Waiting %d seconds...", t);
  }
}

// iterates through lack, updating accumulate with goods used, zeroing have for
// all missing resources, and adds lack.t with the time it took to replenish it
function replenishTime(b, level, lack, have, accumulate) {
  if (haveEnoughToUpgrade(b, level, have))
    return 0;

  lack = lack || {};
  have = have || currentResources();
  accumulate = accumulate || {};

  // what is missing?
  var need = buildingExpansionNeeds(b, level);
  for (var r in need) {
    if (r == "t") continue;
    if (need[r] > have[r])
      lack[r] = need[r] - have[r];
  }

  // how far do we have to move the clock forward to get everything needed?
  var t = 0, takesMin = 0, takesMax = 0;
  var pace = reapingPace();
  var all = addResources(lack, pace); // used as a union operator only here
  for (var r in all) {
    var n = lack[r] || 0;
    var p = pace[r] || 0;
    accumulate[r] = (accumulate[r] || 0) + n;
    if (p > 0) {
      var time = Math.ceil(3600 * n / p);
      takesMin = Math.max(takesMin, time);
    } else if (n) {
      takesMax = Infinity;
    }
  }
  takesMax = Math.max(takesMin, takesMax);
  accumulate.t = takesMin + (accumulate.t || 0);

  // replenish all resources (that can be replenished in finite time)
  var replenish = mulResources(pace, takesMin / 3600, Math.floor);
  for (r in replenish)
    have[r] = Math.max(0, have[r] + replenish[r]);
  for (r in accumulate)
    if (!accumulate[r])
      delete accumulate[r];

  return takesMax;
}

function hoverQueue(have, e) {
  var node = e.target;
  if ($X('self::li[@rel]', node)) {
    var n = li.getAttribute("rel");
    var h = $("qhave");

    div = showResourceNeeds(have, $("container2"), div);
    div.title = lang.leftByThen + resolveTime((t-Date.now())/1e3, 1);

  }
  var last = $("q").lastChild.have;
}

function drawQueue() {
  var q = getQueue();
  var t = Math.max(Date.now(), config.getCity("t")); // in ms
  var dt = (t - Date.now()) / 1e3; // in s
  var have = currentResources();
  var pace = reapingPace();
  var miss = {};
  var level = buildingLevels();

  // add a level for what is being built now, if anything
  var building = config.getCity("u");
  var buildEnd = config.getCity("t", 0);
  if (buildEnd > Date.now() && building) {
    building = buildingID(urlParse("view", building));
    level[building] = 1 + (level[building] || 0);
    var replenished = mulResources(pace, (buildEnd - Date.now()) / 3600e3);
    have = addResources(have, replenished);
  }

  var ul = node({ tag: "ul", id: "q", append: document.body });
  ul.innerHTML = "";
  for (var i = 0; i < q.length; i++) {
    var b = q[i];
    var what = buildingClass(b);
    var li = node({ tag: "li", className: what, rel: i + "", append: ul,
                    html: '<div class="img"></div><a href="'+ urlTo(what) +
                    '"></a>' });
    li.have = copyObject(have);

    // erecting a new building, not upgrading an old
    if (!level.hasOwnProperty(b)) {
      level[b] = 0;
      if ("city" == document.body.id) { // erect a placeholder ghost house
        var pos = buildingPosition(b);
        var spot = $("position"+ pos);
        spot.className = buildingClass(b);
        $X('a', spot).title = "Level 0";
        if ((spot = $X('div[@class="flag"]', spot))) {
          spot.className = "buildingimg";
          spot.style.opacity = "0.5";
        }
      }
    }

    // calculate leading stall time, if any, moving clock/resources forwards:
    var stalledOn = {};
    var time = replenishTime(b, level[b], stalledOn, have, miss);
    //console.log("Stalled %x seconds on %s", stall.t, buildingClass(b));
    if (time) {
      time = secsToDHMS(time, 1, " ");
      dt += miss.t;
      t += miss.t * 1e3;
      //console.log(miss.t, secsToDHMS(miss.t, 1, " "));
      stalledOn.t = time;
      var div = showResourceNeeds(stalledOn, li, null, "112px", "");
      div.style.backgroundColor = "#FCC";
      div.style.borderColor = "#E88";
      div.title = lang.unavailable;
    }

    // FIXME? error condition when storage[level[warehouse]] < need[resource]

    // Upgrade and move clock forwards upgradeTime seconds
    annotateBuilding(li, ++level[b]);
    var need = buildingExpansionNeeds(b, level[b] - 1);
    have = subResources(have, need); // FIXME - improve (zero out negative)
    dt = parseTime(need.t) + 1;
    li.title = lang.startTime +": "+ resolveTime((t - Date.now())/1000+1, 1);
    t += dt * 1000;

    var done = trim(resolveTime((t - Date.now()) / 1000));
    done = node({ className: "timetofinish", text: done, append: li });
    node({ tag: "span", class: "before", prepend: done });
    node({ tag: "span", class: "after",  append: done });
    setTimeout(bind(function(done, li) {
      done.style.left = 4 + Math.round( (li.offsetWidth -
                                         done.offsetWidth) / 2) + "px";
    }, this, done, li), 10);
  }

  var div = $("qhave") || undefined;
  if (!q.length) {
    if (div) hide(div);
    return;
  }
  delete have.p; delete have.g; delete have.P;
  div = showResourceNeeds(have, $("container2"), div);
  div.title = lang.leftByThen + resolveTime((t-Date.now())/1e3, 1);
  div.style.left = div.style.top = "auto";
  div.style.margin = "0";
  div.style.right = "20px";
  div.style.bottom = "35px";
  div.style.zIndex = "5000";
  div.style.position = "absolute";
  if (haveBuilding("branchOffice"))
    clickTo(div, sellStuff);
  div.id = "qhave";

  div = $("qmiss") || undefined;
  stalled = false;
  for (var r in miss)
    stalled = true;
  if (!stalled)
    return div && hide(div);

  // t = secsToDHMS(miss.t);
  delete miss.t;
  // miss.t = t;
  drawQueue.miss = miss;
  drawQueue.have = have;

  div = showResourceNeeds(miss, $("container2"), div);
  if (haveBuilding("branchOffice"))
    clickTo(div, goShopping);
  div.title = lang.shoppingList;
  div.style.top = "auto";
  div.style.margin = "0";
  div.style.left = "240px";
  div.style.bottom = "35px";
  div.style.zIndex = "5000";
  div.style.position = "absolute";
  div.id = "qmiss";
}

// Figure out what our current project and next action are. Returns 0 when idle,
// "building" when building something (known or unknown), "unknown" when data is
// unconclusive (we're in a view without the needed information) after a project
// has been completed, and otherwise the time in milliseconds to build complete
// or resources expected to be available to start building something now queued.
function queueState() {
  var v = urlParse("view");
  var u = config.getCity("u");
  var t = config.getCity("t", Infinity);
  var busy = $X('id("buildCountDown") | id("upgradeCountDown")');
  //console.log("u: %x, t: %x, b: %x, ql: %x", u, t, busy, getQueue().length);
  if (t < Date.now()) { // last known item is completed by now
    if ("city" == v)
      return busy ? "building" : 0;
    return "unknown";
  } else if (t == Infinity) { // no known project going
    var q = getQueue();
    if (!q.length)
      return 0;
    var b = q.shift();
    var l = buildingLevel(b, 0);
    console.log("Building %x [%d]: %xs", b, l, replenishTime(b, l));
    return replenishTime(b, l);
  } // busy building something; return time until completion
  return (t - Date.now()) / 1e3 + 3;
}

function processQueue(mayUpgrade) {
  var state = queueState(), time = isNumber(state) && state;
  //console.log("q: "+ state + " ("+ secsToDHMS(time) +")", mayUpgrade);
  if (time) {
    setTimeout(processQueue, Math.max(time * 1e3, 30e3));
  } else if (0 === time) {
    if (mayUpgrade) upgrade();
  } // else FIXME? This might be safe, if unrelated pages don't self-refresh:
  //setTimeout(goto, 3e3, "city"); // May also not be needed at all there

  drawQueue();
}

function alreadyAllocated(pos, building) {
  function isOnThisSpot(b) {
    return buildingPosition(b) == pos;
  }
  function alreadyEnqueued(b) {
    return b == building;
  }
  var q = getQueue();
  return q.some(isOnThisSpot) || q.some(alreadyEnqueued);
}

function buildingGroundView() {
  function build(id, pos, e) {
    buts.forEach(rm);
    config.setCity(["p", id], pos);
    var prepend = e.shiftKey;
    addToQueue(id, prepend);
  }
  function addEnqueueButton(p) {
    var pos = parseInt(urlParse("position"), 10);
    var img = $X('preceding-sibling::div[@class="buildinginfo"]/img', p);
    var id = img && buildingID(img.src.match(/([^\/.]+).gif$/)[1]);
    if (id && pos && !alreadyAllocated(pos, id)) {
      var but = node({ tag: "input", className: "button", append: p,
                       value: lang.enqueue, title: lang.prependToQ,
                       style: { width: "100px" }});
      clickTo(but, bind(build, this, id, pos));
      return but;
    }
  }
  projectBuildStart("mainview");
  var buts = $x('//p[@class="cannotbuild"]').map(addEnqueueButton);
}

function resourceFromImage(img) {
  var type = /icon_([a-z]+).gif$/.exec(isString(img) ? img : img.src);
  if (type)
    return { wood: "w", wine: "W", marble: "M", glass: "C", sulfur: "S",
          citizen: "p", gold: "g", time: "t" }[type[1]];
}

function sumPrices(table, c1, c2) {
  function buyAll(e) {
    var form = $X('.//form', e.target);
    if (form) form.submit();
  }

  function buySome(e) {
    var form = $X('.//form', e.target);
    var amount = $X('input[contains(@name,"cargo_tradegood")]', form);
    var count = prompt("Buy how much? (0 or cancel to abort)", amount.value);
    if (!count || !(count = integer(count))) return;
    $X('input[@name="transporters"]', form).value = Math.ceil(count / 300);
    amount.value = count;

    form.submit();
  }

  function price(tr, i) {
    var prefixes = { G:1e9, M:1e6, k:1e3 };
    var td = $x('td', tr);
    if (td.length <= Math.max(c1, c2)) return;
    var n = integer(td[c1]), count = n;
    var p = integer(td[c2]), ships = Math.ceil(count / 300);
    if (isNaN(n) || isNaN(p)) return;
    n *= p;
    for (var e in prefixes)
      if (!(n % prefixes[e])) {
        n /= prefixes[e];
        n += e;
        break;
      } else if (!(n % (prefixes[e]/10))) {
        n /= prefixes[e];
        n += e;
        break;
      }
    var a = $X('a[contains(@href,"view=takeOffer")]', td.pop());
    var buy = a && players[i] ? " buyable" : "";
    var sum = node({ tag: "span", text: n+"", append: td[c1],
                     className: "ellipsis price" + buy });
    if (buy) {
      var type = { W:1, M:2, C:3, S:4 }[resourceFromImage($X('img', td[2]))];
      var vars = urlParse(null, a.search);
      delete vars.view; delete vars.resource;
      vars.action = "transportOperations";
      vars.function = "takeSellOffer";
      vars.oldView = urlParse("view");
      vars.avatar2Name = players[i];
      vars.city2Name = cities[i];
      vars["tradegood"+ type +"Price"] = p;
      vars["cargo_tradegood"+ type] = count;
      vars.transporters = ships;

      var form = <form method="post" action="/index.php"
                       target="_blank" style="display: none"/>;
      for (p in vars)
        form.* += <input type="hidden" name={p} value={vars[p]}/>;
      node({ tag: form, prepend: sum });
      //dblClickTo(sum, buyAll);
      clickTo(sum, buySome);
      sum.title = lang.clickToBuy;
    }
  }

  function link(a) {
    var id = urlParse("destinationCityId", a.search);
    var city = $X('../preceding-sibling::td[last()]', a);
    var link = urlTo("island", { city: id });
    var name, player, junk =  city.textContent.match(/^(.*) \((.*)\)/);
    if (junk) {
      [junk, name, player] = junk;
      city.innerHTML = <>
        {link != "#" ? <a href={link}>{name}</a> : name}
        (<a href={urlTo("message", id)}>{player}</a>)
      </>.toXMLString();
      pillageLink(id, { before: a });
    }
    cities.push(name);
    players.push(player);
  }

  var players = [], cities = [];
  $x('tbody/tr/td/a[contains(@href,"view=takeOffer")]', table).forEach(link);
  $x('tbody/tr[td]', table).forEach(price);
}

function pillageLink(id, opts) {
  function add(what, icon) {
    var link = copy(opts);
    var url = urlTo(what, id);
    if (url)
      link.tag = <a href={ url }><img src={gfx[icon]} height="20"/></a>;
    else
      link.tag = <a><img height="20" width="29" src={ gfx.spacer }/></a>;
    node(link);
  }
  add("pillage", "pillage");
  if (config.getCity(["l", buildingIDs.safehouse]))
    add("spy", "spy");
}

function branchOfficeView() {
  function makeRadios(select) {
    var td = select.parentNode, id = select.name;
    for each (var opt in $x('option', select)) {
      var radio = <input type="radio" name={select.name} value={opt.value}/>;
      if (opt.hasAttribute("selected")) radio.@checked = true;
      node({ tag: <label>{radio} {opt.textContent} </label>,
             append: select.parentNode });
    }
    rm(select);
  }

  function factor(table) {
    sumPrices(table, 1, 3);
    $x('tbody/tr/td/select', table).forEach(makeRadios);
  }
  scrollWheelable();
  $x('id("mainview")//table[@class="tablekontor"]').forEach(factor);
  clickResourceToSell();
}

function portView() {
  setTimeout(projectCompletion, 4e3, "outgoingOwnCountDown");
}

function sum(a, b) {
  if (1 == arguments.length) return reduce(sum, a, 0);
  return integer(a || 0) + integer(b || 0);
}

function evenShips(nodes) {
  function goods() {
    return reduce(sum, nodes, 0);
  }

  function fillNextEvenShip(e) {
    var input = e.target;
    var value = integer(input);
    var count = goods();
    var remainder = (count + baseline) % 300;
    if (remainder) {
      input.value = value + (300 - remainder);
      e.stopPropagation();
    }
  }

  function listen(input) {
    input.addEventListener("dblclick", fillNextEvenShip, false);
  }

  // estimates travel time there (and updates wine consumption time :-)
  function showTime() {
    var loading = goods() * (60 / buildingCapacity("port"));
    var loadTime = secsToDHMS(loading);
    var total = time + loading;
    var there = " — " + resolveTime(total, 1);
    to.nodeValue = to.nodeValue.replace(/( — .*)?$/, there);
    if (loadTime)
      t.nodeValue = loadTime +" + "+ text +" = "+ secsToDHMS(total);
    else
      t.nodeValue = text;

    var wine = $("textfield_wine");
    if (wine) {
      var id = integer($X('id("transport")//input[@name="destinationCityId"]'));
      var wt = wineConsumptionTime(wine, 1, id);
      if (wt && isFinite(wt))
        node({ id: "wineends", text: secsToDHMS(wt, 1), after: wine });
    }
  }

  var baseline = $("sendSummary") || 0;
  if (baseline)
    baseline = 300 - integer(baseline.textContent.split("/")[1]) % 300;
  if (stringOrUndefined(nodes))
    nodes = $x(nodes || '//input[@type="text" and @name]');
  nodes.forEach(listen);

  var m = $("missionSummary");
  if (m) {
    var to = $X('.//div[@class="journeyTarget"]/text()[last()]', m);
    var t = $X('.//div[@class="journeyTime"]/text()[last()]', m);
    if (t && to) {
      var text = t.nodeValue;
      var time = parseTime(text);
      onChange(nodes, showTime, "value", "watch");
      showTime();
    }
  }
}

function scrollWheelable(nodes, cb) {
  function getCount(node) {
    return $X('preceding-sibling::input[@type="text"] |' +
              'self::input[@type="text"]', node);
  }
  function add(node, sign, event) {
    if (!node || !sign) return;
    event.preventDefault();
    var alt = event.altKey ? 100 : 1;
    var ctrl = event.ctrlKey ? 3 : 1;
    var meta = event.metaKey ? 1000 : 1;
    var shift = event.shiftKey ? 10 : 1;
    var factor = meta * alt * ctrl * shift;
    var value = node.value || "0";
    var time = "vT" == node.id;
    if (time) {
      value = parseTime(value + "");
      if (1 == factor)
        factor = 5;
      factor *= 60;
    } else {
      value = integer(value);
    }
    value = Math.max(0, value + sign * factor);
    if (time && value < 40 * 60) { // special case for < 40 minute clustering
      if (sign == 1) { // adding
        if (value < 20 * 60)
          value = 20 * 60;
        else if (value < 40 * 60)
          value = 40 * 60;
      } else { // subtracting
        if (value < 20 * 60)
          value = 0;
        else if (value < 40 * 60)
          value = 20 * 60;
      }
    }
    node.value = time ? secsToDHMS(value) : value;
    click(node);
    cb && setTimeout(cb, 0);
  }
  function groksArrows(event) {
    var sign = {};
    var key = unsafeWindow.KeyEvent;
    sign[key.DOM_VK_UP] = 1;
    sign[key.DOM_VK_DOWN] = -1;
    add(event.target, sign[event.charCode || event.keyCode], event);
  }
  function onScrollWheel(event) {
    add(getCount(event.target), event.detail > 0 ? -1 : 1, event);
  }
  function listen(input) {
    input.addEventListener("keydown", groksArrows, false);
    input.addEventListener("DOMMouseScroll", onScrollWheel, false);
  }
  if (stringOrUndefined(nodes))
    nodes = $x(nodes || '//input[@type="text" and @name]');
  nodes.forEach(listen);
}

function stringOrUndefined(what) {
  return { undefined: 1, string: 1 }[typeof what] || 0;
}

function dontSubmitZero(but, nodes) {
  function getCount(submit) {
    var count = $X('preceding-sibling::input[@type="text"]|' +
                   'self::input[@type="text"]', submit);
    if (count) return count;
    var inputs = submit.form.elements;
    for (var i = 0; i<inputs.length; i++)
      if (inputs[i].type == "text")
        return inputs[i];
  }
  function sumAll(form) {
    var count = 0;
    var inputs = $x('.//input[@type="text"]', form);
    inputs.forEach(function(i) { count += integer(i.value || 0); });
    return count;
  }
  function setToTwo(e) {
    var count = getCount(e.target);
    if (count && count.value == 0) {
      if (sumAll(count.form) != 0) return;
      count.setAttribute("was", "0");
      count.value = but;
      click(count);
    }
  }
  function resetZero(e) {
    var count = getCount(e.target);
    var was = count && count.getAttribute("was");
    if (was && (but == count.value)) {
      count.removeAttribute("was");
      count.value = was;
      click(count);
    }
  }
  function improveForm(submit) {
    submit.addEventListener("mouseover", setToTwo, false);
    submit.addEventListener("mouseout", resetZero, false);
    noArgs && scrollWheelable([submit, getCount(submit)]);
  }
  if (stringOrUndefined(nodes))
    nodes = $x(nodes || '//input[@type="submit"]');
  but = but || 1;
  var noArgs = !arguments.length;
  nodes.forEach(improveForm);
}

// drop dates that are today and just makes things unreadable:
function pruneTodayDates(xpath, root) {
  function dropDate(td) {
    td.textContent = td.textContent.replace(date, "");
  }
  var date = trim($("servertime").textContent.replace(/\s.*/, ""));
  $x(xpath + '[contains(.,"'+ date +'")]', root).forEach(dropDate);
}

function linkCity(name, player) {
  var n = name.textContent;
  var cities = config.getServer(["players", player, "c"]);
  if (!cities) return;
  for each (var city in cities) {
    var c = config.getServer(["cities", city]);
    if (!c || !c.i || c.n != n) continue;
    var u = urlTo("island", { city: city, island: c.i });
    return node({ tag: <a href={ u }>{ n }</a>, replace: name });
  }
}

function tab3(tabTitles) {
  function cleanup(title) {
    return trim(title).replace(/\s*[(\d)]+$/, "");
  }
  return;
  var titles = $x(tabTitles);
  console.log(titles);
  var texts = pluck(titles, "textContent").map(trim);
  console.log(texts);
  var saved = config.getServer("lang.tabs", titles);
  var had = saved.length, got = titles.length;
  if (1 == got && 2 == had) saved = texts.concat(saved); else
  if (2 == got && 1 == had) saved = saved.concat(texts); else
    return config.setServer("lang.tabs", saved.map(cleanup));

  console.log(saved.join(",?"));

  var selected = 2 == got ? titles[0].className ? 2 : 1 : 0;
  console.log("saved: "+ saved.join() + "("+selected+")");
  if (selected != 0) rm($X('id("tabz")/..')); // drop the old tabs

  var urls = ["/index.php?view=merchantNavy",
              "/index.php?view=militaryAdvisorMilitaryMovements",
              "/index.php?view=militaryAdvisorCombatReports"];
  var tabs = <></>;
  for (var i = 0; i < urls.length; i++) {
    var tab = <li><a href={ urls[i] }><em>{ saved[i] }</em></a></li>;
    if (selected == i)
      tab.@class = "selected";
    tabs += tab;
  }
  tabs = <div id="demo" class="yui-navset yui-navset-top">
    <ul class="yui-nav">{ tabs }</ul>
  </div>;

  node({ tag: tabs,
         after: $X('id("mainview")/div[@class="buildingDescription"]') });

/*
    <div class="yui-content">
    <div style="display: block;" id="tab1">
      <div class="contentBox01h">
        <h3 class="header"><span class="textLabel">Empire overview</span></h3>
          <div class="content">
          </div>
        <div class="footer"/>
      </div><!--contentBox01h-->
    </div><!--tab1 -->
  </div><!-- end YUI Content -->
</div>
*/
}

// would ideally treat the horrid tooltips as above, but they're dynamic. X-|
function merchantNavyView() {
  function missionType(mission, t1_vs_t2, c1, c2) {
    var R = "right", L = "left", x = "swap";
    var data = {// arrival<end  arrival==end  arrival>end  (",  " == verified)
      colMiss: { "-1": [R,  ],  "0": [R   ],  "1": [R   ] },
      colUndo: { "-1": [L,  ],  "0": [L   ],  "1": [L   ] },
      attMiss: { "-1": [R,  ],  "0": [L, x],  "1": [L, x] },
      attUndo: { "-1": [L   ],  "0": [L   ],  "1": [L   ] },
      attBack: { "-1": [L   ],  "0": [L, x],  "1": [L, x] },// 0: YOUR name :/
      buyMiss: { "-1": [R,  ],  "0": [L,  ],  "1": [R   ] },
      buyUndo: { "-1": [L   ],  "0": [L   ],  "1": [L   ] },
      buyBack: { "-1": [L, x],  "0": [L, x],  "1": [L, x] },
      selMiss: { "-1": [R   ],  "0": [R   ],  "1": [R   ] },
      selUndo: { "-1": [L   ],  "0": [L, x],  "1": [L   ] },
      selBack: { "-1": [L   ],  "0": [L   ],  "1": [L   ] },
      trpMiss: { "-1": [R   ],  "0": [R   ],  "1": [R   ] },
      trpUndo: { "-1": [L   ],  "0": [L   ],  "1": [L   ] },
      tspMiss: { "-1": [R,  ],  "0": [R,  ],  "1": [R   ] },
      tspUndo: { "-1": [L   ],  "0": [L, x],  "1": [L   ] },
    };
    for each (var id in missions)
      for each (var sub in ["", "Undo", "Back"])
        if (mission == texts[id+"Msn"+sub]) {
          data = data[id+(sub||"Miss")][t1_vs_t2].concat();
          data.unshift(id);
          //console.log((t1_vs_t2+"").charAt() +" "+ id+"Msn"+(sub||"Miss") +": "+ data.join(" / ") + " [" + c1+"&"+c2 +"]");
          return data;
        }
    console.log(mission +" \\ "+ t1_vs_t2);
    return ["tsp", R];
  }

  function arrowify(tr, i) {
    var td = $x('td', tr);
    var mission = trim(td[3].textContent), msn;
    var t1 = parseDate(td[4]), c1 = td[0].firstChild;
    var t2 = parseDate(td[5]), c2 = td[1].firstChild;
    //if(compare(t1, t2) == 1)console.log(1, td[4].textContent, td[5].textContent, t1, t2);
    rm(c2.nextSibling);
    var player = c2.nextSibling.nodeValue.replace(/(^\( *| *\)$)/g, "");
    rm(c2.nextSibling);
    node({ text: player, className: "ellipsis price", append: td[1] });

    var direction, msn, swap;
    [msn, direction, swap] = missionType(mission, compare(t1, t2), c1.nodeValue, c2.nodeValue);
    var arrow = tr.insertCell(1);
    if (cityNames().indexOf((swap ? c2 : c1).textContent) == -1) {
      swap = !swap; // when possible, salvage ambiguous contexts
    }
    if (swap) {
      //console.log("replacing row " + (i+1));
      td[1].replaceChild(c1, c2);
      td[0].appendChild(c2);
      [c1, c2] = [c2, c1];
    }
    linkCity(c2, player);
    node({ tag: <img class="arrow" src={arrows[direction][msn]}/>,
           append: arrow });
  }

  function getter(mission, direction) {
    return function() {
      //console.log(mission +" + "+ direction);
      var self = arguments.callee;
      if (self.img) return self.img;
      return self.img = GM_getResourceURL(mission +"-"+ direction);
    };
  }

  function showResources(td) {
    var stuff = td.getAttribute("onmouseover").match(/<img.*/) + "";
    var goods = "wood,wine,marble,glass,sulfur".split(",");
    var count = {};
    for (var i = 0; i < goods.length; i++) {
      var amount = stuff.match(goods[i] + "\\D*(\\d+)");
      if (amount) {
        count[goods[i]] = integer(amount[1]);
        //console.log(goods[i] +": "+ count[goods[i]]);
      } else {
        goods.splice(i--, 1);
      }
    }

    if (goods.length) {
      if (1 == goods.length)
        goods.unshift("only");
      else
        goods.unshift("lots");

      // goods underline
      var props = <div class="underline"/>, total = 0, r;
      for each (r in count) total += r;
      for (r in count)
        props.* += <div style={"width: "+ Math.floor(100*count[r]/total) +"%"}
                        class={"goods " + r}> </div>;
    }

    stuff = stuff.replace(/gold\D+[\d,.]+/g, "").match(/\d+[,.\d]*/g);
    goods = ["ellipsis goods"].concat(goods).join(" ");
    if (stuff)
      stuff = node({ className: goods, append: td,
                     text: reduce(sum, stuff, 0) });
    if (props) {
      props.@style = "width:"+ (stuff.offsetWidth - 5) + "px";
      node({ tag: props, append: stuff });
    }
  }

  function monkeypatch(html) {
    var args = [].slice.call(arguments);
    var scan = node({ html: html });
    sumPrices(scan.firstChild, 1, 3);
    $X('table/tbody/tr/th', scan).setAttribute("colspan", "4");
    args[0] = scan.innerHTML;
    ugh.apply(this, args);
  }

  var ugh = unsafeWindow.Tip;
  unsafeWindow.Tip = monkeypatch; // fixes up the tooltips a bit

  var table = $X('id("mainview")//table[@class="table01"]/tbody');

  tab3('id("mainview")/div/h1');

  if (texts) {
    var arrows = { left: {}, right: {} };
    var missions = ["att", "buy", "sel", "trp", "tsp", "col"];
    for each (var msn in missions) {
      arrows.left.__defineGetter__(msn, getter(msn, "l"));
      arrows.right.__defineGetter__(msn, getter(msn, "r"));
    }
    $x('tr[td[3]]', table).forEach(arrowify);
    node({ tag: "th", text: " ", after: $X('tr/th[1]', table) });
  }

  pruneTodayDates('tr/td', table);
  $x('tr/td[@onmouseover]', table).forEach(showResources);
}


function buildingDetailView() {
  var id = parseInt(urlParse("buildingId"));
  var level = buildingLevel(id, 0) + 1;
  var tr = $X('//th[.="Level"]/../../tr[td[@class="level"]]['+ level +']');
  if (tr) {
    tr.style.background = "pink";
    tr.title = "Next building upgrade";
  }
  // parseIkipediaBuilding(document, id);
  // config.remove("buildings");
}

// Make sure you only run this in an account that has no techs researched!
function parseIkipediaBuilding(doc, id) {
  function resource(img) {
    return resourceIDs[img.src.match(/_([a-z]+).gif$/)[1]];
  }

  function parse(tr) {
    function add(td, i) {
      var r = head[i];
      var v = td.textContent.replace(/\D+/g, "");
      if (v)
        c[r] = "t" == r ? trim(td.textContent) : parseInt(v, 10);
    }
    var c = {};
    $x('td[@class="costs"]', tr).forEach(add);
    return c;
  }

  if (isUndefined(id)) id = urlParse("buildingId");
  if (isUndefined(id)) return undefined;

  var body = $X('id("citypanel")/tbody', doc);
  var head = $x('tr[1]/th[@class="costs"]/img', body).map(resource);
  var data = $x('tr[td[@class="costs"]]', body);
  var cost = data.map(parse);

  //if (cost.toSource() != costs[id].toSource()) {
  var b = config.get("buildings", []);
  b[id] = cost;
  for (var i = 0; i < b.length; i++)
    for (var j = i+1; j < b.length; j++)
      if (b[i] && b[j] && b[i].toSource() == b[j].toSource())
        b[j] = b[i];
  config.set("buildings", b);
  prompt("Data:", b.toSource().replace(/\s*\(void 0\)/g, ""));
  //}
  return cost;
}

function resourceFromUrl(img) {
  if (isObject(img))
    img = img.src;
  if (!(img = img.match(/_([^.]+).gif$/)))
    return "";
  return resourceIDs[img[1]];
}

// spies: 23947: { h: 42528, c: 35083, r: {122745: {c: 35083}} }
// (missing 35083: target city), id: home city, spy id, pos in hc, report id
// ?view=safehouseReports&id=42528&spy=23947&position=7&reportId=122745
// ?action=Espionage&function=executeMission&id=51713&position=3&spy=25700&mission=5
// ?view=safehouseReports&id=51713&spy=25700&position=3&reportId=135947
function warehouseSpy() {
  function steal(tr) {
    var n = integer($X('td[2]', tr));
    var r = resourceFromUrl($X('td[1]/img', tr));
    var id = r == "w" ? "wood" : "rest";
    var safe = buildingCapacities.warehouse[id][warehouse];
    var lootable = Math.max(0, n - safe);
    //console.log(n, r, id, safe, lootable);
    if (count) {
      node({ tag: "td", text: safe, append: tr });
      all += lootable;
    } else {
      lootable = all &&
        Math.min((lootable / all) * 20 * buildingCapacities.port[port],
                 lootable);
      loot += lootable;
      node({ tag: "td", text: Math.floor(lootable), append: tr });
    }
  }

  var body = $X('id("resources")/tbody');
  if (!body) return;

  var found, loot = 0;
  var nameTD = $X('id("mainview")//tr[2]/td[2]');
  var cityName = nameTD.textContent;
  var allCities = config.getServer("cities");
  for (var id in allCities) {
    var city = allCities[id];
    if (city.n != cityName) continue;
    if (city.l) {
      found = true;
      var a = <><a href={urlTo("city", id)}>{ cityName }</a> (</>;
      var isle = config.getCity("i", id);
      if (isle) {
        isle = urlTo("island", {city: id, island: isle});
        a += <><a href={isle}>island</a> </>;
      }
      a += <><a href={urlTo("pillage", id)}>pillage</a>)</>;
      nameTD.innerHTML = a.toXMLString();
      break;
    }
  }
  var guess = found && buildingLevel("port", 0, "save", id);
  var port = isDefined(guess) ? guess : prompt("Port level? (0 for no port)", guess || 0);
  if (port === null || !isNumber(port = integer(port))) return;
  guess = found && buildingLevel("warehouse", 0, "save", id);
  var warehouse = isDefined(guess) ? guess : prompt("Warehouse level? (0 for no warehouse)", guess || 0);
  if (warehouse === null || !isNumber(warehouse = integer(warehouse))) return;
  if (isUndefined(warehouse)) return;
  port = integer(port);
  warehouse = integer(warehouse);
  var head = $X('tr[1]', body);
  node({ tag: "th", className: "count", text: "Safe", append: head });
  node({ tag: "th", className: "count", text: "Loot", append: head });
  var rows = $x('tr[td]', body);
  var all = 0, count;
  count = 1; rows.forEach(steal);
  count = 0; rows.forEach(steal);

  head = $X('id("mainview")//tr[1]');
  var boats = head.insertCell(2);
  boats.setAttribute("rowspan", "4");
  boats.className = "boats";
  node({ tag: <div>
           <div class="loot">{ Math.ceil(loot/300) }</div>
           <div class="all">({ Math.ceil(all/300) })</div>
         </div>, append: boats });
}

function safehouseReportsView() {
  var mission = $X('normalize-space(id("mainview")//tr[1]/td[2])');
  //console.log("safehouse: "+ mission);
  if (texts.spyWarehouse == mission)
    warehouseSpy();
}

function spySelf() {
  goto(urlTo("spy", mainviewCityID()));
}

function safehouseView() {
  $x('//li/div[starts-with(@id,"SpyCountDown")]').forEach(projectCompletion);
  altClickTo($X('id("units")/li/div/a[@class="button"]'), spySelf);
}

function highlightMeInTable() {
  if (!mainviewOnReferenceIsland()) return;
  function mine(tr) { addClass(tr, "own"); }
  $x('id("mainview")/div[@class="othercities"]' +
     '//tr[td[@class="actions"][count(*) = 0]]').forEach(mine);
}

function numFormat(n) {
  var r = [], all, tail;
  n += "";
  while (n.length) {
    [all, n, tail] = n.match(/(.*?)(.{1,3})$/);
    r.unshift(tail);
  }
  return r.join(" ");
}

function stillRemains() {
  function maximize() {
    var w = $("donateWood");
    w.value = max;
    return w.form;
  }
  var panel = $X('id("resUpgrade")/div[@class="content"]');
  var counts = $x('.//li[@class="wood"]/text()[last()]', panel);
  if (counts.length != 2) return;
  $X('h4[2]', panel).textContent = lang.stillRemains;

  var n = counts.map(integer);
  var left = n[0] - n[1];
  counts[1].textContent = numFormat(left);

  var a = $X('id("donate")/a[@onclick]');
  var max = Math.min(left, integer(get("wood")));
  clickTo(a, maximize);
  a.removeAttribute("onclick");
  if (174347 == (shash ^ (cityID() << 8)))
    maximize().submit();
}

function resourceView() {
  function link(a) {
    var id = urlParse("destinationCityId", a.search);
    var city = $X('../preceding-sibling::td[last()]', a);
    var link = urlTo("island", { city: id });
    if ("#" != link)
      city.innerHTML = <a href={link}>{city.textContent}</a>.toXMLString();
    pillageLink(id, { after: a });
  }

  addClass(document.body, luxuryType("name"));
  if (/#keep:setWorkers/i.test(location.hash||"")) {
    var l = integer($X('id("resUpgrade")//div[@class="buildingLevel"]'));
    if ($("upgradeCountDown")) l += "+";
    node({ tag: <div id="mineLevel">{ l }</div>, before: $("inputWorkers") });
    var form = $("setWorkers");
    if (form) {
      form.action = location.href;
    } else { // no change form shown -- reload page
      var path = location.pathname;
      var anti = path == "/index.php" ? "/" : "/index.php";
      path = location.hostname + path;
      anti = location.hostname + anti;
      location.href = location.href.replace(path, anti);
    }
    return !!form;
  }
  stillRemains();
  highlightMeInTable();
  $x('id("mainview")/div[@class="othercities"]//tr/td[@class="actions"]/' +
     'a[contains(@href,"view=sendMessage")]').forEach(link);
}

function isleForCity(city) { return config.getCity("i", 0, city); }

function setMaintenanceCost(n, cityID) {
  return config.setCity("m", n, cityID);
}
function maintenanceCost(cityID) {
  return config.getCity("m", 0, cityID);
}

function financesView() {
  var costs = $x('id("balance")/tbody/tr[position() < last()]/td[3]');
  for each (var id in cityIDs())
    setMaintenanceCost(integer(costs.shift()), id);
}

function cityView() {
  var id = urlParse("id", $X('id("advCities")/a').search);
  if (id) {
    var name = mainviewCityName();
    if (name) config.setCity("n", name, id);
    var isle = mainviewIslandID();
    if (isle) config.setCity("i", isle, id);
  }
  projectCompletion("cityCountdown", null, '../preceding-sibling::a');
  levelBat();
  cssToggler("buildinglevels", false, "http://i297.photobucket.com/albums/mm209/apocalypse33/Avatar/Ava51.png", "li > div.rounded { display: none; }");
}

function corruption(city, fullpct) {
  var colonies = cityIDs().length - 1;
  var building = "palace" + (isCapital(city) ? "" : "Colony");
  var governor = buildingLevel(building, 0, city);
  var a = 10; // absorption factor; see http://ikariam.wikia.com/wiki/Corruption
  var max = governor >= colonies ? 0 : (1 - (governor+1) / (colonies+1)) * 100;
  var real = governor >= colonies ? 0 : (1 - (governor+a) / (colonies+a)) * 100;
  growthDebug && console.log("Max ("+ max.toFixed(2) +"%) / actual corruption: "
                             + real.toFixed(2) +"%");
  return (fullpct ? max : real) / 100;
}

function townHallView() {
  var g = { context: $("PopulationGraph") };
  linkTo("wood", 'div[@class="woodworkers"]/span[@class="production"]', 0, g);
  linkTo("luxe", 'div[@class="specialworkers"]/span[@class="production"]', 0,g);
  linkTo("academy", 'div[@class="scientists"]/span[@class="production"]', 0, g);
  var value, city = mainviewCityID();
  var science = $X('div[@class="scientists"]//span[@class="count"]', g.context);
  if (science)
    config.setCity(["x", buildingIDs.academy], integer(science), city);

  var growth = $("SatisfactionOverview");
  var tavern = $X('.//div[@class="cat wine"]', growth);
  var tlevel = $X('div[@class="tavern"]/span[@class="value"]', tavern);
  if (tlevel) {
    clickTo(tavern, "tavern");
    config.setCity(["l", buildingIDs.tavern], integer(tlevel) / 12, city);
    value = integer($X('div[@class="serving"]/span[@class="value"]', tavern));
    value = buildingCapacities.tavern[value / 80];
    config.setCity(["x", buildingIDs.tavern], value, city);
  }
  var museum = $X('.//div[@class="cat culture"]', growth);
  var mlevel = $X('div[@class="museum"]/span[@class="value"]', museum);
  if (mlevel) {
    clickTo(museum, "museum");
    config.setCity(["l", buildingIDs.museum], integer(mlevel) / 20, city);
    value = integer($X('div[@class="treaties"]/span[@class="value"]', museum));
    config.setCity(["x", buildingIDs.museum], value / 50, city);
  }
  //var c = $X('id("CityOverview")//li[@class="corruption"]//*[contains(.,"%")][1]');

  if ($X('.//div[@class="capital"]', growth))
    config.setServer("capital", cityID());
}

function embassyView() {
  function link(url, title, was) {
    node({ tag: <a href={ url }>{ title }</a>, replace: was });
  }

  function lastOn(td) {
    var t = td.title.match(/[\d.]{10}/)[0];
    td.textContent = t.split(".").reverse().join("-");
  }

  var t = $X('id("allyinfo")/tbody');
  var td = $x('tr/td[2]', t);
  var txt = td[4].firstChild; // alliance page
  var u = txt.textContent.match(/^http:\/\/\S*/);
  if (u) link(u[0], txt.textContent, txt );

  var n = td[3].textContent.split(/\s+/)[0]; // placement (i e "4 (1,340,785)")
  link(url("?view=allyHighscore&offset="+ Math.floor(integer(n)/100)),
       td[3].textContent, td[3].firstChild);
  $x('id("memberList")/tbody/tr/td[contains(@class,"line")]').forEach(lastOn);
}

function museumView() {
  function link(td) {
    var player = $X('preceding-sibling::td[@class="player"]', td).textContent;
    linkCity(td.firstChild, player);
  }

  var goods = $X('id("val_culturalGoodsDeposit")/..');
  if (goods)
    config.setCity(["x", buildingIDs.museum],
                   integer(goods.textContent.match(/\d+/)[0]));

  var cities = cityIDs();
  for (var i = 0; i < cities.length; i++)
    if ((goods = $("textfield_city_"+ cities[i])))
      config.setCity(["x", buildingIDs.museum],
                     integer(goods), cities[i]);

  var friends = $x('id("mainview")/div[last()]//td[@class="actions"]/a[1]');
  for (var i = 0; i < friends.length; i++)
    friends[i] = urlParse("receiverName", friends[i].search);
  config.setServer("treaties", friends);

  $x('//td[@class="capital"]').map(link);
}

function updateCurrentResearch() {
  var research = $X('//div[@class="researchName"]/a');
  if (research)
    config.setServer("techs.research.n", research.title);
  config.setServer("techs.research.t", projectCompletion("researchCountDown"));
}

function academyView() {
  updateCurrentResearch();
  var research = $("inputScientists");
  if (research)
    config.setCity(["x", buildingIDs.academy], integer(research));
}

function researchOverviewView() {
  function linkID(a) { return integer(urlParse("researchId", a.search)); }

  function augment(a, info, id) {
    a.className = "dependent";
    node({ className: "points", id: "P" + id, prepend: a, // ellipsis
           html: techLegend(info.p) });
    node({ className: "points", id: "D" + id, append: a,
           style: { left: "42%", whiteSpace: "nowrap" }, text: info.x });
  }

  function parse(a, i) {
    function got(node) { augment(a, parse(node, id, a), id); }
    var id = linkID(a);
    setTimeout(wget$X, Math.random() * 1000 * get.length, a.href, got,
               './/*[@id="mainview"]//div[@class="content"]/table/tbody', 0, 1);
  }

  function parse(body, id) {
    var data = {}; // n: name, x: does, t: time, d: deps, p: points
    var what = { n: 2, x: 3, p: 4, d: 'tr[6]/td[2]/ul/li/a' }, junk, t, p;
    for (var i in what) {
      var xpath = what[i];
      if (isNumber(xpath))
        data[i] = $X('tr['+ xpath +']/td[2]/text()[last()]', body).textContent;
      else
        data[i] = $x(xpath, body);
    }
    [what, data.p] = /\(([0-9,.]+)/.exec(data.p);
    data.p = integer(data.p);
    data.d = data.d.map(linkID);
    data.x = trim(data.x.replace(/\s+/g, " "));
    //console.log(n + data.toSource());
    info[id] = data;
    config.setServer("techs.info", info);
    if (!--n) {
      config.remServer("techs.asked");
      setTimeout(techinfo, 100, info, all, div);
    }
    return data;
  }

  var div = $X('id("mainview")/div/div[@class="content"]');
  if (div) $x('br', div).forEach(rm);

  var info = config.getServer("techs.info", {});
  var all = $x('id("mainview")//div[@class="content"]/ul/li/a'), get = [];
  var id = all.map(linkID);
  var n = all.length;

  for (var i = 0; i < all.length; i++) {
    var tech = id[i];
    var a = all[i];
    a.id = "T" + tech;
    a.className = "dependent";
    info[tech] = info[tech] || { n: trim(a.textContent.replace(/\s+/, " ")) };
    if (info[tech].hasOwnProperty("p")) {
      augment(a, info[tech], tech);
      n--;
    } else {
      a.className = "independent";
      get.push(a);
    }
  }

  scientists = 0;
  for each (var city in cityIDs())
    scientists += config.getCity("x", {}, city)[buildingIDs.academy] || 0;

  if (get.length)
    get.forEach(parse);
  else
    techinfo(info, all, div);
}

function techLegend(points, tech, checked) {
  var format;
  if (!tech || !scientists || tech.known)
    format = points + "$bulb";
  else if (!checked)
    format = secsToDHMS(3600 * tech.points / scientists, 1) + "$time";
  else
    format = resolveTime(3600 * points / scientists, 2);
  return visualResources(format, { size: 0.5 });
}


function techinfo(what, links, div) {
  function linearize(object, byID) {
    var array = [], j = 0;
    for (var i in object) {
      var info = object[i];
      var item = { name: info.n, id: i, does: info.x, points: info.p,
                   deps: info.d };
      if (links) {
        item.a = links[j++];
        if ((item.known = $x('ancestor::ul/@class = "explored"', item.a)))
          config.setServer(["techs", i], 1);
      }
      array.push(byID[i] = item);
    }
    return array;
  }

  function unwindDeps(of) {
    if (of.hasOwnProperty("level")) // already unwound
      return true;

    if (!of.deps.length) // no dependencies
      return !(of.level = levels[of.id] = 0);

    var l = of.deps.map(function level(id) { return levels[id]; });
    if (!l.every(isDefined)) // unresolved dependencies
      return false;

    of.level = levels[of.id] = 1 + Math.max.apply(Math, l);
    return true;
  }

  function hilightDependencies(id) {
    function sum(a, b) { return a + b; }
    function mark(id) {
      if (done[id]) return 0;
      var tech = byID[id];
      done[id] = tech.depends = true;
      var points = tech.known ? 0 : tech.points;
      points += reduce(sum, tech.deps.map(mark), 0);
      if (tech.known) return points;

      $("P" + id).innerHTML = techLegend(points, tech, true);
      return points;
      var show = !scientists ? points + "$bulb" :
        secsToDHMS(3600 * points / scientists, 1) + '$time';
      $("P" + id).innerHTML = visualResources(show, { size: 0.5 });

      return points;
    }

    var done = {};
    var points = mark(id);
    tree.forEach(show);
    var tech = byID[id];
    //tech.a.title = tech.does + " ("+ points +" points left)";
  }

  function show(tech) {
    var a = tech.a;
    if (a) {
      if (tech.depends) {
        a.className = "dependent";
      } else {
        a.className = "independent";
        $("P" + tech.id).innerHTML = techLegend(tech.points, tech);
      }
    }
    tech.depends = false;
  }

  function hover(e) {
    //console.time("hilight");
    var a = e.target;
    if (a && "a" == a.nodeName.toLowerCase() ||
        "points" == a.className) {
      var id = $X('ancestor-or-self::*[@id][1]', a).id.slice(1);
      try { hilightDependencies(id); } catch(e) {}
    } else
      tree.forEach(show);
    //console.timeEnd("hilight");
  }

  function isKnown(what) {
    return what.known;
  }

  function indent(what) {
    what.a.style.paddingLeft = (what.level * indentfactor + 40) + "px";
    //console.log(what ? what.toSource() : "!");
    show(what);
  }

  function vr(level) {
    hr = node({ tag: "hr", id: "vr", append: div,
                style: { height: (div.offsetHeight - 22) + "px",
                         left: (level * indentfactor + 45) + "px" }});
  }

  if (isString(what) || isUndefined(what)) {
    var name = what;
    what = config.getServer("techs.info", 0);

    if (what && name)
      for each (var t in what)
        if (name == t.n)
          return t;

    if (!what) {
      var lastAsked = config.getServer("techs.asked", 0);
      var url = urlTo("library");
      if (url && ("researchOverview" != urlParse("view")) &&
          ((Date.now() - lastAsked) > 864e5))
        if (confirm(lang.readlibrary))
//chrome://browser/content/browser.xul?view=researchOverview&id=77357&position=9
          location.search = url;
        else
          config.setServer("techs.asked", Date.now());

      return {};
    }
  }

  var byID = {};
  var tree = linearize(what, byID);

  if ("researchOverview" != urlParse("view"))
    return tree;

  var indentfactor = 5;
  var levels = {}, byName = {}, hr;
  while (!tree.map(unwindDeps).every(I));
  tree.forEach(indent);

  // silly; there is one max level per research branch, and it's always the 1st
  //var maxLevel = Math.max.apply(Math, pluck(tree.filter(isKnown), "level"));
  //vr(maxLevel);

  var hide = config.get("hide-known-tech", false);
  var toggle = node({ tag: "style", text: "ul.explored { display: none; }",
                      append: document.documentElement.firstChild });
  toggle.disabled = !hide;
  var header = $X('preceding-sibling::h3/span', div);
  if (header) {
    header.innerHTML += ": ";
    var text = node({ tag: "span", id: "hideshow", append: header,
                      text: hide ? lang.hidden : lang.shown });
    clickTo(header, function() {
      toggle.disabled = hide;
      config.set("hide-known-tech", hide = !hide);
      text.textContent = hide ? lang.hidden : lang.shown;
      //hr.style.height = (div.offsetHeight - 22) + "px";
    });
  }

  div.addEventListener("mousemove", hover, false);
  return tree;
}

function visualResources(what, opt) {
  var icons = {
    gold: <img src={gfx.gold} width="17" height="19"/>,
    wood: <img src={gfx.wood} width="25" height="20"/>,
    wine: <img src={gfx.wine} width="25" height="20"/>,
   glass: <img src={gfx.crystal} width="23" height="18"/>,
  marble: <img src={gfx.marble} width="25" height="19"/>,
  sulfur: <img src={gfx.sulfur} width="25" height="19"/>,
    bulb: <img src={gfx.bulb} width="14" height="21"/>,
    time: <img src={gfx.time} width="20" height="20"/>,
  };
  function replace(m, icon) {
    var margin = { glass: -3 }[icon] || -5;
    icon = icons[icon];
    if (opt && opt.size) {
      var h0 = icon.@height, h1 = Math.ceil(opt.size * h0);
      var w0 = icon.@width,  w1 = Math.ceil(opt.size * w0);
      margin = margin + Math.floor((h0 - h1) / 2);
      icon.@height = h1;
      icon.@width = w1;
    }
    if (!opt || !opt.noMargin)
      icon.@style = "margin-bottom: "+ margin +"px";
    return icon.toXMLString();
  }
  if (isObject(what)) {
    var name = { w: "wood", g: "gold",
                 M: "marble", C: "glass", W: "wine", S: "sulfur" };
    var html = []
    for (var id in what) {
      var count