Index: ps/trunk/binaries/data/mods/public/gui/common/tooltips.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/tooltips.js +++ ps/trunk/binaries/data/mods/public/gui/common/tooltips.js @@ -621,6 +621,26 @@ }); } +/** + * Returns the resources this entity supplies in the specified entity's tooltip + */ +function getResourceSupplyTooltip(template) +{ + if (!template.supply) + return ""; + + let supply = template.supply; + let type = supply.type[0] == "treasure" ? supply.type[1] : supply.type[0]; + + // Translation: Label in tooltip showing the resource type and quantity of a given resource supply. + return sprintf(translate("%(label)s %(component)s %(amount)s"), { + "label": headerFont(translate("Resource Supply:")), + "component": resourceIcon(type), + // Translation: Marks that a resource supply entity has an unending, infinite, supply of its resource. + "amount": Number.isFinite(+supply.amount) ? supply.amount : translate("∞") + }); +} + function getResourceTrickleTooltip(template) { if (!template.resourceTrickle) Index: ps/trunk/binaries/data/mods/public/gui/reference/common/Buttons/CivInfoButton.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/Buttons/CivInfoButton.js +++ ps/trunk/binaries/data/mods/public/gui/reference/common/Buttons/CivInfoButton.js @@ -0,0 +1,27 @@ +class CivInfoButton +{ + constructor(parentPage) + { + this.parentPage = parentPage; + + this.civInfoButton = Engine.GetGUIObjectByName("civInfoButton"); + this.civInfoButton.onPress = this.onPress.bind(this); + this.civInfoButton.caption = this.Caption; + this.civInfoButton.tooltip = colorizeHotkey(this.Tooltip, this.Hotkey); + } + + onPress() + { + Engine.PopGuiPage({ "civ": this.parentPage.activeCiv, "nextPage": "page_civinfo.xml" }); + } + +} + +CivInfoButton.prototype.Caption = + translate("Civilization Overview"); + +CivInfoButton.prototype.Hotkey = + "civinfo"; + +CivInfoButton.prototype.Tooltip = + translate("%(hotkey)s: Switch to Civilization Overview."); Index: ps/trunk/binaries/data/mods/public/gui/reference/common/Buttons/CivInfoButton.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/Buttons/CivInfoButton.xml +++ ps/trunk/binaries/data/mods/public/gui/reference/common/Buttons/CivInfoButton.xml @@ -0,0 +1,8 @@ + + Index: ps/trunk/binaries/data/mods/public/gui/reference/common/Buttons/CloseButton.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/Buttons/CloseButton.js +++ ps/trunk/binaries/data/mods/public/gui/reference/common/Buttons/CloseButton.js @@ -0,0 +1,16 @@ +class CloseButton +{ + constructor(parentPage) + { + this.closeButton = Engine.GetGUIObjectByName("closeButton"); + this.closeButton.onPress = parentPage.closePage.bind(parentPage); + this.closeButton.caption = this.Caption; + this.closeButton.tooltip = colorizeHotkey(parentPage.CloseButtonTooltip, this.Hotkey); + } +} + +CloseButton.prototype.Caption = + translate("Close"); + +CloseButton.prototype.Hotkey = + "close"; Index: ps/trunk/binaries/data/mods/public/gui/reference/common/Buttons/CloseButton.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/Buttons/CloseButton.xml +++ ps/trunk/binaries/data/mods/public/gui/reference/common/Buttons/CloseButton.xml @@ -0,0 +1,8 @@ + + Index: ps/trunk/binaries/data/mods/public/gui/reference/common/Dropdowns/CivSelectDropdown.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/Dropdowns/CivSelectDropdown.js +++ ps/trunk/binaries/data/mods/public/gui/reference/common/Dropdowns/CivSelectDropdown.js @@ -0,0 +1,63 @@ +class CivSelectDropdown +{ + constructor(civData) + { + this.handlers = new Set(); + + let civList = Object.keys(civData).map(civ => ({ + "name": civData[civ].Name, + "code": civ, + })).sort(sortNameIgnoreCase); + + this.civSelectionHeading = Engine.GetGUIObjectByName("civSelectionHeading"); + this.civSelectionHeading.caption = this.Caption; + + this.civSelection = Engine.GetGUIObjectByName("civSelection"); + this.civSelection.list = civList.map(c => c.name); + this.civSelection.list_data = civList.map(c => c.code); + this.civSelection.onSelectionChange = () => this.onSelectionChange(this); + } + + onSelectionChange() + { + let civCode = this.civSelection.list_data[this.civSelection.selected]; + + for (let handler of this.handlers) + handler(civCode); + } + + registerHandler(handler) + { + this.handlers.add(handler); + } + + unregisterHandler(handler) + { + this.handlers.delete(handler); + } + + hasCivs() + { + return this.civSelection.list.length != 0; + } + + selectCiv(civCode) + { + if (!civCode) + return; + + let index = this.civSelection.list_data.indexOf(civCode); + if (index == -1) + return; + + this.civSelection.selected = index; + } + + selectFirstCiv() + { + this.civSelection.selected = 0; + } +} + +CivSelectDropdown.prototype.Caption = + translate("Civilization:"); Index: ps/trunk/binaries/data/mods/public/gui/reference/common/Dropdowns/CivSelectDropdown.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/Dropdowns/CivSelectDropdown.xml +++ ps/trunk/binaries/data/mods/public/gui/reference/common/Dropdowns/CivSelectDropdown.xml @@ -0,0 +1,12 @@ + + + + + Index: ps/trunk/binaries/data/mods/public/gui/reference/common/ReferencePage.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/ReferencePage.js +++ ps/trunk/binaries/data/mods/public/gui/reference/common/ReferencePage.js @@ -0,0 +1,67 @@ +/** + * This class contains code common to the Structure Tree, Template Viewer, and any other "Reference Page" that may be added in the future. + */ +class ReferencePage +{ + constructor() + { + this.civData = loadCivData(true, false); + + this.TemplateLoader = new TemplateLoader(); + this.TemplateLister = new TemplateLister(this.TemplateLoader); + this.TemplateParser = new TemplateParser(this.TemplateLoader); + + this.activeCiv = this.TemplateLoader.DefaultCiv; + + this.currentTemplateLists = {}; + } + + setActiveCiv(civCode) + { + if (civCode == this.TemplateLoader.DefaultCiv) + return; + + this.activeCiv = civCode; + + this.currentTemplateLists = this.TemplateLister.compileTemplateLists(this.activeCiv, this.civData); + this.TemplateParser.deriveModifications(this.activeCiv); + this.TemplateParser.derivePhaseList(this.currentTemplateLists.techs.keys(), this.activeCiv); + } + + /** + * Concatanates the return values of the array of passed functions. + * + * @param {object} template + * @param {array} textFunctions + * @param {string} joiner + * @return {string} The built text. + */ + static buildText(template, textFunctions=[], joiner="\n") + { + return textFunctions.map(func => func(template)).filter(tip => tip).join(joiner); + } +} + +ReferencePage.prototype.IconPath = "session/portraits/"; + +/** + * List of functions that get the statistics of any template or entity, + * formatted in such a way as to appear in a tooltip. + * + * The functions listed are defined in gui/common/tooltips.js + */ +ReferencePage.prototype.StatsFunctions = [ + getHealthTooltip, + getHealerTooltip, + getAttackTooltip, + getSplashDamageTooltip, + getArmorTooltip, + getGarrisonTooltip, + getProjectilesTooltip, + getSpeedTooltip, + getGatherTooltip, + getResourceSupplyTooltip, + getPopulationBonusTooltip, + getResourceTrickleTooltip, + getLootTooltip +]; Index: ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLister.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLister.js +++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLister.js @@ -0,0 +1,137 @@ +/** + * This class compiles and stores lists of which templates can be built/trained/researched by other templates. + */ +class TemplateLister +{ + constructor(TemplateLoader) + { + this.TemplateLoader = TemplateLoader; + this.templateLists = new Map(); + } + + /** + * Compile lists of templates buildable/trainable/researchable of a given civ. + * + * @param {object} civCode + * @param {object} civData - Data defining every civ in the game. + */ + compileTemplateLists(civCode, civData) + { + if (this.hasTemplateLists(civCode)) + return this.templateLists.get(civCode); + + let templatesToParse = civData[civCode].StartEntities.map(entity => entity.Template); + + let templateLists = { + "units": new Map(), + "structures": new Map(), + "techs": new Map(), + "wallsetPieces": new Map() + }; + + do + { + const templatesThisIteration = templatesToParse; + templatesToParse = []; + + for (let templateBeingParsed of templatesThisIteration) + { + let list = this.deriveTemplateListsFromTemplate(templateBeingParsed, civCode); + for (let type in list) + for (let templateName of list[type]) + if (!templateLists[type].has(templateName)) + { + templateLists[type].set(templateName, [templateBeingParsed]); + if (type != "techs") + templatesToParse.push(templateName); + } + else if (templateLists[type].get(templateName).indexOf(templateBeingParsed) == -1) + templateLists[type].get(templateName).push(templateBeingParsed); + } + } while (templatesToParse.length); + + // Expand/filter tech pairs + for (let [techCode, researcherList] of templateLists.techs) + { + if (!this.TemplateLoader.isPairTech(techCode)) + continue; + + for (let subTech of this.TemplateLoader.loadTechnologyPairTemplate(techCode, civCode).techs) + if (!templateLists.techs.has(subTech)) + templateLists.techs.set(subTech, researcherList); + else + for (let researcher of researcherList) + if (templateLists.techs.get(subTech).indexOf(researcher) == -1) + templateLists.techs.get(subTech).push(researcher); + + templateLists.techs.delete(techCode); + } + + // Remove wallset pieces, as they've served their purpose. + delete templateLists.wallsetPieces; + + this.templateLists.set(civCode, templateLists); + return this.templateLists.get(civCode); + } + + /** + * Returns a civ's template list. + * + * Note: this civ must have gone through the compilation process above! + * + * @param {string} civCode + * @return {object} containing lists of template names, grouped by type. + */ + getTemplateLists(civCode) + { + if (this.hasTemplateLists(civCode)) + return this.templateLists.get(civCode); + + error("Template lists of \"" + civCode + "\" requested, but this civ has not been loaded."); + return {}; + } + + /** + * Returns whether the civ of the given civCode has been loaded into cache. + * + * @param {string} civCode + * @return {boolean} + */ + hasTemplateLists(civCode) + { + return this.templateLists.has(civCode); + } + + /** + * Compiles lists of buildable, trainable, or researchable entities from + * a named template. + */ + deriveTemplateListsFromTemplate(templateName, civCode) + { + if (!templateName || !Engine.TemplateExists(templateName)) + return {}; + + // If this is a non-promotion variant (ie. {civ}_support_female_citizen_house) + // then it is functionally equivalent to another unit being processed, so skip it. + if (this.TemplateLoader.getBaseTemplateName(templateName, civCode) != templateName) + return {}; + + let template = this.TemplateLoader.loadEntityTemplate(templateName, civCode); + + let templateLists = this.TemplateLoader.deriveProductionQueue(template, civCode); + templateLists.structures = this.TemplateLoader.deriveBuildQueue(template, civCode); + + if (template.WallSet) + { + templateLists.wallsetPieces = []; + for (let segment in template.WallSet.Templates) + { + segment = template.WallSet.Templates[segment].replace(/\{(civ|native)\}/g, civCode); + if (Engine.TemplateExists(segment)) + templateLists.wallsetPieces.push(segment); + } + } + + return templateLists; + } +} Index: ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js +++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js @@ -0,0 +1,251 @@ +/** + * This class handles the loading of files. + */ +class TemplateLoader +{ + constructor() + { + /** + * Raw Data Caches. + */ + this.auraData = {}; + this.technologyData = {}; + this.templateData = {}; + + /** + * Partly-composed data. + */ + this.autoResearchTechList = this.findAllAutoResearchedTechs(); + } + + /** + * Loads raw aura template. + * + * Loads from local cache if available, else from file system. + * + * @param {string} templateName + * @return {object} Object containing raw template data. + */ + loadAuraTemplate(templateName) + { + if (!(templateName in this.auraData)) + { + let data = Engine.ReadJSONFile(this.AuraPath + templateName + ".json"); + translateObjectKeys(data, this.AuraTranslateKeys); + + this.auraData[templateName] = data; + } + + return this.auraData[templateName]; + } + + /** + * Loads raw entity template. + * + * Loads from local cache if data present, else from file system. + * + * @param {string} templateName + * @param {string} civCode + * @return {object} Object containing raw template data. + */ + loadEntityTemplate(templateName, civCode) + { + if (!(templateName in this.templateData)) + { + // We need to clone the template because we want to perform some translations. + let data = clone(Engine.GetTemplate(templateName)); + translateObjectKeys(data, this.EntityTranslateKeys); + + if (data.Auras) + for (let auraID of data.Auras._string.split(/\s+/)) + this.loadAuraTemplate(auraID); + + if (data.Identity.Civ != this.DefaultCiv && civCode != this.DefaultCiv && data.Identity.Civ != civCode) + warn("The \"" + templateName + "\" template has a defined civ of \"" + data.Identity.Civ + "\". " + + "This does not match the currently selected civ \"" + civCode + "\"."); + + this.templateData[templateName] = data; + } + + return this.templateData[templateName]; + } + + /** + * Loads raw technology template. + * + * Loads from local cache if available, else from file system. + * + * @param {string} templateName + * @return {object} Object containing raw template data. + */ + loadTechnologyTemplate(templateName) + { + if (!(templateName in this.technologyData)) + { + let data = Engine.ReadJSONFile(this.TechnologyPath + templateName + ".json"); + translateObjectKeys(data, this.TechnologyTranslateKeys); + + this.technologyData[templateName] = data; + } + + return this.technologyData[templateName]; + } + + /** + * @param {string} templateName + * @param {string} civCode + * @return {object} Contains a list and the requirements of the techs in the pair + */ + loadTechnologyPairTemplate(templateName, civCode) + { + let template = this.loadTechnologyTemplate(templateName); + return { + "techs": [template.top, template.bottom], + "reqs": DeriveTechnologyRequirements(template, civCode) + }; + } + + deriveProductionQueue(template, civCode) + { + let production = { + "techs": [], + "units": [] + }; + + if (!template.ProductionQueue) + return production; + + if (template.ProductionQueue.Entities && template.ProductionQueue.Entities._string) + for (let templateName of template.ProductionQueue.Entities._string.split(" ")) + { + templateName = templateName.replace(/\{(civ|native)\}/g, civCode); + if (Engine.TemplateExists(templateName)) + production.units.push(this.getBaseTemplateName(templateName, civCode)); + } + + if (template.ProductionQueue.Technologies && template.ProductionQueue.Technologies._string) + for (let technologyName of template.ProductionQueue.Technologies._string.split(" ")) + { + if (technologyName.indexOf("{civ}") != -1) + { + let civTechName = technologyName.replace("{civ}", civCode); + technologyName = TechnologyTemplateExists(civTechName) ? civTechName : technologyName.replace("{civ}", "generic"); + } + + if (this.isPairTech(technologyName)) + Array.prototype.push.apply(production.techs, this.loadTechnologyPairTemplate(technologyName, civCode).techs); + else + production.techs.push(technologyName); + } + + return production; + } + + deriveBuildQueue(template, civCode) + { + let buildQueue = []; + + if (!template.Builder || !template.Builder.Entities._string) + return buildQueue; + + for (let build of template.Builder.Entities._string.split(" ")) + { + build = build.replace(/\{(civ|native)\}/g, civCode); + if (Engine.TemplateExists(build)) + buildQueue.push(build); + } + + return buildQueue; + } + + deriveModifications(civCode) + { + let techData = []; + for (let techName of this.autoResearchTechList) + techData.push(GetTechnologyBasicDataHelper(this.loadTechnologyTemplate(techName), civCode)); + + return DeriveModificationsFromTechnologies(techData); + } + + /** + * Crudely iterates through every tech JSON file and identifies those + * that are auto-researched. + * + * @return {array} List of techs that are researched automatically + */ + findAllAutoResearchedTechs() + { + let techList = []; + for (let templateName of listFiles(this.TechnologyPath, ".json", true)) + { + let data = this.loadTechnologyTemplate(templateName); + if (data && data.autoResearch) + techList.push(templateName); + } + return techList; + } + + /** + * Returns the name of a template's base form (without `_house`, `_trireme`, or similar), + * or the template's own name if the base is of a different promotion rank. + */ + getBaseTemplateName(templateName, civCode) + { + if (!templateName || !Engine.TemplateExists(templateName)) + return undefined; + + templateName = removeFiltersFromTemplateName(templateName); + let template = this.loadEntityTemplate(templateName, civCode); + + if (!dirname(templateName) || dirname(template["@parent"]) != dirname(templateName)) + return templateName; + + let parentTemplate = this.loadEntityTemplate(template["@parent"], civCode); + + if (parentTemplate.Identity && parentTemplate.Identity.Rank && + parentTemplate.Identity.Rank != template.Identity.Rank) + return templateName; + + if (!parentTemplate.Cost) + return templateName; + + if (parentTemplate.Upgrade) + for (let upgrade in parentTemplate.Upgrade) + if (parentTemplate.Upgrade[upgrade].Entity) + return templateName; + + for (let res in parentTemplate.Cost.Resources) + if (+parentTemplate.Cost.Resources[res]) + return this.getBaseTemplateName(template["@parent"], civCode); + + return templateName; + } + + isPairTech(technologyCode) + { + return !!this.loadTechnologyTemplate(technologyCode).top; + } + + isPhaseTech(technologyCode) + { + return basename(technologyCode).startsWith("phase"); + } + +} + +/** + * Paths to certain files. + * + * It might be nice if we could get these from somewhere, instead of having them hardcoded here. + */ +TemplateLoader.prototype.AuraPath = "simulation/data/auras/"; +TemplateLoader.prototype.TechnologyPath = "simulation/data/technologies/"; + +TemplateLoader.prototype.DefaultCiv = "gaia"; + +/** + * Keys of template values that are to be translated on load. + */ +TemplateLoader.prototype.AuraTranslateKeys = ["auraName", "auraDescription"]; +TemplateLoader.prototype.EntityTranslateKeys = ["GenericName", "SpecificName", "Tooltip", "History"]; +TemplateLoader.prototype.TechnologyTranslateKeys = ["genericName", "tooltip", "description"]; Index: ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js +++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js @@ -0,0 +1,321 @@ +/** + * This class parses and stores parsed template data. + */ +class TemplateParser +{ + constructor(TemplateLoader) + { + this.TemplateLoader = TemplateLoader; + + /** + * Parsed Data Stores + */ + this.entities = {}; + this.techs = {}; + this.phases = {}; + this.modifiers = {}; + + this.phaseList = []; + } + + /** + * Load and parse a structure, unit, resource, etc from its entity template file. + * + * @param {string} templateName + * @param {string} civCode + * @return {(object|null)} Sanitized object about the requested template or null if entity template doesn't exist. + */ + getEntity(templateName, civCode) + { + if (templateName in this.entities) + return this.entities[templateName]; + + if (!Engine.TemplateExists(templateName)) + return null; + + let template = this.TemplateLoader.loadEntityTemplate(templateName, civCode); + let parsed = GetTemplateDataHelper(template, null, this.TemplateLoader.auraData, this.modifiers[civCode] || {}); + parsed.name.internal = templateName; + + parsed.history = template.Identity.History; + + parsed.production = this.TemplateLoader.deriveProductionQueue(template, civCode); + if (template.Builder) + parsed.builder = this.TemplateLoader.deriveBuildQueue(template, civCode); + + // Set the minimum phase that this entity is available. + // For gaia objects, this is meaningless. + if (!parsed.requiredTechnology) + parsed.phase = this.phaseList[0]; + else if (this.TemplateLoader.isPhaseTech(parsed.requiredTechnology)) + parsed.phase = this.getActualPhase(parsed.requiredTechnology); + else + parsed.phase = this.getPhaseOfTechnology(parsed.requiredTechnology, civCode); + + if (template.Identity.Rank) + parsed.promotion = { + "current_rank": template.Identity.Rank, + "entity": template.Promotion && template.Promotion.Entity + }; + + if (template.ResourceSupply) + parsed.supply = { + "type": template.ResourceSupply.Type.split("."), + "amount": template.ResourceSupply.Amount, + }; + + if (parsed.upgrades) + parsed.upgrades = this.getActualUpgradeData(parsed.upgrades, civCode); + + if (parsed.wallSet) + { + parsed.wallset = {}; + + if (!parsed.upgrades) + parsed.upgrades = []; + + // Note: An assumption is made here that wall segments all have the same armor and auras + let struct = this.getEntity(parsed.wallSet.templates.long, civCode); + parsed.armour = struct.armour; + parsed.auras = struct.auras; + + // For technology cost multiplier, we need to use the tower + struct = this.getEntity(parsed.wallSet.templates.tower, civCode); + parsed.techCostMultiplier = struct.techCostMultiplier; + + let health; + + for (let wSegm in parsed.wallSet.templates) + { + if (wSegm == "fort" || wSegm == "curves") + continue; + + let wPart = this.getEntity(parsed.wallSet.templates[wSegm], civCode); + parsed.wallset[wSegm] = wPart; + + for (let research of wPart.production.techs) + parsed.production.techs.push(research); + + if (wPart.upgrades) + Array.prototype.push.apply(parsed.upgrades, 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 (parsed.wallSet.templates.curves) + for (let curve of parsed.wallSet.templates.curves) + { + let wPart = this.getEntity(curve, civCode); + health.min = Math.min(health.min, wPart.health); + health.max = Math.max(health.max, wPart.health); + } + + if (health.min == health.max) + parsed.health = health.min; + else + parsed.health = sprintf(translate("%(health_min)s to %(health_max)s"), { + "health_min": health.min, + "health_max": health.max + }); + } + + this.entities[templateName] = parsed; + return parsed; + } + + /** + * Load and parse technology from json template. + * + * @param {string} technologyName + * @param {string} civCode + * @return {object} Sanitized data about the requested technology. + */ + getTechnology(technologyName, civCode) + { + if (!TechnologyTemplateExists(technologyName)) + return null; + + if (this.TemplateLoader.isPhaseTech(technologyName) && technologyName in this.phases) + return this.phases[technologyName]; + + if (!(civCode in this.techs)) + this.techs[civCode] = {}; + else if (technologyName in this.techs[civCode]) + return this.techs[civCode][technologyName]; + + let template = this.TemplateLoader.loadTechnologyTemplate(technologyName); + let tech = GetTechnologyDataHelper(template, civCode, g_ResourceData); + tech.name.internal = technologyName; + + if (template.pair !== undefined) + { + tech.pair = template.pair; + tech.reqs = this.mergeRequirements(tech.reqs, this.TemplateLoader.loadTechnologyPairTemplate(template.pair).reqs); + } + + if (this.TemplateLoader.isPhaseTech(technologyName)) + { + tech.actualPhase = technologyName; + if (tech.replaces !== undefined) + tech.actualPhase = tech.replaces[0]; + this.phases[technologyName] = tech; + } + else + this.techs[civCode][technologyName] = tech; + return tech; + } + + /** + * @param {string} phaseCode + * @param {string} civCode + * @return {object} Sanitized object containing phase data + */ + getPhase(phaseCode, civCode) + { + return this.getTechnology(phaseCode, civCode); + } + + /** + * 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. + */ + getActualUpgradeData(upgradesInfo, civCode) + { + let newUpgrades = []; + for (let upgrade of upgradesInfo) + { + upgrade.entity = upgrade.entity.replace(/\{(civ|native)\}/g, civCode); + + let data = GetTemplateDataHelper(this.TemplateLoader.loadEntityTemplate(upgrade.entity, civCode), null, this.TemplateLoader.auraData); + data.name.internal = upgrade.entity; + data.cost = upgrade.cost; + data.icon = upgrade.icon || data.icon; + data.tooltip = upgrade.tooltip || data.tooltip; + data.requiredTechnology = upgrade.requiredTechnology || data.requiredTechnology; + + if (!data.requiredTechnology) + data.phase = this.phaseList[0]; + else if (this.TemplateLoader.isPhaseTech(data.requiredTechnology)) + data.phase = this.getActualPhase(data.requiredTechnology); + else + data.phase = this.getPhaseOfTechnology(data.requiredTechnology, civCode); + + newUpgrades.push(data); + } + return newUpgrades; + } + + /** + * 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 + * @param {string} civCode + * @return The name of the phase the technology belongs to, or false if + * the current civ can't research this tech + */ + getPhaseOfTechnology(techName, civCode) + { + let phaseIdx = -1; + + if (basename(techName).startsWith("phase")) + { + if (!this.phases[techName].reqs) + return false; + + phaseIdx = this.phaseList.indexOf(this.getActualPhase(techName)); + if (phaseIdx > 0) + return this.phaseList[phaseIdx - 1]; + } + + let techReqs = this.getTechnology(techName, civCode).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, this.phaseList.indexOf(this.getPhaseOfTechnology(tech, civCode))); + } + return this.phaseList[phaseIdx] || false; + } + + /** + * Returns the actual phase a certain phase tech represents or stands in for. + * + * For example, passing `phase_city_athen` would result in `phase_city`. + * + * @param {string} phaseName + * @return {string} + */ + getActualPhase(phaseName) + { + if (this.phases[phaseName]) + return this.phases[phaseName].actualPhase; + + warn("Unrecognized phase (" + phaseName + ")"); + return this.phaseList[0]; + } + + getModifiers(civCode) + { + return this.modifiers[civCode]; + } + + deriveModifications(civCode) + { + this.modifiers[civCode] = this.TemplateLoader.deriveModifications(civCode); + } + + derivePhaseList(technologyList, civCode) + { + // Load all of a civ's specific phase technologies + for (let techcode of technologyList) + if (this.TemplateLoader.isPhaseTech(techcode)) + this.getTechnology(techcode, civCode); + + this.phaseList = UnravelPhases(this.phases); + + // Make sure all required generic phases are loaded and parsed + for (let phasecode of this.phaseList) + this.getTechnology(phasecode, civCode); + } + + mergeRequirements(reqsA, reqsB) + { + if (!reqsA || !reqsB) + return false; + + let finalReqs = clone(reqsA); + + for (let option of reqsB) + for (let type in option) + for (let opt in finalReqs) + { + if (!finalReqs[opt][type]) + finalReqs[opt][type] = []; + Array.prototype.push.apply(finalReqs[opt][type], option[type]); + } + + return finalReqs; + } +} Index: ps/trunk/binaries/data/mods/public/gui/reference/common/common.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/common.js +++ ps/trunk/binaries/data/mods/public/gui/reference/common/common.js @@ -0,0 +1,28 @@ +/** + * This needs to stay in the global scope, as it is used by various functions + * within gui/common/tooltip.js + */ +var g_ResourceData = new Resources(); + +var g_Page; + +/** + * 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 the TemplateLoader + * class already. + */ +function GetTemplateData(templateName) +{ + let template = g_Page.TemplateLoader.loadEntityTemplate(templateName, g_Page.activeCiv); + return GetTemplateDataHelper(template, null, g_Page.TemplateLoader.auraData, g_Page.TemplateParser.getModifiers(g_Page.activeCiv)); +} + +/** + * This would ideally be an Engine method. + * Or part of globalscripts. Either would be better than here. + */ +function TechnologyTemplateExists(templateName) +{ + return Engine.FileExists(g_Page.TemplateLoader.TechnologyPath + templateName + ".json"); +} Index: ps/trunk/binaries/data/mods/public/gui/reference/common/core.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/core.js +++ ps/trunk/binaries/data/mods/public/gui/reference/common/core.js @@ -1,202 +0,0 @@ -var g_SelectedCiv = "gaia"; - -/** - * Compile lists of templates buildable/trainable/researchable of a given civ. - * - * @param {string} civCode - Code of the civ to get template list for. Optional, - * defaults to g_SelectedCiv. - * @return {object} containing lists of template names, grouped by type. - */ -function compileTemplateLists(civCode) -{ - if (!civCode || civCode == "gaia") - return {}; - - let templatesToParse = []; - for (let entity of g_CivData[civCode].StartEntities) - templatesToParse.push(entity.Template); - - let templateLists = { - "units": new Map(), - "structures": new Map(), - "techs": new Map(), - "wallsetPieces": new Map() - }; - - do { - const templatesThisIteration = templatesToParse; - templatesToParse = []; - - for (let templateBeingParsed of templatesThisIteration) - { - let list = getTemplateListsFromTemplate(templateBeingParsed); - for (let type in list) - for (let templateName of list[type]) - if (!templateLists[type].has(templateName)) - { - templateLists[type].set(templateName, [templateBeingParsed]); - if (type != "techs") - templatesToParse.push(templateName); - } - else if (templateLists[type].get(templateName).indexOf(templateBeingParsed) == -1) - templateLists[type].get(templateName).push(templateBeingParsed); - } - } while (templatesToParse.length); - - // Expand/filter tech pairs - for (let [techCode, researcherList] of templateLists.techs) - { - if (!isPairTech(techCode)) - continue; - - for (let subTech of loadTechnologyPair(techCode).techs) - if (!templateLists.techs.has(subTech)) - templateLists.techs.set(subTech, researcherList); - else - for (let researcher of researcherList) - if (templateLists.techs.get(subTech).indexOf(researcher) == -1) - templateLists.techs.get(subTech).push(researcher); - - templateLists.techs.delete(techCode); - } - - // Remove wallset pieces, as they've served their purpose. - delete templateLists.wallsetPieces; - - return templateLists; -} - -/** - * Compiles lists of buildable, trainable, or researchable entities from - * a named template. - */ -function getTemplateListsFromTemplate(templateName) -{ - if (!templateName || !Engine.TemplateExists(templateName)) - return {}; - - // If this is a non-promotion variant (ie. {civ}_support_female_citizen_house) - // then it is functionally equivalent to another unit being processed, so skip it. - if (getBaseTemplateName(templateName) != templateName) - return {}; - - let template = loadTemplate(templateName); - - let templateLists = loadProductionQueue(template); - templateLists.structures = loadBuildQueue(template); - - if (template.WallSet) - { - templateLists.wallsetPieces = []; - for (let segment in template.WallSet.Templates) - { - segment = template.WallSet.Templates[segment].replace(/\{(civ|native)\}/g, g_SelectedCiv); - if (Engine.TemplateExists(segment)) - templateLists.wallsetPieces.push(segment); - } - } - - return templateLists; -} - -function loadProductionQueue(template) -{ - let production = { - "techs": [], - "units": [] - }; - - if (!template.ProductionQueue) - return production; - - if (template.ProductionQueue.Entities && template.ProductionQueue.Entities._string) - for (let templateName of template.ProductionQueue.Entities._string.split(" ")) - { - templateName = templateName.replace(/\{(civ|native)\}/g, g_SelectedCiv); - if (Engine.TemplateExists(templateName)) - production.units.push(getBaseTemplateName(templateName)); - } - - if (template.ProductionQueue.Technologies && template.ProductionQueue.Technologies._string) - for (let technologyName of template.ProductionQueue.Technologies._string.split(" ")) - { - if (technologyName.indexOf("{civ}") != -1) - { - let civTechName = technologyName.replace("{civ}", g_SelectedCiv); - technologyName = techDataExists(civTechName) ? civTechName : technologyName.replace("{civ}", "generic"); - } - - if (isPairTech(technologyName)) - for (let pairTechnologyName of loadTechnologyPair(technologyName).techs) - production.techs.push(pairTechnologyName); - else - production.techs.push(technologyName); - } - - return production; -} - -function loadBuildQueue(template) -{ - let buildQueue = []; - - if (!template.Builder || !template.Builder.Entities._string) - return buildQueue; - - for (let build of template.Builder.Entities._string.split(" ")) - { - build = build.replace(/\{(civ|native)\}/g, g_SelectedCiv); - if (Engine.TemplateExists(build)) - buildQueue.push(build); - } - - return buildQueue; -} - -/** - * Returns the name of a template's base form (without `_house`, `_trireme`, or similar), - * or the template's own name if the base is of a different promotion rank. - */ -function getBaseTemplateName(templateName) -{ - if (!templateName || !Engine.TemplateExists(templateName)) - return undefined; - - templateName = removeFiltersFromTemplateName(templateName); - let template = loadTemplate(templateName); - - if (!dirname(templateName) || dirname(template["@parent"]) != dirname(templateName)) - return templateName; - - let parentTemplate = loadTemplate(template["@parent"]); - - if (parentTemplate.Identity && parentTemplate.Identity.Rank && - parentTemplate.Identity.Rank != template.Identity.Rank) - return templateName; - - if (!parentTemplate.Cost) - return templateName; - - if (parentTemplate.Upgrade) - for (let upgrade in parentTemplate.Upgrade) - if (parentTemplate.Upgrade[upgrade].Entity) - return templateName; - - for (let res in parentTemplate.Cost.Resources) - if (+parentTemplate.Cost.Resources[res]) - return getBaseTemplateName(template["@parent"]); - - return templateName; -} - -function setViewerOnPress(guiObjectName, templateName) -{ - let viewerFunc = () => { - Engine.PushGuiPage("page_viewer.xml", { - "templateName": templateName, - "civ": g_SelectedCiv - }); - }; - Engine.GetGUIObjectByName(guiObjectName).onPress = viewerFunc; - Engine.GetGUIObjectByName(guiObjectName).onPressRight = viewerFunc; -} Index: ps/trunk/binaries/data/mods/public/gui/reference/common/draw.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/draw.js +++ ps/trunk/binaries/data/mods/public/gui/reference/common/draw.js @@ -1,73 +0,0 @@ -/** - * GUI limits. Populated if needed by a predraw() function. - */ -var g_DrawLimits = {}; - -/** - * List of functions that get the statistics of any template or entity, - * formatted in such a way as to appear in a tooltip. - * - * The functions listed are defined in gui/common/tooltips.js - */ -var g_StatsFunctions = [ - getHealthTooltip, - getHealerTooltip, - getAttackTooltip, - getSplashDamageTooltip, - getArmorTooltip, - getGarrisonTooltip, - getProjectilesTooltip, - getSpeedTooltip, - getGatherTooltip, - getResourceSupplyTooltip, - getPopulationBonusTooltip, - getResourceTrickleTooltip, - getLootTooltip -]; - -/** - * Concatanates the return values of the array of passed functions. - * - * @param {object} template - * @param {array} textFunctions - * @param {string} joiner - * @return {string} The built text. - */ -function buildText(template, textFunctions=[], joiner="\n") -{ - return textFunctions.map(func => func(template)).filter(tip => tip).join(joiner); -} - -/** - * Creates text in the following format: - * Header: value1, value2, ..., valueN - */ -function buildListText(headerString, arrayOfValues) -{ - // Translation: Label followed by a list of values. - return sprintf(translate("%(listHeader)s %(listOfValues)s"), { - "listHeader": headerFont(headerString), - // Translation: List separator. - "listOfValues": bodyFont(arrayOfValues.join(translate(", "))) - }); -} - -/** - * Returns the resources this entity supplies in the specified entity's tooltip - */ -function getResourceSupplyTooltip(template) -{ - if (!template.supply) - return ""; - - let supply = template.supply; - let type = supply.type[0] == "treasure" ? supply.type[1] : supply.type[0]; - - // Translation: Label in tooltip showing the resource type and quantity of a given resource supply. - return sprintf(translate("%(label)s %(component)s %(amount)s"), { - "label": headerFont(translate("Resource Supply:")), - "component": resourceIcon(type), - // Translation: Marks that a resource supply entity has an unending, infinite, supply of its resource. - "amount": Number.isFinite(+supply.amount) ? supply.amount : translate("∞") - }); -} Index: ps/trunk/binaries/data/mods/public/gui/reference/common/helper.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/helper.js +++ ps/trunk/binaries/data/mods/public/gui/reference/common/helper.js @@ -1,153 +0,0 @@ -var g_CurrentModifiers = {}; - -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|native)\}/g, g_SelectedCiv); - - let data = GetTemplateDataHelper(loadTemplate(upgrade.entity), null, g_AuraData); - data.name.internal = upgrade.entity; - 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; -} - -/** - * 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 \"" + techName + "\" 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; -} - -/** - * Returns the actual phase a certain phase tech represents or stands in for. - * - * For example, passing `phase_city_athen` would result in `phase_city`. - * - * @param {string} phaseName - * @return {string} - */ -function getActualPhase(phaseName) -{ - if (g_ParsedData.phases[phaseName]) - return g_ParsedData.phases[phaseName].actualPhase; - - warn("Unrecognised phase (" + phaseName + ")"); - return g_ParsedData.phaseList[0]; -} - -/** - * Returns the required phase of a given unit or structure. - * - * @param {object} template - * @return {string} - */ -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); -} - -/** - * 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) -{ - let template = loadTemplate(templateName); - return GetTemplateDataHelper(template, null, g_AuraData, g_CurrentModifiers); -} - -function isPairTech(technologyCode) -{ - return !!loadTechData(technologyCode).top; -} - -function mergeRequirements(reqsA, reqsB) -{ - if (reqsA === false || reqsB === false) - return false; - - let finalReqs = clone(reqsA); - - for (let option of reqsB) - for (let type in option) - for (let opt in finalReqs) - { - if (!finalReqs[opt][type]) - finalReqs[opt][type] = []; - finalReqs[opt][type] = finalReqs[opt][type].concat(option[type]); - } - - return finalReqs; -} Index: ps/trunk/binaries/data/mods/public/gui/reference/common/load.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/load.js +++ ps/trunk/binaries/data/mods/public/gui/reference/common/load.js @@ -1,283 +0,0 @@ -/** - * Paths to certain files. - */ -const g_TechnologyPath = "simulation/data/technologies/"; -const g_AuraPath = "simulation/data/auras/"; - -/** - * Raw Data Caches. - */ -var g_AuraData = {}; -var g_TemplateData = {}; -var g_TechnologyData = {}; -var g_CivData = loadCivData(true, false); - -/** - * Parsed Data Stores. - */ -var g_ParsedData = {}; -var g_ResourceData = new Resources(); - -// This must be defined after the g_TechnologyData cache object is declared. -var g_AutoResearchTechList = findAllAutoResearchedTechs(); - -/** - * Loads raw entity template. - * - * Loads from local cache if data present, else from file system. - * - * @param {string} templateName - * @return {object} Object containing raw template data. - */ -function loadTemplate(templateName) -{ - if (!(templateName in g_TemplateData)) - { - // We need to clone the template because we want to perform some translations. - let data = clone(Engine.GetTemplate(templateName)); - translateObjectKeys(data, ["GenericName", "SpecificName", "Tooltip", "History"]); - - if (data.Auras) - for (let auraID of data.Auras._string.split(/\s+/)) - loadAuraData(auraID); - - if (data.Identity.Civ != "gaia" && g_SelectedCiv != "gaia" && data.Identity.Civ != g_SelectedCiv) - warn("The \"" + templateName + "\" template has a defined civ of \"" + data.Identity.Civ + "\". " + - "This does not match the currently selected civ \"" + g_SelectedCiv + "\"."); - - g_TemplateData[templateName] = data; - } - - return g_TemplateData[templateName]; -} - -/** - * Loads raw technology template. - * - * Loads from local cache if available, else from file system. - * - * @param {string} templateName - * @return {object} Object containing raw template data. - */ -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 techDataExists(templateName) -{ - return Engine.FileExists("simulation/data/technologies/" + templateName + ".json"); -} - -/** - * Loads raw aura template. - * - * Loads from local cache if available, else from file system. - * - * @param {string} templateName - * @return {object} Object containing raw template data. - */ -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]; -} - -/** - * Load and parse a structure, unit, resource, etc from its entity template file. - * - * @return {(object|null)} Sanitized object about the requested template or null if entity template doesn't exist. - */ -function loadEntityTemplate(templateName) -{ - if (!Engine.TemplateExists(templateName)) - return null; - - let template = loadTemplate(templateName); - let parsed = GetTemplateDataHelper(template, null, g_AuraData, g_CurrentModifiers); - parsed.name.internal = templateName; - - parsed.history = template.Identity.History; - - parsed.production = loadProductionQueue(template); - if (template.Builder) - parsed.builder = loadBuildQueue(template); - - if (template.Identity.Rank) - parsed.promotion = { - "current_rank": template.Identity.Rank, - "entity": template.Promotion && template.Promotion.Entity - }; - - if (template.ResourceSupply) - parsed.supply = { - "type": template.ResourceSupply.Type.split("."), - "amount": template.ResourceSupply.Amount, - }; - - if (parsed.upgrades) - parsed.upgrades = getActualUpgradeData(parsed.upgrades); - - if (parsed.wallSet) - { - parsed.wallset = {}; - - if (!parsed.upgrades) - parsed.upgrades = []; - - // Note: An assumption is made here that wall segments all have the same armor and auras - let struct = loadEntityTemplate(parsed.wallSet.templates.long); - parsed.armour = struct.armour; - parsed.auras = struct.auras; - - // For technology cost multiplier, we need to use the tower - struct = loadEntityTemplate(parsed.wallSet.templates.tower); - parsed.techCostMultiplier = struct.techCostMultiplier; - - let health; - - for (let wSegm in parsed.wallSet.templates) - { - if (wSegm == "fort" || wSegm == "curves") - continue; - - let wPart = loadEntityTemplate(parsed.wallSet.templates[wSegm]); - parsed.wallset[wSegm] = wPart; - - for (let research of wPart.production.techs) - parsed.production.techs.push(research); - - if (wPart.upgrades) - parsed.upgrades = parsed.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 (parsed.wallSet.templates.curves) - for (let curve of parsed.wallSet.templates.curves) - { - let wPart = loadEntityTemplate(curve); - health.min = Math.min(health.min, wPart.health); - health.max = Math.max(health.max, wPart.health); - } - - if (health.min == health.max) - parsed.health = health.min; - else - parsed.health = sprintf(translate("%(health_min)s to %(health_max)s"), { - "health_min": health.min, - "health_max": health.max - }); - } - - return parsed; -} - -/** - * Load and parse technology from json template. - * - * @param {string} templateName - * @return {object} Sanitized data about the requested technology. - */ -function loadTechnology(techName) -{ - let template = loadTechData(techName); - let tech = GetTechnologyDataHelper(template, g_SelectedCiv, g_ResourceData); - tech.name.internal = techName; - - if (template.pair !== undefined) - { - tech.pair = template.pair; - tech.reqs = mergeRequirements(tech.reqs, loadTechnologyPair(template.pair).reqs); - } - - return tech; -} - -/** - * Crudely iterates through every tech JSON file and identifies those - * that are auto-researched. - * - * @return {array} List of techs that are researched automatically - */ -function findAllAutoResearchedTechs() -{ - let techList = []; - - for (let filename of Engine.ListDirectoryFiles(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; -} - -/** - * @param {string} phaseCode - * @return {object} Sanitized object containing phase data - */ -function loadPhase(phaseCode) -{ - let phase = loadTechnology(phaseCode); - - phase.actualPhase = phaseCode; - if (phase.replaces !== undefined) - phase.actualPhase = phase.replaces[0]; - - return phase; -} - -/** - * @param {string} pairCode - * @return {object} Contains a list and the requirements of the techs in the pair - */ -function loadTechnologyPair(pairCode) -{ - var pairInfo = loadTechData(pairCode); - - return { - "techs": [ pairInfo.top, pairInfo.bottom ], - "reqs": DeriveTechnologyRequirements(pairInfo, g_SelectedCiv) - }; -} - -/** - * @param {string} modCode - * @return {object} Sanitized object containing modifier tech data - */ -function loadModifierTech(modCode) -{ - if (!Engine.FileExists("simulation/data/technologies/"+modCode+".json")) - return {}; - return loadTechData(modCode); -} Index: ps/trunk/binaries/data/mods/public/gui/reference/common/tooltips.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/tooltips.js +++ ps/trunk/binaries/data/mods/public/gui/reference/common/tooltips.js @@ -0,0 +1,76 @@ +/** + * Creates text in the following format: + * Header: value1, value2, ..., valueN + * + * This function is only used below, nowhere else. + */ +function buildListText(headerString, arrayOfValues) +{ + // Translation: Label followed by a list of values. + return sprintf(translate("%(listHeader)s %(listOfValues)s"), { + "listHeader": headerFont(headerString), + // Translation: List separator. + "listOfValues": bodyFont(arrayOfValues.join(translate(", "))) + }); +} + +/** + * The following functions in this file work on the same basis as those in gui/common/tooltips.js + * + * Note: Due to quirks in loading order, this file might not be loaded before ReferencePage.js. + * Do not put anything in here that you wish to access static'ly there. + */ + +function getBuiltByText(template) +{ + // Translation: Label before a list of the names of units that build the structure selected. + return template.builtByListOfNames ? buildListText(translate("Built by:"), template.builtByListOfNames) : ""; +} + +function getTrainedByText(template) +{ + // Translation: Label before a list of the names of structures or units that train the unit selected. + return template.trainedByListOfNames ? buildListText(translate("Trained by:"), template.trainedByListOfNames) : ""; +} + +function getResearchedByText(template) +{ + // Translation: Label before a list of names of structures or units that research the technology selected. + return template.researchedByListOfNames ? buildListText(translate("Researched at:"), template.researchedByListOfNames) : ""; +} + +/** + * @return {string} List of the names of the buildings the selected unit can build. + */ +function getBuildText(template) +{ + // Translation: Label before a list of the names of structures the selected unit can construct or build. + return template.buildListOfNames ? buildListText(translate("Builds:"), template.buildListOfNames) : ""; +} + +/** + * @return {string} List of the names of the technologies the selected structure/unit can research. + */ +function getResearchText(template) +{ + // Translation: Label before a list of the names of technologies the selected unit or structure can research. + return template.researchListOfNames ? buildListText(translate("Researches:"), template.researchListOfNames) : ""; +} + +/** + * @return {string} List of the names of the units the selected unit can train. + */ +function getTrainText(template) +{ + // Translation: Label before a list of the names of units the selected unit or structure can train. + return template.trainListOfNames ? buildListText(translate("Trains:"), template.trainListOfNames) : ""; +} + +/** + * @return {string} List of the names of the buildings/units the selected structure/unit can upgrade to. + */ +function getUpgradeText(template) +{ + // Translation: Label before a list of the names of units or structures the selected unit or structure can be upgradable to. + return template.upgradeListOfNames ? buildListText(translate("Upgradable to:"), template.upgradeListOfNames) : ""; +} Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/Boxes/EntityBox.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/Boxes/EntityBox.js +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/Boxes/EntityBox.js @@ -0,0 +1,83 @@ +/** + * Class inherited by StructureBox and TrainerBox classes. + */ +class EntityBox +{ + constructor(page) + { + this.page = page; + } + + static setViewerOnPress(guiObject, templateName, civCode) + { + let viewerFunc = () => { + Engine.PushGuiPage("page_viewer.xml", { + "templateName": templateName, + "civ": civCode + }); + }; + guiObject.onPress = viewerFunc; + guiObject.onPressRight = viewerFunc; + } + + draw(templateName, civCode) + { + this.template = this.page.TemplateParser.getEntity(templateName, civCode); + this.gui.hidden = false; + + let caption = this.gui.children[0]; + caption.caption = translate(this.template.name.specific); + + let icon = this.gui.children[1]; + icon.sprite = "stretched:" + this.page.IconPath + this.template.icon; + icon.tooltip = this.constructor.compileTooltip(this.template); + this.constructor.setViewerOnPress(icon, this.template.name.internal, civCode); + } + + captionWidth() + { + // We make the assumption that the caption's padding is equal on both sides + let caption = this.gui.children[0]; + return Engine.GetTextWidth(caption.font, caption.caption) + (caption.size.left + caption.buffer_zone) * 2; + } + + static compileTooltip(template) + { + return ReferencePage.buildText(template, this.prototype.TooltipFunctions) + "\n" + showTemplateViewerOnClickTooltip(); + } + + /** + * Returns the height between the top of the EntityBox, and the top of the production rows. + * + * Used within the TreeSection class to position the production rows, + * and used with the PhaseIdent class to position grey bars under them. + */ + static IconAndCaptionHeight() + { + let height = Engine.GetGUIObjectByName("structure[0]_icon").size.bottom + this.prototype.IconPadding; + + // Replace function so the above is only run once. + this.IconAndCaptionHeight = () => height; + return height; + } + +} + +/** + * Minimum width of the boxes, the margins between them (Horizontally and Vertically), + * and the padding between the main icon and the production row(s) beneath it. + */ +EntityBox.prototype.MinWidth = 96; +EntityBox.prototype.HMargin = 8; +EntityBox.prototype.VMargin = 12; +EntityBox.prototype.IconPadding = 8; + +/** + * Functions used to collate the contents of a tooltip. + */ +EntityBox.prototype.TooltipFunctions = [ + getEntityNamesFormatted, + getEntityCostTooltip, + getEntityTooltip, + getAurasTooltip +].concat(ReferencePage.prototype.StatsFunctions); Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/Boxes/ProductionIcon.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/Boxes/ProductionIcon.js +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/Boxes/ProductionIcon.js @@ -0,0 +1,52 @@ +class ProductionIcon +{ + constructor(page, guiObject) + { + this.page = page; + this.productionIcon = guiObject; + } + + /* Returns the dimensions of a single icon, including some "helper" attributes. + * + * Two assumptions are made: (1) that all production icons are the same size, + * and (2) that the size will never change for the duration of the life of the + * containing page. + * + * As such, the method replaces itself after being run once, so the calculations + * within are only performed once. + */ + static Size() + { + let baseObject = Engine.GetGUIObjectByName("phase[0]_bar[0]_icon").size; + let size = {}; + + // Icon dimensions + size.width = baseObject.right - baseObject.left; + size.height = baseObject.bottom - baseObject.top; + + // Horizontal and Vertical Margins. + size.hMargin = baseObject.left; + size.vMargin = baseObject.top; + + // Width and Height padded with margins on all sides. + size.paddedWidth = size.width + size.hMargin * 2; + size.paddedHeight = size.height + size.vMargin * 2; + + // Padded dimensions to use when in production rows. + size.rowWidth = size.width + size.hMargin; + size.rowHeight = size.paddedHeight + size.vMargin * 2; + size.rowGap = size.rowHeight - size.paddedHeight; + + // Replace static method and return + this.Size = () => size; + return size; + } + + draw(template, civCode) + { + this.productionIcon.sprite = "stretched:" + this.page.IconPath + template.icon; + this.productionIcon.tooltip = EntityBox.compileTooltip(template); + this.productionIcon.hidden = false; + EntityBox.setViewerOnPress(this.productionIcon, template.name.internal, civCode); + } +} Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/Boxes/ProductionRow.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/Boxes/ProductionRow.js +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/Boxes/ProductionRow.js @@ -0,0 +1,58 @@ +class ProductionRow +{ + constructor(page, guiObject, rowIndex) + { + this.page = page; + this.productionRow = guiObject; + this.productionIconsDrawn = 0; + this.rowIndex = rowIndex; + this.phaseOffset = 0; + + horizontallySpaceObjects(this.productionRow.name, ProductionIcon.Size().hMargin); + + this.productionIcons = []; + for (let icon of guiObject.children) + this.productionIcons.push(new ProductionIcon(this.page, icon)); + } + + startDraw(phaseOffset) + { + this.productionIconsDrawn = 0; + this.phaseOffset = phaseOffset; + } + + drawIcon(template, civCode) + { + if (this.productionIconsDrawn == this.productionIcons.length) + { + error("The currently displayed civ has more production options " + + "than can be supported by the current GUI layout"); + return; + } + + this.productionIcons[this.productionIconsDrawn].draw(template, civCode); + ++this.productionIconsDrawn; + } + + finishDraw() + { + hideRemaining(this.productionRow.name, this.productionIconsDrawn); + + const IconSize = ProductionIcon.Size(); + let rowOffset = IconSize.rowHeight * (this.phaseOffset - this.rowIndex); + let rowWidth = this.productionIconsDrawn * IconSize.rowWidth + IconSize.hMargin; + + let size = this.productionRow.size; + size.left = -rowWidth / 2; + size.top = -rowOffset; + this.productionRow.size = size; + + this.productionRow.hidden = false; + return rowWidth; + } + + hide() + { + this.productionRow.hidden = true + } +} Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/Boxes/ProductionRowManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/Boxes/ProductionRowManager.js +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/Boxes/ProductionRowManager.js @@ -0,0 +1,79 @@ +class ProductionRowManager +{ + constructor(page, guiName, sortByPhase) + { + this.page = page; + this.width = 0; + this.sortProductionsByPhase = sortByPhase; + + this.productionRows = []; + for (let row of Engine.GetGUIObjectByName(guiName).children) + this.productionRows.push(new ProductionRow(this.page, row, this.productionRows.length)); + } + + draw(template, civCode, phaseIdx=0) + { + this.width = 0; + + if (this.sortProductionsByPhase) + for (let r = 0; r < this.page.TemplateParser.phaseList.length; ++r) + this.productionRows[r].startDraw(this.page.TemplateParser.phaseList.length - phaseIdx); + else + this.productionRows[0].startDraw(1); + + // (Want to draw Units before Techs) + for (let prodType of Object.keys(template.production).reverse()) + for (let prod of template.production[prodType]) + { + let pIdx = 0; + switch (prodType) + { + + case "units": + prod = this.page.TemplateParser.getEntity(prod, civCode); + pIdx = this.page.TemplateParser.phaseList.indexOf(prod.phase); + break; + + case "techs": + pIdx = this.page.TemplateParser.phaseList.indexOf(this.page.TemplateParser.getPhaseOfTechnology(prod, civCode)); + prod = clone(this.page.TemplateParser.getTechnology(prod, civCode)); + for (let res in template.techCostMultiplier) + if (prod.cost[res]) + prod.cost[res] *= template.techCostMultiplier[res]; + break; + + default: + continue; + } + + let rowIdx = this.sortProductionsByPhase ? Math.max(0, pIdx - phaseIdx) : 0; + this.productionRows[rowIdx].drawIcon(prod, civCode) + } + + if (template.upgrades) + for (let upgrade of template.upgrades) + { + let pIdx = 0; + if (this.phaseSort) + pIdx = this.page.TemplateParser.phaseList.indexOf(upgrade.phase); + let rowIdx = Math.max(0, pIdx - phaseIdx); + this.productionRows[rowIdx].drawIcon(upgrade, civCode); + } + + if (template.wallset) + this.productionRows[0].drawIcon(template.wallset.tower, civCode); + + let r = 0; + + // Tell the production rows used we've finished + if (this.sortProductionsByPhase) + for (; r < this.page.TemplateParser.phaseList.length; ++r) + this.width = Math.max(this.width, this.productionRows[r].finishDraw()); + else + this.width = this.productionRows[r++].finishDraw(); + + // Hide any remaining phase rows + for (; r < this.productionRows.length; ++r) + this.productionRows[r].hide(); + } +} Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/Boxes/StructureBox.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/Boxes/StructureBox.js +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/Boxes/StructureBox.js @@ -0,0 +1,37 @@ +/** + * This code wraps the gui representing buildable structures within the structree. + * + * An instance of this class is created for each child of the gui element named "structures". + */ +class StructureBox extends EntityBox +{ + constructor(page, structureIdx) + { + super(page); + this.gui = Engine.GetGUIObjectByName("structure[" + structureIdx + "]"); + this.ProductionRows = new ProductionRowManager(this.page, "structure[" + structureIdx + "]_productionRows", true); + } + + draw(templateName, civCode, runningWidths) + { + super.draw(templateName, civCode); + + this.phaseIdx = this.page.TemplateParser.phaseList.indexOf(this.template.phase); + + // Draw the production rows + this.ProductionRows.draw(this.template, civCode, this.phaseIdx); + + let boxWidth = Math.max(this.MinWidth, this.captionWidth(), this.ProductionRows.width); + + // Set position of the Structure Box + let size = this.gui.size; + size.left = this.HMargin + runningWidths[this.phaseIdx]; + size.right = this.HMargin + runningWidths[this.phaseIdx] + boxWidth; + size.top = TreeSection.getPositionOffset(this.phaseIdx, this.page.TemplateParser); + size.bottom = TreeSection.getPositionOffset(this.phaseIdx + 1, this.page.TemplateParser) - this.VMargin; + this.gui.size = size; + + // Update new right-side-edge dimension + runningWidths[this.phaseIdx] += boxWidth + this.HMargin / 2; + } +} Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/Boxes/TrainerBox.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/Boxes/TrainerBox.js +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/Boxes/TrainerBox.js @@ -0,0 +1,42 @@ +/** + * This code wraps the gui representing "trainer units" (a unit that can train other units) within the structree. + * + * An instance of this class is created for each child of the gui element named "trainers". + */ +class TrainerBox extends EntityBox +{ + constructor(page, trainerIdx) + { + super(page); + + this.gui = Engine.GetGUIObjectByName("trainer[" + trainerIdx + "]"); + this.ProductionRows = new ProductionRowManager(this.page, "trainer[" + trainerIdx + "]_productionRows", false); + + let rowHeight = ProductionIcon.Size().rowHeight; + let size = this.gui.size; + + // Adjust height to accommodate production row + size.bottom += rowHeight; + + // We make the assumuption that all trainer boxes have the same height + let boxHeight = this.VMargin / 2 + (size.bottom - size.top + this.VMargin) * trainerIdx; + size.top += boxHeight; + size.bottom += boxHeight; + + // Make the box adjust automatically to column width + size.rright = 100; + size.right = -size.left; + + this.gui.size = size; + } + + draw(templateName, civCode) + { + super.draw(templateName, civCode); + + this.ProductionRows.draw(this.template, civCode); + + // Return the box width + return Math.max(this.MinWidth, this.captionWidth(), this.ProductionRows.width); + } +} Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/Sections/Trainer/TrainerSection.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/Sections/Trainer/TrainerSection.js +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/Sections/Trainer/TrainerSection.js @@ -0,0 +1,65 @@ +class TrainerSection +{ + constructor(page) + { + this.page = page; + this.width = 0; + this.widthChangedHandlers = new Set(); + + this.TrainerSection = Engine.GetGUIObjectByName("trainerSection"); + this.Trainers = Engine.GetGUIObjectByName("trainers"); + + this.TrainerSectionHeading = Engine.GetGUIObjectByName("trainerSectionHeading"); + this.TrainerSectionHeading.caption = this.Caption; + + this.trainerBoxes = []; + for (let boxIdx in this.Trainers.children) + this.trainerBoxes.push(new TrainerBox(this.page, boxIdx)); + } + + registerWidthChangedHandler(handler) + { + this.widthChangedHandlers.add(handler); + } + + draw(units, civCode) + { + let caption = this.TrainerSectionHeading; + this.width = Engine.GetTextWidth(caption.font, caption.caption) + (caption.size.left + caption.buffer_zone) * 2; + let count = 0; + + for (let unitCode of units.keys()) + { + let unitTemplate = this.page.TemplateParser.getEntity(unitCode, civCode); + if (!unitTemplate.production.units.length && !unitTemplate.production.techs.length && !unitTemplate.upgrades) + continue; + + if (count > this.trainerBoxes.length) + { + error("\"" + this.activeCiv + "\" has more unit trainers than can be supported by the current GUI layout"); + break; + } + + this.width = Math.max( + this.width, + this.trainerBoxes[count].draw(unitCode, civCode) + ); + + ++count; + } + hideRemaining(this.Trainers.name, count); + + // Update width and visibility of section + let size = this.TrainerSection.size; + this.width += EntityBox.prototype.HMargin; + size.left = -this.width + size.right; + this.TrainerSection.size = size; + this.TrainerSection.hidden = count == 0; + + for (let handler of this.widthChangedHandlers) + handler(this.width, !this.TrainerSection.hidden); + } +} + +TrainerSection.prototype.Caption = + translate("Trainer Units"); Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/Sections/Trainer/TrainerSection.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/Sections/Trainer/TrainerSection.xml +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/Sections/Trainer/TrainerSection.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/Sections/Tree/PhaseIdent.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/Sections/Tree/PhaseIdent.js +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/Sections/Tree/PhaseIdent.js @@ -0,0 +1,58 @@ +class PhaseIdent +{ + constructor(page, phaseIdx) + { + this.page = page; + this.phaseIdx = +phaseIdx; + + this.Ident = Engine.GetGUIObjectByName("phase[" + this.phaseIdx + "]_ident"); + this.Icon = Engine.GetGUIObjectByName("phase[" + this.phaseIdx + "]_icon"); + this.Bars = Engine.GetGUIObjectByName("phase[" + this.phaseIdx + "]_bars"); + + let prodIconSize = ProductionIcon.Size(); + let entityBoxHeight = EntityBox.IconAndCaptionHeight(); + for (let i = 0; i < this.Bars.children.length; ++i) + { + let size = this.Bars.children[i].size; + size.top = entityBoxHeight + prodIconSize.rowHeight * (i + 1); + size.bottom = entityBoxHeight + prodIconSize.rowHeight * (i + 2) - prodIconSize.rowGap; + this.Bars.children[i].size = size; + } + } + + draw(phaseList, barLength, civCode) + { + // Position ident + let identSize = this.Ident.size; + identSize.top = TreeSection.getPositionOffset(this.phaseIdx, this.page.TemplateParser); + identSize.bottom = TreeSection.getPositionOffset(this.phaseIdx + 1, this.page.TemplateParser); + this.Ident.size = identSize; + + // Draw main icon + this.drawPhaseIcon(this.Icon, this.phaseIdx, civCode); + + // Draw the phase bars + let i = 1; + for (; i < phaseList.length - this.phaseIdx; ++i) + { + let prodBar = this.Bars.children[(i - 1)]; + let prodBarSize = prodBar.size; + prodBarSize.right = barLength; + prodBar.size = prodBarSize; + prodBar.hidden = false; + + this.drawPhaseIcon(prodBar.children[0], this.phaseIdx + i, civCode); + } + hideRemaining(this.Bars.name, i - 1); + } + + drawPhaseIcon(phaseIcon, phaseIndex, civCode) + { + let phaseName = this.page.TemplateParser.phaseList[phaseIndex]; + let prodPhaseTemplate = this.page.TemplateParser.getTechnology(phaseName + "_" + civCode, civCode) || this.page.TemplateParser.getTechnology(phaseName, civCode); + + phaseIcon.sprite = "stretched:" + this.page.IconPath + prodPhaseTemplate.icon; + phaseIcon.tooltip = getEntityNamesFormatted(prodPhaseTemplate); + }; + +} Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/Sections/Tree/PhaseIdentManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/Sections/Tree/PhaseIdentManager.js +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/Sections/Tree/PhaseIdentManager.js @@ -0,0 +1,23 @@ +class PhaseIdentManager +{ + constructor(page) + { + this.page = page; + this.idents = []; + + this.PhaseIdents = Engine.GetGUIObjectByName("phaseIdents"); + this.Idents = []; + for (let identIdx in this.PhaseIdents.children) + this.Idents.push(new PhaseIdent(this.page, identIdx)); + } + + draw(phaseList, civCode, runningWidths, leftMargin) + { + for (let i = 0; i < phaseList.length; ++i) + { + let barLength = leftMargin + runningWidths[i] + EntityBox.prototype.HMargin * 0.75; + this.Idents[i].draw(phaseList, barLength, civCode); + } + hideRemaining(this.PhaseIdents.name, phaseList.length); + } +} Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/Sections/Tree/TreeSection.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/Sections/Tree/TreeSection.js +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/Sections/Tree/TreeSection.js @@ -0,0 +1,77 @@ +class TreeSection +{ + constructor(page) + { + this.page = page; + + this.TreeSection = Engine.GetGUIObjectByName("treeSection"); + this.Structures = Engine.GetGUIObjectByName("structures"); + + this.PhaseIdents = new PhaseIdentManager(this.page); + + this.rightMargin = this.TreeSection.size.right; + + this.structureBoxes = []; + for (let boxIdx in this.Structures.children) + this.structureBoxes.push(new StructureBox(this.page, boxIdx)); + + page.TrainerSection.registerWidthChangedHandler(this.onTrainerSectionWidthChange.bind(this)); + } + + draw(structures, civCode) + { + if (structures.size > this.structureBoxes.length) + error("\"" + this.activeCiv + "\" has more structures than can be supported by the current GUI layout"); + + // Draw structures + let phaseList = this.page.TemplateParser.phaseList; + let count = Math.min(structures.size, this.structureBoxes.length); + let runningWidths = Array(phaseList.length).fill(0); + let structureIterator = structures.keys(); + for (let idx = 0; idx < count; ++idx) + this.structureBoxes[idx].draw(structureIterator.next().value, civCode, runningWidths); + hideRemaining(this.Structures.name, count); + + // Position phase idents + this.PhaseIdents.draw(phaseList, civCode, runningWidths, this.Structures.size.left); + } + + drawPhaseIcon(phaseIcon, phaseIndex, civCode) + { + let phaseName = this.page.TemplateParser.phaseList[phaseIndex]; + let prodPhaseTemplate = this.page.TemplateParser.getTechnology(phaseName + "_" + civCode, civCode) || this.page.TemplateParser.getTechnology(phaseName, civCode); + + phaseIcon.sprite = "stretched:" + this.page.IconPath + prodPhaseTemplate.icon; + phaseIcon.tooltip = getEntityNamesFormatted(prodPhaseTemplate); + }; + + onTrainerSectionWidthChange(trainerSectionWidth, trainerSectionVisible) + { + let size = this.TreeSection.size; + size.right = this.rightMargin; + if (trainerSectionVisible) + size.right -= trainerSectionWidth + this.page.SectionGap; + this.TreeSection.size = size; + } + + /** + * Calculate row position offset (accounting for different number of prod rows per phase). + * + * This is a static method as it is also used from within the StructureBox and PhaseIdent classes. + * + * @param {number} idx + * @return {number} + */ + static getPositionOffset(idx, TemplateParser) + { + let phases = TemplateParser.phaseList.length; + let rowHeight = ProductionIcon.Size().rowHeight; + + let size = EntityBox.IconAndCaptionHeight() * idx; // text, image and offset + size += EntityBox.prototype.VMargin * (idx + 1); // Margin above StructureBoxes + size += rowHeight * (phases * idx - (idx - 1) * idx / 2); // phase rows (phase-currphase+1 per row) + + return size; + }; + +} Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/Sections/Tree/TreeSection.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/Sections/Tree/TreeSection.xml +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/Sections/Tree/TreeSection.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/StructreePage.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/StructreePage.js +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/StructreePage.js @@ -0,0 +1,58 @@ +/** + * This class represents the Structure Tree GUI page. + * + * Further methods are described within draw.js + */ +class StructreePage extends ReferencePage +{ + constructor(data) + { + super(); + + this.structureBoxes = []; + this.trainerBoxes = []; + + this.CivEmblem = Engine.GetGUIObjectByName("civEmblem"); + this.CivName = Engine.GetGUIObjectByName("civName"); + this.CivHistory = Engine.GetGUIObjectByName("civHistory"); + + this.TrainerSection = new TrainerSection(this); + this.TreeSection = new TreeSection(this); + + this.civSelection = new CivSelectDropdown(this.civData); + if (!this.civSelection.hasCivs()) + { + this.closePage(); + return; + } + this.civSelection.registerHandler(this.selectCiv.bind(this)); + + let civInfoButton = new CivInfoButton(this); + let closeButton = new CloseButton(this); + Engine.SetGlobalHotkey("structree", "Press", this.closePage.bind(this)); + } + + closePage() + { + Engine.PopGuiPage({ "civ": this.activeCiv, "page": "page_structree.xml" }); + } + + selectCiv(civCode) + { + this.setActiveCiv(civCode); + + this.CivEmblem.sprite = "stretched:" + this.civData[this.activeCiv].Emblem; + this.CivName.caption = this.civData[this.activeCiv].Name; + this.CivHistory.caption = this.civData[this.activeCiv].History; + + let templateLists = this.TemplateLister.getTemplateLists(this.activeCiv); + this.TreeSection.draw(templateLists.structures, this.activeCiv); + this.TrainerSection.draw(templateLists.units, this.activeCiv); + } +} + +StructreePage.prototype.CloseButtonTooltip = + translate("%(hotkey)s: Close Structure Tree."); + +// Gap between the `TreeSection` and `TrainerSection` gui objects (when the latter is visible) +StructreePage.prototype.SectionGap = 12; Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/draw.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/draw.js +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/draw.js @@ -1,432 +0,0 @@ -/** - * Functions used to collate the contents of a tooltip. - */ -var g_StructreeTooltipFunctions = [ - getEntityNamesFormatted, - getEntityCostTooltip, - getEntityTooltip, - getAurasTooltip -].concat(g_StatsFunctions); - -/** - * 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(); - - let leftMargin = Engine.GetGUIObjectByName("tree_display").size.left; - let defWidth = 96; - let 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_BuildList[g_SelectedCiv][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 = - compileTooltip(stru); - - Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]_name").caption = - translate(stru.name.specific); - - setViewerOnPress("phase["+i+"]_struct["+s+"]_icon", stru.name.internal); - 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.techs[prod_pha]) - for (let prod of stru.production.techs[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); - - // 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_TrainList[g_SelectedCiv]) - { - 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 = compileTooltip(trainer); - Engine.GetGUIObjectByName("trainer["+t+"]_name").caption = translate(trainer.name.specific); - setViewerOnPress("trainer["+t+"]_icon", trainer.name.internal); - 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)) - 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; -} - -/** - * Draws production icons. - * - * These are the small icons on the gray bars. - * - * @param {string} phase - The phase that the parent entity (the entity that - * builds/trains/researches this entity) belongs to. - * @param {number} parentID - Which parent entity it belongs to on the main phase rows. - * @param {number} rowID - Which production row of the parent entity the production - * icon sits on, if applicable. - * @param {number} iconID - Which production icon to affect. - * @param {object} template - The entity being produced. - * @return {boolean} True is successfully drawn, False if no space left to draw. - */ -function drawProdIcon(phase, parentID, rowID, iconID, template) -{ - let prodEle = Engine.GetGUIObjectByName("phase["+phase+"]_struct["+parentID+"]_row["+rowID+"]_prod["+iconID+"]"); - - if (phase === null) - prodEle = Engine.GetGUIObjectByName("trainer["+parentID+"]_prod["+iconID+"]"); - - if (prodEle === undefined) - { - error("The " + (phase === 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/"+template.icon; - prodEle.tooltip = compileTooltip(template); - prodEle.hidden = false; - setViewerOnPress(prodEle.name, template.name.internal); - return true; -} - -function compileTooltip(template) -{ - return buildText(template, g_StructreeTooltipFunctions) + "\n" + showTemplateViewerOnClickTooltip(); -} - -/** - * Calculate row position offset (accounting for different number of prod rows per phase). - * - * @param {number} idx - * @return {number} - */ -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 position 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 r = phaseCount - i; r < phaseCount; ++r) - Engine.GetGUIObjectByName("phase["+i+"]_struct["+s+"]_row["+r+"]").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; -} - -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); - } -} - -/** - * @param {string} guiObjectName - * @param {number} phaseIndex - */ -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/reference/structree/rows.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/rows.xml +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/rows.xml @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/structree.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/structree.js +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/structree.js @@ -1,194 +1,14 @@ /** - * Array of structure template names when given a civ and a phase name. - */ -var g_BuildList = {}; - -/** - * Array of template names that can be trained from a unit, given a civ and unit template name. - */ -var g_TrainList = {}; - -/** * Initialize the page * * @param {object} data - Parameters passed from the code that calls this page into existence. */ function init(data = {}) { - let civList = Object.keys(g_CivData).map(civ => ({ - "name": g_CivData[civ].Name, - "code": civ, - })).sort(sortNameIgnoreCase); - - if (!civList.length) - { - closePage(); - return; - } - - g_ParsedData = { - "units": {}, - "structures": {}, - "techs": {}, - "phases": {} - }; - - let civSelection = Engine.GetGUIObjectByName("civSelection"); - civSelection.list = civList.map(c => c.name); - civSelection.list_data = civList.map(c => c.code); - civSelection.selected = data.civ ? civSelection.list_data.indexOf(data.civ) : 0; - - Engine.GetGUIObjectByName("civinfo").tooltip = colorizeHotkey(translate("%(hotkey)s: Switch to Civilization Overview."), "civinfo"); - Engine.GetGUIObjectByName("close").tooltip = colorizeHotkey(translate("%(hotkey)s: Close Structure Tree."), "cancel"); -} - -function switchToCivInfoPage() -{ - Engine.PopGuiPage({ "civ": g_SelectedCiv, "nextPage": "page_civinfo.xml" }); -} - -function closePage() -{ - Engine.PopGuiPage({ "civ": g_SelectedCiv, "page": "page_structree.xml" }); -} - -/** - * @param {string} civCode - */ -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_BuildList[g_SelectedCiv]) - { - draw(); - drawPhaseIcons(); - return; - } - - let templateLists = compileTemplateLists(civCode); - - for (let u of templateLists.units.keys()) - if (!g_ParsedData.units[u]) - g_ParsedData.units[u] = loadEntityTemplate(u); - - for (let s of templateLists.structures.keys()) - if (!g_ParsedData.structures[s]) - g_ParsedData.structures[s] = loadEntityTemplate(s); - - // Load technologies - g_ParsedData.techs[civCode] = {}; - for (let techcode of templateLists.techs.keys()) - if (basename(techcode).startsWith("phase")) - g_ParsedData.phases[techcode] = loadPhase(techcode); - else - g_ParsedData.techs[civCode][techcode] = loadTechnology(techcode); - - // Establish phase order - g_ParsedData.phaseList = UnravelPhases(g_ParsedData.phases); - - // Load any required generic phases that aren't already loaded - for (let phasecode of g_ParsedData.phaseList) - if (!g_ParsedData.phases[phasecode]) - g_ParsedData.phases[phasecode] = loadPhase(phasecode); - - // Group production and upgrade lists of structures by phase - for (let structCode of templateLists.structures.keys()) - { - let structInfo = g_ParsedData.structures[structCode]; - structInfo.phase = getPhaseOfTemplate(structInfo); - let structPhaseIdx = g_ParsedData.phaseList.indexOf(structInfo.phase); - - // If this structure is shared with another civ, - // it may have already gone through the grouping process already. - if (!Array.isArray(structInfo.production.techs)) - continue; - - // Sort techs by phase - let newProdTech = {}; - for (let prod of structInfo.production.techs) - { - 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) - { - 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 = { - "techs": 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 templateLists.structures.keys()) - { - let phase = g_ParsedData.structures[structCode].phase; - buildList[phase].push(structCode); - } - - for (let unitCode of templateLists.units.keys()) - { - let unitTemplate = g_ParsedData.units[unitCode]; - if (!unitTemplate.production.units.length && !unitTemplate.production.techs.length && !unitTemplate.upgrades) - continue; - - trainerList.push(unitCode); - } - - g_BuildList[g_SelectedCiv] = buildList; - g_TrainList[g_SelectedCiv] = trainerList; + g_Page = new StructreePage(data); - draw(); - drawPhaseIcons(); + if (data.civ) + g_Page.civSelection.selectCiv(data.civ); + else + g_Page.civSelection.selectFirstCiv(); } Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/structree.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/structree.xml +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/structree.xml @@ -2,14 +2,15 @@