mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-08-15 14:43:32 -07:00
Optimise GetEntityState
GetEntityState is a performance-critical function in the GUI when a large number of units are selected. The goal of this patch is to increase the efficiency of it without modifying the returned states visible to rest of the GUI in any way (it renames a few properties of entities states or moves them around, but the information they contain and the way to access it remain the complete same) As explained the comments, certain parts (the template data) of an entity state are "predictable" and can be reused between entities with the same template. What one has to account for, however, is that values of the template data can be modified. This happens on two different levels: Firstly, player-wide modifications, which apply to all entities owned by that player. This includes stuff like bonuses from researched techs, civs bonuses, team bonuses. And secondly, entity-local modifications, which apply to individual entities. This includes buffs or debuffs from status effects or auras (of other entities). So we can construct a whole entity state by first just computing the dynamic state (stuff like current hitpoints, which differs from entity to entity) and then adding the (potentially already cached) template data to it, which can be reused between entities with the same template and owning player. And then overwrite the values affected by entity-local modifications, which are usually just a few and even they can be cached and reused for entities with the same owning player, template, and entity-local modifications (identified by the "modifications ID"). This saves the effort of retrieving/computing a ton of data each turn and also saves the time it takes the engine to clone the data from the simulation to the GUI (as the return value of the GUI interface call), which previously took just as long as retrieving the entity states themselves. Since only the dynamic state is read from the components, a number of small getter methods have become unused; they are kept (for now at least) since they might be useful again in the future or for mods.
This commit is contained in:
parent
70ace09a7e
commit
8ddbaf72e6
25 changed files with 1566 additions and 607 deletions
|
|
@ -204,10 +204,11 @@ class TemplateHelper
|
|||
_context = Object.seal({
|
||||
"template": {},
|
||||
"player": null, // null or a player id
|
||||
"civ": "",
|
||||
"entity": null, // null or an entity id
|
||||
"auraTemplates": {},
|
||||
"resources": {},
|
||||
"customModifiers": null,
|
||||
"customModifiers": null, // null or an object defining modifications
|
||||
"applyValueModifications": () => 0
|
||||
});
|
||||
|
||||
|
|
@ -217,11 +218,12 @@ class TemplateHelper
|
|||
* @param {Object} template - A valid template as returned by the template loader.
|
||||
* @param {Object} auraTemplates - In the form of { key: { "auraName": "", "auraDescription": "" } }.
|
||||
* @param {Object} resources - An instance of the Resources class.
|
||||
* @param {string} civ - The civ of which the template is part of. Does not have to match the template's identity's own civ.
|
||||
* @param {string[]} [components] - An array of components to process, if undefined all components are processed.
|
||||
*/
|
||||
getBasicData(template, auraTemplates, resources, components)
|
||||
getBasicData(template, auraTemplates, resources, civ, components)
|
||||
{
|
||||
return this.computeDataFromModifiers(template, auraTemplates, resources, {}, components);
|
||||
return this.computeDataFromModifiers(template, auraTemplates, resources, {}, civ, components);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -230,13 +232,15 @@ class TemplateHelper
|
|||
* @param {Object} template - A valid template as returned by the template loader.
|
||||
* @param {Object} auraTemplates - In the form of { key: { "auraName": "", "auraDescription": "" } }.
|
||||
* @param {Object} resources - An instance of the Resources class.
|
||||
* @param {Object} modifiers - Modifications to apply to the template, e.g. from auto-researched techs or unit upgrades
|
||||
* @param {Object} modifiers - Modifications to apply to the template, e.g. from auto-researched techs or civ bonuses.
|
||||
* @param {string} civ - The civ of which the template is part of. Does not have to match the template's identity's own civ.
|
||||
* @param {string[]} [components] - An array of components to process, if undefined all components are processed.
|
||||
*/
|
||||
computeDataFromModifiers(template, auraTemplates, resources, modifiers, components)
|
||||
computeDataFromModifiers(template, auraTemplates, resources, modifiers, civ, components)
|
||||
{
|
||||
this._context.template = template;
|
||||
this._context.player = null;
|
||||
this._context.civ = civ;
|
||||
this._context.entity = null;
|
||||
this._context.auraTemplates = auraTemplates;
|
||||
this._context.resources = resources;
|
||||
|
|
@ -254,12 +258,14 @@ class TemplateHelper
|
|||
* @param {Object} auraTemplates - In the form of { key: { "auraName": "", "auraDescription": "" } }.
|
||||
* @param {Object} resources - An instance of the Resources class.
|
||||
* @param {number} player - ID of the target player.
|
||||
* @param {string} playerCiv - Civ of the target player.
|
||||
* @param {string[]} [components] - An array of components to process, if undefined all components are processed.
|
||||
*/
|
||||
computeDataFromPlayer(template, auraTemplates, resources, player, components)
|
||||
computeDataFromPlayer(template, auraTemplates, resources, player, playerCiv, components)
|
||||
{
|
||||
this._context.template = template;
|
||||
this._context.player = player;
|
||||
this._context.civ = playerCiv;
|
||||
this._context.entity = null;
|
||||
this._context.auraTemplates = auraTemplates;
|
||||
this._context.resources = resources;
|
||||
|
|
@ -269,6 +275,32 @@ class TemplateHelper
|
|||
return this._computeModifiedTemplateData(template, components);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull basic information from a template and to it apply all modifiers registered (in the simulation) to a given entity or
|
||||
* its owning player.
|
||||
* Must only be called in simulation context.
|
||||
*
|
||||
* @param {Object} template - A valid template as returned by the template loader.
|
||||
* @param {Object} auraTemplates - In the form of { key: { "auraName": "", "auraDescription": "" } }.
|
||||
* @param {Object} resources - An instance of the Resources class.
|
||||
* @param {number} entity - ID of the target entity.
|
||||
* @param {string} ownerCiv - Civ of the target entity's owner.
|
||||
* @param {undefined|string[]} components - An array of components to process, if undefined all components are processed.
|
||||
*/
|
||||
computeDataFromEntity(template, auraTemplates, resources, entity, ownerCiv, components)
|
||||
{
|
||||
this._context.template = template;
|
||||
this._context.player = null;
|
||||
this._context.civ = ownerCiv;
|
||||
this._context.entity = entity;
|
||||
this._context.auraTemplates = auraTemplates;
|
||||
this._context.resources = resources;
|
||||
this._context.customModifiers = null;
|
||||
this._context.applyValueModifications = this._applyEntityModifications.bind(this);
|
||||
|
||||
return this._computeModifiedTemplateData(template, components);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the manually-passed ("custom") modifiers to a given value.
|
||||
*/
|
||||
|
|
@ -289,19 +321,29 @@ class TemplateHelper
|
|||
return ApplyValueModificationsToTemplate(modKey, currentValue, this._context.player, this._context.template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply all modifiers of the current entity and of its owner to a given value.
|
||||
*/
|
||||
_applyEntityModifications(currentValue, modKey)
|
||||
{
|
||||
return ApplyValueModificationsToEntity(modKey, currentValue, this._context.entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a given value from the current template, and apply the current modifications to it.
|
||||
* Only call this for values that can theoretically be modified (i.e. the simulation supports it), like attack damge or max HP.
|
||||
* Static, unmodifiable values (like turret points or footprint size) are better read from the template directly.
|
||||
*/
|
||||
_getModifiedValue(valuePath, modKey, defaultValue = 0)
|
||||
_getModifiedValue(valuePath, modKey, isNumeric = true, defaultValue = 0)
|
||||
{
|
||||
let currentValue = this._context.template;
|
||||
for (const property of valuePath.split("/"))
|
||||
currentValue = currentValue[property] || defaultValue;
|
||||
|
||||
return isNumeric ?
|
||||
// Using .toFixed() to get around spidermonkey's treatment of numbers (3 * 1.1 = 3.3000000000000003 for instance).
|
||||
return +this._context.applyValueModifications(+currentValue, modKey || valuePath).toFixed(8);
|
||||
+this._context.applyValueModifications(+currentValue, modKey || valuePath).toFixed(8) :
|
||||
this._context.applyValueModifications(currentValue, modKey || valuePath);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -348,8 +390,14 @@ class TemplateHelper
|
|||
/**
|
||||
* The methods performing the core logic, pulling and processing the values of the template's individual
|
||||
* components.
|
||||
* The structure of each component's data has to be coordinated with the one in EntityStateRetriever, so that they
|
||||
* can be merged into one.
|
||||
*/
|
||||
_componentHandlers = {
|
||||
"AlertRaiser": (alertRaiser, ret) =>
|
||||
{
|
||||
ret.alertRaiser = { "classes": alertRaiser.List._string };
|
||||
},
|
||||
"Attack": (attack, ret) =>
|
||||
{
|
||||
ret.attack = {};
|
||||
|
|
@ -430,14 +478,23 @@ class TemplateHelper
|
|||
this._getModifiedValue("BuildRestrictions/Distance/MaxDistance");
|
||||
}
|
||||
},
|
||||
"Builder": (builder, ret) =>
|
||||
{
|
||||
ret.builder = true;
|
||||
},
|
||||
"BuildingAI": (buildingAI, ret) =>
|
||||
{
|
||||
ret.buildingAI = {
|
||||
"defaultArrowCount": Math.round(this._getModifiedValue("BuildingAI/DefaultArrowCount")),
|
||||
"maxArrowCount": Math.round(this._getModifiedValue("BuildingAI/MaxArrowCount")),
|
||||
"garrisonArrowMultiplier": this._getModifiedValue("BuildingAI/GarrisonArrowMultiplier"),
|
||||
"maxArrowCount": Math.round(this._getModifiedValue("BuildingAI/MaxArrowCount"))
|
||||
"garrisonArrowClasses": buildingAI.GarrisonArrowClasses?.split(/\s+/)
|
||||
};
|
||||
},
|
||||
"Capturable": (capturable, ret) =>
|
||||
{
|
||||
ret.maxCapturePoints = Math.round(this._getModifiedValue("Capturable/CapturePoints"));
|
||||
},
|
||||
"Cost": (cost, ret) =>
|
||||
{
|
||||
ret.cost = {};
|
||||
|
|
@ -482,6 +539,7 @@ class TemplateHelper
|
|||
"GarrisonHolder": (garrisonHolder, ret) =>
|
||||
{
|
||||
ret.garrisonHolder = {
|
||||
"allowedClasses": this._getModifiedValue("GarrisonHolder/List/_string", null, false, ""),
|
||||
"buffHeal": this._getModifiedValue("GarrisonHolder/BuffHeal"),
|
||||
"capacity": this._getModifiedValue("GarrisonHolder/Max")
|
||||
};
|
||||
|
|
@ -491,7 +549,9 @@ class TemplateHelper
|
|||
ret.heal = {
|
||||
"health": this._getModifiedValue("Heal/Health"),
|
||||
"range": this._getModifiedValue("Heal/Range"),
|
||||
"interval": this._getModifiedValue("Heal/Interval")
|
||||
"interval": this._getModifiedValue("Heal/Interval"),
|
||||
"unhealableClasses": heal.UnhealableClasses._string || "",
|
||||
"healableClasses": heal.HealableClasses._string || ""
|
||||
};
|
||||
},
|
||||
"Health": (health, ret) =>
|
||||
|
|
@ -500,6 +560,7 @@ class TemplateHelper
|
|||
},
|
||||
"Identity": (identity, ret) =>
|
||||
{
|
||||
const allClasses = GetIdentityClasses(identity);
|
||||
ret.selectionGroupName = identity.SelectionGroupName;
|
||||
ret.name = {
|
||||
"specific": (identity.SpecificName || identity.GenericName),
|
||||
|
|
@ -507,10 +568,14 @@ class TemplateHelper
|
|||
};
|
||||
ret.icon = identity.Icon;
|
||||
ret.tooltip = identity.Tooltip;
|
||||
ret.identityClasses = allClasses;
|
||||
ret.visibleIdentityClasses = GetVisibleIdentityClasses(identity);
|
||||
ret.nativeCiv = identity.Civ;
|
||||
ret.requirements = identity.Requirements;
|
||||
ret.rank = identity.Rank;
|
||||
ret.undeletable = identity.Undeletable;
|
||||
ret.enablesBartering = allClasses.includes("Barter") && !this._context.template.Foundation;
|
||||
|
||||
},
|
||||
"Loot": (loot, ret) =>
|
||||
{
|
||||
|
|
@ -520,6 +585,19 @@ class TemplateHelper
|
|||
ret.loot[type] =
|
||||
this._getModifiedValue("Loot/" + type);
|
||||
},
|
||||
"Market": (market, ret) =>
|
||||
{
|
||||
const tradeTypes = market.TradeType.split(/\s+/);
|
||||
|
||||
ret.market = {
|
||||
"land": tradeTypes.includes("land"),
|
||||
"naval": tradeTypes.includes("naval")
|
||||
};
|
||||
},
|
||||
"Mirage": (mirage, ret) =>
|
||||
{
|
||||
ret.mirage = true;
|
||||
},
|
||||
"Obstruction": (obstruction, ret) =>
|
||||
{
|
||||
ret.obstruction = {
|
||||
|
|
@ -565,6 +643,12 @@ class TemplateHelper
|
|||
"bonus": this._getModifiedValue("Population/Bonus")
|
||||
};
|
||||
},
|
||||
"Promotion": (promotion, ret) =>
|
||||
{
|
||||
ret.promotion = {
|
||||
"req": this._getModifiedValue("Promotion/RequiredXp")
|
||||
};
|
||||
},
|
||||
"Researcher": (researcher, ret) =>
|
||||
{
|
||||
ret.researcher = {
|
||||
|
|
@ -573,7 +657,7 @@ class TemplateHelper
|
|||
|
||||
for (const res of this._context.resources.GetCodes().concat(["time"]))
|
||||
ret.researcher.techCostMultiplier[res] =
|
||||
this._getModifiedValue("Researcher/TechCostMultiplier/" + res, null, 1);
|
||||
this._getModifiedValue("Researcher/TechCostMultiplier/" + res, null, true, 1);
|
||||
},
|
||||
"Resistance": (resistance, ret) =>
|
||||
{
|
||||
|
|
@ -621,6 +705,20 @@ class TemplateHelper
|
|||
ret.resourceGatherRates[type] =
|
||||
this._getModifiedValue("ResourceGatherer/Rates/" + type) * baseSpeed;
|
||||
},
|
||||
"ResourceSupply": (resourceSupply, ret) =>
|
||||
{
|
||||
const i = resourceSupply.Type.indexOf('.');
|
||||
ret.resourceSupply = {
|
||||
"isInfinite": !isFinite(+resourceSupply.Max),
|
||||
"max": this._getModifiedValue("ResourceSupply/Max"),
|
||||
"type": {
|
||||
"generic": resourceSupply.Type.slice(0, i),
|
||||
"specific": resourceSupply.Type.substring(i + 1)
|
||||
},
|
||||
"killBeforeGather": resourceSupply.KillBeforeGather === "true",
|
||||
"maxGatherers": +resourceSupply.MaxGatherers
|
||||
};
|
||||
},
|
||||
"ResourceTrickle": (resourceTrickle, ret) =>
|
||||
{
|
||||
ret.resourceTrickle = {
|
||||
|
|
@ -649,10 +747,20 @@ class TemplateHelper
|
|||
ret.treasure.resources[resource] =
|
||||
this._getModifiedValue("Treasure/Resources/" + resource);
|
||||
},
|
||||
"TreasureCollector": (treasureCollector, ret) =>
|
||||
{
|
||||
ret.treasureCollector = true;
|
||||
},
|
||||
"TurretHolder": (turretHolder, ret) =>
|
||||
{
|
||||
ret.turretHolder = {
|
||||
"turretPoints": turretHolder.TurretPoints
|
||||
"numTurretPoints": Object.keys(turretHolder.TurretPoints).length
|
||||
};
|
||||
},
|
||||
"UnitAI": (unitAI, ret) =>
|
||||
{
|
||||
ret.unitAI = {
|
||||
"formations": unitAI.Formations?._string?.split(/\s+/) || []
|
||||
};
|
||||
},
|
||||
"UnitMotion": (unitMotion, ret) =>
|
||||
|
|
@ -681,7 +789,7 @@ class TemplateHelper
|
|||
},
|
||||
"Upgrade": (upgrade, ret) =>
|
||||
{
|
||||
ret.upgrades = [];
|
||||
ret.upgrade = { "options": [] };
|
||||
|
||||
for (const upgradeName in upgrade)
|
||||
{
|
||||
|
|
@ -697,8 +805,10 @@ class TemplateHelper
|
|||
cost.time =
|
||||
this._getModifiedValue("Upgrade/" + upgradeName + "/Time", "Upgrade/Time");
|
||||
|
||||
ret.upgrades.push({
|
||||
"entity": upgr.Entity,
|
||||
ret.upgrade.options.push({
|
||||
"entity": upgr.Entity
|
||||
.replace(/\{civ\}/g, this._context.civ)
|
||||
.replace(/\{native\}/g, this._context.template.Identity.Civ),
|
||||
"tooltip": upgr.Tooltip,
|
||||
cost,
|
||||
"icon": upgr.Icon,
|
||||
|
|
|
|||
|
|
@ -619,7 +619,7 @@ function getTurretsTooltip(template)
|
|||
return "";
|
||||
return sprintf(translate("%(label)s: %(turretsLimit)s"), {
|
||||
"label": headerFont(translate("Turret Positions")),
|
||||
"turretsLimit": Object.keys(template.turretHolder.turretPoints).length
|
||||
"turretsLimit": template.turretHolder.numTurretPoints
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,8 @@ class TemplateParser
|
|||
return null;
|
||||
|
||||
const template = this.TemplateLoader.loadEntityTemplate(templateName, civCode);
|
||||
const parsed = g_TemplateHelper.computeDataFromModifiers(template, this.TemplateLoader.auraData, g_ResourceData, this.modifiers[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;
|
||||
|
|
@ -103,15 +104,15 @@ class TemplateParser
|
|||
"amount": template.ResourceSupply.Max,
|
||||
};
|
||||
|
||||
if (parsed.upgrades)
|
||||
parsed.upgrades = this.getActualUpgradeData(parsed.upgrades, civCode);
|
||||
if (parsed.upgrade)
|
||||
parsed.upgrade.options = this.getActualUpgradeData(parsed.upgrade.options, civCode);
|
||||
|
||||
if (parsed.wallSet)
|
||||
{
|
||||
parsed.wallset = {};
|
||||
|
||||
if (!parsed.upgrades)
|
||||
parsed.upgrades = [];
|
||||
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);
|
||||
|
|
@ -135,8 +136,8 @@ class TemplateParser
|
|||
for (const research of wPart.production.techs)
|
||||
parsed.production.techs.push(research);
|
||||
|
||||
if (wPart.upgrades)
|
||||
Array.prototype.push.apply(parsed.upgrades, wPart.upgrades);
|
||||
if (wPart.upgrade)
|
||||
Array.prototype.push.apply(parsed.upgrade.options, wPart.upgrade.options);
|
||||
|
||||
if (["gate", "tower"].indexOf(wSegm) != -1)
|
||||
continue;
|
||||
|
|
@ -262,9 +263,8 @@ class TemplateParser
|
|||
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] || {});
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ var g_Page;
|
|||
function GetTemplateData(templateName)
|
||||
{
|
||||
const template = g_Page.TemplateLoader.loadEntityTemplate(templateName, g_Page.activeCiv);
|
||||
return g_TemplateHelper.computeDataFromModifiers(template, g_Page.TemplateLoader.auraData, g_ResourceData, g_Page.TemplateParser.getModifiers(g_Page.activeCiv));
|
||||
return g_TemplateHelper.computeDataFromModifiers(template, g_Page.TemplateLoader.auraData, g_ResourceData,
|
||||
g_Page.TemplateParser.getModifiers(g_Page.activeCiv), g_Page.activeCiv);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@ class ProductionRowManager
|
|||
this.productionRows[rowIdx].drawIcon(prod, civCode);
|
||||
}
|
||||
|
||||
if (template.upgrades)
|
||||
for (const upgrade of template.upgrades)
|
||||
if (template.upgrade)
|
||||
for (const upgrade of template.upgrade.options)
|
||||
{
|
||||
let pIdx = 0;
|
||||
if (this.sortProductionsByPhase)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class TrainerSection
|
|||
for (const unitCode of units.keys())
|
||||
{
|
||||
const unitTemplate = this.page.TemplateParser.getEntity(unitCode, civCode);
|
||||
if (!unitTemplate.production.units.length && !unitTemplate.production.techs.length && !unitTemplate.upgrades)
|
||||
if (!unitTemplate.production.units.length && !unitTemplate.production.techs.length && !unitTemplate.upgrade?.options)
|
||||
continue;
|
||||
|
||||
if (count > this.trainerBoxes.length)
|
||||
|
|
|
|||
|
|
@ -105,8 +105,8 @@ class ViewerPage extends ReferencePage
|
|||
}
|
||||
}
|
||||
|
||||
if (this.currentTemplate.upgrades)
|
||||
this.currentTemplate.upgradeListOfNames = this.currentTemplate.upgrades.map(upgrade =>
|
||||
if (this.currentTemplate.upgrade)
|
||||
this.currentTemplate.upgradeListOfNames = this.currentTemplate.upgrade.options.map(upgrade =>
|
||||
getEntityNames(upgrade.name ? upgrade : this.TemplateParser.getEntity(upgrade.entity, this.activeCiv))
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class PanelEntity
|
|||
this.panelEntButton.hidden = false;
|
||||
|
||||
const entityState = GetEntityState(entityID);
|
||||
const template = GetTemplateData(entityState.template);
|
||||
const template = GetTemplateData(entityState.templateName);
|
||||
this.nameTooltip = setStringTags(g_SpecificNamesPrimary ? template.name.specific : template.name.generic, this.NameTags) + "\n";
|
||||
|
||||
Engine.GetGUIObjectByName("panelEntityHealthSection[" + buttonID + "]").hidden = !entityState.hitpoints;
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ class PanelEntityManager
|
|||
const entityState = GetEntityState(entityID);
|
||||
|
||||
const orderKey = this.entityOrder.findIndex(entClass =>
|
||||
entityState.identity.classes.indexOf(entClass) != -1);
|
||||
entityState.identityClasses.indexOf(entClass) != -1);
|
||||
|
||||
// Sort depending on given order
|
||||
const insertPos = this.handlers.reduce(
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class DeveloperOverlayEntityState
|
|||
const entState = GetEntityState(selection[0]);
|
||||
if (entState)
|
||||
{
|
||||
const template = GetTemplateData(entState.template, entState.player);
|
||||
const template = GetTemplateData(entState.templateName, entState.player);
|
||||
text += "\n\nentity: {\n";
|
||||
for (const k in entState)
|
||||
text += " " + k + ":" + uneval(entState[k]) + "\n";
|
||||
|
|
|
|||
|
|
@ -1157,18 +1157,18 @@ function handleInputAfterGui(ev)
|
|||
|
||||
if (ev.clicks == 2)
|
||||
{
|
||||
templateToMatch = GetEntityState(clickedEntity).identity.selectionGroupName;
|
||||
templateToMatch = GetEntityState(clickedEntity).selectionGroupName;
|
||||
if (templateToMatch)
|
||||
matchRank = false;
|
||||
else
|
||||
// No selection group name defined, so fall back to exact match.
|
||||
templateToMatch = GetEntityState(clickedEntity).template;
|
||||
templateToMatch = GetEntityState(clickedEntity).templateName;
|
||||
|
||||
}
|
||||
else
|
||||
// Triple click
|
||||
// Select units matching exact template name (same rank).
|
||||
templateToMatch = GetEntityState(clickedEntity).template;
|
||||
templateToMatch = GetEntityState(clickedEntity).templateName;
|
||||
|
||||
// TODO: Should we handle "control all units" here as well?
|
||||
ents = Engine.PickSimilarPlayerEntities(templateToMatch, showOffscreen, matchRank, false);
|
||||
|
|
@ -1990,7 +1990,7 @@ function findIdleUnit(classes)
|
|||
Engine.CameraMoveTo(entityState.position.x, entityState.position.z);
|
||||
|
||||
// Move the idle class index to the first class an idle unit was found for.
|
||||
const indexChange = data.idleClasses.findIndex(elem => MatchesClassList(entityState.identity.classes, elem));
|
||||
const indexChange = data.idleClasses.findIndex(elem => MatchesClassList(entityState.identityClasses, elem));
|
||||
currIdleClassIndex = (currIdleClassIndex + indexChange) % classes.length;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ EntityGroups.prototype.add = function(ents)
|
|||
if (!entState)
|
||||
continue;
|
||||
|
||||
var templateName = entState.template;
|
||||
var templateName = entState.templateName;
|
||||
var key = GetTemplateData(templateName, entState.player).selectionGroupName || templateName;
|
||||
|
||||
// Group the ents by player and template
|
||||
|
|
@ -235,7 +235,7 @@ EntitySelection.prototype.getTemplateNames = function()
|
|||
{
|
||||
const entState = GetEntityState(ent);
|
||||
if (entState)
|
||||
templateNames.push(entState.template);
|
||||
templateNames.push(entState.templateName);
|
||||
}
|
||||
return templateNames;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ function updateGarrisonHealthBar(entState, selection)
|
|||
// Fills out information that most entities have
|
||||
function displaySingle(entState)
|
||||
{
|
||||
const template = GetTemplateData(entState.template);
|
||||
const template = GetTemplateData(entState.templateName);
|
||||
|
||||
const primaryName = g_SpecificNamesPrimary ? template.name.specific : template.name.generic;
|
||||
let secondaryName;
|
||||
|
|
@ -75,13 +75,13 @@ function displaySingle(entState)
|
|||
playerName = sprintf(translate("\\[OFFLINE] %(player)s"), { "player": playerName });
|
||||
|
||||
// Rank
|
||||
if (entState.identity && entState.identity.rank && entState.identity.classes)
|
||||
if (entState.rank && entState.identityClasses)
|
||||
{
|
||||
const rankObj = GetTechnologyData(entState.identity.rankTechName, playerState.civ);
|
||||
const rankTooltip = (entState.rank && GetTechnologyData("unit_" + entState.rank.toLowerCase())?.tooltip) || "";
|
||||
Engine.GetGUIObjectByName("rankIcon").tooltip = sprintf(translate("%(rank)s Rank"), {
|
||||
"rank": translateWithContext("Rank", entState.identity.rank)
|
||||
}) + (rankObj ? "\n" + rankObj.tooltip : "");
|
||||
Engine.GetGUIObjectByName("rankIcon").sprite = "stretched:session/icons/ranks/" + entState.identity.rank + ".png";
|
||||
"rank": translateWithContext("Rank", entState.rank)
|
||||
}) + rankTooltip;
|
||||
Engine.GetGUIObjectByName("rankIcon").sprite = "stretched:session/icons/ranks/" + entState.rank + ".png";
|
||||
Engine.GetGUIObjectByName("rankIcon").hidden = false;
|
||||
}
|
||||
else
|
||||
|
|
@ -339,7 +339,7 @@ function displaySingle(entState)
|
|||
|
||||
iconBorder.onPressRight = () =>
|
||||
{
|
||||
showTemplateDetails(entState.template, playerState.civ);
|
||||
showTemplateDetails(entState.templateName, playerState.civ);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ g_SelectionPanels.Barter = {
|
|||
"getItems": function(unitEntStates)
|
||||
{
|
||||
// If more than `rowLength` resources, don't display icons.
|
||||
if (unitEntStates.every(state => !state.isBarterMarket) || g_ResourceData.GetBarterableCodes().length > this.rowLength)
|
||||
if (unitEntStates.every(state => !state.enablesBartering) || g_ResourceData.GetBarterableCodes().length > this.rowLength)
|
||||
return [];
|
||||
return g_ResourceData.GetBarterableCodes();
|
||||
},
|
||||
|
|
@ -344,13 +344,13 @@ g_SelectionPanels.Garrison = {
|
|||
{
|
||||
const entState = GetEntityState(data.item.ents[0]);
|
||||
|
||||
const template = GetTemplateData(entState.template, entState.player);
|
||||
const template = GetTemplateData(entState.templateName, entState.player);
|
||||
if (!template)
|
||||
return false;
|
||||
|
||||
data.button.onPress = function()
|
||||
{
|
||||
unloadTemplate(template.selectionGroupName || entState.template, entState.player);
|
||||
unloadTemplate(template.selectionGroupName || entState.templateName, entState.player);
|
||||
};
|
||||
|
||||
data.countDisplay.caption = data.item.ents.length > 1 ? data.item.ents.length : "";
|
||||
|
|
@ -1048,7 +1048,7 @@ g_SelectionPanels.Research = {
|
|||
{
|
||||
showTemplateDetails(
|
||||
t,
|
||||
GetTemplateData(baseData.unitEntStates.find(state => state.id == baseData.item.researchFacilityId).template, state.player).nativeCiv
|
||||
GetTemplateData(baseData.unitEntStates.find(state => state.id == baseData.item.researchFacilityId).templateName, state.player).nativeCiv
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -1114,7 +1114,7 @@ g_SelectionPanels.Selection = {
|
|||
"setupButton": function(data)
|
||||
{
|
||||
const entState = GetEntityState(data.item.ents[0]);
|
||||
const template = GetTemplateData(entState.template, entState.player);
|
||||
const template = GetTemplateData(entState.templateName, entState.player);
|
||||
if (!template)
|
||||
return false;
|
||||
|
||||
|
|
@ -1334,10 +1334,10 @@ g_SelectionPanels.Upgrade = {
|
|||
"getItems": function(unitEntStates)
|
||||
{
|
||||
// Interface becomes complicated with multiple different units and this is meant per-entity, so prevent it if the selection has multiple different units.
|
||||
if (unitEntStates.some(state => state.template != unitEntStates[0].template))
|
||||
if (unitEntStates.some(state => state.templateName != unitEntStates[0].templateName))
|
||||
return false;
|
||||
|
||||
return unitEntStates[0].upgrade && unitEntStates[0].upgrade.upgrades;
|
||||
return unitEntStates[0].upgrade?.options;
|
||||
},
|
||||
"setupButton": function(data)
|
||||
{
|
||||
|
|
@ -1355,7 +1355,7 @@ g_SelectionPanels.Upgrade = {
|
|||
});
|
||||
|
||||
const limits = getEntityLimitAndCount(data.playerState, data.item.entity);
|
||||
const upgradingEntStates = data.unitEntStates.filter(state => state.upgrade.template == data.item.entity);
|
||||
const upgradingEntStates = data.unitEntStates.filter(state => state.upgrade.templateName == data.item.entity);
|
||||
|
||||
const upgradableEntStates = data.unitEntStates.filter(state =>
|
||||
!state.upgrade.progress &&
|
||||
|
|
@ -1422,7 +1422,7 @@ g_SelectionPanels.Upgrade = {
|
|||
};
|
||||
|
||||
if (!requirementsMet || limits.canBeAddedCount == 0 &&
|
||||
!upgradableEntStates.some(state => hasSameRestrictionCategory(data.item.entity, state.template, state.player)))
|
||||
!upgradableEntStates.some(state => hasSameRestrictionCategory(data.item.entity, state.templateName, state.player)))
|
||||
{
|
||||
data.button.enabled = false;
|
||||
modifier = "color:0 0 0 127:grayscale:";
|
||||
|
|
|
|||
|
|
@ -126,8 +126,8 @@ var g_ShowAllStatusBars = false;
|
|||
* Cache of simulation state and template data (apart from TechnologyData, updated on every simulation update).
|
||||
*/
|
||||
var g_SimState;
|
||||
var g_EntityStates = {};
|
||||
var g_TemplateData = {};
|
||||
var g_EntityStates = new Map();
|
||||
var g_TemplateData = new Map();
|
||||
var g_TechnologyData = {};
|
||||
|
||||
var g_ResourceData = new Resources();
|
||||
|
|
@ -189,59 +189,149 @@ function GetSimState()
|
|||
return g_SimState;
|
||||
}
|
||||
|
||||
function GetMultipleEntityStates(ents)
|
||||
/**
|
||||
* Piece together the entire entity state, either from g_TemplateData or the data provided (one of which
|
||||
* should always be possible) and then cache it in g_EntityStates.
|
||||
* Intended for internal use only.
|
||||
*/
|
||||
function _processNewEntityState(data, entId)
|
||||
{
|
||||
if (!ents.length)
|
||||
if (!data?.dynamicState)
|
||||
{
|
||||
g_EntityStates.set(entId, null);
|
||||
return null;
|
||||
const entityStates = Engine.GuiInterfaceCall("GetMultipleEntityStates", ents);
|
||||
for (const item of entityStates)
|
||||
g_EntityStates[item.entId] = item.state && deepfreeze(item.state);
|
||||
return entityStates;
|
||||
}
|
||||
|
||||
// Certain parts (the template data) of an entity state are "predictable" and can therefore be reused between
|
||||
// entities with the same template. What one has to account for, however, is that values of the template data
|
||||
// can be modified. This happens on two different levels:
|
||||
// - Firstly, player-global modifications, which apply to all entities owned by that player. This includes stuff
|
||||
// like bonuses from researched techs, civs bonuses, team bonuses.
|
||||
// - Secondly, entity-local modifications, which apply to individual entities. This includes buffs or debuffs
|
||||
// from status effects or auras (of other entities).
|
||||
//
|
||||
// To take advantage of this, the whole entity state consists of three parts that we need to combine here:
|
||||
// 1. The template data of the entity's template, which only has the player-global modifications applied. It is
|
||||
// the same for all entities with the same template and owner, so it cached and reused when possible.
|
||||
// 2. The modified template data of the entity, which can contain a subset of the template data's values,
|
||||
// overwriting them. It has both the player-global and entity-local modifications applied, but only contains the
|
||||
// values that actually differ from its template data. It is therefore the same for all entities with the same
|
||||
// owner, template, and modifications ID, so it cached and reused when possible.
|
||||
// 3. the dynamic state, which differs from entity to entity and therefore isn't cached.
|
||||
|
||||
const state = data.dynamicState;
|
||||
|
||||
// This is the only two-level-deep value of the dynamic state supposed to overwrite the corresponding value in the
|
||||
// (modified) template data, so we have to treat it explicitly here, which is quite ugly.
|
||||
const elevationAdaptedRange = state.attack?.Ranged?.elevationAdaptedRange;
|
||||
|
||||
const playerCache = g_TemplateData.get(state.player);
|
||||
let templateData = data.templateData;
|
||||
if (templateData)
|
||||
{
|
||||
translateObjectKeys(templateData, ["specific", "generic", "tooltip"]);
|
||||
playerCache.set(state.templateName, deepfreeze(templateData));
|
||||
}
|
||||
else
|
||||
templateData = playerCache.get(state.templateName);
|
||||
|
||||
let modifiedTemplateData;
|
||||
if (data.modificationsID)
|
||||
{
|
||||
const cacheKey = `${state.templateName}: ${data.modificationsID}`;
|
||||
modifiedTemplateData = data.modifiedTemplateData;
|
||||
if (modifiedTemplateData)
|
||||
playerCache.set(cacheKey, deepfreeze(modifiedTemplateData));
|
||||
else
|
||||
modifiedTemplateData = playerCache.get(cacheKey);
|
||||
}
|
||||
|
||||
for (const key in templateData)
|
||||
{
|
||||
const templateVal = modifiedTemplateData?.[key] || templateData[key];
|
||||
const stateVal = state[key];
|
||||
// Individual entries of dynamic state don't overlap with the ones in the (modified) template data, but they are
|
||||
// organised into child objects. So we are only combining the two here, not actually overwriting anything.
|
||||
// (elevationAdaptedRange is the only exception)
|
||||
if (typeof stateVal == "object")
|
||||
Object.assign(stateVal, templateVal);
|
||||
else
|
||||
state[key] = templateVal;
|
||||
}
|
||||
|
||||
if (elevationAdaptedRange)
|
||||
state.attack.Ranged.elevationAdaptedRange = elevationAdaptedRange;
|
||||
|
||||
g_EntityStates.set(entId, deepfreeze(state));
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull states of multiple entities from the simulation and cache them.
|
||||
* This is faster than doing it for each individually.
|
||||
*/
|
||||
function PreloadMultipleEntityStates(ents)
|
||||
{
|
||||
const newEnts = ents.filter(ent => !g_EntityStates.has(ent));
|
||||
if (!newEnts.length)
|
||||
return;
|
||||
|
||||
const items = Engine.GuiInterfaceCall("GetMultipleEntityStates", newEnts);
|
||||
for (let i = 0; i < items.length; i++)
|
||||
_processNewEntityState(items[i], newEnts[i]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current state of a given entity. The data is pulled from the simulation.
|
||||
*
|
||||
* An entity state is like a subtype of template data (returned by GetTemplateData), it contains
|
||||
* all of the values present in the latter, plus some more. Substitutability is given, it can be
|
||||
* used everywhere where a template is expected.
|
||||
* However, even the values also in present template data could be modified entity-locally, by
|
||||
* stuff like auras and status effects. For example, two spearman of the same player can have
|
||||
* different attack damage with both different from the value in the spearman template data.
|
||||
* To obtain the values that are consistent player-wide (which a new entity with that template of
|
||||
* that player would have) call GetTemplateData(GetEntityState(entId).templateName)
|
||||
*
|
||||
* The state is null, if the ID is undefined, invalid or no entity with the ID exists (anymore).
|
||||
*
|
||||
* Note about performance: Calling this the first time for an entity in a simulation turn is rather
|
||||
* expensive, but every time after that extremely cheap, since the states are cached.
|
||||
*/
|
||||
function GetEntityState(entId)
|
||||
{
|
||||
if (!entId || entId == INVALID_ENTITY)
|
||||
return null;
|
||||
|
||||
if (!g_EntityStates[entId])
|
||||
{
|
||||
const entityState = Engine.GuiInterfaceCall("GetEntityState", entId);
|
||||
g_EntityStates[entId] = entityState && deepfreeze(entityState);
|
||||
}
|
||||
let state = g_EntityStates.get(entId);
|
||||
// The cached state can very well be null, we only need to retrieve it from the simulation if it's undefined
|
||||
// (i.e not yet present in the cache at all)
|
||||
if (state === undefined)
|
||||
state = _processNewEntityState(Engine.GuiInterfaceCall("GetEntityState", entId), entId);
|
||||
|
||||
return g_EntityStates[entId];
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns template data calling GetTemplateData defined in GuiInterface.js
|
||||
* and deepfreezing returned object.
|
||||
* @param {string} templateName - Data of this template will be returned.
|
||||
* @param {number|undefined} player - Modifications of this player will be applied to the template.
|
||||
* If undefined, id of player calling this method will be used.
|
||||
* Returns template data of a given template accounting for player-wide modifications such
|
||||
* as researched techs or team bonuses.
|
||||
* @param {string} templateName - Name of the template.
|
||||
* @param {number} [player] - The player whose modifications to apply to the data.
|
||||
* If undefined, the currently viewed player is be used.
|
||||
*/
|
||||
function GetTemplateData(templateName, player)
|
||||
{
|
||||
const targetPlayer = player || g_ViewedPlayer;
|
||||
let cache = g_TemplateData[targetPlayer];
|
||||
if (!cache)
|
||||
{
|
||||
cache = {};
|
||||
g_TemplateData[targetPlayer] = cache;
|
||||
}
|
||||
const targetPlayer = player || (g_ViewedPlayer >= 0 ? g_ViewedPlayer : 0);
|
||||
const playerCache = g_TemplateData.get(targetPlayer);
|
||||
|
||||
let templateData = cache[templateName];
|
||||
let templateData = playerCache.get(templateName);
|
||||
if (!templateData)
|
||||
{
|
||||
templateData = Engine.GuiInterfaceCall("GetTemplateData", { "templateName": templateName, "player": targetPlayer });
|
||||
translateObjectKeys(templateData, ["specific", "generic", "tooltip"]);
|
||||
deepfreeze(templateData);
|
||||
cache[templateName] = templateData;
|
||||
playerCache.set(templateName, deepfreeze(templateData));
|
||||
}
|
||||
|
||||
return templateData;
|
||||
}
|
||||
|
||||
|
|
@ -287,6 +377,9 @@ async function init(initData, hotloadData)
|
|||
restoreSavedGameData(initData.savedGUIData);
|
||||
}
|
||||
|
||||
// Necessary when hotloading in order to repopulate g_TemplateData
|
||||
Engine.GuiInterfaceCall("ResetEntityStateRetriever");
|
||||
|
||||
if (g_InitAttributes.campaignData)
|
||||
g_CampaignSession = new CampaignSession(g_InitAttributes.campaignData);
|
||||
|
||||
|
|
@ -461,6 +554,10 @@ function updatePlayerData()
|
|||
"guid": undefined, // network guid for players controlled by hosts
|
||||
"offline": g_Players[i] && !!g_Players[i].offline
|
||||
});
|
||||
|
||||
// This is done here in order to theoretically support the number of players changing.
|
||||
if (i >= g_TemplateData.size)
|
||||
g_TemplateData.set(i, new Map());
|
||||
}
|
||||
|
||||
for (const guid in g_PlayerAssignments)
|
||||
|
|
@ -641,7 +738,7 @@ function onTick()
|
|||
{
|
||||
g_Selection.dirty = false;
|
||||
// When selection changed, get the entityStates of new entities
|
||||
GetMultipleEntityStates(g_Selection.filter(entId => !g_EntityStates[entId]));
|
||||
PreloadMultipleEntityStates(g_Selection.toList());
|
||||
|
||||
for (const handler of g_EntitySelectionChangeHandlers)
|
||||
handler();
|
||||
|
|
@ -660,7 +757,7 @@ function onTick()
|
|||
|
||||
function onSimulationUpdate()
|
||||
{
|
||||
g_EntityStates = {};
|
||||
g_EntityStates.clear();
|
||||
g_SimState = undefined;
|
||||
|
||||
// Some changes may require re-rendering the selection.
|
||||
|
|
@ -673,7 +770,7 @@ function onSimulationUpdate()
|
|||
if (!GetSimState())
|
||||
return;
|
||||
|
||||
GetMultipleEntityStates(g_Selection.toList());
|
||||
PreloadMultipleEntityStates(g_Selection.toList());
|
||||
|
||||
for (const handler of g_SimulationUpdateHandlers)
|
||||
handler();
|
||||
|
|
@ -722,7 +819,7 @@ function updateGroups()
|
|||
const getCostSum = (ent) =>
|
||||
{
|
||||
const entState = GetEntityState(ent);
|
||||
const cost = GetTemplateData(entState.template, entState.player).cost;
|
||||
const cost = GetTemplateData(entState.templateName, entState.player).cost;
|
||||
return cost ? Object.keys(cost).map(key => cost[key]).reduce((sum, cur) => sum + cur) : 0;
|
||||
};
|
||||
|
||||
|
|
@ -744,7 +841,7 @@ function updateGroups()
|
|||
if (pre.ents.length == cur.ents.length)
|
||||
return getCostSum(pre.ents[0]) > getCostSum(cur.ents[0]) ? pre : cur;
|
||||
return pre.ents.length > cur.ents.length ? pre : cur;
|
||||
}).ents[0]).template).icon;
|
||||
}).ents[0]).templateName).icon;
|
||||
|
||||
Engine.GetGUIObjectByName("unitGroupIcon[" + i + "]").sprite =
|
||||
icon ? ("stretched:session/portraits/" + icon) : "groupsIcon";
|
||||
|
|
|
|||
|
|
@ -386,11 +386,11 @@ var g_UnitActions =
|
|||
return false;
|
||||
|
||||
const unhealableClasses = entState.heal.unhealableClasses;
|
||||
if (MatchesClassList(targetState.identity.classes, unhealableClasses))
|
||||
if (MatchesClassList(targetState.identityClasses, unhealableClasses))
|
||||
return false;
|
||||
|
||||
const healableClasses = entState.heal.healableClasses;
|
||||
if (!MatchesClassList(targetState.identity.classes, healableClasses))
|
||||
if (!MatchesClassList(targetState.identityClasses, healableClasses))
|
||||
return false;
|
||||
|
||||
return { "possible": true };
|
||||
|
|
@ -776,7 +776,7 @@ var g_UnitActions =
|
|||
return false;
|
||||
|
||||
if (!targetState.turretHolder.turretPoints.find(point =>
|
||||
!point.allowedClasses || MatchesClassList(entState.identity.classes, point.allowedClasses)))
|
||||
!point.allowedClasses || MatchesClassList(entState.identityClasses, point.allowedClasses)))
|
||||
return false;
|
||||
|
||||
const occupiedTurrets = targetState.turretHolder.turretPoints.filter(point => point.entity != null);
|
||||
|
|
@ -858,7 +858,7 @@ var g_UnitActions =
|
|||
if (targetState.garrisonHolder.occupiedSlots + extraCount > targetState.garrisonHolder.capacity)
|
||||
tooltip = coloredText(tooltip, "orange");
|
||||
|
||||
if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses))
|
||||
if (!MatchesClassList(entState.identityClasses, targetState.garrisonHolder.allowedClasses))
|
||||
return false;
|
||||
|
||||
return {
|
||||
|
|
@ -1224,7 +1224,7 @@ var g_UnitActions =
|
|||
|
||||
data.command = "gather-near-position";
|
||||
data.resourceType = resourceType;
|
||||
data.resourceTemplate = targetState.template;
|
||||
data.resourceTemplate = targetState.templateName;
|
||||
if (!targetState.speed)
|
||||
{
|
||||
data.command = "gather";
|
||||
|
|
@ -1397,12 +1397,7 @@ var g_UnitActions =
|
|||
"actionCheck": function(target, selection)
|
||||
{
|
||||
// Only show this action if all entities are marked uncontrollable.
|
||||
const playerState = g_SimState.players[g_ViewedPlayer];
|
||||
if (playerState && playerState.controlsAll || selection.some(ent =>
|
||||
{
|
||||
const entState = GetEntityState(ent);
|
||||
return entState && entState.identity && entState.identity.controllable;
|
||||
}))
|
||||
if (g_SimState.players[g_ViewedPlayer]?.controlsAll || selection.some(ent => GetEntityState(ent)?.controllable))
|
||||
return false;
|
||||
|
||||
return {
|
||||
|
|
@ -1585,7 +1580,7 @@ var g_EntityCommands =
|
|||
"getInfo": function(entStates)
|
||||
{
|
||||
const classes = ["Soldier", "Warship", "Siege", "Healer"];
|
||||
if (entStates.every(entState => !MatchesClassList(entState.identity.classes, classes)))
|
||||
if (entStates.every(entState => !MatchesClassList(entState.identityClasses, classes)))
|
||||
return false;
|
||||
return {
|
||||
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.calltoarms") +
|
||||
|
|
@ -1997,7 +1992,7 @@ function allowedPlayersCheck(entStates, validPlayers)
|
|||
function hasClass(entState, className)
|
||||
{
|
||||
// note: use the functions in globalscripts/Templates.js for more versatile matching
|
||||
return entState.identity && entState.identity.classes.indexOf(className) != -1;
|
||||
return entState?.identityClasses?.indexOf(className) != -1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2015,7 +2010,7 @@ function isUndeletable(entState)
|
|||
if (entState.capturePoints && entState.capturePoints[entState.player] < entState.maxCapturePoints / 2)
|
||||
return translate("You cannot destroy this entity as you own less than half the capture points");
|
||||
|
||||
if (!entState.identity.canDelete)
|
||||
if (entState.undeletable)
|
||||
return translate("This entity is undeletable");
|
||||
|
||||
return false;
|
||||
|
|
@ -2076,7 +2071,7 @@ function getActionInfo(action, target, selection)
|
|||
if (!entState)
|
||||
continue;
|
||||
|
||||
if (playerState && !playerState.controlsAll && !entState.identity.controllable)
|
||||
if (playerState && !playerState.controlsAll && !entState.controllable)
|
||||
continue;
|
||||
|
||||
if (g_UnitActions[action] && g_UnitActions[action].getActionInfo)
|
||||
|
|
|
|||
|
|
@ -136,8 +136,7 @@ function updateUnitCommands(entStates, supplementalDetailsPanel, commandsPanel)
|
|||
const playerState = playerStates[Engine.GetPlayerID()];
|
||||
|
||||
if (g_IsObserver || entStates.every(entState =>
|
||||
controlsPlayer(entState.player) &&
|
||||
(!entState.identity || entState.identity.controllable)) ||
|
||||
controlsPlayer(entState.player) && entState.controllable) ||
|
||||
playerState.controlsAll)
|
||||
{
|
||||
for (const guiName of g_PanelsOrder)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ function loadWallsetsFromCivData()
|
|||
function loadWallset(wallsetPath, civ)
|
||||
{
|
||||
const newWallset = { "curves": [] };
|
||||
const wallsetData = g_TemplateHelper.getBasicData(wallsetPath, {}, g_Resources, ["WallSet"]).wallSet;
|
||||
const wallsetData = g_TemplateHelper.getBasicData(wallsetPath, {}, g_Resources, civ, ["WallSet"]).wallSet;
|
||||
|
||||
for (const element in wallsetData.templates)
|
||||
if (element == "curves")
|
||||
|
|
@ -271,7 +271,7 @@ function getWallElement(element, style)
|
|||
function readyWallElement(path, civCode)
|
||||
{
|
||||
path = path.replace(/\{civ\}/g, civCode);
|
||||
const template = g_TemplateHelper.getBasicData(Engine.GetTemplate(path), null, g_Resources, ["WallPiece", "Obstruction"]);
|
||||
const template = g_TemplateHelper.getBasicData(Engine.GetTemplate(path), null, g_Resources, civCode, ["WallPiece", "Obstruction"]);
|
||||
const length = template.wallPiece ? template.wallPiece.length : template.obstruction.shape.width;
|
||||
|
||||
return deepfreeze({
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ GuiInterface.prototype.Init = function()
|
|||
this.entsRallyPointsDisplayed = [];
|
||||
this.entsWithAuraAndStatusBars = new Set();
|
||||
this.enabledVisualRangeOverlayTypes = {};
|
||||
this.entityStateRetriever = new EntityStateRetriever();
|
||||
this.selectionDirty = {};
|
||||
this.obstructionSnap = new ObstructionSnap();
|
||||
};
|
||||
|
|
@ -249,377 +250,35 @@ GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
|
|||
this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
|
||||
};
|
||||
|
||||
GuiInterface.prototype.ResetEntityStateRetriever = function(player)
|
||||
{
|
||||
this.entityStateRetriever.reset();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get common entity info, often used in the gui.
|
||||
* See helpers/EntityStateRetriever.js for details.
|
||||
* Mustn't be invoked outside of GUI script calls, in order to guarantee completeness of the data returned in future
|
||||
* calls.
|
||||
* Everywhere else, read the data from the template instead, or use g_TemplateHelper.computeDataFromPlayer.
|
||||
*/
|
||||
GuiInterface.prototype.GetTemplateData = function(player, data)
|
||||
{
|
||||
return this.entityStateRetriever.getTemplateData(data.player || player, data.templateName);
|
||||
};
|
||||
|
||||
/**
|
||||
* See helpers/EntityStateRetriever.js for details.
|
||||
* Mustn't be invoked outside of GUI script calls, in order to guarantee completeness of the data returned in future
|
||||
* calls.
|
||||
* Everywhere else, read information directly from the components instead.
|
||||
*/
|
||||
GuiInterface.prototype.GetEntityState = function(player, ent)
|
||||
{
|
||||
if (!ent)
|
||||
return null;
|
||||
|
||||
// All units must have a template; if not then it's a nonexistent entity id.
|
||||
const template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetCurrentTemplateName(ent);
|
||||
if (!template)
|
||||
return null;
|
||||
|
||||
const ret = {
|
||||
"id": ent,
|
||||
"player": INVALID_PLAYER,
|
||||
"template": template
|
||||
};
|
||||
|
||||
const cmpAuras = Engine.QueryInterface(ent, IID_Auras);
|
||||
if (cmpAuras)
|
||||
ret.auras = cmpAuras.GetDescriptions();
|
||||
|
||||
const cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
|
||||
if (cmpMirage)
|
||||
ret.mirage = true;
|
||||
|
||||
const cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
|
||||
if (cmpIdentity)
|
||||
ret.identity = {
|
||||
"rank": cmpIdentity.GetRank(),
|
||||
"rankTechName": cmpIdentity.GetRankTechName(),
|
||||
"classes": cmpIdentity.GetClassesList(),
|
||||
"selectionGroupName": cmpIdentity.GetSelectionGroupName(),
|
||||
"canDelete": !cmpIdentity.IsUndeletable(),
|
||||
"controllable": cmpIdentity.IsControllable()
|
||||
};
|
||||
|
||||
const cmpFormation = Engine.QueryInterface(ent, IID_Formation);
|
||||
if (cmpFormation)
|
||||
ret.formation = {
|
||||
"members": cmpFormation.GetMembers()
|
||||
};
|
||||
|
||||
const cmpPosition = Engine.QueryInterface(ent, IID_Position);
|
||||
if (cmpPosition && cmpPosition.IsInWorld())
|
||||
ret.position = cmpPosition.GetPosition();
|
||||
|
||||
const cmpHealth = QueryMiragedInterface(ent, IID_Health);
|
||||
if (cmpHealth)
|
||||
{
|
||||
ret.hitpoints = cmpHealth.GetHitpoints();
|
||||
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
|
||||
ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.IsInjured();
|
||||
ret.needsHeal = !cmpHealth.IsUnhealable();
|
||||
}
|
||||
|
||||
const cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
|
||||
if (cmpCapturable)
|
||||
{
|
||||
ret.capturePoints = cmpCapturable.GetCapturePoints();
|
||||
ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
|
||||
}
|
||||
|
||||
const cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
|
||||
if (cmpBuilder)
|
||||
ret.builder = true;
|
||||
|
||||
const cmpMarket = QueryMiragedInterface(ent, IID_Market);
|
||||
if (cmpMarket)
|
||||
ret.market = {
|
||||
"land": cmpMarket.HasType("land"),
|
||||
"naval": cmpMarket.HasType("naval")
|
||||
};
|
||||
|
||||
const cmpPack = Engine.QueryInterface(ent, IID_Pack);
|
||||
if (cmpPack)
|
||||
ret.pack = {
|
||||
"packed": cmpPack.IsPacked(),
|
||||
"progress": cmpPack.GetProgress()
|
||||
};
|
||||
|
||||
const cmpPopulation = Engine.QueryInterface(ent, IID_Population);
|
||||
if (cmpPopulation)
|
||||
ret.population = {
|
||||
"bonus": cmpPopulation.GetPopBonus()
|
||||
};
|
||||
|
||||
const cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
|
||||
if (cmpUpgrade)
|
||||
ret.upgrade = {
|
||||
"upgrades": cmpUpgrade.GetUpgrades(),
|
||||
"progress": cmpUpgrade.GetProgress(),
|
||||
"template": cmpUpgrade.GetUpgradingTo(),
|
||||
"isUpgrading": cmpUpgrade.IsUpgrading()
|
||||
};
|
||||
|
||||
const cmpResearcher = Engine.QueryInterface(ent, IID_Researcher);
|
||||
if (cmpResearcher)
|
||||
ret.researcher = {
|
||||
"technologies": cmpResearcher.GetTechnologiesList(),
|
||||
"techCostMultiplier": cmpResearcher.GetTechCostMultiplier()
|
||||
};
|
||||
|
||||
const cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver);
|
||||
if (cmpStatusEffects)
|
||||
ret.statusEffects = cmpStatusEffects.GetActiveStatuses();
|
||||
|
||||
const cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
|
||||
if (cmpProductionQueue)
|
||||
ret.production = {
|
||||
"queue": cmpProductionQueue.GetQueue(),
|
||||
"autoqueue": cmpProductionQueue.IsAutoQueueing()
|
||||
};
|
||||
|
||||
const cmpTrainer = Engine.QueryInterface(ent, IID_Trainer);
|
||||
if (cmpTrainer)
|
||||
ret.trainer = {
|
||||
"entities": cmpTrainer.GetEntitiesList()
|
||||
};
|
||||
|
||||
const cmpTrader = Engine.QueryInterface(ent, IID_Trader);
|
||||
if (cmpTrader)
|
||||
ret.trader = {
|
||||
"goods": cmpTrader.GetGoods()
|
||||
};
|
||||
|
||||
const cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
|
||||
if (cmpFoundation)
|
||||
ret.foundation = {
|
||||
"numBuilders": cmpFoundation.GetNumBuilders(),
|
||||
"buildTime": cmpFoundation.GetBuildTime()
|
||||
};
|
||||
|
||||
const cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
|
||||
if (cmpRepairable)
|
||||
ret.repairable = {
|
||||
"numBuilders": cmpRepairable.GetNumBuilders(),
|
||||
"buildTime": cmpRepairable.GetBuildTime()
|
||||
};
|
||||
|
||||
const cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
|
||||
if (cmpOwnership)
|
||||
ret.player = cmpOwnership.GetOwner();
|
||||
|
||||
const cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
|
||||
if (cmpRallyPoint)
|
||||
ret.rallyPoint = { "position": cmpRallyPoint.GetPositions(player)[0] }; // undefined or {x,z} object
|
||||
|
||||
const cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
|
||||
if (cmpGarrisonHolder)
|
||||
ret.garrisonHolder = {
|
||||
"entities": cmpGarrisonHolder.GetEntities(),
|
||||
"buffHeal": cmpGarrisonHolder.GetHealRate(),
|
||||
"allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
|
||||
"capacity": cmpGarrisonHolder.GetCapacity(),
|
||||
"occupiedSlots": cmpGarrisonHolder.OccupiedSlots()
|
||||
};
|
||||
|
||||
const cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder);
|
||||
if (cmpTurretHolder)
|
||||
ret.turretHolder = {
|
||||
"turretPoints": cmpTurretHolder.GetTurretPoints()
|
||||
};
|
||||
|
||||
const cmpTurretable = Engine.QueryInterface(ent, IID_Turretable);
|
||||
if (cmpTurretable)
|
||||
ret.turretable = {
|
||||
"ejectable": cmpTurretable.IsEjectable(),
|
||||
"holder": cmpTurretable.HolderID()
|
||||
};
|
||||
|
||||
const cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
|
||||
if (cmpGarrisonable)
|
||||
ret.garrisonable = {
|
||||
"holder": cmpGarrisonable.HolderID(),
|
||||
"size": cmpGarrisonable.UnitSize()
|
||||
};
|
||||
|
||||
const cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
|
||||
if (cmpUnitAI)
|
||||
ret.unitAI = {
|
||||
"state": cmpUnitAI.GetCurrentState(),
|
||||
"orders": cmpUnitAI.GetOrders(),
|
||||
"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
|
||||
"canGuard": cmpUnitAI.CanGuard(),
|
||||
"isGuarding": cmpUnitAI.IsGuardOf(),
|
||||
"canPatrol": cmpUnitAI.CanPatrol(),
|
||||
"selectableStances": cmpUnitAI.GetSelectableStances(),
|
||||
"isIdle": cmpUnitAI.IsIdle(),
|
||||
"formations": cmpUnitAI.GetFormationsList(),
|
||||
"formation": cmpUnitAI.GetFormationController()
|
||||
};
|
||||
|
||||
const cmpGuard = Engine.QueryInterface(ent, IID_Guard);
|
||||
if (cmpGuard)
|
||||
ret.guard = {
|
||||
"entities": cmpGuard.GetEntities()
|
||||
};
|
||||
|
||||
const cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
|
||||
if (cmpResourceGatherer)
|
||||
{
|
||||
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
|
||||
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
|
||||
}
|
||||
|
||||
const cmpGate = Engine.QueryInterface(ent, IID_Gate);
|
||||
if (cmpGate)
|
||||
ret.gate = {
|
||||
"locked": cmpGate.IsLocked()
|
||||
};
|
||||
|
||||
const cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
|
||||
if (cmpAlertRaiser)
|
||||
ret.alertRaiser = {
|
||||
"classes": cmpAlertRaiser.GetTargetClasses()
|
||||
};
|
||||
|
||||
const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
|
||||
|
||||
const cmpAttack = Engine.QueryInterface(ent, IID_Attack);
|
||||
if (cmpAttack)
|
||||
{
|
||||
const types = cmpAttack.GetAttackTypes();
|
||||
if (types.length)
|
||||
ret.attack = {};
|
||||
|
||||
for (const type of types)
|
||||
{
|
||||
ret.attack[type] = {};
|
||||
|
||||
Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type));
|
||||
|
||||
ret.attack[type].attackName = cmpAttack.GetAttackName(type);
|
||||
|
||||
ret.attack[type].splash = cmpAttack.GetSplashData(type);
|
||||
if (ret.attack[type].splash)
|
||||
Object.assign(ret.attack[type].splash, cmpAttack.GetAttackEffectsData(type, true));
|
||||
|
||||
ret.attack[type].projectileCount = cmpAttack.GetProjectileCount(type);
|
||||
|
||||
const range = cmpAttack.GetRange(type);
|
||||
ret.attack[type].minRange = range.min;
|
||||
ret.attack[type].maxRange = range.max;
|
||||
ret.attack[type].yOrigin = cmpAttack.GetAttackYOrigin(type);
|
||||
|
||||
const timers = cmpAttack.GetTimers(type);
|
||||
ret.attack[type].prepareTime = timers.prepare;
|
||||
ret.attack[type].repeatTime = timers.repeat;
|
||||
|
||||
if (type != "Ranged")
|
||||
{
|
||||
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cmpPosition && cmpPosition.IsInWorld())
|
||||
// For units, take the range in front of it, no spread, so angle = 0,
|
||||
// else, take the average elevation around it: angle = 2 * pi.
|
||||
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, ret.attack[type].yOrigin, cmpUnitAI ? 0 : 2 * Math.PI);
|
||||
else
|
||||
// Not in world, set a default?
|
||||
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
|
||||
}
|
||||
}
|
||||
|
||||
const cmpResistance = QueryMiragedInterface(ent, IID_Resistance);
|
||||
if (cmpResistance)
|
||||
ret.resistance = cmpResistance.GetResistanceOfForm(cmpFoundation ? "Foundation" : "Entity");
|
||||
|
||||
const cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
|
||||
if (cmpBuildingAI)
|
||||
ret.buildingAI = {
|
||||
"defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
|
||||
"maxArrowCount": cmpBuildingAI.GetMaxArrowCount(),
|
||||
"garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
|
||||
"garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
|
||||
"arrowCount": cmpBuildingAI.GetArrowCount()
|
||||
};
|
||||
|
||||
if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
|
||||
ret.turretParent = cmpPosition.GetTurretParent();
|
||||
|
||||
const cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
|
||||
if (cmpResourceSupply)
|
||||
ret.resourceSupply = {
|
||||
"isInfinite": cmpResourceSupply.IsInfinite(),
|
||||
"max": cmpResourceSupply.GetMaxAmount(),
|
||||
"amount": cmpResourceSupply.GetCurrentAmount(),
|
||||
"type": cmpResourceSupply.GetType(),
|
||||
"killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
|
||||
"maxGatherers": cmpResourceSupply.GetMaxGatherers(),
|
||||
"numGatherers": cmpResourceSupply.GetNumGatherers()
|
||||
};
|
||||
|
||||
const cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
|
||||
if (cmpResourceDropsite)
|
||||
ret.resourceDropsite = {
|
||||
"types": cmpResourceDropsite.GetTypes(),
|
||||
"sharable": cmpResourceDropsite.IsSharable(),
|
||||
"shared": cmpResourceDropsite.IsShared()
|
||||
};
|
||||
|
||||
const cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
|
||||
if (cmpPromotion)
|
||||
ret.promotion = {
|
||||
"curr": cmpPromotion.GetCurrentXp(),
|
||||
"req": cmpPromotion.GetRequiredXp()
|
||||
};
|
||||
|
||||
if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("Barter"))
|
||||
ret.isBarterMarket = true;
|
||||
|
||||
const cmpHeal = Engine.QueryInterface(ent, IID_Heal);
|
||||
if (cmpHeal)
|
||||
ret.heal = {
|
||||
"health": cmpHeal.GetHealth(),
|
||||
"range": cmpHeal.GetRange().max,
|
||||
"interval": cmpHeal.GetInterval(),
|
||||
"unhealableClasses": cmpHeal.GetUnhealableClasses(),
|
||||
"healableClasses": cmpHeal.GetHealableClasses()
|
||||
};
|
||||
|
||||
const cmpLoot = Engine.QueryInterface(ent, IID_Loot);
|
||||
if (cmpLoot)
|
||||
{
|
||||
ret.loot = cmpLoot.GetResources();
|
||||
ret.loot.xp = cmpLoot.GetXp();
|
||||
}
|
||||
|
||||
const cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle);
|
||||
if (cmpResourceTrickle)
|
||||
ret.resourceTrickle = {
|
||||
"interval": cmpResourceTrickle.GetInterval(),
|
||||
"rates": cmpResourceTrickle.GetRates()
|
||||
};
|
||||
|
||||
const cmpTreasure = Engine.QueryInterface(ent, IID_Treasure);
|
||||
if (cmpTreasure)
|
||||
ret.treasure = {
|
||||
"collectTime": cmpTreasure.CollectionTime(),
|
||||
"resources": cmpTreasure.Resources()
|
||||
};
|
||||
|
||||
const cmpTreasureCollector = Engine.QueryInterface(ent, IID_TreasureCollector);
|
||||
if (cmpTreasureCollector)
|
||||
ret.treasureCollector = true;
|
||||
|
||||
const cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
|
||||
if (cmpUnitMotion)
|
||||
ret.speed = {
|
||||
"walk": cmpUnitMotion.GetWalkSpeed(),
|
||||
"run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier(),
|
||||
"acceleration": cmpUnitMotion.GetAcceleration()
|
||||
};
|
||||
|
||||
const cmpUpkeep = Engine.QueryInterface(ent, IID_Upkeep);
|
||||
if (cmpUpkeep)
|
||||
ret.upkeep = {
|
||||
"interval": cmpUpkeep.GetInterval(),
|
||||
"rates": cmpUpkeep.GetRates()
|
||||
};
|
||||
|
||||
return ret;
|
||||
return this.entityStateRetriever.getEntityState(player, ent);
|
||||
};
|
||||
|
||||
GuiInterface.prototype.GetMultipleEntityStates = function(player, ents)
|
||||
{
|
||||
return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) }));
|
||||
return ents.map(ent => this.entityStateRetriever.getEntityState(player, ent));
|
||||
};
|
||||
|
||||
GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
|
||||
|
|
@ -640,12 +299,6 @@ GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
|
|||
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, yOrigin, 2 * Math.PI);
|
||||
};
|
||||
|
||||
GuiInterface.prototype.GetTemplateData = function(player, data)
|
||||
{
|
||||
const template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager)?.GetTemplate(data.templateName);
|
||||
return template && g_TemplateHelper.computeDataFromPlayer(template, AuraTemplates.GetAll(), Resources, data.player || player);
|
||||
};
|
||||
|
||||
GuiInterface.prototype.AreRequirementsMet = function(player, data)
|
||||
{
|
||||
return !data.requirements || RequirementsHelper.AreRequirementsMet(data.requirements,
|
||||
|
|
@ -707,15 +360,18 @@ GuiInterface.prototype.GetNeededResources = function(player, data)
|
|||
};
|
||||
|
||||
/**
|
||||
* State of the templateData (player dependent): true when some template values have been modified
|
||||
* and need to be reloaded by the gui.
|
||||
* Some player-specific modifications have changed, so the GUI will have to reload all of player's templates.
|
||||
*/
|
||||
GuiInterface.prototype.OnTemplateModification = function(msg)
|
||||
{
|
||||
// TODO: Don't reset everything, only the templates actually affected.
|
||||
// And inside them, possible only the values affected.
|
||||
|
||||
this.PushNotification({
|
||||
"players": [msg.player],
|
||||
"type": "templates-modified"
|
||||
});
|
||||
this.entityStateRetriever.onPlayerModificationsChanged(msg.player);
|
||||
this.selectionDirty[msg.player] = true;
|
||||
};
|
||||
|
||||
|
|
@ -1333,7 +989,11 @@ GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
|
|||
this.placementWallEntities[tpl] = {
|
||||
"numUsed": 0,
|
||||
"entities": [],
|
||||
"templateData": this.GetTemplateData(player, { "templateName": tpl }),
|
||||
"templateData": g_TemplateHelper.computeDataFromPlayer(
|
||||
Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager)?.GetTemplate(tpl),
|
||||
AuraTemplates.GetAll(), Resources, player, QueryPlayerIDInterface(player, IID_Identity).GetCiv(),
|
||||
["Cost", "WallPiece"]
|
||||
),
|
||||
};
|
||||
|
||||
if (!this.placementWallEntities[tpl].templateData.wallPiece)
|
||||
|
|
@ -2071,6 +1731,7 @@ GuiInterface.prototype.exposedFunctions = {
|
|||
"GetCampaignGameEndData": 1,
|
||||
"GetRenamedEntities": 1,
|
||||
"ClearRenamedEntities": 1,
|
||||
"ResetEntityStateRetriever": 1,
|
||||
"GetEntityState": 1,
|
||||
"GetMultipleEntityStates": 1,
|
||||
"GetAverageRangeForBuildings": 1,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ ModifiersManager.prototype.Init = function()
|
|||
// TODO: it might be worth keying by classes here.
|
||||
this.playerEntitiesCached = new Map(); // Keyed by player ID, property name, entity ID.
|
||||
|
||||
// Contains the modifier infos for each entity, since piecing them together from scratch it is a bit slow
|
||||
// and they're very frequently retrieved.
|
||||
this.cachedInfos = new Map();
|
||||
|
||||
this.modifiersStorage = new MultiKeyMap(); // Keyed by property name, entity.
|
||||
|
||||
this.modifiersStorage._OnItemModified = (prim, sec, itemID) => this.ModifiersChanged.apply(this, [prim, sec, itemID]);
|
||||
|
|
@ -87,6 +91,7 @@ ModifiersManager.prototype.InvalidatePlayerEntCache = function(valueCache, prope
|
|||
|
||||
ModifiersManager.prototype.InvalidateCache = function(propertyName, entity, playerCache)
|
||||
{
|
||||
this.cachedInfos.delete(entity);
|
||||
const valueCache = this.cachedValues.get(propertyName);
|
||||
if (!valueCache)
|
||||
return;
|
||||
|
|
@ -123,6 +128,45 @@ ModifiersManager.prototype.Cache = function(classesList, propertyName, originalV
|
|||
cache2.set(originalValue, newValue);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get information about an entity's local modifications.
|
||||
* This includes a list of modified components and a "modifications ID", which is consistent
|
||||
* and uniquely identifies the entire set of modifiers registered specifically to the entity.
|
||||
*/
|
||||
ModifiersManager.prototype.GetModifiersInfo = function(entity)
|
||||
{
|
||||
let info = this.cachedInfos.get(entity);
|
||||
if (!info)
|
||||
{
|
||||
const modifications = this.modifiersStorage.GetAllItems(entity);
|
||||
const components = new Set();
|
||||
let id = "";
|
||||
for (const value in modifications)
|
||||
{
|
||||
id += `${value}:`;
|
||||
|
||||
const idx = value.indexOf("/");
|
||||
const component = idx > 0 ? value.slice(0, idx) : value;
|
||||
components.add(component);
|
||||
|
||||
// Sort the modifiers to ignore insertion order.
|
||||
const modifiers = modifications[value].sort((a, b) =>
|
||||
a._ID > b._ID ? 1 : -1 // the IDs can't be the same
|
||||
);
|
||||
for (const modifier of modifiers)
|
||||
id += `${modifier._ID}(${modifier._count})`;
|
||||
|
||||
id += ' ';
|
||||
}
|
||||
info = {
|
||||
"modifiedComponents": components,
|
||||
"modificationsID": id
|
||||
};
|
||||
this.cachedInfos.set(entity, info);
|
||||
}
|
||||
return info;
|
||||
};
|
||||
|
||||
/**
|
||||
* Caching system in front of FetchModifiedProperty(), as calling that every time is quite slow.
|
||||
* This recomputes lazily.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,737 @@
|
|||
Engine.LoadHelperScript("EntityStateRetriever.js");
|
||||
Engine.LoadHelperScript("ObstructionSnap.js");
|
||||
Engine.LoadHelperScript("Player.js");
|
||||
Engine.LoadHelperScript("ValueModification.js");
|
||||
Engine.LoadComponentScript("interfaces/BuildingAI.js");
|
||||
Engine.LoadComponentScript("interfaces/Capturable.js");
|
||||
Engine.LoadComponentScript("interfaces/Formation.js");
|
||||
Engine.LoadComponentScript("interfaces/Foundation.js");
|
||||
Engine.LoadComponentScript("interfaces/Garrisonable.js");
|
||||
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
|
||||
Engine.LoadComponentScript("interfaces/Gate.js");
|
||||
Engine.LoadComponentScript("interfaces/Guard.js");
|
||||
Engine.LoadComponentScript("interfaces/Heal.js");
|
||||
Engine.LoadComponentScript("interfaces/Health.js");
|
||||
Engine.LoadComponentScript("interfaces/Loot.js");
|
||||
Engine.LoadComponentScript("interfaces/Market.js");
|
||||
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
|
||||
Engine.LoadComponentScript("interfaces/Pack.js");
|
||||
Engine.LoadComponentScript("interfaces/Population.js");
|
||||
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
|
||||
Engine.LoadComponentScript("interfaces/Promotion.js");
|
||||
Engine.LoadComponentScript("interfaces/Repairable.js");
|
||||
Engine.LoadComponentScript("interfaces/Researcher.js");
|
||||
Engine.LoadComponentScript("interfaces/Resistance.js");
|
||||
Engine.LoadComponentScript("interfaces/ResourceDropsite.js");
|
||||
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
|
||||
Engine.LoadComponentScript("interfaces/ResourceTrickle.js");
|
||||
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
|
||||
Engine.LoadComponentScript("interfaces/Trader.js");
|
||||
Engine.LoadComponentScript("interfaces/Trainer.js");
|
||||
Engine.LoadComponentScript("interfaces/TurretHolder.js");
|
||||
Engine.LoadComponentScript("interfaces/Turretable.js");
|
||||
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
|
||||
Engine.LoadComponentScript("interfaces/UnitAI.js");
|
||||
Engine.LoadComponentScript("interfaces/Upgrade.js");
|
||||
Engine.LoadComponentScript("interfaces/Upkeep.js");
|
||||
Engine.LoadComponentScript("GuiInterface.js");
|
||||
Engine.LoadComponentScript("ModifiersManager.js");
|
||||
Engine.LoadComponentScript("RallyPoint.js");
|
||||
|
||||
// This script tests the GetEntityState and GetTemplateData methods of the GuiInterface component and the underlying
|
||||
// logic of the EntityStateRetriever, ModifiersManager, and globalscripts/Templates.js.
|
||||
// This data is not actually serialised, but passed to the GUI. However, it is essential that it is complete and
|
||||
// crucial for performance that each call returns as little data as necessary, that nothing is computed twice.
|
||||
// See helpers/EntityStateRetriever.js for details.
|
||||
|
||||
const cmpGuiInterface = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
|
||||
const cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager");
|
||||
|
||||
const templateNames = {};
|
||||
const templates = {};
|
||||
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
|
||||
"GetCurrentTemplateName": function(ent) { return templateNames[ent]; },
|
||||
"GetTemplate": function(name) { return templates[name]; }
|
||||
});
|
||||
|
||||
templates.example_structure_template = {
|
||||
"@parent": "parent_structure_emplate",
|
||||
"AlertRaiser": {
|
||||
"List": { "@datatype": "tokens", "_string": "UnitClass1 UnitClass2" },
|
||||
"RaiseAlertRange": 120, "EndOfAlertRange": 180, "SearchRange": 100
|
||||
},
|
||||
"Attack": {
|
||||
"Ranged": {
|
||||
"AttackName": "Bow", "Damage": { "Pierce": "8" },
|
||||
"MaxRange": "60", "PrepareTime": "400",
|
||||
"PreferredClasses": { "@datatype": "tokens", "_string": "Human" },
|
||||
"Projectile": {
|
||||
"FriendlyFire": "false", "Gravity": "50",
|
||||
"LaunchPoint": { "@y": "0" },
|
||||
"Speed": "100", "Spread": "2"
|
||||
},
|
||||
"RepeatTime": "4000"
|
||||
}
|
||||
},
|
||||
"Auras": { "@datatype": "tokens", "_string": "aura1" },
|
||||
"BuildingAI": {
|
||||
"DefaultArrowCount": "6", "GarrisonArrowClasses": "UnitClass1",
|
||||
"GarrisonArrowMultiplier": "1"
|
||||
},
|
||||
"BuildRestrictions": {
|
||||
"Territory": "own ally", "PlacementType": "shore",
|
||||
"Category": "Structure1",
|
||||
"Distance": { "FromClass": "StructureClass0", "MinDistance": 100 }
|
||||
},
|
||||
"Capturable": {
|
||||
"CapturePoints": "2500", "GarrisonRegenRate": "1.0", "RegenRate": "30"
|
||||
},
|
||||
"Cost": {
|
||||
"BuildTime": "500", "Population": "0",
|
||||
"Resources": { "food": "0", "metal": "250", "stone": "300", "wood": "300" }
|
||||
},
|
||||
"DeathDamage": {
|
||||
"Shape": "Circular", "Range": 20, "FriendlyFire": "true", "Damage": { "Fire": 500 }
|
||||
},
|
||||
"Decay": {
|
||||
"Active": "false", "DelayTime": "0.0", "SinkAccel": "9.8",
|
||||
"SinkProb": "1.0", "SinkRate": "3.0", "SinkingAnim": "false"
|
||||
},
|
||||
"GarrisonHolder": {
|
||||
"BuffHeal": "1", "EjectHealth": "0.1",
|
||||
"EjectClassesOnDestroy": { "@datatype": "tokens", "_string": "Unit" },
|
||||
"List": { "@datatype": "tokens", "_string": "Support Infantry Cavalry" },
|
||||
"LoadingRange": "1", "Max": "20"
|
||||
},
|
||||
"Health": {
|
||||
"DamageVariants": {
|
||||
"heavydamage": "0.35", "lightdamage": "0.85", "mediumdamage": "0.65"
|
||||
},
|
||||
"DeathType": "corpse", "IdleRegenRate": "0",
|
||||
"Max": "3000", "RegenRate": "0",
|
||||
"SpawnEntityOnDeath": "decay|rubble/rubble_stone_6x6",
|
||||
"Unhealable": "true"
|
||||
},
|
||||
"Identity": {
|
||||
"Civ": "civ1",
|
||||
"Classes": { "@datatype": "tokens", "_string": "StructureClass1 StructureClass2 StructureClass3" },
|
||||
"GenericName": "Example Structure",
|
||||
"Icon": "example_structure.png",
|
||||
"SelectionGroupName": "example_structure",
|
||||
"Tooltip": "This is an example structure.",
|
||||
"Undeletable": "false",
|
||||
"VisibleClasses": { "@datatype": "tokens", "_string": "StructureVisibleClass1 StructureVisibleClass2" }
|
||||
},
|
||||
"Loot": { "food": "10", "metal": "50", "stone": "60", "wood": "60", "xp": "30" },
|
||||
"Population": { "Bonus": 5 },
|
||||
"Position": {
|
||||
"Altitude": "0", "Anchor": "upright", "FloatDepth": "0",
|
||||
"Floating": "false", "TurnRate": "6"
|
||||
},
|
||||
"ProductionQueue": {},
|
||||
"Repairable": { "RepairTimeRatio": "2.0" },
|
||||
"Researcher": {
|
||||
"Technologies": { "@datatype": "tokens", "_string": "tech1 tech2 tech3" }
|
||||
},
|
||||
"Resistance": {
|
||||
"Entity": {
|
||||
"ApplyStatus": { "Poisoned": { "BlockChance": "1", "Duration": "0.0" } },
|
||||
"Damage": { "Crush": "3", "Hack": "29", "Pierce": "35" }
|
||||
},
|
||||
"Foundation": {
|
||||
"ApplyStatus": { "Poisoned": { "BlockChance": "1", "Duration": "0.0" } },
|
||||
"Damage": { "Crush": "1", "Hack": "1", "Pierce": "10" }
|
||||
}
|
||||
},
|
||||
"ResourceDropsite": { "Sharable": "true", "Types": "food stone metal" },
|
||||
"ResourceTrickle": { "Rates": { "food": 1.3 }, "Interval": 1500 },
|
||||
"TerritoryDecay": { "DecayRate": "20", "Territory": "neutral enemy" },
|
||||
"TerritoryInfluence": { "Radius": "140", "Root": "true", "Weight": "10000" },
|
||||
"Trainer": {
|
||||
"BatchTimeModifier": "0.8",
|
||||
"Entities": { "@datatype": "tokens", "_string": "template1 template2 template3" }
|
||||
},
|
||||
"Visibility": {
|
||||
"AlwaysVisible": "false", "Corpse": "false",
|
||||
"Preview": "false", "RetainInFog": "true"
|
||||
},
|
||||
"Vision": { "Range": "90" },
|
||||
"VisionSharing": { "Bribable": "false" }
|
||||
};
|
||||
templates.example_unit_template = {
|
||||
"@parent": "parent_unit_template",
|
||||
"Attack": {
|
||||
"Capture": {
|
||||
"AttackName": "Capture", "Capture": "2.5",
|
||||
"MaxRange": "4", "RepeatTime": "1000",
|
||||
"RestrictedClasses": { "@datatype": "tokens", "_string": "RestrictedClass1" }
|
||||
},
|
||||
"Melee": {
|
||||
"AttackName": "Spear", "MaxRange": "4",
|
||||
"Bonuses": { "BonusCavMelee": { "Classes": "Cavalry", "Multiplier": "2.5" } },
|
||||
"Damage": { "Hack": "4.5", "Pierce": "4" },
|
||||
"PreferredClasses": { "@datatype": "tokens", "_string": "PreferredClass1" },
|
||||
"PrepareTime": "500", "RepeatTime": "1000"
|
||||
},
|
||||
"Slaughter": {
|
||||
"AttackName": "Slaughter", "Damage": { "Hack": "1000" },
|
||||
"MaxRange": "2", "PrepareTime": "900", "RepeatTime": "1000",
|
||||
}
|
||||
},
|
||||
"Builder": {
|
||||
"Entities": {
|
||||
"@datatype": "tokens",
|
||||
"_string": "buildable1 buildable2 buildable3 buildable4"
|
||||
},
|
||||
"Rate": "1.0"
|
||||
},
|
||||
"Cost": {
|
||||
"BuildTime": "10", "Population": "1",
|
||||
"Resources": { "food": "50", "metal": "0", "stone": "0", "wood": "50" }
|
||||
},
|
||||
"Garrisonable": { "Size": "1" },
|
||||
"Heal": {
|
||||
"Range": 20, "Health": 10, "Interval": 2500,
|
||||
"UnhealableClasses": { "@datatype": "tokens", "_string": "" },
|
||||
"HealableClasses": { "@datatype": "tokens", "_string": "HealableClass1" },
|
||||
"LineTexture": "heal_line_texture.png",
|
||||
"LineTextureMask": "heal_line_mask.png",
|
||||
"LineThickness": 0.36
|
||||
},
|
||||
"Health": {
|
||||
"DeathType": "corpse", "IdleRegenRate": "0",
|
||||
"Max": "100", "RegenRate": "0", "Unhealable": "false"
|
||||
},
|
||||
"Identity": {
|
||||
"Civ": "civ2", "Lang": "greek", "Rank": "Basic",
|
||||
"Classes": { "@datatype": "tokens", "_string": "UnitClass1 UnitClass2 UnitClass3" },
|
||||
"GenericName": "Example Unit",
|
||||
"Icon": "example_unit.png",
|
||||
"Phenotype": { "@datatype": "tokens", "_string": "male" },
|
||||
"SelectionGroupName": "example_unit",
|
||||
"Tooltip": "This is an example unit.",
|
||||
"Undeletable": "false",
|
||||
"VisibleClasses": {
|
||||
"@datatype": "tokens",
|
||||
"_string": "VisibleUnitClass1 VisibleUnitClass2"
|
||||
}
|
||||
},
|
||||
"Loot": { "food": "5", "metal": "4", "stone": "3", "wood": "5", "xp": "100" },
|
||||
"Position": {
|
||||
"Altitude": "0", "Anchor": "upright",
|
||||
"FloatDepth": "0.0", "Floating": "false",
|
||||
"TurnRate": "14"
|
||||
},
|
||||
"Promotion": { "Entity": "promoted_unit_template", "RequiredXp": "100" },
|
||||
"Resistance": {
|
||||
"Entity": { "Damage": { "Crush": "15", "Hack": "3", "Pierce": "3" } }
|
||||
},
|
||||
"ResourceGatherer": {
|
||||
"BaseSpeed": "1.0",
|
||||
"Capacities": { "food": "10", "metal": "11", "stone": "12", "wood": "13" },
|
||||
"MaxDistance": "2.0",
|
||||
"Rates": {
|
||||
"food.fruit": "0.5", "food.grain": "0.6", "food.meat": "0.7",
|
||||
"metal.ore": "0.8", "metal.ruins": "0.9", "stone.rock": "1.0",
|
||||
"stone.ruins": "1.1", "wood.ruins": "1.2", "wood.tree": "1.3"
|
||||
}
|
||||
},
|
||||
"TreasureCollector": { "MaxDistance": "2" },
|
||||
"UnitAI": {
|
||||
"CanGuard": "true", "CanPatrol": "true",
|
||||
"CheeringTime": "2800",
|
||||
"DefaultStance": "aggressive",
|
||||
"FleeDistance": "12.0",
|
||||
"FormationController": "false",
|
||||
"Formations": { "@datatype": "tokens", "_string": "formation1 formation2 formation3" },
|
||||
"PatrolWaitTime": "1"
|
||||
},
|
||||
"UnitMotion": {
|
||||
"Acceleration": "35", "FormationController": "false",
|
||||
"InstantTurnAngle": "1.5",
|
||||
"PassabilityClass": "default",
|
||||
"RunMultiplier": "2",
|
||||
"WalkSpeed": "9.5",
|
||||
"Weight": "10"
|
||||
},
|
||||
"Visibility": {
|
||||
"AlwaysVisible": "false", "Corpse": "false",
|
||||
"Preview": "false", "RetainInFog": "false"
|
||||
},
|
||||
"Vision": { "Range": "80" },
|
||||
"VisionSharing": { "Bribable": "false" }
|
||||
};
|
||||
|
||||
const expectedTemplateData = {
|
||||
"example_structure_template": {
|
||||
"alertRaiser": { "classes": "UnitClass1 UnitClass2" },
|
||||
"attack": {
|
||||
"Ranged": {
|
||||
"Damage": { "Pierce": 8 }, "attackName": { "name": "Bow" },
|
||||
"minRange": 0, "maxRange": 60, "yOrigin": 0, "elevationAdaptedRange": 60,
|
||||
"repeatTime": 4000, "projectileCount": 1, "friendlyFire": false
|
||||
}
|
||||
},
|
||||
"auras": {
|
||||
"aura1": {
|
||||
"name": { "generic": "Aura 1" }, "description": "Aura Description",
|
||||
"modifications": { "value": "Resistance/Damage/Crush", "add": 1 }, "radius": 30 }
|
||||
},
|
||||
"buildingAI": {
|
||||
"defaultArrowCount": 6, "maxArrowCount": 0,
|
||||
"garrisonArrowMultiplier": 1, "garrisonArrowClasses": ["UnitClass1"]
|
||||
},
|
||||
"buildRestrictions": {
|
||||
"placementType": "shore", "territory": "own ally", "category": "Structure1",
|
||||
"distance": { "fromClass": "StructureClass0", "min": 100 }
|
||||
},
|
||||
"maxCapturePoints": 2500,
|
||||
"cost": { "food": 0, "metal": 250, "stone": 300, "wood": 300, "population": 0, "time": 500 },
|
||||
"deathDamage": { "Damage": { "Fire": 500 }, "friendlyFire": true },
|
||||
"garrisonHolder": { "allowedClasses": "Support Infantry Cavalry", "buffHeal": 1, "capacity": 20 },
|
||||
"maxHitpoints": 3000,
|
||||
"selectionGroupName": "example_structure",
|
||||
"name": { "specific": "Example Structure", "generic": "Example Structure" },
|
||||
"icon": "example_structure.png",
|
||||
"tooltip": "This is an example structure.",
|
||||
"identityClasses": ["StructureClass1", "StructureClass2", "StructureClass3",
|
||||
"StructureVisibleClass1", "StructureVisibleClass2"],
|
||||
"visibleIdentityClasses": ["StructureVisibleClass1", "StructureVisibleClass2"],
|
||||
"nativeCiv": "civ1",
|
||||
"requirements": undefined,
|
||||
"rank": undefined,
|
||||
"undeletable": "false",
|
||||
"enablesBartering": false,
|
||||
"loot": { "food": 10, "metal": 50, "stone": 60, "wood": 60, "xp": 30 },
|
||||
"population": { "bonus": 5 },
|
||||
"researcher": {
|
||||
"techCostMultiplier": { "food": 1, "metal": 1, "stone": 1, "wood": 1, "time": 1 }
|
||||
},
|
||||
"resistance": {
|
||||
"Damage": { "Crush": 3, "Hack": 29, "Pierce": 35 },
|
||||
"ApplyStatus": { "Poisoned": { "blockChance": 1, "duration": 0 } }
|
||||
},
|
||||
"resourceDropsite": { "types": ["food", "stone", "metal"] },
|
||||
"resourceTrickle": { "interval": 1500, "rates": { "food": 1.3 } }
|
||||
},
|
||||
|
||||
"example_unit_template": {
|
||||
"attack": {
|
||||
"Capture": {
|
||||
"Capture": 2.5, "attackName": { "name": "Capture" },
|
||||
"minRange": 0, "maxRange": 4, "yOrigin": 0,
|
||||
"elevationAdaptedRange": 4, "repeatTime": 1000
|
||||
},
|
||||
"Melee": {
|
||||
"Damage": { "Hack": 4.5, "Pierce": 4 }, "attackName": { "name": "Spear" },
|
||||
"minRange": 0, "maxRange": 4, "yOrigin": 0,
|
||||
"elevationAdaptedRange": 4, "repeatTime": 1000
|
||||
},
|
||||
"Slaughter": {
|
||||
"Damage": { "Hack": 1000 }, "attackName": { "name": "Slaughter" },
|
||||
"minRange": 0, "maxRange": 2, "yOrigin": 0,
|
||||
"elevationAdaptedRange": 2, "repeatTime": 1000
|
||||
}
|
||||
},
|
||||
"builder": true,
|
||||
"cost": { "food": 50, "metal": 0, "stone": 0, "wood": 50, "population": 1, "time": 10 },
|
||||
"garrisonable": { "size": 1 },
|
||||
"heal": {
|
||||
"health": 10, "range": 20, "interval": 2500, "unhealableClasses": "", "healableClasses": "HealableClass1"
|
||||
},
|
||||
"maxHitpoints": 100,
|
||||
"selectionGroupName": "example_unit",
|
||||
"name": { "specific": "Example Unit", "generic": "Example Unit" },
|
||||
"icon": "example_unit.png",
|
||||
"tooltip": "This is an example unit.",
|
||||
"identityClasses": [
|
||||
"UnitClass1", "UnitClass2", "UnitClass3",
|
||||
"VisibleUnitClass1", "VisibleUnitClass2", "Basic"
|
||||
],
|
||||
"visibleIdentityClasses": ["VisibleUnitClass1", "VisibleUnitClass2"],
|
||||
"nativeCiv": "civ2",
|
||||
"requirements": undefined,
|
||||
"rank": "Basic",
|
||||
"undeletable": "false",
|
||||
"enablesBartering": false,
|
||||
"loot": { "food": 5, "metal": 4, "stone": 3, "wood": 5, "xp": 100 },
|
||||
"promotion": { "req": 100 },
|
||||
"resistance": { "Damage": { "Crush": 15, "Hack": 3, "Pierce": 3 } },
|
||||
"resourceGatherRates": {
|
||||
"food.fruit": 0.5, "food.grain": 0.6, "food.meat": 0.7,
|
||||
"metal.ore": 0.8, "metal.ruins": 0.9,
|
||||
"stone.rock": 1, "stone.ruins": 1.1,
|
||||
"wood.ruins": 1.2, "wood.tree": 1.3
|
||||
},
|
||||
"treasureCollector": true,
|
||||
"unitAI": { "formations": ["formation1", "formation2", "formation3"] },
|
||||
"unitMotion": { "walk": 9.5, "run": 19, "acceleration": 35 }
|
||||
}
|
||||
};
|
||||
|
||||
global.AuraTemplates = { "GetAll": () => ({
|
||||
"aura1": {
|
||||
"auraName": "Aura 1",
|
||||
"auraDescription": "Aura Description",
|
||||
"modifications": { "value": "Resistance/Damage/Crush", "add": 1 },
|
||||
"radius": 30
|
||||
}
|
||||
}) };
|
||||
|
||||
Resources = {
|
||||
"GetCodes": () => ["food", "metal", "stone", "wood"],
|
||||
"GetNames": () => ({
|
||||
"food": "Food",
|
||||
"metal": "Metal",
|
||||
"stone": "Stone",
|
||||
"wood": "Wood"
|
||||
}),
|
||||
"GetResource": resource => ({
|
||||
"aiAnalysisInfluenceGroup":
|
||||
resource == "food" ? "ignore" :
|
||||
resource == "wood" ? "abundant" : "sparse"
|
||||
})
|
||||
};
|
||||
|
||||
const PLAYER_1_ENT_ID = 101;
|
||||
const PLAYER_2_ENT_ID = 102;
|
||||
cmpModifiersManager.OnGlobalPlayerEntityChanged({ "player": 1, "from": INVALID_PLAYER, "to": PLAYER_1_ENT_ID });
|
||||
cmpModifiersManager.OnGlobalPlayerEntityChanged({ "player": 2, "from": INVALID_PLAYER, "to": PLAYER_2_ENT_ID });
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
|
||||
"GetPlayerByID": (id) => 100 + id
|
||||
});
|
||||
|
||||
AddMock(PLAYER_1_ENT_ID, IID_Identity, {
|
||||
"GetCiv": () => "civ1"
|
||||
});
|
||||
AddMock(PLAYER_1_ENT_ID, IID_Player, {
|
||||
"GetPlayerID": () => 1
|
||||
});
|
||||
|
||||
AddMock(PLAYER_2_ENT_ID, IID_Identity, {
|
||||
"GetCiv": () => "civ1"
|
||||
});
|
||||
AddMock(PLAYER_2_ENT_ID, IID_Player, {
|
||||
"GetPlayerID": () => 2
|
||||
});
|
||||
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetTemplateData(1, { "templateName": "example_structure_template" }), expectedTemplateData.example_structure_template);
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetTemplateData(1, { "templateName": "example_unit_template" }), expectedTemplateData.example_unit_template);
|
||||
const TEST_ENTITY_STRUCTURE = 11;
|
||||
const TEST_ENTITY_UNIT_1 = 21;
|
||||
const TEST_ENTITY_UNIT_2 = 22;
|
||||
templateNames[TEST_ENTITY_STRUCTURE] = "example_structure_template";
|
||||
templateNames[TEST_ENTITY_UNIT_1] = "example_unit_template";
|
||||
templateNames[TEST_ENTITY_UNIT_2] = "example_unit_template";
|
||||
|
||||
const AddMockToEnts = (ents, iid, mock) =>
|
||||
{
|
||||
for (const ent of ents)
|
||||
AddMock(ent, iid, mock);
|
||||
};
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
||||
"GetLosVisibility": () => "visible",
|
||||
"GetElevationAdaptedRange": () => 80,
|
||||
"GetEntitiesByPlayer": (player) => player == 1 ? [TEST_ENTITY_STRUCTURE, TEST_ENTITY_UNIT_1, TEST_ENTITY_UNIT_2] : []
|
||||
});
|
||||
AddMockToEnts([TEST_ENTITY_STRUCTURE, TEST_ENTITY_UNIT_1, TEST_ENTITY_UNIT_2], IID_Ownership, {
|
||||
"GetOwner": () => 1
|
||||
});
|
||||
AddMock(TEST_ENTITY_STRUCTURE, IID_Position, {
|
||||
"GetTurretParent": () => INVALID_ENTITY,
|
||||
"GetPosition": () => new Vector3D(1, 2, 3),
|
||||
"GetRotation": () => new Vector3D(0, 1, 0),
|
||||
"IsInWorld": () => true
|
||||
});
|
||||
AddMock(TEST_ENTITY_UNIT_1, IID_Position, {
|
||||
"GetTurretParent": () => INVALID_ENTITY,
|
||||
"GetPosition": () => new Vector3D(10, 12, 19),
|
||||
"GetRotation": () => new Vector3D(0, 1, 0),
|
||||
"IsInWorld": () => true
|
||||
});
|
||||
AddMock(TEST_ENTITY_UNIT_2, IID_Position, {
|
||||
"GetTurretParent": () => INVALID_ENTITY,
|
||||
"GetPosition": () => new Vector3D(20, 22, 24),
|
||||
"GetRotation": () => new Vector3D(0, 1, 1),
|
||||
"IsInWorld": () => true
|
||||
});
|
||||
const structureAttack = expectedTemplateData.example_structure_template.attack;
|
||||
AddMock(TEST_ENTITY_STRUCTURE, IID_Attack, {
|
||||
"GetAttackTypes": () => Object.keys(structureAttack),
|
||||
"GetRange": (type) => ({ "min": structureAttack[type].minRange, "max": structureAttack[type].maxRange }),
|
||||
"GetAttackYOrigin": (type) => structureAttack[type].yOrigin
|
||||
});
|
||||
AddMockToEnts([TEST_ENTITY_UNIT_1, TEST_ENTITY_UNIT_2], IID_Attack, {
|
||||
"GetAttackTypes": () => Object.keys(expectedTemplateData.example_unit_template.attack)
|
||||
});
|
||||
AddMock(TEST_ENTITY_STRUCTURE, IID_BuildingAI, {
|
||||
"GetArrowCount": () => 10
|
||||
});
|
||||
AddMock(TEST_ENTITY_STRUCTURE, IID_Capturable, {
|
||||
"GetCapturePoints": () => 1567
|
||||
});
|
||||
AddMockToEnts([TEST_ENTITY_UNIT_1, TEST_ENTITY_UNIT_2], IID_Garrisonable, {
|
||||
"HolderID": () => INVALID_ENTITY
|
||||
});
|
||||
AddMock(TEST_ENTITY_STRUCTURE, IID_GarrisonHolder, {
|
||||
"GetEntities": () => [51, 52, 53],
|
||||
"OccupiedSlots": () => 3
|
||||
});
|
||||
AddMock(TEST_ENTITY_UNIT_1, IID_Guard, {
|
||||
"GetEntities": () => [TEST_ENTITY_STRUCTURE]
|
||||
});
|
||||
AddMock(TEST_ENTITY_UNIT_2, IID_Guard, {
|
||||
"GetEntities": () => []
|
||||
});
|
||||
AddMock(TEST_ENTITY_STRUCTURE, IID_Health, {
|
||||
"GetHitpoints": () => 2139,
|
||||
"IsRepairable": () => true,
|
||||
"IsInjured": () => true,
|
||||
"IsUnhealable": () => true
|
||||
});
|
||||
AddMock(TEST_ENTITY_UNIT_1, IID_Health, {
|
||||
"GetHitpoints": () => 39,
|
||||
"IsRepairable": () => false,
|
||||
"IsInjured": () => true,
|
||||
"IsUnhealable": () => false
|
||||
});
|
||||
AddMock(TEST_ENTITY_UNIT_2, IID_Health, {
|
||||
"GetHitpoints": () => 100,
|
||||
"IsRepairable": () => false,
|
||||
"IsInjured": () => false,
|
||||
"IsUnhealable": () => false
|
||||
});
|
||||
AddMockToEnts([TEST_ENTITY_UNIT_1, TEST_ENTITY_UNIT_2], IID_Identity, {
|
||||
"GetClassesList": () => expectedTemplateData.example_unit_template.identityClasses,
|
||||
"IsControllable": () => true
|
||||
});
|
||||
AddMock(TEST_ENTITY_STRUCTURE, IID_Identity, {
|
||||
"GetClassesList": () => expectedTemplateData.example_structure_template.identityClasses,
|
||||
"IsControllable": () => true
|
||||
});
|
||||
AddMock(TEST_ENTITY_STRUCTURE, IID_ProductionQueue, {
|
||||
"GetQueue": () => [{
|
||||
"unitTemplate": "trainable1", "count": 2, "neededSlots": 0, "progress": 0.375,
|
||||
"timeRemaining": 5000, "paused": undefined, "metadata": undefined, "id": 12
|
||||
}],
|
||||
"IsAutoQueueing": () => true
|
||||
});
|
||||
AddMock(TEST_ENTITY_UNIT_1, IID_Promotion, {
|
||||
"GetCurrentXp": () => 89
|
||||
});
|
||||
AddMock(TEST_ENTITY_UNIT_2, IID_Promotion, {
|
||||
"GetCurrentXp": () => 56
|
||||
});
|
||||
AddMock(TEST_ENTITY_STRUCTURE, IID_RallyPoint, {
|
||||
"GetPositions": () => [{ "x": 10, "y": 50 }, { "x": 20, "y": 40 }]
|
||||
});
|
||||
AddMock(TEST_ENTITY_STRUCTURE, IID_Repairable, {
|
||||
"GetNumBuilders": () => 2,
|
||||
"GetBuildTime": () => 234
|
||||
});
|
||||
AddMock(TEST_ENTITY_UNIT_1, IID_ResourceGatherer, {
|
||||
"GetCarryingStatus": () => [ { "type": "wood", "amount": 7, "max": 13 } ]
|
||||
});
|
||||
AddMock(TEST_ENTITY_UNIT_2, IID_ResourceGatherer, {
|
||||
"GetCarryingStatus": () => [ { "type": "wood", "amount": 2, "max": 13 } ]
|
||||
});
|
||||
AddMock(TEST_ENTITY_STRUCTURE, IID_Researcher, {
|
||||
"GetTechnologiesList": () => ["tech1", { "pair": ["tech2_1", "tech2_2"] }]
|
||||
});
|
||||
AddMock(TEST_ENTITY_STRUCTURE, IID_Trainer, {
|
||||
"GetEntitiesList": () => ["trainable1", "trainable2", "trainable3"]
|
||||
});
|
||||
const FORMATION_CONTROLLER_ENT_ID = 43;
|
||||
AddMockToEnts([TEST_ENTITY_UNIT_1, TEST_ENTITY_UNIT_2], IID_UnitAI, {
|
||||
"GetCurrentState": () => "INDIVIDUAL.IDLE",
|
||||
"GetOrders": () => [{ "type": "Walk", "data": { "x": 11, "z": 22, "force": true, "relaxed": true } }],
|
||||
"HasWorkOrders": () => true,
|
||||
"IsGuardOf": () => false,
|
||||
"GetSelectableStances": () => ["stance1", "stance2", "stance3", "stance4", "stance5"],
|
||||
"IsIdle": () => true,
|
||||
"GetFormationController": () => FORMATION_CONTROLLER_ENT_ID
|
||||
});
|
||||
|
||||
const expectedDynamicStates = {
|
||||
[TEST_ENTITY_STRUCTURE]: {
|
||||
"id": TEST_ENTITY_STRUCTURE,
|
||||
"player": 1,
|
||||
"templateName": "example_structure_template",
|
||||
"attack": { "Ranged": { "elevationAdaptedRange": 80 } },
|
||||
"buildingAI": { "arrowCount": 10 },
|
||||
"capturePoints": 1567,
|
||||
"controllable": true,
|
||||
"garrisonHolder": { "entities": [51, 52, 53], "occupiedSlots": 3 },
|
||||
"hitpoints": 2139,
|
||||
"needsRepair": true,
|
||||
"needsHeal": false,
|
||||
"position": { "x": 1, "y": 2, "z": 3 },
|
||||
"production": {
|
||||
"queue": [{
|
||||
"unitTemplate": "trainable1", "count": 2, "neededSlots": 0, "progress": 0.375,
|
||||
"timeRemaining": 5000, "paused": undefined, "metadata": undefined, "id": 12
|
||||
}],
|
||||
"autoqueue": true
|
||||
},
|
||||
"rallyPoint": { "position": { "x": 10, "y": 50 } },
|
||||
"repairable": { "numBuilders": 2, "buildTime": 234 },
|
||||
"researcher": { "technologies": ["tech1", { "pair": ["tech2_1", "tech2_2"] }] },
|
||||
"trainer": { "entities": ["trainable1", "trainable2", "trainable3"] },
|
||||
"visibility": "visible"
|
||||
},
|
||||
[TEST_ENTITY_UNIT_1]: {
|
||||
"id": TEST_ENTITY_UNIT_1,
|
||||
"player": 1,
|
||||
"templateName": "example_unit_template",
|
||||
"controllable": true,
|
||||
"garrisonable": { "holder": INVALID_ENTITY },
|
||||
"guard": { "entities": [TEST_ENTITY_STRUCTURE] },
|
||||
"hitpoints": 39,
|
||||
"needsRepair": false,
|
||||
"needsHeal": true,
|
||||
"position": { "x": 10, "y": 12, "z": 19 },
|
||||
"promotion": { "curr": 89 },
|
||||
"resourceCarrying": [{ "type": "wood", "amount": 7, "max": 13 }],
|
||||
"unitAI": {
|
||||
"state": "INDIVIDUAL.IDLE",
|
||||
"orders": [{ "type": "Walk", "data": { "x": 11, "z": 22, "force": true, "relaxed": true } }],
|
||||
"hasWorkOrders": true,
|
||||
"isGuarding": false,
|
||||
"selectableStances": ["stance1", "stance2", "stance3", "stance4", "stance5"],
|
||||
"isIdle": true,
|
||||
"formation": FORMATION_CONTROLLER_ENT_ID
|
||||
},
|
||||
"visibility": "visible"
|
||||
},
|
||||
[TEST_ENTITY_UNIT_2]: {
|
||||
"id": TEST_ENTITY_UNIT_2,
|
||||
"player": 1,
|
||||
"templateName": "example_unit_template",
|
||||
"controllable": true,
|
||||
"garrisonable": { "holder": INVALID_ENTITY },
|
||||
"guard": { "entities": [] },
|
||||
"hitpoints": 100,
|
||||
"needsRepair": false,
|
||||
"needsHeal": true,
|
||||
"position": { "x": 20, "y": 22, "z": 24 },
|
||||
"promotion": { "curr": 56 },
|
||||
"resourceCarrying": [{ "type": "wood", "amount": 2, "max": 13 }],
|
||||
"unitAI": {
|
||||
"state": "INDIVIDUAL.IDLE",
|
||||
"orders": [{ "type": "Walk", "data": { "x": 11, "z": 22, "force": true, "relaxed": true } }],
|
||||
"hasWorkOrders": true,
|
||||
"isGuarding": false,
|
||||
"selectableStances": ["stance1", "stance2", "stance3", "stance4", "stance5"],
|
||||
"isIdle": true,
|
||||
"formation": FORMATION_CONTROLLER_ENT_ID
|
||||
},
|
||||
"visibility": "visible"
|
||||
}
|
||||
};
|
||||
|
||||
// The template data of both templates has been retrieved already, so it shouldn't be computed again now.
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_STRUCTURE), {
|
||||
"dynamicState": expectedDynamicStates[TEST_ENTITY_STRUCTURE]
|
||||
});
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_UNIT_1), {
|
||||
"dynamicState": expectedDynamicStates[TEST_ENTITY_UNIT_1]
|
||||
});
|
||||
|
||||
Engine.PostMessage = (ent, mtid, msg) =>
|
||||
{
|
||||
if (mtid == MT_TemplateModification && ent == SYSTEM_ENTITY)
|
||||
cmpGuiInterface.OnTemplateModification(msg);
|
||||
};
|
||||
|
||||
// This makes the previously returned templateData out-of-date, so it should now be recomputed (once) the next time
|
||||
// queried for it.
|
||||
cmpModifiersManager.AddModifier(
|
||||
"Attack/Ranged/Damage/Pierce", "DamageBuff", [{ "affects": ["StructureClass2"], "add": 1 }], PLAYER_1_ENT_ID
|
||||
);
|
||||
|
||||
expectedTemplateData.example_structure_template.attack.Ranged.Damage.Pierce += 1;
|
||||
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_STRUCTURE), {
|
||||
"dynamicState": expectedDynamicStates[TEST_ENTITY_STRUCTURE],
|
||||
"templateData": expectedTemplateData.example_structure_template
|
||||
});
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_UNIT_1), {
|
||||
"dynamicState": expectedDynamicStates[TEST_ENTITY_UNIT_1],
|
||||
"templateData": expectedTemplateData.example_unit_template
|
||||
});
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_UNIT_2), {
|
||||
"dynamicState": expectedDynamicStates[TEST_ENTITY_UNIT_2]
|
||||
});
|
||||
|
||||
// On the other hand, changes of other playes' modifiers should be ignored.
|
||||
cmpGuiInterface.OnTemplateModification({ "player": 2 });
|
||||
TS_ASSERT(!cmpGuiInterface.GetEntityState(1, TEST_ENTITY_STRUCTURE).templateData);
|
||||
|
||||
|
||||
const increasedMaxHPModifiers = [{ "affects": ["UnitClass1", "StructureClass1"], "add": 50 }];
|
||||
cmpModifiersManager.AddModifier("Health/Max", "IncreasedMaxHP", increasedMaxHPModifiers, TEST_ENTITY_UNIT_1);
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_UNIT_1), {
|
||||
"dynamicState": expectedDynamicStates[TEST_ENTITY_UNIT_1],
|
||||
"modificationsID": "Health/Max:IncreasedMaxHP(1) ",
|
||||
// New modificationsID, so modifiedTemplateData should be computed. Only for the affected components, though.
|
||||
"modifiedTemplateData": { "maxHitpoints": 150 }
|
||||
});
|
||||
|
||||
cmpModifiersManager.AddModifier("Health/Max", "IncreasedMaxHP", increasedMaxHPModifiers, TEST_ENTITY_UNIT_2);
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_UNIT_2), {
|
||||
"dynamicState": expectedDynamicStates[TEST_ENTITY_UNIT_2],
|
||||
"modificationsID": "Health/Max:IncreasedMaxHP(1) "
|
||||
// modifiedTemplateData for owner 1, example_unit_template and the above modificationsID has been returned in the
|
||||
// previous call already, so it shouldn't be computed again now.
|
||||
});
|
||||
|
||||
cmpModifiersManager.AddModifier("Promotion/RequiredXp", "FasterPromotion", [{ "affects": ["UnitClass2"], "add": -10 }], TEST_ENTITY_UNIT_2, true);
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_UNIT_2), {
|
||||
"dynamicState": expectedDynamicStates[TEST_ENTITY_UNIT_2],
|
||||
"modificationsID": "Health/Max:IncreasedMaxHP(1) Promotion/RequiredXp:FasterPromotion(1) ",
|
||||
// New modificationsID, so modifiedTemplateData should be computed again.
|
||||
"modifiedTemplateData": { "maxHitpoints": 150, "promotion": { "req": 90 } }
|
||||
});
|
||||
|
||||
cmpModifiersManager.AddModifier("Health/Max", "IncreasedMaxHP", increasedMaxHPModifiers, TEST_ENTITY_STRUCTURE);
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_STRUCTURE), {
|
||||
"dynamicState": expectedDynamicStates[TEST_ENTITY_STRUCTURE],
|
||||
"modificationsID": "Health/Max:IncreasedMaxHP(1) ",
|
||||
// modifiedTemplateData for owner 1 and the above modificationsID has been returned in a previous call but only for
|
||||
// different template (example_unit_template and not example_structure_template), so it should be computed again now.
|
||||
"modifiedTemplateData": { "maxHitpoints": 3050 }
|
||||
});
|
||||
|
||||
|
||||
// This makes any previously returned templateData and modifiedTemplateData out-of-date, so they should now be recomputed
|
||||
// (once) the next time queried for them.
|
||||
cmpModifiersManager.AddModifier("Health/Max", "DecreasedMaxHP", [{ "affects": ["UnitClass1"], "add": -1 }], PLAYER_1_ENT_ID);
|
||||
expectedTemplateData.example_unit_template.maxHitpoints -= 1;
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_UNIT_1), {
|
||||
"dynamicState": expectedDynamicStates[TEST_ENTITY_UNIT_1],
|
||||
"templateData": expectedTemplateData.example_unit_template,
|
||||
"modificationsID": "Health/Max:IncreasedMaxHP(1) ",
|
||||
"modifiedTemplateData": { "maxHitpoints": 149 }
|
||||
});
|
||||
cmpModifiersManager.RemoveModifier("Promotion/RequiredXp", "FasterPromotion", TEST_ENTITY_UNIT_2);
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_UNIT_2), {
|
||||
"dynamicState": expectedDynamicStates[TEST_ENTITY_UNIT_2],
|
||||
"modificationsID": "Health/Max:IncreasedMaxHP(1) ",
|
||||
// modifiedTemplateData for owner 1, example_unit_template and the above modificationsID has been returned in the
|
||||
// previous call already, so it shouldn't be computed again now.
|
||||
});
|
||||
|
||||
AddMock(TEST_ENTITY_UNIT_2, IID_Ownership, {
|
||||
"GetOwner": () => 2
|
||||
});
|
||||
expectedDynamicStates[TEST_ENTITY_UNIT_2].player = 2;
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetEntityState(1, TEST_ENTITY_UNIT_2), {
|
||||
"dynamicState": expectedDynamicStates[TEST_ENTITY_UNIT_2],
|
||||
"templateData": { ...expectedTemplateData.example_unit_template, "maxHitpoints": 100 },
|
||||
"modificationsID": "Health/Max:IncreasedMaxHP(1) ",
|
||||
// modifiedTemplateData for example_unit_template and the above modificationsID has been returned in the previous
|
||||
// call, but for a different owner (player 1, not player 2), so it should be computed again now.
|
||||
"modifiedTemplateData": { "maxHitpoints": 150 }
|
||||
});
|
||||
|
|
@ -1,51 +1,15 @@
|
|||
Engine.LoadHelperScript("EntityStateRetriever.js");
|
||||
Engine.LoadHelperScript("ObstructionSnap.js");
|
||||
Engine.LoadHelperScript("Player.js");
|
||||
Engine.LoadComponentScript("interfaces/AlertRaiser.js");
|
||||
Engine.LoadComponentScript("interfaces/Auras.js");
|
||||
Engine.LoadComponentScript("interfaces/Barter.js");
|
||||
Engine.LoadComponentScript("interfaces/Builder.js");
|
||||
Engine.LoadComponentScript("interfaces/Capturable.js");
|
||||
Engine.LoadComponentScript("interfaces/CeasefireManager.js");
|
||||
Engine.LoadComponentScript("interfaces/DeathDamage.js");
|
||||
Engine.LoadComponentScript("interfaces/Diplomacy.js");
|
||||
Engine.LoadComponentScript("interfaces/EndGameManager.js");
|
||||
Engine.LoadComponentScript("interfaces/EntityLimits.js");
|
||||
Engine.LoadComponentScript("interfaces/Formation.js");
|
||||
Engine.LoadComponentScript("interfaces/Foundation.js");
|
||||
Engine.LoadComponentScript("interfaces/Garrisonable.js");
|
||||
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
|
||||
Engine.LoadComponentScript("interfaces/Gate.js");
|
||||
Engine.LoadComponentScript("interfaces/Guard.js");
|
||||
Engine.LoadComponentScript("interfaces/Heal.js");
|
||||
Engine.LoadComponentScript("interfaces/Health.js");
|
||||
Engine.LoadComponentScript("interfaces/Loot.js");
|
||||
Engine.LoadComponentScript("interfaces/Market.js");
|
||||
Engine.LoadComponentScript("interfaces/Pack.js");
|
||||
Engine.LoadComponentScript("interfaces/Population.js");
|
||||
Engine.LoadComponentScript("interfaces/PopulationCapManager.js");
|
||||
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
|
||||
Engine.LoadComponentScript("interfaces/Promotion.js");
|
||||
Engine.LoadComponentScript("interfaces/Repairable.js");
|
||||
Engine.LoadComponentScript("interfaces/Researcher.js");
|
||||
Engine.LoadComponentScript("interfaces/Resistance.js");
|
||||
Engine.LoadComponentScript("interfaces/ResourceDropsite.js");
|
||||
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
|
||||
Engine.LoadComponentScript("interfaces/ResourceTrickle.js");
|
||||
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
|
||||
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
|
||||
Engine.LoadComponentScript("interfaces/Trader.js");
|
||||
Engine.LoadComponentScript("interfaces/Trainer.js");
|
||||
Engine.LoadComponentScript("interfaces/TurretHolder.js");
|
||||
Engine.LoadComponentScript("interfaces/Timer.js");
|
||||
Engine.LoadComponentScript("interfaces/Treasure.js");
|
||||
Engine.LoadComponentScript("interfaces/TreasureCollector.js");
|
||||
Engine.LoadComponentScript("interfaces/Turretable.js");
|
||||
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
|
||||
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
|
||||
Engine.LoadComponentScript("interfaces/UnitAI.js");
|
||||
Engine.LoadComponentScript("interfaces/Upgrade.js");
|
||||
Engine.LoadComponentScript("interfaces/Upkeep.js");
|
||||
Engine.LoadComponentScript("interfaces/BuildingAI.js");
|
||||
Engine.LoadComponentScript("GuiInterface.js");
|
||||
|
||||
Resources = {
|
||||
|
|
@ -63,7 +27,7 @@ Resources = {
|
|||
})
|
||||
};
|
||||
|
||||
var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
|
||||
const cmpGuiInterface = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
|
||||
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_Barter, {
|
||||
|
|
@ -88,13 +52,7 @@ AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
|
|||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
||||
"GetLosVisibility": function(ent, player) { return "visible"; },
|
||||
"GetLosCircular": function() { return false; }
|
||||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
|
||||
"GetCurrentTemplateName": function(ent) { return "example"; },
|
||||
"GetTemplate": function(name) { return ""; }
|
||||
"GetLosCircular": function() { return false; },
|
||||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_PopulationCapManager, {
|
||||
|
|
@ -302,7 +260,7 @@ AddMock(101, IID_StatisticsTracker, {
|
|||
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
|
||||
// because uneval preserves property order. So make sure this object
|
||||
// matches the ordering in GuiInterface.
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetSimulationState(), {
|
||||
"players": [
|
||||
{
|
||||
"name": "Player 1",
|
||||
|
|
@ -413,7 +371,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
|
|||
"populationCap": 200
|
||||
});
|
||||
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpGuiInterface.GetExtendedSimulationState(), {
|
||||
"players": [
|
||||
{
|
||||
"name": "Player 1",
|
||||
|
|
@ -569,72 +527,3 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
|
|||
"populationCapType": "player",
|
||||
"populationCap": 200
|
||||
});
|
||||
|
||||
|
||||
AddMock(10, IID_Builder, {
|
||||
"GetEntitiesList": function()
|
||||
{
|
||||
return ["test1", "test2"];
|
||||
},
|
||||
});
|
||||
|
||||
AddMock(10, IID_Health, {
|
||||
"GetHitpoints": function() { return 50; },
|
||||
"GetMaxHitpoints": function() { return 60; },
|
||||
"IsRepairable": function() { return false; },
|
||||
"IsUnhealable": function() { return false; }
|
||||
});
|
||||
|
||||
AddMock(10, IID_Identity, {
|
||||
"GetClassesList": function() { return ["class1", "class2"]; },
|
||||
"GetRank": function() { return "foo"; },
|
||||
"GetSelectionGroupName": function() { return "Selection Group Name"; },
|
||||
"HasClass": function() { return true; },
|
||||
"IsUndeletable": function() { return false; },
|
||||
"IsControllable": function() { return true; }
|
||||
});
|
||||
|
||||
AddMock(10, IID_Position, {
|
||||
"GetTurretParent": function() { return INVALID_ENTITY; },
|
||||
"GetPosition": function()
|
||||
{
|
||||
return { "x": 1, "y": 2, "z": 3 };
|
||||
},
|
||||
"IsInWorld": function()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
AddMock(10, IID_ResourceTrickle, {
|
||||
"GetInterval": () => 1250,
|
||||
"GetRates": () => ({ "food": 2, "wood": 3, "stone": 5, "metal": 9 })
|
||||
});
|
||||
|
||||
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
|
||||
// because uneval preserves property order. So make sure this object
|
||||
// matches the ordering in GuiInterface.
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmp.GetEntityState(-1, 10), {
|
||||
"id": 10,
|
||||
"player": INVALID_PLAYER,
|
||||
"template": "example",
|
||||
"identity": {
|
||||
"rank": "foo",
|
||||
"classes": ["class1", "class2"],
|
||||
"selectionGroupName": "Selection Group Name",
|
||||
"canDelete": true,
|
||||
"controllable": true,
|
||||
},
|
||||
"position": { "x": 1, "y": 2, "z": 3 },
|
||||
"hitpoints": 50,
|
||||
"maxHitpoints": 60,
|
||||
"needsRepair": false,
|
||||
"needsHeal": true,
|
||||
"builder": true,
|
||||
"visibility": "visible",
|
||||
"isBarterMarket": true,
|
||||
"resourceTrickle": {
|
||||
"interval": 1250,
|
||||
"rates": { "food": 2, "wood": 3, "stone": 5, "metal": 9 }
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -248,3 +248,64 @@ TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_D", 10, 5), 16);
|
|||
Engine.PostMessage = oldPostMessage;
|
||||
Engine.BroadcastMessage = oldBroadcastMessage;
|
||||
})();
|
||||
|
||||
|
||||
(function Test_ModifiersInfo()
|
||||
{
|
||||
const TEST_ENTITY_1 = 13;
|
||||
const TEST_ENTITY_2 = 14;
|
||||
|
||||
cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager", {});
|
||||
cmpModifiersManager.Init();
|
||||
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpModifiersManager.GetModifiersInfo(TEST_ENTITY_1), {
|
||||
"modifiedComponents": new Set(),
|
||||
"modificationsID": ""
|
||||
});
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpModifiersManager.GetModifiersInfo(TEST_ENTITY_2), {
|
||||
"modifiedComponents": new Set(),
|
||||
"modificationsID": ""
|
||||
});
|
||||
|
||||
cmpModifiersManager.AddModifier("Component1/Property1", "Modifier1", [{ "affects": ["Unit"], "add": 1 }], TEST_ENTITY_1, true);
|
||||
cmpModifiersManager.AddModifier("Component1/Property1", "Modifier1", [{ "affects": ["Unit"], "add": 1 }], TEST_ENTITY_1, true);
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpModifiersManager.GetModifiersInfo(TEST_ENTITY_1), {
|
||||
"modifiedComponents": new Set(["Component1"]),
|
||||
"modificationsID": "Component1/Property1:Modifier1(2) "
|
||||
});
|
||||
|
||||
cmpModifiersManager.AddModifier("Component1/Property1", "Modifier2", [{ "affects": ["Unit"], "add": 2 }], TEST_ENTITY_1);
|
||||
cmpModifiersManager.AddModifier("Component1/Property2", "Modifier3", [{ "affects": ["Unit"], "add": 3 }], TEST_ENTITY_1);
|
||||
cmpModifiersManager.AddModifier("Component2/Property1", "Modifier1", [{ "affects": ["Unit"], "add": 10 }], TEST_ENTITY_1);
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpModifiersManager.GetModifiersInfo(TEST_ENTITY_1), {
|
||||
"modifiedComponents": new Set(["Component1", "Component2"]),
|
||||
"modificationsID": "Component1/Property1:Modifier1(2)Modifier2(1) Component1/Property2:Modifier3(1) Component2/Property1:Modifier1(1) "
|
||||
});
|
||||
cmpModifiersManager.RemoveModifier("Component1/Property2", "Modifier3", TEST_ENTITY_1);
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpModifiersManager.GetModifiersInfo(TEST_ENTITY_1), {
|
||||
"modifiedComponents": new Set(["Component1", "Component2"]),
|
||||
"modificationsID": "Component1/Property1:Modifier1(2)Modifier2(1) Component2/Property1:Modifier1(1) "
|
||||
});
|
||||
|
||||
const assertModificationsIDsEqual = equal =>
|
||||
{
|
||||
let expected = cmpModifiersManager.GetModifiersInfo(TEST_ENTITY_1).modificationsID ==
|
||||
cmpModifiersManager.GetModifiersInfo(TEST_ENTITY_2).modificationsID;
|
||||
if (!equal)
|
||||
expected = !expected;
|
||||
|
||||
TS_ASSERT(expected);
|
||||
};
|
||||
|
||||
// Even if modifiers were added in different orders, two entities with the same one should have the same modifications ID.
|
||||
cmpModifiersManager.AddModifier("Component2/Property1", "Modifier1", [{ "affects": ["Unit"], "add": 10 }], TEST_ENTITY_2);
|
||||
cmpModifiersManager.AddModifier("Component1/Property1", "Modifier1", [{ "affects": ["Unit"], "add": 1 }], TEST_ENTITY_2, true);
|
||||
cmpModifiersManager.AddModifier("Component1/Property1", "Modifier2", [{ "affects": ["Unit"], "add": 2 }], TEST_ENTITY_2);
|
||||
assertModificationsIDsEqual(false);
|
||||
cmpModifiersManager.AddModifier("Component1/Property1", "Modifier1", [{ "affects": ["Unit"], "add": 1 }], TEST_ENTITY_2, true);
|
||||
assertModificationsIDsEqual(true);
|
||||
cmpModifiersManager.RemoveModifier("Component1/Property1", "Modifier3", TEST_ENTITY_1);
|
||||
assertModificationsIDsEqual(true);
|
||||
cmpModifiersManager.RemoveModifier("Component2/Property1", "Modifier1", TEST_ENTITY_1);
|
||||
assertModificationsIDsEqual(false);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -141,12 +141,12 @@ cmpUpgrade.OnOwnershipChanged({ "to": playerID });
|
|||
* To start with, no techs are researched...
|
||||
*/
|
||||
// T1: Check the cost of the upgrade without accounting for any player modifications (as it would be in the structree).
|
||||
let parsed_template = g_TemplateHelper.getBasicData(template, {}, Resources, ["Upgrade"]);
|
||||
TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 });
|
||||
let parsed_template = g_TemplateHelper.getBasicData(template, {}, Resources, civCode, ["Upgrade"]);
|
||||
TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrade.options[0].cost, { "stone": 100, "wood": 50, "time": 100 });
|
||||
|
||||
// T2: Check the value, this time accounting for player modifiers (as it would be in-session).
|
||||
parsed_template = g_TemplateHelper.computeDataFromPlayer(template, {}, Resources, playerID, ["Upgrade"]);
|
||||
TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 });
|
||||
parsed_template = g_TemplateHelper.computeDataFromPlayer(template, {}, Resources, playerID, civCode, ["Upgrade"]);
|
||||
TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrade.options[0].cost, { "stone": 100, "wood": 50, "time": 100 });
|
||||
|
||||
// T3: Check that the value is correct within the Update Component.
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 100, "wood": 50, "time": 100 });
|
||||
|
|
@ -159,12 +159,12 @@ cmpUpgrade.Upgrade("structures/" + civCode + "/defense_tower");
|
|||
isResearched = true;
|
||||
|
||||
// T4: Check that the player-less value hasn't increased...
|
||||
parsed_template = g_TemplateHelper.getBasicData(template, {}, Resources, ["Upgrade"]);
|
||||
TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 });
|
||||
parsed_template = g_TemplateHelper.getBasicData(template, {}, Resources, civCode, ["Upgrade"]);
|
||||
TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrade.options[0].cost, { "stone": 100, "wood": 50, "time": 100 });
|
||||
|
||||
// T5: ...but the player-backed value has.
|
||||
parsed_template = g_TemplateHelper.computeDataFromPlayer(template, {}, Resources, playerID, ["Upgrade"]);
|
||||
TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 160, "wood": 25, "time": 90 });
|
||||
parsed_template = g_TemplateHelper.computeDataFromPlayer(template, {}, Resources, playerID, civCode, ["Upgrade"]);
|
||||
TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrade.options[0].cost, { "stone": 160, "wood": 25, "time": 90 });
|
||||
|
||||
// T6: The upgrade component should still be using the old resource cost (but new time cost) for the upgrade in progress...
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 100, "wood": 50, "time": 90 });
|
||||
|
|
|
|||
|
|
@ -0,0 +1,365 @@
|
|||
/**
|
||||
* Helper class for the GuiInterface for retrieving an information about an entity or a template.
|
||||
* It assumes a certain level of caching on the receiving side (the GUI) and leverages it by only computing
|
||||
* the absolutely necessary ("unpredictable") parts of the desired data and leaves it up to the receiver to
|
||||
* put the whole data together by combining it with what has been returned previously already.
|
||||
*
|
||||
* It works like this:
|
||||
* Certain parts (the template data) of an entity state are "predictable" and can therefore be reused between
|
||||
* entities with the same template. What one has to account for, however, is that values of the template data
|
||||
* can be modified. This happens on two different levels:
|
||||
* - Firstly, player-global modifications, which apply to all entities owned by that player. This includes stuff
|
||||
* like bonuses from researched techs, civs bonuses, team bonuses.
|
||||
* - Secondly, entity-local modifications, which apply to individual entities. This includes buffs or debuffs
|
||||
* from status effects or auras (of other entities).
|
||||
*
|
||||
* To take advantage of this, we separate the whole entity state into three parts:
|
||||
* 1. The template data of the entity's template, which only has the player-global modifications applied. It is
|
||||
* the same for all entities with the same template and owner.
|
||||
* 2. The modified template data of the entity, which can contain a subset of the template data's values,
|
||||
* overwriting them. It has both the player-global and entity-local modifications applied, but only contains the
|
||||
* values that actually differ from its template data. It is therefore the same for all entities with the same
|
||||
* owner, template, and modifications ID.
|
||||
* 3. the dynamic state, which differs from entity to entity and has to always be computed from scratch. It should be kept
|
||||
* as small as possible.
|
||||
*/
|
||||
class EntityStateRetriever
|
||||
{
|
||||
_computedTemplateData = {};
|
||||
|
||||
/**
|
||||
* Compute basic information about a given template accounting for all modifications registered to a given player.
|
||||
* The returned data is (of course) consistent. So to avoid unnecessary work, this should never be called twice for the
|
||||
* same player and template, unless the player's modifications changed in the meantime. It is the caller's responsibility
|
||||
* to cache and reuse the results.
|
||||
*/
|
||||
getTemplateData(player, templateName)
|
||||
{
|
||||
if (this._hasComputedTemplateData(player, templateName))
|
||||
warn("GetTemplateData called multiple times for the template '" + templateName + "'and player " + player + ". The return value should have been cached in the GUI.");
|
||||
|
||||
this._addComputedTemplateData(player, templateName);
|
||||
|
||||
const template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager)?.GetTemplate(templateName);
|
||||
const civ = QueryPlayerIDInterface(player, IID_Identity).GetCiv();
|
||||
return template && g_TemplateHelper.computeDataFromPlayer(template, AuraTemplates.GetAll(), Resources, player, civ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute basic information about a given entity. This includes the template data, the modified template data, and
|
||||
* the dynamic state, but only what hasn't been computed since the last time that the player modifications changed.
|
||||
* The caller has to cache and reuse the data where possible. See the class description.
|
||||
*/
|
||||
getEntityState(player, ent)
|
||||
{
|
||||
if (!ent || ent == INVALID_ENTITY)
|
||||
return null;
|
||||
|
||||
const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
|
||||
if (!cmpTemplateManager)
|
||||
return null;
|
||||
|
||||
const templateName = cmpTemplateManager.GetCurrentTemplateName(ent);
|
||||
const template = templateName && cmpTemplateManager.GetTemplate(templateName);
|
||||
const owner = Engine.QueryInterface(ent, IID_Ownership)?.GetOwner();
|
||||
// All entities must have a template and an owner; if not then it's a nonexistent entity id.
|
||||
if (!template || owner === undefined)
|
||||
return null;
|
||||
|
||||
const ret = {
|
||||
"dynamicState": this._computeDynamicState(owner, player, ent, templateName)
|
||||
};
|
||||
|
||||
const civ = QueryPlayerIDInterface(owner, IID_Identity).GetCiv();
|
||||
if (!this._hasComputedTemplateData(owner, templateName))
|
||||
{
|
||||
ret.templateData = g_TemplateHelper.computeDataFromPlayer(template, AuraTemplates.GetAll(), Resources, owner, civ);
|
||||
this._addComputedTemplateData(owner, templateName);
|
||||
}
|
||||
|
||||
const info = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager)?.GetModifiersInfo(ent);
|
||||
// A falsy modifications ID means the entity doesn't have any modifications applied locally, just the ones from its owner.
|
||||
if (info.modificationsID)
|
||||
{
|
||||
ret.modificationsID = info.modificationsID;
|
||||
if (!this._hasComputedTemplateData(owner, templateName, info.modificationsID))
|
||||
{
|
||||
ret.modifiedTemplateData = g_TemplateHelper.computeDataFromEntity(template, AuraTemplates.GetAll(), Resources, ent, civ, info.modifiedComponents);
|
||||
this._addComputedTemplateData(owner, templateName, info.modificationsID);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
onPlayerModificationsChanged(player)
|
||||
{
|
||||
this._computedTemplateData[player]?.clear();
|
||||
}
|
||||
|
||||
reset()
|
||||
{
|
||||
for (const player in this._computedTemplateData)
|
||||
this._computedTemplateData[player].clear();
|
||||
}
|
||||
|
||||
_addComputedTemplateData(player, templateName, modificationsID = "")
|
||||
{
|
||||
let playerTable = this._computedTemplateData[player];
|
||||
if (!playerTable)
|
||||
{
|
||||
playerTable = new Set();
|
||||
this._computedTemplateData[player] = playerTable;
|
||||
}
|
||||
|
||||
playerTable.add(`${templateName} ${modificationsID}`);
|
||||
}
|
||||
|
||||
_hasComputedTemplateData(player, templateName, modificationsID = "")
|
||||
{
|
||||
return this._computedTemplateData[player]?.has(`${templateName} ${modificationsID}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity's dynamic state, which is unpredictable and differs from entity to entity.
|
||||
* The information is pulled from its components.
|
||||
* The structure of each component's data has to be coordinated with the one in TemplateHelper, so that they
|
||||
* can later be merged into one.
|
||||
* @param {number} owner - The owner of the entity.
|
||||
* @param {number} player - The player, for whom to calculate the state for.
|
||||
* @param {number} ent - ID of the entity.
|
||||
* @param {string} templateName - The entity's template.
|
||||
*/
|
||||
_computeDynamicState(owner, player, ent, templateName)
|
||||
{
|
||||
const ret = {
|
||||
"id": ent,
|
||||
// TODO: Should maybe be renamed to owner for clarity.
|
||||
"player": owner,
|
||||
"templateName": templateName
|
||||
};
|
||||
|
||||
const cmpAttack = Engine.QueryInterface(ent, IID_Attack);
|
||||
const cmpPosition = Engine.QueryInterface(ent, IID_Position);
|
||||
const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||
const cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
|
||||
if (cmpPosition?.IsInWorld() && cmpAttack?.GetAttackTypes().includes("Ranged"))
|
||||
{
|
||||
ret.attack = {
|
||||
"Ranged": {
|
||||
// For units, take the range in front of it, no spread, so angle = 0,
|
||||
// else, take the average elevation around it: angle = 2 * pi.
|
||||
"elevationAdaptedRange": cmpRangeManager.GetElevationAdaptedRange(
|
||||
cmpPosition.GetPosition(), cmpPosition.GetRotation(), cmpAttack.GetRange("Ranged").max,
|
||||
cmpAttack.GetAttackYOrigin("Ranged"), cmpUnitAI ? 0 : 2 * Math.PI
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
|
||||
if (cmpBuildingAI)
|
||||
ret.buildingAI = {
|
||||
"arrowCount": cmpBuildingAI.GetArrowCount()
|
||||
};
|
||||
|
||||
const cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
|
||||
if (cmpCapturable)
|
||||
ret.capturePoints = cmpCapturable.GetCapturePoints();
|
||||
|
||||
const cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
|
||||
ret.controllable = !cmpIdentity || cmpIdentity.IsControllable();
|
||||
|
||||
const cmpFormation = Engine.QueryInterface(ent, IID_Formation);
|
||||
if (cmpFormation)
|
||||
ret.formation = {
|
||||
"members": cmpFormation.GetMembers()
|
||||
};
|
||||
|
||||
const cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
|
||||
if (cmpFoundation)
|
||||
ret.foundation = {
|
||||
"numBuilders": cmpFoundation.GetNumBuilders(),
|
||||
"buildTime": cmpFoundation.GetBuildTime()
|
||||
};
|
||||
|
||||
const cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
|
||||
if (cmpGarrisonable)
|
||||
ret.garrisonable = {
|
||||
"holder": cmpGarrisonable.HolderID()
|
||||
};
|
||||
|
||||
const cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
|
||||
if (cmpGarrisonHolder)
|
||||
ret.garrisonHolder = {
|
||||
"entities": cmpGarrisonHolder.GetEntities(),
|
||||
"occupiedSlots": cmpGarrisonHolder.OccupiedSlots()
|
||||
};
|
||||
|
||||
const cmpGate = Engine.QueryInterface(ent, IID_Gate);
|
||||
if (cmpGate)
|
||||
ret.gate = {
|
||||
"locked": cmpGate.IsLocked()
|
||||
};
|
||||
|
||||
const cmpGuard = Engine.QueryInterface(ent, IID_Guard);
|
||||
if (cmpGuard)
|
||||
ret.guard = {
|
||||
"entities": cmpGuard.GetEntities()
|
||||
};
|
||||
|
||||
const cmpHealth = QueryMiragedInterface(ent, IID_Health);
|
||||
if (cmpHealth)
|
||||
{
|
||||
ret.hitpoints = cmpHealth.GetHitpoints();
|
||||
ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.IsInjured();
|
||||
ret.needsHeal = !cmpHealth.IsUnhealable();
|
||||
}
|
||||
|
||||
if (cmpPosition)
|
||||
{
|
||||
if (cmpPosition.IsInWorld())
|
||||
ret.position = cmpPosition.GetPosition();
|
||||
if (cmpPosition.GetTurretParent() != INVALID_ENTITY)
|
||||
ret.turretParent = cmpPosition.GetTurretParent();
|
||||
}
|
||||
|
||||
const cmpPack = Engine.QueryInterface(ent, IID_Pack);
|
||||
if (cmpPack)
|
||||
ret.pack = {
|
||||
"packed": cmpPack.IsPacked(),
|
||||
"progress": cmpPack.GetProgress()
|
||||
};
|
||||
|
||||
const cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
|
||||
if (cmpProductionQueue)
|
||||
ret.production = {
|
||||
"queue": cmpProductionQueue.GetQueue(),
|
||||
"autoqueue": cmpProductionQueue.IsAutoQueueing()
|
||||
};
|
||||
|
||||
const cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
|
||||
if (cmpPromotion)
|
||||
ret.promotion = {
|
||||
"curr": cmpPromotion.GetCurrentXp(),
|
||||
};
|
||||
|
||||
const cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
|
||||
if (cmpRallyPoint)
|
||||
ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
|
||||
|
||||
const cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
|
||||
if (cmpRepairable)
|
||||
ret.repairable = {
|
||||
"numBuilders": cmpRepairable.GetNumBuilders(),
|
||||
"buildTime": cmpRepairable.GetBuildTime()
|
||||
};
|
||||
|
||||
const cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
|
||||
if (cmpResourceDropsite)
|
||||
ret.resourceDropsite = {
|
||||
"shared": cmpResourceDropsite.IsShared()
|
||||
};
|
||||
|
||||
const cmpResearcher = Engine.QueryInterface(ent, IID_Researcher);
|
||||
if (cmpResearcher)
|
||||
ret.researcher = {
|
||||
"technologies": cmpResearcher.GetTechnologiesList()
|
||||
};
|
||||
|
||||
const cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
|
||||
if (cmpResourceGatherer)
|
||||
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
|
||||
|
||||
const cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
|
||||
if (cmpResourceSupply)
|
||||
ret.resourceSupply = {
|
||||
"amount": cmpResourceSupply.GetCurrentAmount(),
|
||||
"numGatherers": cmpResourceSupply.GetNumGatherers()
|
||||
};
|
||||
|
||||
const cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver);
|
||||
if (cmpStatusEffects)
|
||||
ret.statusEffects = cmpStatusEffects.GetActiveStatuses();
|
||||
|
||||
const cmpTrader = Engine.QueryInterface(ent, IID_Trader);
|
||||
if (cmpTrader)
|
||||
ret.trader = {
|
||||
"goods": cmpTrader.GetGoods()
|
||||
};
|
||||
|
||||
const cmpTrainer = Engine.QueryInterface(ent, IID_Trainer);
|
||||
if (cmpTrainer)
|
||||
ret.trainer = {
|
||||
// TODO: This is technically not "dynamic", since it only depends on the template and modifications,
|
||||
// so it should be made part of the template data instead in some way without causing too much code
|
||||
// duplication.
|
||||
"entities": cmpTrainer.GetEntitiesList()
|
||||
};
|
||||
|
||||
const cmpTurretable = Engine.QueryInterface(ent, IID_Turretable);
|
||||
if (cmpTurretable)
|
||||
ret.turretable = {
|
||||
"ejectable": cmpTurretable.IsEjectable(),
|
||||
"holder": cmpTurretable.HolderID()
|
||||
};
|
||||
|
||||
const cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder);
|
||||
if (cmpTurretHolder)
|
||||
ret.turretHolder = {
|
||||
"turretPoints": cmpTurretHolder.GetTurretPoints()
|
||||
};
|
||||
|
||||
|
||||
if (cmpUnitAI)
|
||||
ret.unitAI = {
|
||||
"state": cmpUnitAI.GetCurrentState(),
|
||||
"orders": cmpUnitAI.GetOrders(),
|
||||
"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
|
||||
"isGuarding": cmpUnitAI.IsGuardOf(),
|
||||
"selectableStances": cmpUnitAI.GetSelectableStances(),
|
||||
"isIdle": cmpUnitAI.IsIdle(),
|
||||
"formation": cmpUnitAI.GetFormationController()
|
||||
};
|
||||
|
||||
const cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
|
||||
if (cmpUpgrade)
|
||||
ret.upgrade = {
|
||||
"progress": cmpUpgrade.GetProgress(),
|
||||
"template": cmpUpgrade.GetUpgradingTo(),
|
||||
"isUpgrading": cmpUpgrade.IsUpgrading()
|
||||
};
|
||||
|
||||
if (cmpRangeManager)
|
||||
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
|
||||
|
||||
|
||||
// Because mirage entities mirage other entities' components, some values can't be statically read from their
|
||||
// template. They therefore are "missing" from their template data, so we retrieve them from the components
|
||||
// directly here and add them to the dynamic state instead, so that they're still present in the entire final
|
||||
// entity state (the GUI expects them).
|
||||
// This is somewhat hacky and it would be good to achieve this in another way.
|
||||
const cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
|
||||
if (cmpMirage)
|
||||
{
|
||||
if (cmpCapturable)
|
||||
ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
|
||||
|
||||
if (cmpHealth)
|
||||
ret.maxHitpoints = cmpHealth.GetMaxHitpointes();
|
||||
|
||||
if (cmpResourceSupply)
|
||||
{
|
||||
ret.resourceSupply.isInfinite = cmpResourceSupply.IsInfinite();
|
||||
ret.resourceSupply.max = cmpResourceSupply.GetMaxAmount();
|
||||
ret.resourceSupply.type = cmpResourceSupply.GetType();
|
||||
ret.resourceSupply.killBeforeGather = cmpResourceSupply.GetKillBeforeGather();
|
||||
ret.resourceSupply.maxGatherers = cmpResourceSupply.GetMaxGatherers();
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
Engine.RegisterGlobal("EntityStateRetriever", EntityStateRetriever);
|
||||
Loading…
Reference in a new issue