/**
* @class
*/
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);
valueCache.set(entity, new Map());
};
/**
* @returns originalValue after modifiers.
*/
ModifiersManager.prototype.FetchModifiedProperty = function(classesList, propertyName, originalValue, target)
{
let modifs = this.modifiersStorage.GetItems(propertyName, target);
if (!modifs.length)
return originalValue;
// Flatten the list of modifications
let modifications = [];
modifs.forEach(item => { modifications = modifications.concat(item.value); });
return GetTechModifiedProperty(modifications, classesList, originalValue);
};
/**
* @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 = QueryMiragedInterface(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.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?
playerModifs[propertyName].forEach(item => item.value.forEach(modif => {
if (!DoesModificationApply(modif, classes))
return;
if (!modifiedComponents[component])
modifiedComponents[component] = [];
modifiedComponents[component].push(propertyName);
}));
}
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);