mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-17 13:53:57 -07:00
Up to now `eslint-plugin-brace-rules` was used to enforce a common brace style for JavaScript code. This plugin was however updated the last time over 9 years ago and will be incompatible with ESLint v10, as that [removes `context.getSourceCode()`][1], the plugin relies on. To keep the eslint config working with ESLint v10, this replaces `eslint-plugin-brace-rules` with the [`@stylistic/brace-style`][2] rule from `@stylistic/eslint-plugin`, a package we already use. While `@stylistic/brace-style` doesn't offer an option to format braces in exactly the same way as before, the "allman" style seems to be the one closest to the existing code. [1]: https://eslint.org/blog/2025/11/eslint-v10.0.0-alpha.0-released/#removed-deprecated-rule-context-members [2]: https://eslint.style/rules/brace-style
304 lines
11 KiB
JavaScript
304 lines
11 KiB
JavaScript
function ModifiersManager() {}
|
|
|
|
ModifiersManager.prototype.Schema =
|
|
"<a:component type='system'/><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.
|
|
const 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)
|
|
{
|
|
const playerCache = this.playerEntitiesCached.get(entity);
|
|
this.InvalidateCache(propertyName, entity, playerCache);
|
|
|
|
if (playerCache)
|
|
{
|
|
const 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
|
|
const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
|
const 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)
|
|
{
|
|
const 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)
|
|
{
|
|
const modifs = this.modifiersStorage.GetItems(propertyName, target);
|
|
if (!modifs.length)
|
|
return originalValue;
|
|
// Flatten the list of modifications
|
|
const modifications = [];
|
|
modifs.forEach(item => { modifications.push(item.value); });
|
|
return GetTechModifiedProperty(modifications.flat(), classesList, originalValue);
|
|
};
|
|
|
|
ModifiersManager.prototype.Cache = function(classesList, propertyName, originalValue, newValue, 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);
|
|
|
|
cache2.set(originalValue, newValue);
|
|
};
|
|
|
|
/**
|
|
* 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 !== undefined)
|
|
{
|
|
newValue = newValue.get(entity);
|
|
if (newValue !== undefined)
|
|
{
|
|
newValue = newValue.get(originalValue);
|
|
if (newValue !== undefined)
|
|
return newValue;
|
|
}
|
|
}
|
|
|
|
newValue = originalValue;
|
|
|
|
const cmpIdentity = QueryMiragedInterface(entity, IID_Identity);
|
|
if (!cmpIdentity)
|
|
return originalValue;
|
|
const classesList = cmpIdentity.GetClassesList();
|
|
|
|
// 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).
|
|
const ownerPlayer = Engine.QueryInterface(entity, IID_Ownership)?.GetOwner();
|
|
|
|
// Apply player-wide modifiers before entity-local modifiers.
|
|
if (ownerPlayer !== undefined && ownerPlayer !== INVALID_PLAYER)
|
|
{
|
|
const ownerEntity = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(ownerPlayer);
|
|
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.FetchModifiedProperty(classesList, propertyName, newValue, entity);
|
|
this.Cache(classesList, propertyName, originalValue, 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;
|
|
|
|
const 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 (const propName of this.cachedValues.keys())
|
|
this.InvalidateCache(propName, msg.entity);
|
|
|
|
const cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity);
|
|
if (!cmpIdentity)
|
|
return;
|
|
const classes = cmpIdentity.GetClassesList();
|
|
|
|
const cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
|
|
const oldOwner = cmpPlayerManager.GetPlayerByID(msg.from);
|
|
const newOwner = cmpPlayerManager.GetPlayerByID(msg.to);
|
|
|
|
// Warn entities that our values have changed.
|
|
// Local modifiers will be added by the relevant components, so no need to check for them here.
|
|
const modifiedComponents = {};
|
|
const fetchPlayerModifiedValueNames = (owner) =>
|
|
{
|
|
if (!owner)
|
|
return;
|
|
const playerModifs = this.modifiersStorage.GetAllItems(owner);
|
|
for (const propertyName in playerModifs)
|
|
{
|
|
// We only need to find one one tech per component for a match.
|
|
const 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] = new Set();
|
|
modifiedComponents[component].add(propertyName);
|
|
}));
|
|
}
|
|
};
|
|
|
|
// We'll assume these are always different.
|
|
fetchPlayerModifiedValueNames(oldOwner);
|
|
fetchPlayerModifiedValueNames(newOwner);
|
|
|
|
for (const component in modifiedComponents)
|
|
Engine.PostMessage(msg.entity, MT_ValueModification, { "entities": [msg.entity], "component": component, "valueNames": Array.from(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);
|