diff --git a/binaries/data/mods/public/gui/page_structree.xml b/binaries/data/mods/public/gui/page_structree.xml
new file mode 100644
index 0000000000..09c778a6ef
--- /dev/null
+++ b/binaries/data/mods/public/gui/page_structree.xml
@@ -0,0 +1,17 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ common/setup_resources.xml
+ common/sprite1.xml
+ common/styles.xml
+ common/common_sprites.xml
+ common/common_styles.xml
+
+ structree/styles.xml
+ structree/sprites.xml
+ structree/structree.xml
+ structree/setup.xml
+
diff --git a/binaries/data/mods/public/gui/pregame/mainmenu.xml b/binaries/data/mods/public/gui/pregame/mainmenu.xml
index 6c796eef05..33ae9a14d6 100644
--- a/binaries/data/mods/public/gui/pregame/mainmenu.xml
+++ b/binaries/data/mods/public/gui/pregame/mainmenu.xml
@@ -139,6 +139,66 @@
size="60 50%-100 300 50%+100"
hidden="true"
>
+
+
+
-
+
+
Learn To Play
- Open the 0 A.D. Game Manual.
+ Learn how to play, discover the technology trees, and the history behind the civilizations
closeMenu();
-
+ openMenu("submenuLearn", (this.parent.size.top+this.size.top), (this.size.bottom-this.size.top), 3);
@@ -436,28 +495,11 @@
-
-
- History
- Learn about the many civilizations featured in 0 A.D.
-
- closeMenu();
-
-
-
-
Exit
diff --git a/binaries/data/mods/public/gui/structree/draw.js b/binaries/data/mods/public/gui/structree/draw.js
new file mode 100644
index 0000000000..9cd9991320
--- /dev/null
+++ b/binaries/data/mods/public/gui/structree/draw.js
@@ -0,0 +1,298 @@
+var g_DrawLimits = {}; // GUI limits. Populated by predraw()
+
+/**
+ * Draw the structree
+ *
+ * (Actually resizes and changes visibility of elements, and populates text)
+ */
+function draw()
+{
+ // Set basic state (positioning of elements mainly), but only once
+ if (!Object.keys(g_DrawLimits).length)
+ predraw();
+
+ var defWidth = 96;
+ var defMargin = 4;
+ var phaseList = g_ParsedData.phaseList;
+
+ Engine.GetGUIObjectByName("civEmblem").sprite = "stretched:"+g_CivData[g_SelectedCiv].Emblem;
+ Engine.GetGUIObjectByName("civName").caption = g_CivData[g_SelectedCiv].Name;
+ Engine.GetGUIObjectByName("civHistory").caption = g_CivData[g_SelectedCiv].History;
+
+ let i = 0;
+ for (let pha of phaseList)
+ {
+ let s = 0;
+ let y = 0;
+
+ for (let stru of g_CivData[g_SelectedCiv].buildList[pha])
+ {
+ let thisEle = Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]");
+ if (thisEle === undefined)
+ {
+ error("\""+g_SelectedCiv+"\" has more structures in phase "+pha+" than can be supported by the current GUI layout");
+ break;
+ }
+
+ let c = 0;
+ let rowCounts = [];
+ stru = g_ParsedData.structures[stru];
+ Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]_icon").sprite = "stretched:session/portraits/"+stru.icon;
+ Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]_icon").tooltip = assembleTooltip(stru);
+ Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]_name").caption = translate(stru.name.specific);
+ thisEle.hidden = false;
+
+ for (let r in g_DrawLimits[pha].prodQuant)
+ {
+ let p = 0;
+ r = +r; // force int
+ let prod_pha = phaseList[phaseList.indexOf(pha) + r];
+ if (stru.production.units[prod_pha])
+ {
+ for (let prod of stru.production.units[prod_pha])
+ {
+ prod = g_ParsedData.units[prod];
+ if (!drawProdIcon(i, s, r, p, prod))
+ break;
+ p++;
+ }
+ }
+ if (stru.wallset && prod_pha == pha)
+ {
+ for (let prod of [stru.wallset.gate, stru.wallset.tower])
+ {
+ if (!drawProdIcon(i, s, r, p, prod))
+ break;
+ p++;
+ }
+ }
+ if (stru.production.technology[prod_pha])
+ {
+ for (let prod of stru.production.technology[prod_pha])
+ {
+ prod = (prod.slice(0,5) == "phase") ? g_ParsedData.phases[prod] : g_ParsedData.techs[prod];
+ if (!drawProdIcon(i, s, r, p, prod))
+ break;
+ p++;
+ }
+ }
+ rowCounts[r] = p;
+ if (p>c)
+ c = p;
+ hideRemaining("phase["+i+"]_struct["+s+"]_row["+r+"]_prod[", p, "]");
+ }
+
+ let size = thisEle.size;
+ size.left = y;
+ size.right = size.left + ((c*24 < defWidth)?defWidth:c*24)+4;
+ y = size.right + defMargin;
+ thisEle.size = size;
+
+ let eleWidth = size.right - size.left;
+ let r;
+ for (r in rowCounts)
+ {
+ let wid = rowCounts[r] * 24 - 4;
+ let phaEle = Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]_row["+r+"]");
+ size = phaEle.size;
+ size.left = (eleWidth - wid)/2;
+ phaEle.size = size;
+ }
+ ++r;
+ hideRemaining("phase["+i+"]_struct["+s+"]_row[", r, "]");
+ ++s;
+ }
+ hideRemaining("phase["+i+"]_struct[", s, "]");
+ ++i;
+ }
+}
+
+function drawProdIcon(pha, s, r, p, prod)
+{
+ var prodEle = Engine.GetGUIObjectByName("phase["+pha+"]_struct["+s+"]_row["+r+"]_prod["+p+"]");
+ if (prodEle === undefined)
+ {
+ error("The structures of \""+g_SelectedCiv+"\" have more production icons in phase "+pha+" than can be supported by the current GUI layout");
+ return false;
+ }
+
+ prodEle.sprite = "stretched:session/portraits/"+prod.icon;
+ prodEle.tooltip = assembleTooltip(prod);
+ prodEle.hidden = false;
+ return true;
+}
+
+/**
+ * Calculate row position offset (accounting for different number of prod rows per phase).
+ */
+function getPositionOffset(idx)
+{
+ var phases = g_ParsedData.phaseList.length;
+
+ var size = 92*idx; // text, image and offset
+ size += 24 * (phases*idx - (idx-1)*idx/2); // phase rows (phase-currphase+1 per row)
+
+ return size;
+}
+
+function hideRemaining(prefix, idx, suffix)
+{
+ let obj = Engine.GetGUIObjectByName(prefix+idx+suffix);
+ while (obj)
+ {
+ obj.hidden = true;
+ ++idx;
+ obj = Engine.GetGUIObjectByName(prefix+idx+suffix);
+ }
+}
+
+
+/**
+ * Positions certain elements that only need to be positioned once
+ * (as does not reposition automatically).
+ *
+ * Also detects limits on what the GUI can display by iterating through the set
+ * elements of the GUI. These limits are then used by draw().
+ */
+function predraw()
+{
+ var phaseList = g_ParsedData.phaseList;
+ var initIconSize = Engine.GetGUIObjectByName("phase[0]_struct[0]_row[0]_prod[0]").size;
+
+ let phaseCount = phaseList.length;
+ let i = 0;
+ for (let pha of phaseList)
+ {
+ let offset = getPositionOffset(i);
+ // Align the phase row
+ Engine.GetGUIObjectByName("phase["+i+"]").size = "8 16+"+offset+" 100% 100%";
+
+ // Set phase icon
+ let phaseIcon = Engine.GetGUIObjectByName("phase["+i+"]_phase");
+ phaseIcon.sprite = "stretched:session/portraits/"+g_ParsedData.phases[pha].icon;
+ phaseIcon.size = "16 32+"+offset+" 48+16 48+32+"+offset;
+
+ // Position prod bars
+ let j = 1;
+ for (; j < phaseCount - i; ++j)
+ {
+ let prodBar = Engine.GetGUIObjectByName("phase["+i+"]_bar["+(j-1)+"]");
+ prodBar.size = "40 1+"+(24*j)+"+98+"+offset+" 100%-8 1+"+(24*j)+"+98+"+offset+"+22";
+ // Set phase icon
+ let prodBarIcon = Engine.GetGUIObjectByName("phase["+i+"]_bar["+(j-1)+"]_icon");
+ prodBarIcon.sprite = "stretched:session/portraits/"+g_ParsedData.phases[phaseList[i+j]].icon;
+ }
+ // Hide remaining prod bars
+ hideRemaining("phase["+i+"]_bar[", j-1, "]");
+
+ let s = 0;
+ let ele = Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]");
+ g_DrawLimits[pha] = {
+ structQuant: 0,
+ prodQuant: []
+ };
+
+ do
+ {
+ // Position production icons
+ for (let r in phaseList.slice(phaseList.indexOf(pha)))
+ {
+ let p=1;
+ let prodEle = Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]_row["+r+"]_prod["+p+"]");
+
+ do
+ {
+ let prodsize = prodEle.size;
+ prodsize.left = (initIconSize.right+4) * p;
+ prodsize.right = (initIconSize.right+4) * (p+1) - 4;
+ prodEle.size = prodsize;
+
+ p++;
+ prodEle = Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]_row["+r+"]_prod["+p+"]");
+ } while (prodEle !== undefined);
+
+ // Set quantity of productions in this row
+ g_DrawLimits[pha].prodQuant[r] = p;
+
+ // Position the prod row
+ Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]_row["+r+"]").size = "4 100%-"+24*(phaseCount - i - r)+" 100%-4 100%";
+ }
+
+ // Hide unused struct rows
+ for (let j = phaseCount - i; j < phaseCount; ++j)
+ Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]_row["+j+"]").hidden = true;
+
+ let size = ele.size;
+ size.bottom += Object.keys(g_DrawLimits[pha].prodQuant).length*24;
+ ele.size = size;
+
+ s++;
+ ele = Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]");
+ } while (ele !== undefined);
+
+ // Set quantity of structures in each phase
+ g_DrawLimits[pha].structQuant = s;
+ ++i;
+ }
+ hideRemaining("phase[", i, "]");
+ hideRemaining("phase[", i, "]_bar");
+}
+
+/**
+ * Assemble a tooltip text
+ *
+ * @param template Information about a Unit, a Structure or a Technology
+ *
+ * @return The tooltip text, formatted.
+ */
+function assembleTooltip(template)
+{
+ var txt = getEntityNamesFormatted(template);
+ txt += '\n' + getEntityCostTooltip(template, 1);
+
+ if (template.tooltip)
+ txt += '\n' + txtFormats.body[0] + translate(template.tooltip) + txtFormats.body[1];
+
+ if (template.auras)
+ for (let aura in template.auras)
+ txt += '\n' + sprintf(translate("%(auralabel)s %(aurainfo)s"), {
+ auralabel: txtFormats.header[0] + sprintf(translate("%(auraname)s:"), {
+ auraname: translate(aura)
+ }) + txtFormats.header[1],
+ aurainfo: txtFormats.body[0] + translate(template.auras[aura]) + txtFormats.body[1]
+ });
+
+ if (template.health)
+ txt += '\n' + sprintf(translate("%(label)s %(details)s"), {
+ label: txtFormats.header[0] + translate("Health:") + txtFormats.header[1],
+ details: template.health
+ });
+
+ if (template.healer)
+ txt += '\n' + getHealerTooltip(template);
+
+ if (template.attack)
+ txt += '\n' + getAttackTooltip(template);
+
+ if (template.armour)
+ txt += '\n' + getArmorTooltip(template.armour);
+
+ txt += '\n' + getSpeedTooltip(template);
+
+ if (template.gather)
+ {
+ var rates = [];
+ for (let type in template.gather)
+ rates.push(sprintf(translate("%(resourceIcon)s %(rate)s"), {
+ resourceIcon: getCostComponentDisplayName(type),
+ rate: template.gather[type]
+ }));
+
+ txt += '\n' + sprintf(translate("%(label)s %(details)s"), {
+ label: txtFormats.header[0] + translate("Gather Rates:") + txtFormats.header[1],
+ details: rates.join(" ")
+ });
+ }
+
+ return txt;
+}
diff --git a/binaries/data/mods/public/gui/structree/helper.js b/binaries/data/mods/public/gui/structree/helper.js
new file mode 100644
index 0000000000..ae85c1dc51
--- /dev/null
+++ b/binaries/data/mods/public/gui/structree/helper.js
@@ -0,0 +1,89 @@
+var g_TemplateData = {};
+var g_TechnologyData = {};
+
+function loadTemplate(templateName)
+{
+ if (!(templateName in g_TemplateData))
+ {
+ // We need to clone the template because we want to perform some translations.
+ var data = clone(Engine.GetTemplate(templateName));
+ translateObjectKeys(data, ["GenericName", "Tooltip"]);
+
+ g_TemplateData[templateName] = data;
+ }
+
+ return g_TemplateData[templateName];
+}
+
+function loadTechData(templateName)
+{
+ if (!(templateName in g_TechnologyData))
+ {
+ var filename = "simulation/data/technologies/" + templateName + ".json";
+ var data = Engine.ReadJSONFile(filename);
+ translateObjectKeys(data, ["genericName", "tooltip"]);
+
+ g_TechnologyData[templateName] = data;
+ }
+
+ return g_TechnologyData[templateName];
+}
+
+/**
+ * Fetch a value from an entity's template
+ *
+ * @param templateName The template to retreive the value from
+ * @param keypath The path to the value to be fetched. "Identity/GenericName"
+ * is equivalent to {"Identity":{"GenericName":"FOOBAR"}}
+ *
+ * @return The content requested at the key-path defined, or a blank array if
+ * not found
+ */
+function fetchValue(templateName, keypath)
+{
+ var keys = keypath.split("/");
+ var template = loadTemplate(templateName);
+
+ let k = 0;
+ for (; k < keys.length-1; ++k)
+ {
+ if (template[keys[k]] === undefined)
+ return [];
+
+ template = template[keys[k]];
+ }
+ if (template[keys[k]] === undefined)
+ return [];
+
+ return template[keys[k]];
+}
+
+/**
+ * Fetch tokens from an entity's template
+ * @return An array containing all tokens if found, else an empty array
+ * @see fetchValue
+ */
+function fetchTokens(templateName, keypath)
+{
+ var val = fetchValue(templateName, keypath);
+ if (!("_string" in val))
+ return [];
+
+ return val._string.split(" ");
+}
+
+function depath(path)
+{
+ return path.slice(path.lastIndexOf("/")+1);
+}
+
+/**
+ * This is needed because getEntityCostTooltip in tooltip.js needs to get
+ * the template data of the different wallSet pieces. In the session this
+ * function does some caching, but here we do that in loadTemplate already.
+ */
+function GetTemplateData(templateName)
+{
+ var template = loadTemplate(templateName);
+ return GetTemplateDataHelper(template);
+}
diff --git a/binaries/data/mods/public/gui/structree/load.js b/binaries/data/mods/public/gui/structree/load.js
new file mode 100644
index 0000000000..9f2a1c2fad
--- /dev/null
+++ b/binaries/data/mods/public/gui/structree/load.js
@@ -0,0 +1,333 @@
+/**
+ * Calculates gather rates.
+ *
+ * All available rates that have a value greater than 0 are summed and averaged
+ */
+function getGatherRates(templateName)
+{
+ // TODO: It would be nice to use the gather rates present in the templates
+ // instead of hard-coding the possible rates here.
+
+ // We ignore ruins here, as those are not that common and would skew the results
+ var types = {
+ "food": ["food", "food.fish", "food.fruit", "food.grain", "food.meat", "food.milk"],
+ "wood": ["wood", "wood.tree"],
+ "stone": ["stone", "stone.rock"],
+ "metal": ["metal", "metal.ore"]
+ };
+ var rates = {};
+
+ for (let type in types)
+ {
+ let count, rate;
+ [rate, count] = types[type].reduce(function(sum, t) {
+ let r = +fetchValue(templateName, "ResourceGatherer/Rates/"+t);
+ return [sum[0] + (r > 0 ? r : 0), sum[1] + (r > 0 ? 1 : 0)];
+ }, [0, 0]);
+
+ if (rate > 0)
+ rates[type] = Math.round(rate / count * 100) / 100;
+ }
+
+ if (!Object.keys(rates).length)
+ return null;
+
+ return rates;
+}
+
+function loadUnit(templateName)
+{
+ var template = loadTemplate(templateName);
+ var unit = GetTemplateDataHelper(template);
+ unit.phase = false;
+
+ if (unit.requiredTechnology)
+ {
+ if (unit.requiredTechnology.slice(0, 5) == "phase")
+ unit.phase = unit.requiredTechnology;
+ else if (unit.requiredTechnology.length)
+ unit.required = unit.requiredTechnology;
+ }
+
+ unit.gather = getGatherRates(templateName);
+
+ if (template.Heal)
+ unit.healer = {
+ "Range": +template.Heal.Range || 0,
+ "HP": +template.Heal.HP || 0,
+ "Rate": +template.Heal.Rate || 0
+ };
+
+ if (template.Builder && template.Builder.Entities._string)
+ for (let build of template.Builder.Entities._string.split(" "))
+ {
+ build = build.replace("{civ}", g_SelectedCiv);
+ if (g_Lists.structures.indexOf(build) < 0)
+ g_Lists.structures.push(build);
+ }
+
+ return unit;
+}
+
+function loadStructure(templateName)
+{
+ var template = loadTemplate(templateName);
+ var structure = GetTemplateDataHelper(template);
+ structure.phase = false;
+
+ if (structure.requiredTechnology)
+ {
+ if (structure.requiredTechnology.slice(0, 5) == "phase")
+ structure.phase = structure.requiredTechnology;
+ else if (structure.requiredTechnology.length)
+ structure.required = structure.requiredTechnology;
+ }
+
+ structure.production = {
+ "technology": [],
+ "units": []
+ };
+ if (template.ProductionQueue)
+ {
+ if (template.ProductionQueue.Entities && template.ProductionQueue.Entities._string)
+ for (let build of template.ProductionQueue.Entities._string.split(" "))
+ {
+ build = build.replace("{civ}", g_SelectedCiv);
+ structure.production.units.push(build);
+ if (g_Lists.units.indexOf(build) < 0)
+ g_Lists.units.push(build);
+ }
+
+ if (template.ProductionQueue.Technologies && template.ProductionQueue.Technologies._string)
+ for (let research of template.ProductionQueue.Technologies._string.split(" "))
+ {
+ structure.production.technology.push(research);
+ if (g_Lists.techs.indexOf(research) < 0)
+ g_Lists.techs.push(research);
+ }
+ }
+
+ if (structure.wallSet)
+ {
+ structure.wallset = {};
+ // Note: Assume wall segments of all lengths have the same armor
+ structure.armour = loadStructure(structure.wallSet.templates["long"]).armour;
+
+ let health;
+
+ for (let wSegm in structure.wallSet.templates)
+ {
+ let wPart = loadStructure(structure.wallSet.templates[wSegm]);
+ structure.wallset[wSegm] = wPart;
+
+ for (let research of wPart.production.technology)
+ structure.production.technology.push(research);
+
+ if (["gate", "tower"].indexOf(wSegm) != -1)
+ continue;
+
+ if (!health)
+ {
+ health = { "min": wPart.health, "max": wPart.health };
+ continue;
+ }
+
+ if (health.min > wPart.health)
+ health.min = wPart.health;
+ else if (health.max < wPart.health)
+ health.max = wPart.health;
+ }
+ if (health.min == health.max)
+ structure.health = health.min;
+ else
+ structure.health = sprintf(translate("%(val1)s to %(val2)s"), {
+ val1: health.min,
+ val2: health.max
+ });
+ }
+
+ return structure;
+}
+
+function loadTechnology(techName)
+{
+ var template = loadTechData(techName);
+ var tech = GetTechnologyDataHelper(template, g_SelectedCiv);
+ tech.reqs = {};
+
+ if (template.pair !== undefined)
+ tech.pair = template.pair;
+
+ if (template.requirements !== undefined)
+ {
+ for (let op in template.requirements)
+ {
+ let val = template.requirements[op];
+ let req = calcReqs(op, val);
+
+ switch (op)
+ {
+ case "tech":
+ tech.reqs.generic = [ req ];
+ break;
+
+ case "civ":
+ tech.reqs[req] = [];
+ break;
+
+ case "any":
+ if (req[0].length > 0)
+ for (let r of req[0])
+ {
+ let v = req[0][r];
+ if (typeof r == "number")
+ tech.reqs[v] = [];
+ else
+ tech.reqs[r] = v;
+ }
+ if (req[1].length > 0)
+ tech.reqs.generic = req[1];
+ break;
+
+ case "all":
+ for (let r of req[0])
+ tech.reqs[r] = req[1];
+ break;
+ }
+ }
+ }
+
+ if (template.supersedes !== undefined)
+ {
+ if (tech.reqs.generic !== undefined)
+ tech.reqs.generic.push(template.supersedes);
+ else
+ for (let ck of Object.keys(tech.reqs))
+ tech.reqs[ck].push(template.supersedes);
+ }
+
+ return tech;
+}
+
+function loadPhase(phaseCode)
+{
+ var template = loadTechData(phaseCode);
+ var phase = GetTechnologyDataHelper(template, g_SelectedCiv);
+ phase.actualPhase = "";
+
+ return phase;
+}
+
+function loadTechnologyPair(pairCode)
+{
+ var pairInfo = loadTechData(pairCode);
+
+ return {
+ "techs": [ pairInfo.top, pairInfo.bottom ],
+ "req": (pairInfo.supersedes !== undefined) ? pairInfo.supersedes : ""
+ };
+}
+
+/**
+ * Calculate the prerequisite requirements of a technology.
+ * Works recursively if needed.
+ *
+ * @param op The base operation. Can be "civ", "tech", "all" or "any".
+ * @param val The value associated with the above operation.
+ *
+ * @return Sorted requirments.
+ */
+function calcReqs(op, val)
+{
+ switch (op)
+ {
+ case "civ":
+ case "tech":
+ // nothing needs doing
+ break;
+
+ case "all":
+ case "any":
+ let t = [];
+ let c = [];
+ for (let nv of val)
+ {
+ for (let o in nv)
+ {
+ let v = nv[o];
+ let r = calcReqs(o, v);
+ switch (o)
+ {
+ case "civ":
+ c.push(r);
+ break;
+
+ case "tech":
+ t.push(r);
+ break;
+
+ case "any":
+ c = c.concat(r[0]);
+ t = t.concat(r[1]);
+ break;
+
+ case "all":
+ for (let ci in r[0])
+ c[ci] = r[1];
+ t = t;
+ }
+ }
+ }
+ return [ c, t ];
+
+ default:
+ warn("Unknown reqs operator: "+op);
+ }
+ return val;
+}
+
+/**
+ * Unravel phases
+ *
+ * @param techs The current available store of techs
+ *
+ * @return List of phases
+ */
+function unravelPhases(techs)
+{
+ var phaseList = [];
+
+ for (let techcode in techs)
+ {
+ let techdata = techs[techcode];
+
+ if (!("generic" in techdata.reqs) || techdata.reqs.generic.length < 2)
+ continue;
+
+ let reqTech = techs[techcode].reqs.generic[1];
+
+ // Tech that can't be researched anywhere
+ if (!(reqTech in techs))
+ continue;
+
+ if (!("generic" in techs[reqTech].reqs))
+ continue;
+
+ let reqPhase = techs[reqTech].reqs.generic[0];
+ let myPhase = techs[techcode].reqs.generic[0];
+
+ if (reqPhase == myPhase || depath(reqPhase).slice(0,5) !== "phase" || depath(myPhase).slice(0,5) !== "phase")
+ continue;
+
+ let reqPhasePos = phaseList.indexOf(reqPhase);
+ let myPhasePos = phaseList.indexOf(myPhase);
+
+ if (phaseList.length === 0)
+ phaseList = [reqPhase, myPhase];
+ else if (reqPhasePos < 0 && myPhasePos > -1)
+ phaseList.splice(myPhasePos, 0, reqPhase);
+ else if (myPhasePos < 0 && reqPhasePos > -1)
+ phaseList.splice(reqPhasePos+1, 0, myPhase);
+ }
+ return phaseList;
+}
diff --git a/binaries/data/mods/public/gui/structree/rows.xml b/binaries/data/mods/public/gui/structree/rows.xml
new file mode 100644
index 0000000000..778a8957e9
--- /dev/null
+++ b/binaries/data/mods/public/gui/structree/rows.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/binaries/data/mods/public/gui/structree/setup.xml b/binaries/data/mods/public/gui/structree/setup.xml
new file mode 100644
index 0000000000..f2aaed2cec
--- /dev/null
+++ b/binaries/data/mods/public/gui/structree/setup.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/binaries/data/mods/public/gui/structree/sprites.xml b/binaries/data/mods/public/gui/structree/sprites.xml
new file mode 100644
index 0000000000..586a822584
--- /dev/null
+++ b/binaries/data/mods/public/gui/structree/sprites.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/binaries/data/mods/public/gui/structree/structree.js b/binaries/data/mods/public/gui/structree/structree.js
new file mode 100644
index 0000000000..a2fe0eaf30
--- /dev/null
+++ b/binaries/data/mods/public/gui/structree/structree.js
@@ -0,0 +1,250 @@
+var g_ParsedData = {
+ units: {},
+ structures: {},
+ techs: {},
+ phases: {}
+};
+var g_Lists = {};
+var g_CivData = {};
+var g_SelectedCiv = "";
+
+
+/**
+ * Initialize the dropdown containing all the available civs
+ */
+function init()
+{
+ g_CivData = loadCivData(true);
+
+ if (!Object.keys(g_CivData).length)
+ return;
+
+ var civList = [ { "name": civ.Name, "code": civ.Code } for each (civ in g_CivData) ];
+
+ // Alphabetically sort the list, ignoring case
+ civList.sort(sortNameIgnoreCase);
+
+ var civListNames = [ civ.name for each (civ in civList) ];
+ var civListCodes = [ civ.code for each (civ in civList) ];
+
+ // Set civ control
+ var civSelection = Engine.GetGUIObjectByName("civSelection");
+ civSelection.list = civListNames;
+ civSelection.list_data = civListCodes;
+ civSelection.selected = 0;
+}
+
+function selectCiv(civCode)
+{
+ if (civCode === g_SelectedCiv || !g_CivData[civCode])
+ return;
+
+ g_SelectedCiv = civCode;
+
+ // If a buildList already exists, then this civ has already been parsed
+ if (g_CivData[g_SelectedCiv].buildList)
+ {
+ draw();
+ return;
+ }
+
+ g_Lists.units = [];
+ g_Lists.structures = [];
+ g_Lists.techs = [];
+
+ // get initial units
+ var startStructs = [];
+ for (let entity of g_CivData[civCode].StartEntities)
+ {
+ if (entity.Template.slice(0, 5) == "units")
+ g_Lists.units.push(entity.Template);
+ else if (entity.Template.slice(0, 6) == "struct")
+ {
+ g_Lists.structures.push(entity.Template);
+ startStructs.push(entity.Template);
+ }
+ }
+
+ // Load units and structures
+ var unitCount = 0;
+ do
+ {
+ for (let u of g_Lists.units)
+ if (!g_ParsedData.units[u])
+ g_ParsedData.units[u] = loadUnit(u);
+
+ unitCount = g_Lists.units.length;
+
+ for (let s of g_Lists.structures)
+ if (!g_ParsedData.structures[s])
+ g_ParsedData.structures[s] = loadStructure(s);
+
+ } while (unitCount < g_Lists.units.length);
+
+ // Load technologies
+ var techPairs = {};
+ for (let techcode of g_Lists.techs)
+ {
+ let realcode = depath(techcode);
+
+ if (realcode.slice(0,4) == "pair")
+ techPairs[techcode] = loadTechnologyPair(techcode);
+ else if (realcode.slice(0,5) == "phase")
+ g_ParsedData.phases[techcode] = loadPhase(techcode);
+ else
+ g_ParsedData.techs[techcode] = loadTechnology(techcode);
+ }
+
+ // Expand tech pairs
+ for (let paircode in techPairs)
+ {
+ let pair = techPairs[paircode];
+ for (let techcode of pair.techs)
+ {
+ let newTech = loadTechnology(techcode);
+
+ if (pair.req !== "")
+ {
+ if ("generic" in newTech.reqs)
+ newTech.reqs.generic.concat(techPairs[pair.req].techs);
+ else
+ for (let civkey of Object.keys(newTech.reqs))
+ newTech.reqs[civkey].concat(techPairs[pair.req].techs);
+ }
+ g_ParsedData.techs[techcode] = newTech;
+ }
+ }
+
+ // Establish phase order
+ g_ParsedData.phaseList = unravelPhases(g_ParsedData.techs);
+ for (let phasecode of g_ParsedData.phaseList)
+ {
+ let phaseInfo = loadTechData(phasecode);
+ g_ParsedData.phases[phasecode] = loadPhase(phasecode);
+
+ if ("requirements" in phaseInfo)
+ for (let op in phaseInfo.requirements)
+ {
+ let val = phaseInfo.requirements[op];
+ if (op == "any")
+ for (let v of val)
+ {
+ let k = Object.keys(v);
+ k = k[0];
+ v = v[k];
+ if (k == "tech" && v in g_ParsedData.phases)
+ g_ParsedData.phases[v].actualPhase = phasecode;
+ }
+ }
+ }
+
+ // Group production lists of structures by phase
+ for (let structCode of g_Lists.structures)
+ {
+ let structInfo = g_ParsedData.structures[structCode];
+
+ // If this building is shared with another civ,
+ // it may have already gone through the grouping process already
+ if (!Array.isArray(structInfo.production.technology))
+ continue;
+
+ // Expand tech pairs
+ for (let prod of structInfo.production.technology)
+ if (prod in techPairs)
+ structInfo.production.technology.splice(
+ structInfo.production.technology.indexOf(prod), 1,
+ techPairs[prod].techs[0], techPairs[prod].techs[1]
+ );
+
+ // Sort techs by phase
+ let newProdTech = {};
+ for (let prod of structInfo.production.technology)
+ {
+ let phase = "";
+
+ if (prod.slice(0,5) === "phase")
+ {
+ phase = g_ParsedData.phaseList.indexOf(g_ParsedData.phases[prod].actualPhase);
+ if (phase > 0)
+ phase = g_ParsedData.phaseList[phase - 1];
+ }
+ else if (g_SelectedCiv in g_ParsedData.techs[prod].reqs)
+ {
+ if (g_ParsedData.techs[prod].reqs[g_SelectedCiv].length > 0)
+ phase = g_ParsedData.techs[prod].reqs[g_SelectedCiv][0];
+ }
+ else if ("generic" in g_ParsedData.techs[prod].reqs)
+ {
+ phase = g_ParsedData.techs[prod].reqs.generic[0];
+ }
+
+ if (depath(phase).slice(0,5) !== "phase")
+ {
+ warn(prod+" doesn't have a specific phase set ("+structCode+")");
+ phase = structInfo.phase;
+ }
+
+ if (!(phase in newProdTech))
+ newProdTech[phase] = [];
+
+ newProdTech[phase].push(prod);
+ }
+
+ // Determine phase for units
+ let newProdUnits = {};
+ for (let prod of structInfo.production.units)
+ {
+ if (!(prod in g_ParsedData.units))
+ {
+ error(prod+" doesn't exist! ("+structCode+")");
+ continue;
+ }
+ let unit = g_ParsedData.units[prod];
+ let phase = "";
+
+ if (unit.phase !== false)
+ phase = unit.phase;
+ else if (unit.required !== undefined)
+ {
+ let reqs = g_ParsedData.techs[unit.required].reqs;
+ if (g_SelectedCiv in reqs)
+ phase = reqs[g_SelectedCiv][0];
+ else
+ phase = reqs.generic[0];
+ }
+ else if (structInfo.phase !== false)
+ phase = structInfo.phase;
+ else
+ phase = g_ParsedData.phaseList[0];
+
+ if (!(phase in newProdUnits))
+ newProdUnits[phase] = [];
+
+ newProdUnits[phase].push(prod);
+ }
+
+ g_ParsedData.structures[structCode].production = {
+ "technology": newProdTech,
+ "units": newProdUnits
+ };
+ }
+
+ // Determine the buildList for the civ (grouped by phase)
+ var buildList = {};
+ for (let structCode of g_Lists.structures)
+ {
+ if (!g_ParsedData.structures[structCode].phase || startStructs.indexOf(structCode) > -1)
+ g_ParsedData.structures[structCode].phase = g_ParsedData.phaseList[0];
+
+ let myPhase = g_ParsedData.structures[structCode].phase;
+
+ if (!(myPhase in buildList))
+ buildList[myPhase] = [];
+ buildList[myPhase].push(structCode);
+ }
+
+ g_CivData[g_SelectedCiv].buildList = buildList;
+
+ // Draw tree
+ draw();
+}
diff --git a/binaries/data/mods/public/gui/structree/structree.xml b/binaries/data/mods/public/gui/structree/structree.xml
new file mode 100644
index 0000000000..138372ece9
--- /dev/null
+++ b/binaries/data/mods/public/gui/structree/structree.xml
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Structure Tree
+
+
+
+
+
+ Civilization:
+
+
+
+ selectCiv(this.list_data[this.selected]);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Close
+ Engine.PopGuiPage();
+
+
+
+
diff --git a/binaries/data/mods/public/gui/structree/styles.xml b/binaries/data/mods/public/gui/structree/styles.xml
new file mode 100644
index 0000000000..f4ed32713a
--- /dev/null
+++ b/binaries/data/mods/public/gui/structree/styles.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+