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 @@ + + + +