/**
* 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;
let template = this.TemplateLoader.loadAuraTemplate(auraName);
let parsed = GetAuraDataHelper(template);
if (template.civ)
parsed.civ = template.civ;
let 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;
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.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.
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.Max,
};
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 resistance and auras
let struct = this.getEntity(parsed.wallSet.templates.long, civCode);
parsed.resistance = struct.resistance;
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[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];
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);
}
/**
* Load and parse the relevant player_{civ}.xml template.
*/
getPlayer(civCode)
{
if (civCode in this.players)
return this.players[civCode];
let template = this.TemplateLoader.loadPlayerTemplate(civCode);
let parsed = {
"civbonuses": [],
"teambonuses": [],
};
if (template.Auras)
for (let 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 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, this.modifiers[civCode] || {});
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)
{
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)
{
// 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;
}
}
// 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"];