mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-08-15 14:43:32 -07:00
This patch is primarily meant as preparation in order to be able to reuse this logic for GetEntityState calls in the future. Key changes: - Clearer API by providing different functions for different purposes. - Slight performance optimisation by storing parameters in a context object and pulling the info from there instead of redeclaring a lambda each time, which it seems Spidermonkey struggled to optimise well. - Allow computing only partial template data, specified by passing a list of desired components. (to be used more in the future) - Make it so the naming and structure of template data and matches 1:1 with the data in entity states. This makes accessing values from them more consistent and would also allow e.g. adopting data between the two in the future.
422 lines
12 KiB
JavaScript
422 lines
12 KiB
JavaScript
/**
|
|
* 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] || {});
|
|
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.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 (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.upgrades)
|
|
Array.prototype.push.apply(parsed.upgrades, wPart.upgrades);
|
|
|
|
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)
|
|
{
|
|
upgrade.entity = upgrade.entity.replace(/\{(civ|native)\}/g, civCode);
|
|
|
|
const data = g_TemplateHelper.computeDataFromModifiers(this.TemplateLoader.loadEntityTemplate(upgrade.entity, civCode), this.TemplateLoader.auraData, g_ResourceData, 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.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"];
|