/** * This class parses and stores parsed template data. */ class TemplateParser { constructor(TemplateLoader) { this.TemplateLoader = TemplateLoader; /** * Parsed Data Stores */ this.auras = {}; this.entities = {}; this.techs = {}; this.phases = {}; this.modifiers = {}; this.players = {}; this.phaseList = []; } getAura(auraName) { if (auraName in this.auras) return this.auras[auraName]; if (!AuraTemplateExists(auraName)) return null; const template = this.TemplateLoader.loadAuraTemplate(auraName); const parsed = GetAuraDataHelper(template); if (template.civ) parsed.civ = template.civ; const affectedPlayers = template.affectedPlayers || this.AuraAffectedPlayerDefault; parsed.affectsTeam = this.AuraTeamIndicators.some(indicator => affectedPlayers.includes(indicator)); parsed.affectsSelf = this.AuraSelfIndicators.some(indicator => affectedPlayers.includes(indicator)); this.auras[auraName] = parsed; return this.auras[auraName]; } /** * 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 (!(civCode in this.entities)) this.entities[civCode] = {}; else if (templateName in this.entities[civCode]) return this.entities[civCode][templateName]; if (!Engine.TemplateExists(templateName)) return null; const template = this.TemplateLoader.loadEntityTemplate(templateName, civCode); const parsed = g_TemplateHelper.computeDataFromModifiers(template, this.TemplateLoader.auraData, g_ResourceData, this.modifiers[civCode] || {}, civCode); parsed.name.internal = templateName; parsed.history = template.Identity.History; parsed.production = this.TemplateLoader.deriveProduction(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. // Complex requirements are too difficult to process for now, so assume the first phase. if (!parsed.requirements?.Techs) parsed.phase = this.phaseList[0]; else { let highestPhaseIndex = 0; for (const tech of parsed.requirements.Techs._string.split(" ")) { if (tech[0] === "!") continue; const phaseIndex = this.phaseList.indexOf( this.TemplateLoader.isPhaseTech(tech) ? this.getActualPhase(tech) : this.getPhaseOfTechnology(tech, civCode)); if (phaseIndex > highestPhaseIndex) highestPhaseIndex = phaseIndex; } parsed.phase = this.phaseList[highestPhaseIndex]; } 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.Max, }; if (parsed.upgrade) parsed.upgrade.options = this.getActualUpgradeData(parsed.upgrade.options, civCode); if (parsed.wallSet) { parsed.wallset = {}; if (!parsed.upgrade) parsed.upgrade = { "options": [] }; // Note: An assumption is made here that wall segments all have the same resistance and auras let struct = this.getEntity(parsed.wallSet.templates.long, civCode); parsed.resistance = struct.resistance; parsed.auras = struct.auras; // For technology cost multiplier (in the researcher component), we need to use the tower struct = this.getEntity(parsed.wallSet.templates.tower, civCode); parsed.researcher = struct.researcher; let hitpoints; for (const wSegm in parsed.wallSet.templates) { if (wSegm == "fort" || wSegm == "curves") continue; const wPart = this.getEntity(parsed.wallSet.templates[wSegm], civCode); parsed.wallset[wSegm] = wPart; for (const research of wPart.production.techs) parsed.production.techs.push(research); if (wPart.upgrade) Array.prototype.push.apply(parsed.upgrade.options, wPart.upgrade.options); if (["gate", "tower"].indexOf(wSegm) != -1) continue; if (!hitpoints) { hitpoints = { "min": wPart.maxHitpoints, "max": wPart.maxHitpoints }; continue; } hitpoints.min = Math.min(hitpoints.min, wPart.maxHitpoints); hitpoints.max = Math.max(hitpoints.max, wPart.maxHitpoints); } if (parsed.wallSet.templates.curves) for (const curve of parsed.wallSet.templates.curves) { const wPart = this.getEntity(curve, civCode); hitpoints.min = Math.min(hitpoints.min, wPart.maxHitpoints); hitpoints.max = Math.max(hitpoints.max, wPart.maxHitpoints); } if (hitpoints.min === hitpoints.max) parsed.maxHitpoints = hitpoints.min; else parsed.maxHitpoints = sprintf(translate("%(health_min)s to %(health_max)s"), { "health_min": hitpoints.min, "health_max": hitpoints.max }); } this.entities[civCode][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]; const template = this.TemplateLoader.loadTechnologyTemplate(technologyName); const tech = GetTechnologyDataHelper(template, civCode, g_ResourceData, this.modifiers[civCode] || {}); tech.name.internal = technologyName; if (template.partOfPair !== undefined) { tech.partOfPair = template.partOfPair; tech.reqs = this.mergeRequirements(tech.reqs, this.TemplateLoader.loadTechnologyPairTemplate(template.partOfPair).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); } /** * Load and parse the relevant player_{civ}.xml template. */ getPlayer(civCode) { if (civCode in this.players) return this.players[civCode]; const template = this.TemplateLoader.loadPlayerTemplate(civCode); const parsed = { "civbonuses": [], "teambonuses": [], }; if (template.Auras) for (const auraTemplateName of template.Auras._string.split(/\s+/)) if (AuraTemplateExists(auraTemplateName)) if (this.getAura(auraTemplateName).affectsTeam) parsed.teambonuses.push(auraTemplateName); else parsed.civbonuses.push(auraTemplateName); this.players[civCode] = parsed; return parsed; } /** * Provided with an array containing basic information about possible * upgrades, such as that generated by globalscript's g_TemplateHelper, * 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) { const newUpgrades = []; for (const upgrade of upgradesInfo) { const data = g_TemplateHelper.computeDataFromModifiers(this.TemplateLoader.loadEntityTemplate(upgrade.entity, civCode), this.TemplateLoader.auraData, g_ResourceData, this.modifiers[civCode] || {}, civCode); data.name.internal = upgrade.entity; data.cost = upgrade.cost; data.icon = upgrade.icon || data.icon; data.tooltip = upgrade.tooltip || data.tooltip; data.requirements = upgrade.requirements || data.requirements; if (!data.requirements?.Techs) data.phase = this.phaseList[0]; else { let highestPhaseIndex = 0; for (const tech of data.requirements.Techs._string.split(" ")) { if (tech[0] === "!") continue; const phaseIndex = this.phaseList.indexOf( this.TemplateLoader.isPhaseTech(tech) ? this.getActualPhase(tech) : this.getPhaseOfTechnology(tech, civCode)); if (phaseIndex > highestPhaseIndex) highestPhaseIndex = phaseIndex; } data.phase = this.phaseList[highestPhaseIndex]; } 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]; } const techReqs = this.getTechnology(techName, civCode).reqs; if (!techReqs) return false; for (const option of techReqs) if (option.techs) for (const 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) { const player = this.getPlayer(civCode); const auraList = clone(player.civbonuses); for (const bonusname of player.teambonuses) if (this.getAura(bonusname).affectsSelf) auraList.push(bonusname); this.modifiers[civCode] = this.TemplateLoader.deriveModifications(civCode, auraList); } derivePhaseList(technologyList, civCode) { // Wipe all cached phase data. It contains the previous civ's SpecificNames which wouldn't be reloaded otherwise. this.phases = {}; // Load all of a civ's specific phase technologies for (const 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 (const phasecode of this.phaseList) this.getTechnology(phasecode, civCode); } mergeRequirements(reqsA, reqsB) { if (!reqsA || !reqsB) return false; const finalReqs = clone(reqsA); for (const option of reqsB) for (const type in option) for (const opt in finalReqs) { if (!finalReqs[opt][type]) finalReqs[opt][type] = []; Array.prototype.push.apply(finalReqs[opt][type], option[type]); } return finalReqs; } } // Default affected player token list to use if an aura doesn't explicitly give one. // Keep in sync with simulation/components/Auras.js TemplateParser.prototype.AuraAffectedPlayerDefault = ["Player"]; // List of tokens that, if found in an aura's "affectedPlayers" attribute, indicate // that the aura applies to team members. TemplateParser.prototype.AuraTeamIndicators = ["MutualAlly", "ExclusiveMutualAlly"]; // List of tokens that, if found in an aura's "affectedPlayers" attribute, indicate // that the aura applies to the aura's owning civ. TemplateParser.prototype.AuraSelfIndicators = ["Player", "Ally", "MutualAlly"];