Add a system component to handle stat modifiers, make technologies and auras use this common interface.
The ModifiersManager system component provides an interface to add and
remove modifiers, and get modified stats.
The goal is to merge all the different stat-modifying systems 0 A.D. has
implemented over the years.
This commit makes technologies and auras use ModifiersManager. Some
cheats and AI bonuses also have a similar stat-modifying effect that
have not yet been updated.
Further, this system component makes it possible for e.g. triggers to
easily add modifiers, enabling the writing of Castle Blood Automatic,
RPG or Tower Defense maps without the need for mods or hacks.
The 'Modifier' name was preferred over 'Modification' as it is shorter
and more readable, along with the logic that 'modifiers' store
'modifications' and this stores modifiers. Renaming of other functions
and classes has been left for future work for now.
Internally, this uses a JS data structure. If performance issues arise
with it in the future, this data structure or the whole component could
be moved to C++.
The performance has been tested to be about as fast as the current
implementations (and specifically much faster for global auras with no
icons). Testing showed that sending value modification messages was by
far the slowest part.
Comments by: leper, Stan, elexis
Differential Revision: https://code.wildfiregames.com/D274
This was SVN commit r22767.
2019-08-24 00:37:07 -07:00
|
|
|
function ModifiersManager() {}
|
|
|
|
|
|
|
|
|
|
ModifiersManager.prototype.Schema =
|
|
|
|
|
"<empty/>";
|
|
|
|
|
|
|
|
|
|
ModifiersManager.prototype.Init = function()
|
|
|
|
|
{
|
|
|
|
|
// TODO:
|
|
|
|
|
// - add a way to show an icon for a given modifier ID
|
|
|
|
|
// > Note that aura code shows icons when the source is selected, so that's specific to them.
|
|
|
|
|
// - support stacking modifiers (MultiKeyMap handles it but not this manager).
|
|
|
|
|
|
|
|
|
|
// The cache computes values lazily when they are needed.
|
|
|
|
|
// Helper functions remove items that have been changed to ensure we stay up-to-date.
|
|
|
|
|
this.cachedValues = new Map(); // Keyed by property name, entity ID, original values.
|
|
|
|
|
|
|
|
|
|
// When changing global modifiers, all entity-local caches are invalidated. This helps with that.
|
|
|
|
|
// TODO: it might be worth keying by classes here.
|
|
|
|
|
this.playerEntitiesCached = new Map(); // Keyed by player ID, property name, entity ID.
|
|
|
|
|
|
|
|
|
|
this.modifiersStorage = new MultiKeyMap(); // Keyed by property name, entity.
|
|
|
|
|
|
|
|
|
|
this.modifiersStorage._OnItemModified = (prim, sec, itemID) => this.ModifiersChanged.apply(this, [prim, sec, itemID]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ModifiersManager.prototype.Serialize = function()
|
|
|
|
|
{
|
|
|
|
|
// The value cache will be affected by property reads from the GUI and other places so we shouldn't serialize it.
|
|
|
|
|
// Furthermore it is cyclically self-referencing.
|
|
|
|
|
// We need to store the player for the Player-Entities cache.
|
|
|
|
|
let players = [];
|
|
|
|
|
this.playerEntitiesCached.forEach((_, player) => players.push(player));
|
|
|
|
|
return {
|
|
|
|
|
"modifiersStorage": this.modifiersStorage.Serialize(),
|
|
|
|
|
"players": players
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ModifiersManager.prototype.Deserialize = function(data)
|
|
|
|
|
{
|
|
|
|
|
this.Init();
|
|
|
|
|
this.modifiersStorage.Deserialize(data.modifiersStorage);
|
|
|
|
|
data.players.forEach(player => this.playerEntitiesCached.set(player, new Map()));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Inform entities that we have changed possibly all values affected by that property.
|
|
|
|
|
* It's not hugely efficient and would be nice to batch.
|
|
|
|
|
* Invalidate caches where relevant.
|
|
|
|
|
*/
|
|
|
|
|
ModifiersManager.prototype.ModifiersChanged = function(propertyName, entity)
|
|
|
|
|
{
|
|
|
|
|
let playerCache = this.playerEntitiesCached.get(entity);
|
|
|
|
|
this.InvalidateCache(propertyName, entity, playerCache);
|
|
|
|
|
|
|
|
|
|
if (playerCache)
|
|
|
|
|
{
|
|
|
|
|
let cmpPlayer = Engine.QueryInterface(entity, IID_Player);
|
|
|
|
|
if (cmpPlayer)
|
|
|
|
|
this.SendPlayerModifierMessages(propertyName, cmpPlayer.GetPlayerID());
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
Engine.PostMessage(entity, MT_ValueModification, { "entities": [entity], "component": propertyName.split("/")[0], "valueNames": [propertyName] });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ModifiersManager.prototype.SendPlayerModifierMessages = function(propertyName, player)
|
|
|
|
|
{
|
|
|
|
|
// TODO: it would be preferable to be able to batch this (i.e. one message for several properties)
|
|
|
|
|
Engine.PostMessage(SYSTEM_ENTITY, MT_TemplateModification, { "player": player, "component": propertyName.split("/")[0], "valueNames": [propertyName] });
|
|
|
|
|
// AIInterface wants the entities potentially affected.
|
|
|
|
|
// TODO: improve on this
|
|
|
|
|
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
|
|
|
|
let ents = cmpRangeManager.GetEntitiesByPlayer(player);
|
|
|
|
|
Engine.BroadcastMessage(MT_ValueModification, { "entities": ents, "component": propertyName.split("/")[0], "valueNames": [propertyName] });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ModifiersManager.prototype.InvalidatePlayerEntCache = function(valueCache, propertyName, entsMap)
|
|
|
|
|
{
|
|
|
|
|
entsMap = entsMap.get(propertyName);
|
|
|
|
|
if (entsMap)
|
|
|
|
|
{
|
|
|
|
|
// Invalidate all local caches directly (for simplicity in ApplyModifiers).
|
|
|
|
|
entsMap.forEach(ent => valueCache.set(ent, new Map()));
|
|
|
|
|
entsMap.clear();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ModifiersManager.prototype.InvalidateCache = function(propertyName, entity, playerCache)
|
|
|
|
|
{
|
|
|
|
|
let valueCache = this.cachedValues.get(propertyName);
|
|
|
|
|
if (!valueCache)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (playerCache)
|
|
|
|
|
this.InvalidatePlayerEntCache(valueCache, propertyName, playerCache);
|
2019-09-01 00:16:02 -07:00
|
|
|
valueCache.set(entity, new Map());
|
Add a system component to handle stat modifiers, make technologies and auras use this common interface.
The ModifiersManager system component provides an interface to add and
remove modifiers, and get modified stats.
The goal is to merge all the different stat-modifying systems 0 A.D. has
implemented over the years.
This commit makes technologies and auras use ModifiersManager. Some
cheats and AI bonuses also have a similar stat-modifying effect that
have not yet been updated.
Further, this system component makes it possible for e.g. triggers to
easily add modifiers, enabling the writing of Castle Blood Automatic,
RPG or Tower Defense maps without the need for mods or hacks.
The 'Modifier' name was preferred over 'Modification' as it is shorter
and more readable, along with the logic that 'modifiers' store
'modifications' and this stores modifiers. Renaming of other functions
and classes has been left for future work for now.
Internally, this uses a JS data structure. If performance issues arise
with it in the future, this data structure or the whole component could
be moved to C++.
The performance has been tested to be about as fast as the current
implementations (and specifically much faster for global auras with no
icons). Testing showed that sending value modification messages was by
far the slowest part.
Comments by: leper, Stan, elexis
Differential Revision: https://code.wildfiregames.com/D274
This was SVN commit r22767.
2019-08-24 00:37:07 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @returns originalValue after modifiers.
|
|
|
|
|
*/
|
|
|
|
|
ModifiersManager.prototype.FetchModifiedProperty = function(classesList, propertyName, originalValue, target)
|
|
|
|
|
{
|
|
|
|
|
let modifs = this.modifiersStorage.GetItems(propertyName, target);
|
|
|
|
|
if (!modifs.length)
|
|
|
|
|
return originalValue;
|
2019-09-22 05:05:04 -07:00
|
|
|
// Flatten the list of modifications
|
|
|
|
|
let modifications = [];
|
|
|
|
|
modifs.forEach(item => { modifications = modifications.concat(item.value); });
|
|
|
|
|
return GetTechModifiedProperty(modifications, classesList, originalValue);
|
Add a system component to handle stat modifiers, make technologies and auras use this common interface.
The ModifiersManager system component provides an interface to add and
remove modifiers, and get modified stats.
The goal is to merge all the different stat-modifying systems 0 A.D. has
implemented over the years.
This commit makes technologies and auras use ModifiersManager. Some
cheats and AI bonuses also have a similar stat-modifying effect that
have not yet been updated.
Further, this system component makes it possible for e.g. triggers to
easily add modifiers, enabling the writing of Castle Blood Automatic,
RPG or Tower Defense maps without the need for mods or hacks.
The 'Modifier' name was preferred over 'Modification' as it is shorter
and more readable, along with the logic that 'modifiers' store
'modifications' and this stores modifiers. Renaming of other functions
and classes has been left for future work for now.
Internally, this uses a JS data structure. If performance issues arise
with it in the future, this data structure or the whole component could
be moved to C++.
The performance has been tested to be about as fast as the current
implementations (and specifically much faster for global auras with no
icons). Testing showed that sending value modification messages was by
far the slowest part.
Comments by: leper, Stan, elexis
Differential Revision: https://code.wildfiregames.com/D274
This was SVN commit r22767.
2019-08-24 00:37:07 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @returns originalValue after modifiers
|
|
|
|
|
*/
|
|
|
|
|
ModifiersManager.prototype.Cache = function(classesList, propertyName, originalValue, entity)
|
|
|
|
|
{
|
|
|
|
|
let cache = this.cachedValues.get(propertyName);
|
|
|
|
|
if (!cache)
|
|
|
|
|
cache = this.cachedValues.set(propertyName, new Map()).get(propertyName);
|
|
|
|
|
|
|
|
|
|
let cache2 = cache.get(entity);
|
|
|
|
|
if (!cache2)
|
|
|
|
|
cache2 = cache.set(entity, new Map()).get(entity);
|
|
|
|
|
|
|
|
|
|
let value = this.FetchModifiedProperty(classesList, propertyName, originalValue, entity);
|
|
|
|
|
cache2.set(originalValue, value);
|
|
|
|
|
return value;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Caching system in front of FetchModifiedProperty(), as calling that every time is quite slow.
|
|
|
|
|
* This recomputes lazily.
|
|
|
|
|
* Applies per-player modifiers before per-entity modifiers, so the latter take priority;
|
|
|
|
|
* @param propertyName - Handle of a technology property (eg Attack/Ranged/Pierce) that was changed.
|
|
|
|
|
* @param originalValue - template/raw/before-modifiers value.
|
|
|
|
|
Note that if this is supposed to be a number (i.e. you call add/multiply on it)
|
|
|
|
|
You must make sure to pass a number and not a string (by using + if necessary)
|
|
|
|
|
* @param ent - ID of the target entity
|
|
|
|
|
* @returns originalValue after the modifiers
|
|
|
|
|
*/
|
|
|
|
|
ModifiersManager.prototype.ApplyModifiers = function(propertyName, originalValue, entity)
|
|
|
|
|
{
|
|
|
|
|
let newValue = this.cachedValues.get(propertyName);
|
|
|
|
|
if (newValue)
|
|
|
|
|
{
|
|
|
|
|
newValue = newValue.get(entity);
|
|
|
|
|
if (newValue)
|
|
|
|
|
{
|
|
|
|
|
newValue = newValue.get(originalValue);
|
|
|
|
|
if (newValue)
|
|
|
|
|
return newValue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the entity ID of the player / owner of the entity, since we use that to store per-player modifiers
|
|
|
|
|
// (this prevents conflicts between player ID and entity ID).
|
|
|
|
|
let ownerEntity = QueryOwnerEntityID(entity);
|
|
|
|
|
if (ownerEntity == entity)
|
|
|
|
|
ownerEntity = null;
|
|
|
|
|
|
|
|
|
|
newValue = originalValue;
|
|
|
|
|
|
|
|
|
|
let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
|
|
|
|
|
if (!cmpIdentity)
|
|
|
|
|
return originalValue;
|
|
|
|
|
let classesList = cmpIdentity.GetClassesList();
|
|
|
|
|
|
|
|
|
|
// Apply player-wide modifiers before entity-local modifiers.
|
|
|
|
|
if (ownerEntity)
|
|
|
|
|
{
|
|
|
|
|
let pc = this.playerEntitiesCached.get(ownerEntity).get(propertyName);
|
|
|
|
|
if (!pc)
|
|
|
|
|
pc = this.playerEntitiesCached.get(ownerEntity).set(propertyName, new Set()).get(propertyName);
|
|
|
|
|
pc.add(entity);
|
|
|
|
|
newValue = this.FetchModifiedProperty(classesList, propertyName, newValue, ownerEntity);
|
|
|
|
|
}
|
|
|
|
|
newValue = this.Cache(classesList, propertyName, newValue, entity);
|
|
|
|
|
|
|
|
|
|
return newValue;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Alternative version of ApplyModifiers, applies to templates instead of entities.
|
|
|
|
|
* Only needs to handle global modifiers.
|
|
|
|
|
*/
|
|
|
|
|
ModifiersManager.prototype.ApplyTemplateModifiers = function(propertyName, originalValue, template, player)
|
|
|
|
|
{
|
|
|
|
|
if (!template || !template.Identity)
|
|
|
|
|
return originalValue;
|
|
|
|
|
|
|
|
|
|
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
|
|
|
|
|
return this.FetchModifiedProperty(GetIdentityClasses(template.Identity), propertyName, originalValue, cmpPlayerManager.GetPlayerByID(player));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* For efficiency in InvalidateCache, keep playerEntitiesCached updated.
|
|
|
|
|
*/
|
|
|
|
|
ModifiersManager.prototype.OnGlobalPlayerEntityChanged = function(msg)
|
|
|
|
|
{
|
|
|
|
|
if (msg.to != INVALID_PLAYER && !this.playerEntitiesCached.has(msg.to))
|
|
|
|
|
this.playerEntitiesCached.set(msg.to, new Map());
|
|
|
|
|
|
|
|
|
|
if (msg.from != INVALID_PLAYER && this.playerEntitiesCached.has(msg.from))
|
|
|
|
|
{
|
|
|
|
|
this.playerEntitiesCached.get(msg.from).forEach(propName => this.InvalidateCache(propName, msg.from));
|
|
|
|
|
this.playerEntitiesCached.delete(msg.from);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle modifiers when an entity changes owner.
|
|
|
|
|
* We do not retain the original modifiers for now.
|
|
|
|
|
*/
|
|
|
|
|
ModifiersManager.prototype.OnGlobalOwnershipChanged = function(msg)
|
|
|
|
|
{
|
|
|
|
|
if (msg.from == INVALID_PLAYER || msg.to == INVALID_PLAYER)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// Invalidate all caches.
|
|
|
|
|
for (let propName of this.cachedValues.keys())
|
|
|
|
|
this.InvalidateCache(propName, msg.entity);
|
|
|
|
|
|
|
|
|
|
let owner = QueryOwnerEntityID(msg.entity);
|
|
|
|
|
if (!owner)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
let cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity);
|
|
|
|
|
if (!cmpIdentity)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
let classes = cmpIdentity.GetClassesList();
|
|
|
|
|
|
|
|
|
|
// Warn entities that our values have changed.
|
|
|
|
|
// Local modifiers will be added by the relevant components, so no need to check for them here.
|
|
|
|
|
let modifiedComponents = {};
|
|
|
|
|
let playerModifs = this.modifiersStorage.GetAllItems(owner);
|
|
|
|
|
for (let propertyName in playerModifs)
|
|
|
|
|
{
|
|
|
|
|
// We only need to find one one tech per component for a match.
|
|
|
|
|
let component = propertyName.split("/")[0];
|
|
|
|
|
// Only inform if the modifier actually applies to the entity as an optimisation.
|
|
|
|
|
// TODO: would it be better to call FetchModifiedProperty here and compare values?
|
2019-09-22 05:05:04 -07:00
|
|
|
playerModifs[propertyName].forEach(item => item.value.forEach(modif => {
|
Add a system component to handle stat modifiers, make technologies and auras use this common interface.
The ModifiersManager system component provides an interface to add and
remove modifiers, and get modified stats.
The goal is to merge all the different stat-modifying systems 0 A.D. has
implemented over the years.
This commit makes technologies and auras use ModifiersManager. Some
cheats and AI bonuses also have a similar stat-modifying effect that
have not yet been updated.
Further, this system component makes it possible for e.g. triggers to
easily add modifiers, enabling the writing of Castle Blood Automatic,
RPG or Tower Defense maps without the need for mods or hacks.
The 'Modifier' name was preferred over 'Modification' as it is shorter
and more readable, along with the logic that 'modifiers' store
'modifications' and this stores modifiers. Renaming of other functions
and classes has been left for future work for now.
Internally, this uses a JS data structure. If performance issues arise
with it in the future, this data structure or the whole component could
be moved to C++.
The performance has been tested to be about as fast as the current
implementations (and specifically much faster for global auras with no
icons). Testing showed that sending value modification messages was by
far the slowest part.
Comments by: leper, Stan, elexis
Differential Revision: https://code.wildfiregames.com/D274
This was SVN commit r22767.
2019-08-24 00:37:07 -07:00
|
|
|
if (!DoesModificationApply(modif, classes))
|
|
|
|
|
return;
|
|
|
|
|
if (!modifiedComponents[component])
|
|
|
|
|
modifiedComponents[component] = [];
|
|
|
|
|
modifiedComponents[component].push(propertyName);
|
2019-09-22 05:05:04 -07:00
|
|
|
}));
|
Add a system component to handle stat modifiers, make technologies and auras use this common interface.
The ModifiersManager system component provides an interface to add and
remove modifiers, and get modified stats.
The goal is to merge all the different stat-modifying systems 0 A.D. has
implemented over the years.
This commit makes technologies and auras use ModifiersManager. Some
cheats and AI bonuses also have a similar stat-modifying effect that
have not yet been updated.
Further, this system component makes it possible for e.g. triggers to
easily add modifiers, enabling the writing of Castle Blood Automatic,
RPG or Tower Defense maps without the need for mods or hacks.
The 'Modifier' name was preferred over 'Modification' as it is shorter
and more readable, along with the logic that 'modifiers' store
'modifications' and this stores modifiers. Renaming of other functions
and classes has been left for future work for now.
Internally, this uses a JS data structure. If performance issues arise
with it in the future, this data structure or the whole component could
be moved to C++.
The performance has been tested to be about as fast as the current
implementations (and specifically much faster for global auras with no
icons). Testing showed that sending value modification messages was by
far the slowest part.
Comments by: leper, Stan, elexis
Differential Revision: https://code.wildfiregames.com/D274
This was SVN commit r22767.
2019-08-24 00:37:07 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let component in modifiedComponents)
|
|
|
|
|
Engine.PostMessage(msg.entity, MT_ValueModification, { "entities": [msg.entity], "component": component, "valueNames": modifiedComponents[component] });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The following functions simply proxy MultiKeyMap's interface.
|
|
|
|
|
*/
|
|
|
|
|
ModifiersManager.prototype.AddModifier = function(propName, ModifID, Modif, entity, stackable = false) {
|
|
|
|
|
return this.modifiersStorage.AddItem(propName, ModifID, Modif, entity, stackable);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ModifiersManager.prototype.AddModifiers = function(ModifID, Modifs, entity, stackable = false) {
|
|
|
|
|
return this.modifiersStorage.AddItems(ModifID, Modifs, entity, stackable);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ModifiersManager.prototype.RemoveModifier = function(propName, ModifID, entity, stackable = false) {
|
|
|
|
|
return this.modifiersStorage.RemoveItem(propName, ModifID, entity, stackable);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ModifiersManager.prototype.RemoveAllModifiers = function(ModifID, entity, stackable = false) {
|
|
|
|
|
return this.modifiersStorage.RemoveAllItems(ModifID, entity, stackable);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ModifiersManager.prototype.HasModifier = function(propName, ModifID, entity) {
|
|
|
|
|
return this.modifiersStorage.HasItem(propName, ModifID, entity);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ModifiersManager.prototype.HasAnyModifier = function(ModifID, entity) {
|
|
|
|
|
return this.modifiersStorage.HasAnyItem(ModifID, entity);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ModifiersManager.prototype.GetModifiers = function(propName, entity, stackable = false) {
|
|
|
|
|
return this.modifiersStorage.GetItems(propName, entity, stackable);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ModifiersManager.prototype.GetAllModifiers = function(entity, stackable = false) {
|
|
|
|
|
return this.modifiersStorage.GetAllItems(entity, stackable);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Engine.RegisterSystemComponentType(IID_ModifiersManager, "ModifiersManager", ModifiersManager);
|