Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 19599) +++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 19600) @@ -1,452 +1,475 @@ /** * Gets an array of all classes for this identity template */ function GetIdentityClasses(template) { var classList = []; if (template.Classes && template.Classes._string) classList = classList.concat(template.Classes._string.split(/\s+/)); if (template.VisibleClasses && template.VisibleClasses._string) classList = classList.concat(template.VisibleClasses._string.split(/\s+/)); if (template.Rank) classList = classList.concat(template.Rank); return classList; } /** * Gets an array with all classes for this identity template * that should be shown in the GUI */ function GetVisibleIdentityClasses(template) { if (template.VisibleClasses && template.VisibleClasses._string) return template.VisibleClasses._string.split(/\s+/); return []; } /** * Check if the classes given in the identity template * match a list of classes * @param classes List of the classes to check against * @param match Either a string in the form * "Class1 Class2+Class3" * where spaces are handled as OR and '+'-signs as AND, * and ! is handled as NOT, thus Class1+!Class2 = Class1 AND NOT Class2 * Or a list in the form * [["Class1"], ["Class2", "Class3"]] * where the outer list is combined as OR, and the inner lists are AND-ed * Or a hybrid format containing a list of strings, where the list is * combined as OR, and the strings are split by space and '+' and AND-ed * * @return undefined if there are no classes or no match object * true if the the logical combination in the match object matches the classes * false otherwise */ function MatchesClassList(classes, match) { if (!match || !classes) return undefined; // transform the string to an array if (typeof match == "string") match = match.split(/\s+/); for (var sublist of match) { // if the elements are still strings, split them by space or by '+' if (typeof sublist == "string") sublist = sublist.split(/[+\s]+/); if (sublist.every(c => (c[0] == "!" && classes.indexOf(c.substr(1)) == -1) || (c[0] != "!" && classes.indexOf(c) != -1))) return true; } return false; } /** * Gets the value originating at the value_path as-is, with no modifiers applied. * * @param {object} template - A valid template as returned from a template loader. * @param {string} value_path - Route to value within the xml template structure. * @return {number} */ function GetBaseTemplateDataValue(template, value_path) { let current_value = template; for (let property of value_path.split("/")) current_value = current_value[property] || 0; return +current_value; } /** * Gets the value originating at the value_path with the modifiers dictated by the mod_key applied. * * @param {object} template - A valid template as returned from a template loader. * @param {string} value_path - Route to value within the xml template structure. * @param {string} mod_key - Tech modification key, if different from value_path. * @param {number} player - Optional player id. * @param {object} modifiers - Value modifiers from auto-researched techs, unit upgrades, * etc. Optional as only used if no player id provided. * @return {number} Modifier altered value. */ function GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers={}) { let current_value = GetBaseTemplateDataValue(template, value_path); mod_key = mod_key || value_path; if (player) current_value = ApplyValueModificationsToTemplate(mod_key, current_value, player, template); else if (modifiers) current_value = GetTechModifiedProperty(modifiers, GetIdentityClasses(template.Identity), mod_key, current_value); // Using .toFixed() to get around spidermonkey's treatment of numbers (3 * 1.1 = 3.3000000000000003 for instance). return +current_value.toFixed(8); } /** * Get information about a template with or without technology modifications. * * NOTICE: The data returned here should have the same structure as * the object returned by GetEntityState and GetExtendedEntityState! * * @param {object} template - A valid template as returned by the template loader. * @param {number} player - An optional player id to get the technology modifications * of properties. * @param {object} auraTemplates - In the form of { key: { "auraName": "", "auraDescription": "" } }. * @param {object} resources - An instance of the Resources prototype. * @param {object} modifiers - Modifications from auto-researched techs, unit upgrades * etc. Optional as only used if there's no player * id provided. */ function GetTemplateDataHelper(template, player, auraTemplates, resources, modifiers={}) { // Return data either from template (in tech tree) or sim state (ingame). // @param {string} value_path - Route to the value within the template. // @param {string} mod_key - Modification key, if not the same as the value_path. let getEntityValue = function(value_path, mod_key) { return GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers); }; let ret = {}; if (template.Armour) ret.armour = { "hack": getEntityValue("Armour/Hack"), "pierce": getEntityValue("Armour/Pierce"), "crush": getEntityValue("Armour/Crush") }; if (template.Attack) { ret.attack = {}; for (let type in template.Attack) { let getAttackStat = function(stat) { return getEntityValue("Attack/" + type + "/" + stat); }; if (type == "Capture") ret.attack.Capture = { "value": getAttackStat("Value") }; else { ret.attack[type] = { "hack": getAttackStat("Hack"), "pierce": getAttackStat("Pierce"), "crush": getAttackStat("Crush"), "minRange": getAttackStat("MinRange"), "maxRange": getAttackStat("MaxRange"), "elevationBonus": getAttackStat("ElevationBonus") }; ret.attack[type].elevationAdaptedRange = Math.sqrt(ret.attack[type].maxRange * (2 * ret.attack[type].elevationBonus + ret.attack[type].maxRange)); } ret.attack[type].repeatTime = getAttackStat("RepeatTime"); if (template.Attack[type].Splash) ret.attack[type].splash = { "hack": getAttackStat("Splash/Hack"), "pierce": getAttackStat("Splash/Pierce"), "crush": getAttackStat("Splash/Crush"), // true if undefined "friendlyFire": template.Attack[type].Splash.FriendlyFire != "false", "shape": template.Attack[type].Splash.Shape }; } } if (template.Auras) { ret.auras = {}; for (let auraID of template.Auras._string.split(/\s+/)) { let aura = auraTemplates[auraID]; ret.auras[auraID] = { "name": aura.auraName, "description": aura.auraDescription || null, "radius": aura.radius || null }; } } if (template.BuildingAI) ret.buildingAI = { "defaultArrowCount": Math.round(getEntityValue("BuildingAI/DefaultArrowCount")), "garrisonArrowMultiplier": getEntityValue("BuildingAI/GarrisonArrowMultiplier"), "maxArrowCount": Math.round(getEntityValue("BuildingAI/MaxArrowCount")) }; if (template.BuildRestrictions) { // required properties ret.buildRestrictions = { "placementType": template.BuildRestrictions.PlacementType, "territory": template.BuildRestrictions.Territory, "category": template.BuildRestrictions.Category, }; // optional properties if (template.BuildRestrictions.Distance) { ret.buildRestrictions.distance = { "fromClass": template.BuildRestrictions.Distance.FromClass, }; if (template.BuildRestrictions.Distance.MinDistance) ret.buildRestrictions.distance.min = +template.BuildRestrictions.Distance.MinDistance; if (template.BuildRestrictions.Distance.MaxDistance) ret.buildRestrictions.distance.max = +template.BuildRestrictions.Distance.MaxDistance; } } if (template.TrainingRestrictions) ret.trainingRestrictions = { "category": template.TrainingRestrictions.Category, }; if (template.Cost) { ret.cost = {}; for (let resCode in template.Cost.Resources) ret.cost[resCode] = getEntityValue("Cost/Resources/" + resCode); if (template.Cost.Population) ret.cost.population = getEntityValue("Cost/Population"); if (template.Cost.PopulationBonus) ret.cost.populationBonus = getEntityValue("Cost/PopulationBonus"); if (template.Cost.BuildTime) ret.cost.time = getEntityValue("Cost/BuildTime"); } if (template.Footprint) { ret.footprint = { "height": template.Footprint.Height }; if (template.Footprint.Square) ret.footprint.square = { "width": +template.Footprint.Square["@width"], "depth": +template.Footprint.Square["@depth"] }; else if (template.Footprint.Circle) ret.footprint.circle = { "radius": +template.Footprint.Circle["@radius"] }; else warn("GetTemplateDataHelper(): Unrecognized Footprint type"); } if (template.GarrisonHolder) { ret.garrisonHolder = { "buffHeal": getEntityValue("GarrisonHolder/BuffHeal") }; if (template.GarrisonHolder.Max) ret.garrisonHolder.capacity = getEntityValue("GarrisonHolder/Max"); } if (template.Heal) ret.heal = { "hp": getEntityValue("Heal/HP"), "range": getEntityValue("Heal/Range"), "rate": getEntityValue("Heal/Rate") }; if (template.ResourceGatherer) { ret.resourceGatherRates = {}; let baseSpeed = getEntityValue("ResourceGatherer/BaseSpeed"); for (let type in template.ResourceGatherer.Rates) ret.resourceGatherRates[type] = getEntityValue("ResourceGatherer/Rates/"+ type) * baseSpeed; } if (template.ResourceTrickle) { ret.resourceTrickle = { "interval": +template.ResourceTrickle.Interval, "rates": {} }; for (let type in template.ResourceTrickle.Rates) ret.resourceTrickle.rates[type] = getEntityValue("ResourceTrickle/Rates/" + type); } if (template.Loot) { ret.loot = {}; for (let type in template.Loot) ret.loot[type] = getEntityValue("Loot/"+ type); } if (template.Obstruction) { ret.obstruction = { "active": ("" + template.Obstruction.Active == "true"), "blockMovement": ("" + template.Obstruction.BlockMovement == "true"), "blockPathfinding": ("" + template.Obstruction.BlockPathfinding == "true"), "blockFoundation": ("" + template.Obstruction.BlockFoundation == "true"), "blockConstruction": ("" + template.Obstruction.BlockConstruction == "true"), "disableBlockMovement": ("" + template.Obstruction.DisableBlockMovement == "true"), "disableBlockPathfinding": ("" + template.Obstruction.DisableBlockPathfinding == "true"), "shape": {} }; if (template.Obstruction.Static) { ret.obstruction.shape.type = "static"; ret.obstruction.shape.width = +template.Obstruction.Static["@width"]; ret.obstruction.shape.depth = +template.Obstruction.Static["@depth"]; } else if (template.Obstruction.Unit) { ret.obstruction.shape.type = "unit"; ret.obstruction.shape.radius = +template.Obstruction.Unit["@radius"]; } else ret.obstruction.shape.type = "cluster"; } if (template.Pack) ret.pack = { "state": template.Pack.State, "time": getEntityValue("Pack/Time"), }; if (template.Health) ret.health = Math.round(getEntityValue("Health/Max")); if (template.Identity) { ret.selectionGroupName = template.Identity.SelectionGroupName; ret.name = { "specific": (template.Identity.SpecificName || template.Identity.GenericName), "generic": template.Identity.GenericName }; ret.icon = template.Identity.Icon; ret.tooltip = template.Identity.Tooltip; ret.requiredTechnology = template.Identity.RequiredTechnology; ret.visibleIdentityClasses = GetVisibleIdentityClasses(template.Identity); } if (template.UnitMotion) { ret.speed = { "walk": getEntityValue("UnitMotion/WalkSpeed"), }; if (template.UnitMotion.Run) ret.speed.run = getEntityValue("UnitMotion/Run/Speed"); } + if (template.Upgrade) + { + ret.upgrades = []; + for (let upgradeName in template.Upgrade) + { + let upgrade = template.Upgrade[upgradeName]; + + let cost = {}; + for (let res in upgrade.Cost) + cost[res] = getEntityValue("Upgrade/" + upgradeName + "/Cost/" + res, "Upgrade/Cost/" + res); + if (upgrade.Time) + cost.time = getEntityValue("Upgrade/" + upgradeName + "/Time", "Upgrade/Time"); + + ret.upgrades.push({ + "entity": upgrade.Entity, + "tooltip": upgrade.Tooltip, + "cost": cost, + "icon": upgrade.Icon || undefined, + "requiredTechnology": upgrade.RequiredTechnology || undefined + }); + } + } + if (template.ProductionQueue) { ret.techCostMultiplier = {}; for (let res in template.ProductionQueue.TechCostMultiplier) ret.techCostMultiplier[res] = getEntityValue("ProductionQueue/TechCostMultiplier/" + res); } if (template.Trader) ret.trader = { "GainMultiplier": getEntityValue("Trader/GainMultiplier") }; if (template.WallSet) ret.wallSet = { "templates": { "tower": template.WallSet.Templates.Tower, "gate": template.WallSet.Templates.Gate, "long": template.WallSet.Templates.WallLong, "medium": template.WallSet.Templates.WallMedium, "short": template.WallSet.Templates.WallShort, }, "maxTowerOverlap": +template.WallSet.MaxTowerOverlap, "minTowerOverlap": +template.WallSet.MinTowerOverlap, }; if (template.WallPiece) ret.wallPiece = { "length": +template.WallPiece.Length }; return ret; } /** * Get basic information about a technology template. * @param {object} template - A valid template as obtained by loading the tech JSON file. * @param {string} civ - Civilization for which the tech requirements should be calculated. */ function GetTechnologyBasicDataHelper(template, civ) { return { "name": { "generic": template.genericName }, "icon": template.icon ? "technologies/" + template.icon : undefined, "description": template.description, "reqs": DeriveTechnologyRequirements(template, civ), "modifications": template.modifications, "affects": template.affects }; } /** * Get information about a technology template. * @param {object} template - A valid template as obtained by loading the tech JSON file. * @param {string} civ - Civilization for which the specific name and tech requirements should be returned. */ function GetTechnologyDataHelper(template, civ, resources) { let ret = GetTechnologyBasicDataHelper(template, civ); if (template.specificName) ret.name.specific = template.specificName[civ] || template.specificName.generic; ret.cost = { "time": template.researchTime ? +template.researchTime : 0 }; for (let type of resources.GetCodes()) ret.cost[type] = +(template.cost && template.cost[type] || 0); ret.tooltip = template.tooltip; ret.requirementsTooltip = template.requirementsTooltip || ""; return ret; } function calculateCarriedResources(carriedResources, tradingGoods) { var resources = {}; if (carriedResources) for (let resource of carriedResources) resources[resource.type] = (resources[resource.type] || 0) + resource.amount; if (tradingGoods && tradingGoods.amount) resources[tradingGoods.type] = (resources[tradingGoods.type] || 0) + (tradingGoods.amount.traderGain || 0) + (tradingGoods.amount.market1Gain || 0) + (tradingGoods.amount.market2Gain || 0); return resources; } Index: ps/trunk/binaries/data/mods/public/gui/structree/draw.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/structree/draw.js (revision 19599) +++ ps/trunk/binaries/data/mods/public/gui/structree/draw.js (revision 19600) @@ -1,411 +1,426 @@ var g_DrawLimits = {}; // GUI limits. Populated by predraw() var g_TooltipFunctions = [ getEntityNamesFormatted, getEntityCostTooltip, getEntityTooltip, getAurasTooltip, getHealthTooltip, getHealerTooltip, getAttackTooltip, getSplashDamageTooltip, getArmorTooltip, getGarrisonTooltip, getProjectilesTooltip, getSpeedTooltip, getGatherTooltip, getPopulationBonusTooltip, getResourceTrickleTooltip, getLootTooltip ]; /** * 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(); const leftMargin = Engine.GetGUIObjectByName("tree_display").size.left; const defWidth = 96; const defMargin = 4; let 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 prodBarWidth = 0; 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++; + ++p; } - if (stru.wallset && prod_pha == pha) - for (let prod of [stru.wallset.gate, stru.wallset.tower]) + if (stru.upgrades[prod_pha]) + for (let upgrade of stru.upgrades[prod_pha]) { - if (!drawProdIcon(i, s, r, p, prod)) + if (!drawProdIcon(i, s, r, p, upgrade)) break; - p++; + ++p; } + if (stru.wallset && prod_pha == pha) + { + if (!drawProdIcon(i, s, r, p, stru.wallset.tower)) + break; + ++p; + } + if (stru.production.technology[prod_pha]) for (let prod of stru.production.technology[prod_pha]) { prod = clone(basename(prod).startsWith("phase") ? g_ParsedData.phases[prod] : g_ParsedData.techs[g_SelectedCiv][prod]); for (let res in stru.techCostMultiplier) if (prod.cost[res]) prod.cost[res] *= stru.techCostMultiplier[res]; if (!drawProdIcon(i, s, r, p, prod)) break; ++p; } rowCounts[r] = p; if (p>c) c = p; hideRemaining("phase["+i+"]_struct["+s+"]_row["+r+"]", 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+"]_rows", r); ++s; prodBarWidth += eleWidth + defMargin; } hideRemaining("phase["+i+"]", s); let offset = getPositionOffset(i); // Resize phase bars for (let j = 1; j < phaseList.length - i; ++j) { let prodBar = Engine.GetGUIObjectByName("phase["+i+"]_bar["+(j-1)+"]"); let prodBarSize = prodBar.size; prodBarSize.right = leftMargin + prodBarWidth; prodBar.size = prodBarSize; } ++i; } let t = 0; for (let trainer of g_CivData[g_SelectedCiv].trainList) { let thisEle = Engine.GetGUIObjectByName("trainer["+t+"]"); if (thisEle === undefined) { error("\""+g_SelectedCiv+"\" has more unit trainers than can be supported by the current GUI layout"); break; } trainer = g_ParsedData.units[trainer]; Engine.GetGUIObjectByName("trainer["+t+"]_icon").sprite = "stretched:session/portraits/"+trainer.icon; Engine.GetGUIObjectByName("trainer["+t+"]_icon").tooltip = assembleTooltip(trainer); Engine.GetGUIObjectByName("trainer["+t+"]_name").caption = translate(trainer.name.specific); thisEle.hidden = false; let p = 0; - for (let prodType in trainer.production) - { - for (let prod of trainer.production[prodType]) - { - switch (prodType) + if (trainer.production) + for (let prodType in trainer.production) + for (let prod of trainer.production[prodType]) { - case "units": - prod = g_ParsedData.units[prod]; - break; - case "techs": - prod = clone(g_ParsedData.techs[g_SelectedCiv][prod]); - for (let res in trainer.techCostMultiplier) - if (prod.cost[res]) - prod.cost[res] *= trainer.techCostMultiplier[res]; - break; - default: - continue; + switch (prodType) + { + case "units": + prod = g_ParsedData.units[prod]; + break; + case "techs": + prod = clone(g_ParsedData.techs[g_SelectedCiv][prod]); + for (let res in trainer.techCostMultiplier) + if (prod.cost[res]) + prod.cost[res] *= trainer.techCostMultiplier[res]; + break; + default: + continue; + } + if (!drawProdIcon(null, t, null, p, prod)) + break; + ++p; } - if (!drawProdIcon(null, t, null, p, prod)) + + if (trainer.upgrades) + for (let upgrade of trainer.upgrades) + { + if (!drawProdIcon(null, t, null, p, upgrade.data)) break; ++p; } - } + hideRemaining("trainer["+t+"]_row", p); let size = thisEle.size; size.right = size.left + Math.max(p*24, defWidth) + 4; thisEle.size = size; let eleWidth = size.right - size.left; let wid = p * 24 - 4; let phaEle = Engine.GetGUIObjectByName("trainer["+t+"]_row"); size = phaEle.size; size.left = (eleWidth - wid)/2; phaEle.size = size; ++t; } hideRemaining("trainers", t); let size = Engine.GetGUIObjectByName("display_tree").size; size.right = t > 0 ? -124 : -4; Engine.GetGUIObjectByName("display_tree").size = size; Engine.GetGUIObjectByName("display_trainers").hidden = t == 0; } function drawProdIcon(pha, s, r, p, prod) { let prodEle = Engine.GetGUIObjectByName("phase["+pha+"]_struct["+s+"]_row["+r+"]_prod["+p+"]"); if (pha === null) prodEle = Engine.GetGUIObjectByName("trainer["+s+"]_prod["+p+"]"); if (prodEle === undefined) { error("The "+(pha === null ? "trainer units" : "structures") + " of \"" + g_SelectedCiv + "\" have more production icons 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) { let phases = g_ParsedData.phaseList.length; let size = 92*idx; // text, image and offset size += 24 * (phases*idx - (idx-1)*idx/2); // phase rows (phase-currphase+1 per row) return size; } /** * 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() { let phaseList = g_ParsedData.phaseList; let 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.size = "16 32+"+offset+" 48+16 48+32+"+offset; // Set initial prod bar size let j = 1; for (; j < phaseList.length - i; ++j) { let prodBar = Engine.GetGUIObjectByName("phase["+i+"]_bar["+(j-1)+"]"); prodBar.size = "40 1+"+(24*j)+"+98+"+offset+" 0 1+"+(24*j)+"+98+"+offset+"+22"; } // Hide remaining prod bars hideRemaining("phase["+i+"]_bars", 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_rows", i); hideRemaining("phase_ident", i); let t = 0; let ele = Engine.GetGUIObjectByName("trainer["+t+"]"); g_DrawLimits.trainer = { "trainerQuant": 0, "prodQuant": 0 }; let x = 4; do { let p = 0; let prodEle = Engine.GetGUIObjectByName("trainer["+t+"]_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("trainer["+t+"]_prod["+p+"]"); } while (prodEle !== undefined); Engine.GetGUIObjectByName("trainer["+t+"]_row").size = "4 100%-24"+" 100%-4 100%"; g_DrawLimits.trainer.prodQuant = p; let size = ele.size; size.top += x; size.bottom += x + 24; x += size.bottom - size.top + 8; ele.size = size; t++; ele = Engine.GetGUIObjectByName("trainer["+t+"]"); } while (ele !== undefined); g_DrawLimits.trainer.trainerQuant = t; } /** * Assemble a tooltip text * * @param template Information about a Unit, a Structure or a Technology * @return The tooltip text, formatted. */ function assembleTooltip(template) { return g_TooltipFunctions.map(func => func(template)).filter(tip => tip).join("\n"); } function drawPhaseIcons() { for (let i = 0; i < g_ParsedData.phaseList.length; ++i) { drawPhaseIcon("phase["+i+"]_phase", i); for (let j = 1; j < g_ParsedData.phaseList.length - i; ++j) drawPhaseIcon("phase["+i+"]_bar["+(j-1)+"]_icon", j+i); } } function drawPhaseIcon(guiObjectName, phaseIndex) { let phaseName = g_ParsedData.phaseList[phaseIndex]; let prodPhaseTemplate = g_ParsedData.phases[phaseName + "_" + g_SelectedCiv] || g_ParsedData.phases[phaseName]; let phaseIcon = Engine.GetGUIObjectByName(guiObjectName); phaseIcon.sprite = "stretched:session/portraits/" + prodPhaseTemplate.icon; phaseIcon.tooltip = getEntityNamesFormatted(prodPhaseTemplate); } Index: ps/trunk/binaries/data/mods/public/gui/structree/helper.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/structree/helper.js (revision 19599) +++ ps/trunk/binaries/data/mods/public/gui/structree/helper.js (revision 19600) @@ -1,158 +1,183 @@ const g_TechnologyPath = "simulation/data/technologies/"; const g_AuraPath = "simulation/data/auras/"; var g_TemplateData = {}; var g_TechnologyData = {}; var g_AuraData = {}; // Must be defined after g_TechnologyData object is declared. const g_AutoResearchTechList = findAllAutoResearchedTechs(); 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", "SpecificName", "Tooltip"]); if (data.Auras) for (let auraID of data.Auras._string.split(/\s+/)) loadAuraData(auraID); g_TemplateData[templateName] = data; } return g_TemplateData[templateName]; } function loadTechData(templateName) { if (!(templateName in g_TechnologyData)) { let data = Engine.ReadJSONFile(g_TechnologyPath + templateName + ".json"); translateObjectKeys(data, ["genericName", "tooltip", "description"]); g_TechnologyData[templateName] = data; } return g_TechnologyData[templateName]; } function loadAuraData(templateName) { if (!(templateName in g_AuraData)) { let data = Engine.ReadJSONFile(g_AuraPath + templateName + ".json"); translateObjectKeys(data, ["auraName", "auraDescription"]); g_AuraData[templateName] = data; } return g_AuraData[templateName]; } function findAllAutoResearchedTechs() { let techList = []; for (let filename of Engine.BuildDirEntList(g_TechnologyPath, "*.json", true)) { // -5 to strip off the file extension let templateName = filename.slice(g_TechnologyPath.length, -5); let data = loadTechData(templateName); if (data && data.autoResearch) techList.push(templateName); } return techList; } function deriveModifications(techList) { let techData = []; for (let techName of techList) techData.push(GetTechnologyBasicDataHelper(loadTechData(techName), g_SelectedCiv)); return DeriveModificationsFromTechnologies(techData); } /** + * Provided with an array containing basic information about possible + * upgrades, such as that generated by globalscript's GetTemplateDataHelper, + * this function loads the actual template data of the upgrades, overwrites + * certain values within, then passes an array containing the template data + * back to caller. + */ +function getActualUpgradeData(upgradesInfo) +{ + let newUpgrades = []; + for (let upgrade of upgradesInfo) + { + upgrade.entity = upgrade.entity.replace("{civ}", g_SelectedCiv); + + let data = GetTemplateDataHelper(loadTemplate(upgrade.entity), null, g_AuraData, g_ResourceData); + data.cost = upgrade.cost + data.icon = upgrade.icon || data.icon; + data.tooltip = upgrade.tooltip || data.tooltip; + data.requiredTechnology = upgrade.requiredTechnology || data.requiredTechnology; + + newUpgrades.push(data); + } + return newUpgrades; +} + +/** * 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, null, g_AuraData, g_ResourceData, g_CurrentModifiers); } /** * Determines and returns the phase in which a given technology can be * first researched. Works recursively through the given tech's * pre-requisite and superseded techs if necessary. * * @param {string} techName - The Technology's name * @return The name of the phase the technology belongs to, or false if * the current civ can't research this tech */ function GetPhaseOfTechnology(techName) { let phaseIdx = -1; if (basename(techName).startsWith("phase")) { if (!g_ParsedData.phases[techName].reqs) return false; phaseIdx = g_ParsedData.phaseList.indexOf(GetActualPhase(techName)); if (phaseIdx > 0) return g_ParsedData.phaseList[phaseIdx - 1]; } if (!g_ParsedData.techs[g_SelectedCiv][techName]) { let techData = loadTechnology(techName); g_ParsedData.techs[g_SelectedCiv][techName] = techData; warn("The \"" + techData.name.generic + "\" technology is not researchable in any structure buildable by the " + g_SelectedCiv + " civilisation, but is required by something that this civ can research, train or build!"); } let techReqs = g_ParsedData.techs[g_SelectedCiv][techName].reqs; if (!techReqs) return false; for (let option of techReqs) if (option.techs) for (let tech of option.techs) { if (basename(tech).startsWith("phase")) return tech; if (basename(tech).startsWith("pair")) continue; phaseIdx = Math.max(phaseIdx, g_ParsedData.phaseList.indexOf(GetPhaseOfTechnology(tech))); } return g_ParsedData.phaseList[phaseIdx] || false; } function GetActualPhase(phaseName) { if (g_ParsedData.phases[phaseName]) return g_ParsedData.phases[phaseName].actualPhase; warn("Unrecognised phase (" + techName + ")"); return g_ParsedData.phaseList[0]; } function GetPhaseOfTemplate(template) { if (!template.requiredTechnology) return g_ParsedData.phaseList[0]; if (basename(template.requiredTechnology).startsWith("phase")) return GetActualPhase(template.requiredTechnology); return GetPhaseOfTechnology(template.requiredTechnology); } Index: ps/trunk/binaries/data/mods/public/gui/structree/load.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/structree/load.js (revision 19599) +++ ps/trunk/binaries/data/mods/public/gui/structree/load.js (revision 19600) @@ -1,204 +1,216 @@ function loadUnit(templateName) { if (!Engine.TemplateExists(templateName)) return null; let template = loadTemplate(templateName); let unit = GetTemplateDataHelper(template, null, g_AuraData, g_ResourceData, g_CurrentModifiers); if (template.ProductionQueue) { unit.production = {}; if (template.ProductionQueue.Entities) { unit.production.units = []; for (let build of template.ProductionQueue.Entities._string.split(" ")) { build = build.replace("{civ}", g_SelectedCiv); unit.production.units.push(build); if (g_Lists.units.indexOf(build) < 0) g_Lists.units.push(build); } } if (template.ProductionQueue.Technologies) { unit.production.techs = []; for (let research of template.ProductionQueue.Technologies._string.split(" ")) { unit.production.techs.push(research); if (g_Lists.techs.indexOf(research) < 0) g_Lists.techs.push(research); } } } 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); } + if (unit.upgrades) + unit.upgrades = getActualUpgradeData(unit.upgrades); + return unit; } function loadStructure(templateName) { let template = loadTemplate(templateName); let structure = GetTemplateDataHelper(template, null, g_AuraData, g_ResourceData, g_CurrentModifiers); 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.upgrades) + structure.upgrades = getActualUpgradeData(structure.upgrades); + if (structure.wallSet) { structure.wallset = {}; + if (!structure.upgrades) + structure.upgrades = []; + // Note: Assume wall segments of all lengths have the same armor and auras let struct = loadStructure(structure.wallSet.templates.long); structure.armour = struct.armour; structure.auras = struct.auras; // For technology cost multiplier, we need to use the tower struct = loadStructure(structure.wallSet.templates.tower); structure.techCostMultiplier = struct.techCostMultiplier; 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 (wPart.upgrades) + structure.upgrades = structure.upgrades.concat(wPart.upgrades); + if (["gate", "tower"].indexOf(wSegm) != -1) continue; if (!health) { health = { "min": wPart.health, "max": wPart.health }; continue; } health.min = Math.min(health.min, wPart.health); health.max = Math.max(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) { let template = loadTechData(techName); let tech = GetTechnologyDataHelper(template, g_SelectedCiv, g_ResourceData); if (template.pair !== undefined) tech.pair = template.pair; return tech; } function loadPhase(phaseCode) { var template = loadTechData(phaseCode); var phase = GetTechnologyDataHelper(template, g_SelectedCiv, g_ResourceData); phase.actualPhase = phaseCode; if (template.replaces !== undefined) phase.actualPhase = template.replaces[0]; return phase; } function loadTechnologyPair(pairCode) { var pairInfo = loadTechData(pairCode); return { "techs": [ pairInfo.top, pairInfo.bottom ], "reqs": DeriveTechnologyRequirements(pairInfo, g_SelectedCiv) }; } /** * Loads the names of all phases required to research the given technologies. * * @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 (!techdata.reqs || !techdata.reqs.length || !techdata.reqs[0].techs || techdata.reqs[0].techs.length < 2) continue; let reqTech = techs[techcode].reqs[0].techs[1]; if (!techs[reqTech] || !techs[reqTech].reqs.length) continue; // Assume the first tech to be a phase let reqPhase = techs[reqTech].reqs[0].techs[0]; let myPhase = techs[techcode].reqs[0].techs[0]; if (reqPhase == myPhase || !basename(reqPhase).startsWith("phase") || !basename(myPhase).startsWith("phase")) continue; let reqPhasePos = phaseList.indexOf(reqPhase); let myPhasePos = phaseList.indexOf(myPhase); // Sort the phases in the order they can be researched if (!phaseList.length) 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); else if (reqPhasePos > myPhasePos) { phaseList.splice(reqPhasePos+1, 0, myPhase); phaseList.splice(myPhasePos, 1); } } return phaseList; } Index: ps/trunk/binaries/data/mods/public/gui/structree/structree.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/structree/structree.js (revision 19599) +++ ps/trunk/binaries/data/mods/public/gui/structree/structree.js (revision 19600) @@ -1,278 +1,297 @@ var g_ParsedData = { "units": {}, "structures": {}, "techs": {}, "phases": {} }; var g_Lists = {}; var g_CivData = {}; var g_SelectedCiv = ""; var g_CurrentModifiers = {}; var g_CallbackSet = false; var g_ResourceData = new Resources(); /** * Initialize the dropdown containing all the available civs */ function init(data = {}) { g_CivData = loadCivData(true); let civList = Object.keys(g_CivData).map(civ => ({ "name": g_CivData[civ].Name, "code": civ, })).sort(sortNameIgnoreCase); if (!civList.length) return; var civSelection = Engine.GetGUIObjectByName("civSelection"); civSelection.list = civList.map(c => c.name); civSelection.list_data = civList.map(c => c.code); if(data.civ) { civSelection.selected = civSelection.list_data.indexOf(data.civ); selectCiv(data.civ); } else civSelection.selected = 0; if (data.callback) g_CallbackSet = true; } function selectCiv(civCode) { if (civCode === g_SelectedCiv || !g_CivData[civCode]) return; g_SelectedCiv = civCode; g_CurrentModifiers = deriveModifications(g_AutoResearchTechList); // If a buildList already exists, then this civ has already been parsed if (g_CivData[g_SelectedCiv].buildList) { draw(); return; } g_Lists = { "units": [], "structures": [], "techs": [] }; g_ParsedData.techs[civCode] = {}; // get initial units for (let entity of g_CivData[civCode].StartEntities) { if (entity.Template.startsWith("units")) g_Lists.units.push(entity.Template); else if (entity.Template.startsWith("struct")) g_Lists.structures.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 = basename(techcode); if (realcode.startsWith("pair") || realcode.indexOf("_pair") > -1) techPairs[techcode] = loadTechnologyPair(techcode); else if (realcode.startsWith("phase")) g_ParsedData.phases[techcode] = loadPhase(techcode); else g_ParsedData.techs[civCode][techcode] = loadTechnology(techcode); } // Expand tech pairs for (let paircode in techPairs) { let pair = techPairs[paircode]; if (pair.reqs === false) continue; for (let techcode of pair.techs) { if (basename(techcode).startsWith("phase")) g_ParsedData.phases[techcode] = loadPhase(techcode); else { let newTech = loadTechnology(techcode); if (!newTech.reqs) newTech.reqs = {}; else if (newTech.reqs === false) continue; for (let option of pair.reqs) for (let type in option) for (let opt in newTech.reqs) { if (!newTech.reqs[opt][type]) newTech.reqs[opt][type] = []; newTech.reqs[opt][type] = newTech.reqs[opt][type].concat(option[type]); } g_ParsedData.techs[civCode][techcode] = newTech; } } } // Establish phase order g_ParsedData.phaseList = unravelPhases(g_ParsedData.techs[civCode]); for (let phasecode of g_ParsedData.phaseList) { let phaseInfo = loadTechData(phasecode); g_ParsedData.phases[phasecode] = loadPhase(phasecode); if (!("requirements" in phaseInfo)) continue; for (let op in phaseInfo.requirements) { let val = phaseInfo.requirements[op]; if (op != "any") continue; for (let v of val) { let k = Object.keys(v); k = k[0]; v = v[k]; if (k != "tech") continue; if (v in g_ParsedData.phases) g_ParsedData.phases[v].actualPhase = phasecode; else if (v in techPairs) { for (let t of techPairs[v].techs) g_ParsedData.phases[t].actualPhase = phasecode; } } } } - // Group production lists of structures by phase + // Group production and upgrade lists of structures by phase for (let structCode of g_Lists.structures) { let structInfo = g_ParsedData.structures[structCode]; structInfo.phase = GetPhaseOfTemplate(structInfo); let structPhaseIdx = g_ParsedData.phaseList.indexOf(structInfo.phase); // 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 ); // Sort techs by phase let newProdTech = {}; for (let prod of structInfo.production.technology) { let phase = GetPhaseOfTechnology(prod); if (phase === false) continue; if (g_ParsedData.phaseList.indexOf(phase) < structPhaseIdx) phase = structInfo.phase; if (!(phase in newProdTech)) newProdTech[phase] = []; newProdTech[phase].push(prod); } // Sort units by phase let newProdUnits = {}; for (let prod of structInfo.production.units) { if (!g_ParsedData.units[prod]) continue; let phase = GetPhaseOfTemplate(g_ParsedData.units[prod]); if (phase === false) continue; if (g_ParsedData.phaseList.indexOf(phase) < structPhaseIdx) phase = structInfo.phase; if (!(phase in newProdUnits)) newProdUnits[phase] = []; newProdUnits[phase].push(prod); } g_ParsedData.structures[structCode].production = { "technology": newProdTech, "units": newProdUnits }; + + // Sort upgrades by phase + let newUpgrades = {}; + if (structInfo.upgrades) + for (let upgrade of structInfo.upgrades) + { + let phase = GetPhaseOfTemplate(upgrade); + + if (g_ParsedData.phaseList.indexOf(phase) < structPhaseIdx) + phase = structInfo.phase; + + if (!newUpgrades[phase]) + newUpgrades[phase] = []; + newUpgrades[phase].push(upgrade); + } + g_ParsedData.structures[structCode].upgrades = newUpgrades; } // Determine the buildList for the civ (grouped by phase) let buildList = {}; let trainerList = []; for (let pha of g_ParsedData.phaseList) buildList[pha] = []; for (let structCode of g_Lists.structures) { let phase = g_ParsedData.structures[structCode].phase; buildList[phase].push(structCode); } + for (let unitCode of g_Lists.units) - if (g_ParsedData.units[unitCode] && g_ParsedData.units[unitCode].production && Object.keys(g_ParsedData.units[unitCode].production).length) + if (g_ParsedData.units[unitCode] && ( + g_ParsedData.units[unitCode].production && Object.keys(g_ParsedData.units[unitCode].production).length + || g_ParsedData.units[unitCode].upgrades)) { // Replace any pair techs with the actual techs of that pair if (g_ParsedData.units[unitCode].production.techs) for (let prod of g_ParsedData.units[unitCode].production.techs) if (prod in techPairs) g_ParsedData.units[unitCode].production.techs.splice( g_ParsedData.units[unitCode].production.techs.indexOf(prod), 1, ...techPairs[prod].techs ); trainerList.push(unitCode); } g_CivData[g_SelectedCiv].buildList = buildList; g_CivData[g_SelectedCiv].trainList = trainerList; draw(); drawPhaseIcons(); } function closeStrucTree() { if (g_CallbackSet) Engine.PopGuiPageCB(0); else Engine.PopGuiPage(); } Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wall_gate.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wall_gate.xml (revision 19599) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wall_gate.xml (revision 19600) @@ -1,38 +1,39 @@ + 20 City Gate Allow units access through a city wall. Can be locked to prevent access. Gates structures/gate.png phase_town interface/complete/building/complete_gate.xml attack/destruction/building_collapse_large.xml actor/gate/stonegate_close.xml actor/gate/stonegate_open.xml interface/select/building/sel_gate.xml interface/select/building/sel_gate.xml structures/fndn_wall_long.xml