Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 19710) +++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 19711) @@ -1,475 +1,476 @@ /** * 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.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 19710) +++ ps/trunk/binaries/data/mods/public/gui/structree/draw.js (revision 19711) @@ -1,426 +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; } if (stru.upgrades[prod_pha]) for (let upgrade of stru.upgrades[prod_pha]) { if (!drawProdIcon(i, s, r, p, upgrade)) break; ++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; if (trainer.production) for (let prodType in trainer.production) for (let prod of trainer.production[prodType]) { 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 (trainer.upgrades) for (let upgrade of trainer.upgrades) { - if (!drawProdIcon(null, t, null, p, upgrade.data)) + if (!drawProdIcon(null, t, null, p, upgrade)) 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/structree.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/structree/structree.js (revision 19710) +++ ps/trunk/binaries/data/mods/public/gui/structree/structree.js (revision 19711) @@ -1,297 +1,300 @@ 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 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 - || g_ParsedData.units[unitCode].upgrades)) + if (g_ParsedData.units[unitCode]) { + let unitTemplate = g_ParsedData.units[unitCode]; + + if ((!unitTemplate.production || !Object.keys(unitTemplate.production).length) && !unitTemplate.upgrades) + continue; + // 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 (unitTemplate.production && unitTemplate.production.techs) + for (let prod of unitTemplate.production.techs) if (prod in techPairs) - g_ParsedData.units[unitCode].production.techs.splice( - g_ParsedData.units[unitCode].production.techs.indexOf(prod), + unitTemplate.production.techs.splice( + unitTemplate.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(); }