mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-08-15 14:43:32 -07:00
Compare commits
6 commits
9239b5952b
...
2728c65a49
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2728c65a49 | ||
|
|
0a7a6aac87 | ||
|
|
8c36cc4879 | ||
|
|
60970320e3 | ||
|
|
072ad01816 | ||
|
|
0a3bd16cb5 |
30 changed files with 1633 additions and 765 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,8 +30,7 @@ class PanelEntity
|
|||
this.panelEntButton.hidden = false;
|
||||
|
||||
const entityState = GetEntityState(entityID);
|
||||
const template = GetTemplateData(entityState.template);
|
||||
this.nameTooltip = setStringTags(g_SpecificNamesPrimary ? template.name.specific : template.name.generic, this.NameTags) + "\n";
|
||||
this.nameTooltip = setStringTags(g_SpecificNamesPrimary ? entityState.name.specific : entityState.name.generic, this.NameTags) + "\n";
|
||||
|
||||
Engine.GetGUIObjectByName("panelEntityHealthSection[" + buttonID + "]").hidden = !entityState.hitpoints;
|
||||
|
||||
|
|
@ -45,7 +44,7 @@ class PanelEntity
|
|||
}
|
||||
|
||||
Engine.GetGUIObjectByName("panelEntityImage[" + buttonID + "]").sprite =
|
||||
"stretched:" + this.PortraitDirectory + template.icon;
|
||||
"stretched:" + this.PortraitDirectory + entityState.icon;
|
||||
}
|
||||
|
||||
destroy()
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -1130,8 +1130,9 @@ function handleInputAfterGui(ev)
|
|||
{
|
||||
if (clickedEntity == INVALID_ENTITY)
|
||||
clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y);
|
||||
const clickedEntityState = GetEntityState(clickedEntity);
|
||||
// Abort if we didn't click on an entity or if the entity was removed before the mousebuttonup event.
|
||||
if (clickedEntity == INVALID_ENTITY || !GetEntityState(clickedEntity))
|
||||
if (clickedEntity == INVALID_ENTITY || !clickedEntityState)
|
||||
{
|
||||
clickedEntity = INVALID_ENTITY;
|
||||
if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove"))
|
||||
|
|
@ -1157,18 +1158,18 @@ function handleInputAfterGui(ev)
|
|||
|
||||
if (ev.clicks == 2)
|
||||
{
|
||||
templateToMatch = GetEntityState(clickedEntity).identity.selectionGroupName;
|
||||
templateToMatch = clickedEntityState.selectionGroupName;
|
||||
if (templateToMatch)
|
||||
matchRank = false;
|
||||
else
|
||||
// No selection group name defined, so fall back to exact match.
|
||||
templateToMatch = GetEntityState(clickedEntity).template;
|
||||
templateToMatch = clickedEntityState.templateName;
|
||||
|
||||
}
|
||||
else
|
||||
// Triple click
|
||||
// Select units matching exact template name (same rank).
|
||||
templateToMatch = GetEntityState(clickedEntity).template;
|
||||
templateToMatch = clickedEntityState.templateName;
|
||||
|
||||
// TODO: Should we handle "control all units" here as well?
|
||||
ents = Engine.PickSimilarPlayerEntities(templateToMatch, showOffscreen, matchRank, false);
|
||||
|
|
@ -1990,7 +1991,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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ function _setMotionOverlay(ents, enabled, motionDebugOverlay, force = false)
|
|||
{
|
||||
resultSet.add(ent);
|
||||
const entState = GetEntityState(ent);
|
||||
if (entState?.unitAI?.formation)
|
||||
if (entState?.unitAI?.formationController)
|
||||
{
|
||||
resultSet.add(entState.unitAI.formation);
|
||||
resultSet.add(entState.unitAI.formationController);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ EntityGroups.prototype.add = function(ents)
|
|||
{
|
||||
if (this.ents[ent])
|
||||
continue;
|
||||
var entState = GetEntityState(ent);
|
||||
const entState = GetEntityState(ent);
|
||||
|
||||
// When this function is called during group rebuild, deleted
|
||||
// entities will not yet have been removed, so entities might
|
||||
|
|
@ -82,8 +82,7 @@ EntityGroups.prototype.add = function(ents)
|
|||
if (!entState)
|
||||
continue;
|
||||
|
||||
var templateName = entState.template;
|
||||
var key = GetTemplateData(templateName, entState.player).selectionGroupName || templateName;
|
||||
let key = entState.selectionGroupName || entState.templateName;
|
||||
|
||||
// Group the ents by player and template
|
||||
if (entState.player !== undefined)
|
||||
|
|
@ -235,7 +234,7 @@ EntitySelection.prototype.getTemplateNames = function()
|
|||
{
|
||||
const entState = GetEntityState(ent);
|
||||
if (entState)
|
||||
templateNames.push(entState.template);
|
||||
templateNames.push(entState.templateName);
|
||||
}
|
||||
return templateNames;
|
||||
};
|
||||
|
|
@ -519,8 +518,8 @@ EntitySelection.prototype.addFormationMembers = function(entities)
|
|||
for (const entity of entities)
|
||||
{
|
||||
const entState = GetEntityState(+entity);
|
||||
if (entState?.unitAI?.formation)
|
||||
for (const member of GetEntityState(+entState.unitAI.formation).formation.members)
|
||||
if (entState?.unitAI?.formationController)
|
||||
for (const member of GetEntityState(+entState.unitAI.formationController).formation.members)
|
||||
result.add(member);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,15 +48,13 @@ function updateGarrisonHealthBar(entState, selection)
|
|||
// Fills out information that most entities have
|
||||
function displaySingle(entState)
|
||||
{
|
||||
const template = GetTemplateData(entState.template);
|
||||
|
||||
const primaryName = g_SpecificNamesPrimary ? template.name.specific : template.name.generic;
|
||||
const primaryName = g_SpecificNamesPrimary ? entState.name.specific : entState.name.generic;
|
||||
let secondaryName;
|
||||
if (g_ShowSecondaryNames)
|
||||
secondaryName = g_SpecificNamesPrimary ? template.name.generic : template.name.specific;
|
||||
secondaryName = g_SpecificNamesPrimary ? entState.name.generic : entState.name.specific;
|
||||
|
||||
// If packed, add that to the generic name (reduces template clutter).
|
||||
if (template.pack && template.pack.state == "packed")
|
||||
if (entState.pack?.state === "packed")
|
||||
{
|
||||
if (secondaryName && g_ShowSecondaryNames)
|
||||
secondaryName = sprintf(translate("%(secondaryName)s — Packed"), { "secondaryName": secondaryName });
|
||||
|
|
@ -75,13 +73,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
|
||||
|
|
@ -327,8 +325,8 @@ function displaySingle(entState)
|
|||
}
|
||||
|
||||
// TODO: we should require all entities to have icons
|
||||
Engine.GetGUIObjectByName("icon").sprite = template.icon ? ("stretched:session/portraits/" + template.icon) : "BackgroundBlack";
|
||||
if (template.icon)
|
||||
Engine.GetGUIObjectByName("icon").sprite = entState.icon ? ("stretched:session/portraits/" + entState.icon) : "BackgroundBlack";
|
||||
if (entState.icon)
|
||||
{
|
||||
const iconBorder = Engine.GetGUIObjectByName("iconBorder");
|
||||
|
||||
|
|
@ -339,7 +337,7 @@ function displaySingle(entState)
|
|||
|
||||
iconBorder.onPressRight = () =>
|
||||
{
|
||||
showTemplateDetails(entState.template, playerState.civ);
|
||||
showTemplateDetails(entState.templateName, playerState.civ);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -373,7 +371,7 @@ function displaySingle(entState)
|
|||
getAurasTooltip,
|
||||
getEntityTooltip,
|
||||
getTreasureTooltip
|
||||
].map(func => func(template)));
|
||||
].map(func => func(entState)));
|
||||
|
||||
const leftClickTooltip = hasClass(entState, "Unit") ? getFollowOnLeftClickTooltip() : getFocusOnLeftClickTooltip();
|
||||
iconTooltips.push(leftClickTooltip + " " + getTemplateViewerOnRightClickTooltip());
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
|
|
@ -343,14 +343,12 @@ g_SelectionPanels.Garrison = {
|
|||
"setupButton": function(data)
|
||||
{
|
||||
const entState = GetEntityState(data.item.ents[0]);
|
||||
|
||||
const template = GetTemplateData(entState.template, entState.player);
|
||||
if (!template)
|
||||
if (!entState)
|
||||
return false;
|
||||
|
||||
data.button.onPress = function()
|
||||
{
|
||||
unloadTemplate(template.selectionGroupName || entState.template, entState.player);
|
||||
unloadTemplate(entState.selectionGroupName || entState.templateName, entState.player);
|
||||
};
|
||||
|
||||
data.countDisplay.caption = data.item.ents.length > 1 ? data.item.ents.length : "";
|
||||
|
|
@ -360,9 +358,9 @@ g_SelectionPanels.Garrison = {
|
|||
data.button.enabled = canUngarrison;
|
||||
|
||||
data.button.tooltip = (canUngarrison ?
|
||||
sprintf(translate("Unload %(name)s"), { "name": getEntityNames(template) }) + "\n" +
|
||||
sprintf(translate("Unload %(name)s"), { "name": getEntityNames(entState) }) + "\n" +
|
||||
translate("Single-click to unload 1. Shift-click to unload all of this type.") :
|
||||
getEntityNames(template)) + "\n" +
|
||||
getEntityNames(entState)) + "\n" +
|
||||
sprintf(translate("Player: %(playername)s"), {
|
||||
"playername": g_Players[entState.player].name
|
||||
});
|
||||
|
|
@ -374,7 +372,7 @@ g_SelectionPanels.Garrison = {
|
|||
// also appear disabled to the owner of the structure.
|
||||
data.icon.sprite =
|
||||
(canUngarrison || g_IsObserver ? "" : "grayscale:") +
|
||||
"stretched:session/portraits/" + template.icon;
|
||||
"stretched:session/portraits/" + entState.icon;
|
||||
|
||||
setPanelObjectPosition(data.button, data.i, data.rowLength);
|
||||
|
||||
|
|
@ -1048,7 +1046,7 @@ g_SelectionPanels.Research = {
|
|||
{
|
||||
showTemplateDetails(
|
||||
t,
|
||||
GetTemplateData(baseData.unitEntStates.find(state => state.id == baseData.item.researchFacilityId).template, state.player).nativeCiv
|
||||
baseData.unitEntStates.find(state => state.id == baseData.item.researchFacilityId).nativeCiv
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -1114,8 +1112,7 @@ g_SelectionPanels.Selection = {
|
|||
"setupButton": function(data)
|
||||
{
|
||||
const entState = GetEntityState(data.item.ents[0]);
|
||||
const template = GetTemplateData(entState.template, entState.player);
|
||||
if (!template)
|
||||
if (!entState)
|
||||
return false;
|
||||
|
||||
for (const ent of data.item.ents)
|
||||
|
|
@ -1152,7 +1149,7 @@ g_SelectionPanels.Selection = {
|
|||
}
|
||||
|
||||
const unitOwner = GetEntityState(data.item.ents[0]).player;
|
||||
let tooltip = getEntityNames(template);
|
||||
let tooltip = getEntityNames(entState);
|
||||
if (data.carried)
|
||||
tooltip += "\n" + Object.keys(data.carried).map(res =>
|
||||
resourceIcon(res) + data.carried[res]
|
||||
|
|
@ -1177,8 +1174,8 @@ g_SelectionPanels.Selection = {
|
|||
};
|
||||
data.button.onPressRight = function() { removeFromSelectionGroup(data.item.key); };
|
||||
|
||||
if (template.icon)
|
||||
data.icon.sprite = "stretched:session/portraits/" + template.icon;
|
||||
if (entState.icon)
|
||||
data.icon.sprite = "stretched:session/portraits/" + entState.icon;
|
||||
|
||||
setPanelObjectPosition(data.button, data.i, data.rowLength);
|
||||
return true;
|
||||
|
|
@ -1192,17 +1189,18 @@ g_SelectionPanels.Stance = {
|
|||
},
|
||||
"getItems": function(unitEntStates)
|
||||
{
|
||||
if (unitEntStates.some(state => !state.unitAI || !hasClass(state, "Unit") || hasClass(state, "Animal")))
|
||||
if (unitEntStates.some(state => !state.unitAI || !hasClass(state, "Unit") || hasClass(state, "Animal")) ||
|
||||
unitEntStates.every(state => state.turretable && state.turretable.holder !== INVALID_ENTITY))
|
||||
return [];
|
||||
|
||||
return unitEntStates[0].unitAI.selectableStances;
|
||||
return Object.keys(this.stancesData);
|
||||
},
|
||||
"setupButton": function(data)
|
||||
{
|
||||
const unitIds = data.unitEntStates.map(state => state.id);
|
||||
data.button.onPress = function() { performStance(unitIds, data.item); };
|
||||
|
||||
data.button.tooltip = getStanceDisplayName(data.item) + "\n" + bodyFont(getStanceTooltip(data.item));
|
||||
data.button.tooltip = this.stancesData[data.item].Name + "\n" + bodyFont(this.stancesData[data.item].Tooltip);
|
||||
|
||||
data.guiSelection.hidden = !Engine.GuiInterfaceCall("IsStanceSelected", {
|
||||
"ents": unitIds,
|
||||
|
|
@ -1213,6 +1211,28 @@ g_SelectionPanels.Stance = {
|
|||
|
||||
setPanelObjectPosition(data.button, data.i, data.rowLength);
|
||||
return true;
|
||||
},
|
||||
"stancesData": {
|
||||
"violent": {
|
||||
"Name": translateWithContext("stance", "Violent"),
|
||||
"Tooltip": translateWithContext("stance", "Attack nearby opponents, focus on attackers and chase while visible")
|
||||
},
|
||||
"aggressive": {
|
||||
"Name": translateWithContext("stance", "Aggressive"),
|
||||
"Tooltip": translateWithContext("stance", "Attack nearby opponents")
|
||||
},
|
||||
"defensive": {
|
||||
"Name": translateWithContext("stance", "Defensive"),
|
||||
"Tooltip": translateWithContext("stance", "Attack nearby opponents, chase a short distance and return to the original location")
|
||||
},
|
||||
"passive": {
|
||||
"Name": translateWithContext("stance", "Passive"),
|
||||
"Tooltip": translateWithContext("stance", "Flee if attacked")
|
||||
},
|
||||
"standground": {
|
||||
"Name": translateWithContext("stance", "Standground"),
|
||||
"Tooltip": translateWithContext("stance", "Attack opponents in range, but don't move")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1334,10 +1354,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 +1375,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 +1442,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:";
|
||||
|
|
|
|||
|
|
@ -45,45 +45,6 @@ function resourcesToAlphaMask(neededResources)
|
|||
return "color:255 0 0 " + Math.min(125, Math.round(+totalCost / 10) + 50);
|
||||
}
|
||||
|
||||
function getStanceDisplayName(name)
|
||||
{
|
||||
switch (name)
|
||||
{
|
||||
case "violent":
|
||||
return translateWithContext("stance", "Violent");
|
||||
case "aggressive":
|
||||
return translateWithContext("stance", "Aggressive");
|
||||
case "defensive":
|
||||
return translateWithContext("stance", "Defensive");
|
||||
case "passive":
|
||||
return translateWithContext("stance", "Passive");
|
||||
case "standground":
|
||||
return translateWithContext("stance", "Standground");
|
||||
default:
|
||||
warn("Internationalization: Unexpected stance found: " + name);
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
function getStanceTooltip(name)
|
||||
{
|
||||
switch (name)
|
||||
{
|
||||
case "violent":
|
||||
return translateWithContext("stance", "Attack nearby opponents, focus on attackers and chase while visible");
|
||||
case "aggressive":
|
||||
return translateWithContext("stance", "Attack nearby opponents");
|
||||
case "defensive":
|
||||
return translateWithContext("stance", "Attack nearby opponents, chase a short distance and return to the original location");
|
||||
case "passive":
|
||||
return translateWithContext("stance", "Flee if attacked");
|
||||
case "standground":
|
||||
return translateWithContext("stance", "Attack opponents in range, but don't move");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format entity count/limit message for the tooltip
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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,15 +757,14 @@ function onTick()
|
|||
|
||||
function onSimulationUpdate()
|
||||
{
|
||||
g_EntityStates = {};
|
||||
g_EntityStates.clear();
|
||||
g_SimState = undefined;
|
||||
|
||||
Engine.ProfileStart("sdf");
|
||||
const info = Engine.GuiInterfaceCall("GetAndClearTemplateChanges");
|
||||
// Templates change depending on technologies and auras, so they have to be reloaded after such a change.
|
||||
// g_TechnologyData data never changes, so it shouldn't be cleared.
|
||||
if (info.playersWithModifiedTemplates)
|
||||
for (const player of info.playersWithModifiedTemplates)
|
||||
if (info.playersWithChangedModifications)
|
||||
for (const player of info.playersWithChangedModifications)
|
||||
// Player-global modifications changed (e.g. by a technology), so the template data cache needs to be cleared.
|
||||
g_TemplateData[player] = {};
|
||||
|
||||
// Some changes require reloading some cached values even if the selection itself didn't change.
|
||||
|
|
@ -680,7 +776,7 @@ function onSimulationUpdate()
|
|||
if (!GetSimState())
|
||||
return;
|
||||
|
||||
GetMultipleEntityStates(g_Selection.toList());
|
||||
PreloadMultipleEntityStates(g_Selection.toList());
|
||||
|
||||
for (const handler of g_SimulationUpdateHandlers)
|
||||
handler();
|
||||
|
|
@ -728,8 +824,7 @@ function updateGroups()
|
|||
// Determine the sum of the costs of a given template
|
||||
const getCostSum = (ent) =>
|
||||
{
|
||||
const entState = GetEntityState(ent);
|
||||
const cost = GetTemplateData(entState.template, entState.player).cost;
|
||||
const cost = GetEntityState(ent).cost;
|
||||
return cost ? Object.keys(cost).map(key => cost[key]).reduce((sum, cur) => sum + cur) : 0;
|
||||
};
|
||||
|
||||
|
|
@ -746,12 +841,12 @@ function updateGroups()
|
|||
// Choose the icon of the most common template (or the most costly if it's not unique)
|
||||
if (g_Groups.groups[i].getTotalCount() > 0)
|
||||
{
|
||||
const icon = GetTemplateData(GetEntityState(g_Groups.groups[i].getEntsGrouped().reduce((pre, cur) =>
|
||||
const icon = GetEntityState(g_Groups.groups[i].getEntsGrouped().reduce((pre, cur) =>
|
||||
{
|
||||
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]).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";
|
||||
|
|
@ -1250,7 +1250,7 @@ var g_UnitActions =
|
|||
let trader;
|
||||
if (entState.trainer?.entities?.length)
|
||||
for (let i = 0; i < entState.trainer.entities.length; ++i)
|
||||
if ((trader = GetTemplateData(entState.trainer.entities[i]).trader))
|
||||
if ((trader = entState.trainer.entities[i].trader))
|
||||
break;
|
||||
|
||||
const traderData = {
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -26,23 +26,6 @@ Auras.prototype.GetModifierIdentifier = function(name)
|
|||
return "aura/" + name;
|
||||
};
|
||||
|
||||
Auras.prototype.GetDescriptions = function()
|
||||
{
|
||||
var ret = {};
|
||||
for (const auraID of this.GetAuraNames())
|
||||
{
|
||||
const aura = AuraTemplates.Get(auraID);
|
||||
ret[auraID] = {
|
||||
"name": {
|
||||
"generic": aura.auraName
|
||||
},
|
||||
"description": aura.auraDescription || null,
|
||||
"radius": this.GetRange(auraID) || null
|
||||
};
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
Auras.prototype.GetAuraNames = function()
|
||||
{
|
||||
return this.template._string.split(/\s+/);
|
||||
|
|
|
|||
|
|
@ -36,8 +36,9 @@ GuiInterface.prototype.Init = function()
|
|||
this.entsRallyPointsDisplayed = [];
|
||||
this.entsWithAuraAndStatusBars = new Set();
|
||||
this.enabledVisualRangeOverlayTypes = {};
|
||||
this.playersWithModifiedTemplates = new Set();
|
||||
this.playersWithChangedModifications = new Set();
|
||||
this.playersWithDirtySelection = new Set();
|
||||
this.entityStateRetriever = new EntityStateRetriever();
|
||||
this.obstructionSnap = new ObstructionSnap();
|
||||
};
|
||||
|
||||
|
|
@ -250,377 +251,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)
|
||||
|
|
@ -641,12 +300,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,
|
||||
|
|
@ -709,14 +362,15 @@ GuiInterface.prototype.GetNeededResources = function(player, data)
|
|||
|
||||
GuiInterface.prototype.OnTemplateModification = function(msg)
|
||||
{
|
||||
// The GUI caches templates, which could become out-of-date now, so tell it
|
||||
// to reload all of them for that player.
|
||||
// TODO: Allow the GUI to do that more selectively by passing which values
|
||||
// of which components have changed and letting it only reload the affected
|
||||
// ones.
|
||||
this.playersWithModifiedTemplates.add(msg.player);
|
||||
// The modifiers of a player changed. Inform the GUI and EntityStateRetriever
|
||||
// that they need to reload all templates of that player.
|
||||
// TODO: Allow them to do that more selectively by telling them exactly
|
||||
// which values of which components changes and letting them only reload the
|
||||
// actually affected ones.
|
||||
this.playersWithChangedModifications.add(msg.player);
|
||||
|
||||
this.playersWithDirtySelection.add(msg.player);
|
||||
this.entityStateRetriever.onPlayerModificationsChanged(msg.player);
|
||||
};
|
||||
|
||||
GuiInterface.prototype.OnDisabledTemplatesChanged = function(msg)
|
||||
|
|
@ -741,13 +395,13 @@ GuiInterface.prototype.SetSelectionDirty = function(player)
|
|||
GuiInterface.prototype.GetAndClearTemplateChanges = function(player)
|
||||
{
|
||||
const ret = {};
|
||||
if (this.playersWithModifiedTemplates.size)
|
||||
ret.playersWithModifiedTemplates = new Set(this.playersWithModifiedTemplates);
|
||||
if (this.playersWithChangedModifications.size)
|
||||
ret.playersWithChangedModifications = new Set(this.playersWithChangedModifications);
|
||||
|
||||
if (this.playersWithDirtySelection.has(player))
|
||||
ret.selectionDirty = true;
|
||||
|
||||
this.playersWithModifiedTemplates.clear();
|
||||
this.playersWithChangedModifications.clear();
|
||||
this.playersWithDirtySelection.clear();
|
||||
|
||||
return ret;
|
||||
|
|
@ -1338,7 +992,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)
|
||||
|
|
@ -2076,6 +1734,7 @@ GuiInterface.prototype.exposedFunctions = {
|
|||
"GetCampaignGameEndData": 1,
|
||||
"GetRenamedEntities": 1,
|
||||
"ClearRenamedEntities": 1,
|
||||
"ResetEntityStateRetriever": 1,
|
||||
"GetEntityState": 1,
|
||||
"GetMultipleEntityStates": 1,
|
||||
"GetAverageRangeForBuildings": 1,
|
||||
|
|
|
|||
|
|
@ -218,11 +218,6 @@ Identity.prototype.GetRank = function()
|
|||
return this.template.Rank || "";
|
||||
};
|
||||
|
||||
Identity.prototype.GetRankTechName = function()
|
||||
{
|
||||
return this.template.Rank ? "unit_" + this.template.Rank.toLowerCase() : "";
|
||||
};
|
||||
|
||||
Identity.prototype.GetClassesList = function()
|
||||
{
|
||||
return this.classesList;
|
||||
|
|
|
|||
|
|
@ -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].toSorted((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.
|
||||
|
|
|
|||
|
|
@ -86,8 +86,7 @@ var g_Stances = {
|
|||
"respondChase": true,
|
||||
"respondChaseBeyondVision": true,
|
||||
"respondStandGround": false,
|
||||
"respondHoldGround": false,
|
||||
"selectable": true
|
||||
"respondHoldGround": false
|
||||
},
|
||||
"aggressive": {
|
||||
"targetVisibleEnemies": true,
|
||||
|
|
@ -97,8 +96,7 @@ var g_Stances = {
|
|||
"respondChase": true,
|
||||
"respondChaseBeyondVision": false,
|
||||
"respondStandGround": false,
|
||||
"respondHoldGround": false,
|
||||
"selectable": true
|
||||
"respondHoldGround": false
|
||||
},
|
||||
"defensive": {
|
||||
"targetVisibleEnemies": true,
|
||||
|
|
@ -108,8 +106,7 @@ var g_Stances = {
|
|||
"respondChase": false,
|
||||
"respondChaseBeyondVision": false,
|
||||
"respondStandGround": false,
|
||||
"respondHoldGround": true,
|
||||
"selectable": true
|
||||
"respondHoldGround": true
|
||||
},
|
||||
"passive": {
|
||||
"targetVisibleEnemies": false,
|
||||
|
|
@ -119,8 +116,7 @@ var g_Stances = {
|
|||
"respondChase": false,
|
||||
"respondChaseBeyondVision": false,
|
||||
"respondStandGround": false,
|
||||
"respondHoldGround": false,
|
||||
"selectable": true
|
||||
"respondHoldGround": false
|
||||
},
|
||||
"standground": {
|
||||
"targetVisibleEnemies": true,
|
||||
|
|
@ -130,8 +126,7 @@ var g_Stances = {
|
|||
"respondChase": false,
|
||||
"respondChaseBeyondVision": false,
|
||||
"respondStandGround": true,
|
||||
"respondHoldGround": false,
|
||||
"selectable": true
|
||||
"respondHoldGround": false
|
||||
},
|
||||
"skittish": {
|
||||
"targetVisibleEnemies": false,
|
||||
|
|
@ -141,8 +136,7 @@ var g_Stances = {
|
|||
"respondChase": false,
|
||||
"respondChaseBeyondVision": false,
|
||||
"respondStandGround": false,
|
||||
"respondHoldGround": false,
|
||||
"selectable": false
|
||||
"respondHoldGround": false
|
||||
},
|
||||
"passive-defensive": {
|
||||
"targetVisibleEnemies": false,
|
||||
|
|
@ -152,8 +146,7 @@ var g_Stances = {
|
|||
"respondChase": false,
|
||||
"respondChaseBeyondVision": false,
|
||||
"respondStandGround": false,
|
||||
"respondHoldGround": true,
|
||||
"selectable": false
|
||||
"respondHoldGround": true
|
||||
},
|
||||
"none": {
|
||||
// Only to be used by AI or trigger scripts
|
||||
|
|
@ -164,8 +157,7 @@ var g_Stances = {
|
|||
"respondChase": false,
|
||||
"respondChaseBeyondVision": false,
|
||||
"respondStandGround": false,
|
||||
"respondHoldGround": false,
|
||||
"selectable": false
|
||||
"respondHoldGround": false
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -6665,13 +6657,6 @@ UnitAI.prototype.GetStance = function()
|
|||
return g_Stances[this.stance];
|
||||
};
|
||||
|
||||
UnitAI.prototype.GetSelectableStances = function()
|
||||
{
|
||||
if (this.IsTurret())
|
||||
return [];
|
||||
return Object.keys(g_Stances).filter(key => g_Stances[key].selectable);
|
||||
};
|
||||
|
||||
UnitAI.prototype.GetStanceName = function()
|
||||
{
|
||||
return this.stance;
|
||||
|
|
|
|||
|
|
@ -128,33 +128,6 @@ Upgrade.prototype.CanUpgradeTo = function(template)
|
|||
return this.upgradeTemplates[template] !== undefined;
|
||||
};
|
||||
|
||||
Upgrade.prototype.GetUpgrades = function()
|
||||
{
|
||||
const ret = [];
|
||||
|
||||
for (const option in this.upgradeTemplates)
|
||||
{
|
||||
const choice = this.template[this.upgradeTemplates[option]];
|
||||
|
||||
let cost = {};
|
||||
if (choice.Cost)
|
||||
cost = this.GetResourceCosts(option);
|
||||
if (choice.Time)
|
||||
cost.time = this.GetUpgradeTime(option);
|
||||
|
||||
const hasCost = choice.Cost || choice.Time;
|
||||
ret.push({
|
||||
"entity": option,
|
||||
"icon": choice.Icon || undefined,
|
||||
"cost": hasCost ? cost : undefined,
|
||||
"tooltip": choice.Tooltip || undefined,
|
||||
"requirements": this.GetRequirements(option),
|
||||
});
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
Upgrade.prototype.CancelTimer = function()
|
||||
{
|
||||
if (!this.timer)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,734 @@
|
|||
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,
|
||||
"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,
|
||||
"isIdle": true,
|
||||
"formationController": 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,
|
||||
"isIdle": true,
|
||||
"formationController": 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,15 +141,17 @@ 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 });
|
||||
// T3: Check that the value is correct within the Upgrade Component.
|
||||
const entity = template.Upgrade.Tower.Entity.replace("{civ}", civCode);
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetResourceCosts(entity), { "stone": 100, "wood": 50 });
|
||||
TS_ASSERT_EQUALS(cmpUpgrade.GetUpgradeTime(entity), 100);
|
||||
|
||||
/**
|
||||
* Tell the Upgrade component to start the Upgrade,
|
||||
|
|
@ -159,16 +161,17 @@ 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 });
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetResourceCosts(entity), { "stone": 100, "wood": 50 });
|
||||
TS_ASSERT_EQUALS(cmpUpgrade.GetUpgradeTime(entity), 90);
|
||||
|
||||
// T7: ...but with the upgrade cancelled, it now uses the modified value.
|
||||
cmpUpgrade.CancelUpgrade(playerID);
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 160, "wood": 25, "time": 90 });
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetResourceCosts(entity), { "stone": 160, "wood": 25 });
|
||||
|
|
|
|||
|
|
@ -0,0 +1,364 @@
|
|||
/**
|
||||
* 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(),
|
||||
"isIdle": cmpUnitAI.IsIdle(),
|
||||
"formationController": 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.GetMaxHitpoints();
|
||||
|
||||
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