mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-17 22:03:56 -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.
This commit is contained in:
parent
040624acff
commit
e16c4c4800
33 changed files with 1097 additions and 656 deletions
|
|
@ -9,17 +9,13 @@
|
|||
* Returns modified property value modified by the applicable tech
|
||||
* modifications.
|
||||
*
|
||||
* @param currentTechModifications Object with mapping of property names to
|
||||
* modification arrays, retrieved from the intended player's TechnologyManager.
|
||||
* @param classes Array contianing the class list of the template.
|
||||
* @param propertyName String encoding the name of the value.
|
||||
* @param propertyValue Number storing the original value. Can also be
|
||||
* @param currentTechModifications array of modificiations
|
||||
* @param classes Array containing the class list of the template.
|
||||
* @param originalValue Number storing the original value. Can also be
|
||||
* non-numberic, but then only "replace" techs can be supported.
|
||||
*/
|
||||
function GetTechModifiedProperty(currentTechModifications, classes, propertyName, propertyValue)
|
||||
function GetTechModifiedProperty(modifications, classes, originalValue)
|
||||
{
|
||||
let modifications = currentTechModifications[propertyName] || [];
|
||||
|
||||
let multiply = 1;
|
||||
let add = 0;
|
||||
|
||||
|
|
@ -34,13 +30,13 @@ function GetTechModifiedProperty(currentTechModifications, classes, propertyName
|
|||
else if (modification.add)
|
||||
add += modification.add;
|
||||
else
|
||||
warn("GetTechModifiedProperty: modification format not recognised (modifying " + propertyName + "): " + uneval(modification));
|
||||
warn("GetTechModifiedProperty: modification format not recognised : " + uneval(modification));
|
||||
}
|
||||
|
||||
// Note, some components pass non-numeric values (for which only the "replace" modification makes sense)
|
||||
if (typeof propertyValue == "number")
|
||||
return propertyValue * multiply + add;
|
||||
return propertyValue;
|
||||
if (typeof originalValue == "number")
|
||||
return originalValue * multiply + add;
|
||||
return originalValue;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -128,8 +128,8 @@ function GetModifiedTemplateDataValue(template, value_path, mod_key, player, mod
|
|||
|
||||
if (player)
|
||||
current_value = ApplyValueModificationsToTemplate(mod_key, current_value, player, template);
|
||||
else if (modifiers)
|
||||
current_value = GetTechModifiedProperty(modifiers, GetIdentityClasses(template.Identity), mod_key, current_value);
|
||||
else if (modifiers && modifiers[mod_key])
|
||||
current_value = GetTechModifiedProperty(modifiers[mod_key], GetIdentityClasses(template.Identity), current_value);
|
||||
|
||||
// Using .toFixed() to get around spidermonkey's treatment of numbers (3 * 1.1 = 3.3000000000000003 for instance).
|
||||
return +current_value.toFixed(8);
|
||||
|
|
|
|||
|
|
@ -1,275 +0,0 @@
|
|||
function AuraManager() {}
|
||||
|
||||
AuraManager.prototype.Schema =
|
||||
"<a:component type='system'/><empty/>";
|
||||
|
||||
AuraManager.prototype.Init = function()
|
||||
{
|
||||
this.modificationsCache = new Map();
|
||||
this.modifications = new Map();
|
||||
this.templateModificationsCache = new Map();
|
||||
this.templateModifications = new Map();
|
||||
|
||||
this.globalAuraSources = [];
|
||||
};
|
||||
|
||||
AuraManager.prototype.RegisterGlobalAuraSource = function(ent)
|
||||
{
|
||||
if (this.globalAuraSources.indexOf(ent) == -1)
|
||||
this.globalAuraSources.push(ent);
|
||||
};
|
||||
|
||||
AuraManager.prototype.UnregisterGlobalAuraSource = function(ent)
|
||||
{
|
||||
let idx = this.globalAuraSources.indexOf(ent);
|
||||
if (idx != -1)
|
||||
this.globalAuraSources.splice(idx, 1);
|
||||
};
|
||||
|
||||
AuraManager.prototype.ensureExists = function(name, value, id, key, defaultData)
|
||||
{
|
||||
var cacheName = name + "Cache";
|
||||
var v = this[name].get(value);
|
||||
if (!v)
|
||||
{
|
||||
v = new Map();
|
||||
this[name].set(value, v);
|
||||
this[cacheName].set(value, new Map());
|
||||
}
|
||||
|
||||
var i = v.get(id);
|
||||
if (!i)
|
||||
{
|
||||
i = new Map();
|
||||
v.set(id, i);
|
||||
this[cacheName].get(value).set(id, defaultData);
|
||||
}
|
||||
|
||||
var k = i.get(key);
|
||||
if (!k)
|
||||
{
|
||||
k = {};
|
||||
i.set(key, k);
|
||||
}
|
||||
return k;
|
||||
};
|
||||
|
||||
AuraManager.prototype.ApplyBonus = function(value, ents, newData, key)
|
||||
{
|
||||
for (let ent of ents)
|
||||
{
|
||||
var data = this.ensureExists("modifications", value, ent, key, { "add":0, "multiply":1 });
|
||||
|
||||
if (data.count)
|
||||
{
|
||||
// this aura is already applied and the bonus shouldn't be given twice,
|
||||
// just count the number of times it is applied
|
||||
data.count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// first time added this aura
|
||||
data.multiply = newData.multiply;
|
||||
data.add = newData.add;
|
||||
data.count = 1;
|
||||
|
||||
if (data.add)
|
||||
this.modificationsCache.get(value).get(ent).add += data.add;
|
||||
if (data.multiply)
|
||||
this.modificationsCache.get(value).get(ent).multiply *= data.multiply;
|
||||
|
||||
// post message to the entity to notify it about the change
|
||||
Engine.PostMessage(ent, MT_ValueModification, {
|
||||
"entities": [ent],
|
||||
"component": value.split("/")[0],
|
||||
"valueNames": [value]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
AuraManager.prototype.ApplyTemplateBonus = function(value, player, classes, newData, key)
|
||||
{
|
||||
var data = this.ensureExists("templateModifications", value, player, key, new Map());
|
||||
|
||||
if (data.count)
|
||||
{
|
||||
// this aura is already applied and the bonus shouldn't be given twice,
|
||||
// just count the number of times it is applied
|
||||
data.count++;
|
||||
return;
|
||||
}
|
||||
|
||||
// first time added this aura
|
||||
data.multiply = newData.multiply;
|
||||
data.add = newData.add;
|
||||
data.count = 1;
|
||||
|
||||
let cache = this.templateModificationsCache.get(value).get(player);
|
||||
|
||||
// Do not use the classes array from the JSON file directly, since that is not synchronized
|
||||
// See MatchesClassList for supported classes formats
|
||||
for (let className of classes)
|
||||
{
|
||||
if (Array.isArray(className))
|
||||
className = className.join("+");
|
||||
|
||||
if (!cache.get(className))
|
||||
cache.set(className, new Map());
|
||||
|
||||
if (!cache.get(className).get(key))
|
||||
cache.get(className).set(key, { "add": 0, "multiply": 1 });
|
||||
|
||||
if (data.add)
|
||||
cache.get(className).get(key).add += data.add;
|
||||
if (data.multiply)
|
||||
cache.get(className).get(key).multiply *= data.multiply;
|
||||
}
|
||||
|
||||
Engine.PostMessage(SYSTEM_ENTITY, MT_TemplateModification, {
|
||||
"player": player,
|
||||
"component": value.split("/")[0],
|
||||
"valueNames": [value]
|
||||
});
|
||||
};
|
||||
|
||||
AuraManager.prototype.RemoveBonus = function(value, ents, key)
|
||||
{
|
||||
var v = this.modifications.get(value);
|
||||
if (!v)
|
||||
return;
|
||||
|
||||
for (let ent of ents)
|
||||
{
|
||||
var e = v.get(ent);
|
||||
if (!e)
|
||||
continue;
|
||||
var data = e.get(key);
|
||||
if (!data || !data.count)
|
||||
continue;
|
||||
|
||||
data.count--;
|
||||
|
||||
if (data.count > 0)
|
||||
continue;
|
||||
|
||||
// out of last aura of this kind, remove modifications
|
||||
if (data.add)
|
||||
this.modificationsCache.get(value).get(ent).add -= data.add;
|
||||
|
||||
if (data.multiply)
|
||||
this.modificationsCache.get(value).get(ent).multiply /= data.multiply;
|
||||
|
||||
// clean up the object
|
||||
e.delete(key);
|
||||
if (e.size == 0)
|
||||
v.delete(ent);
|
||||
|
||||
// post message to the entity to notify it about the change
|
||||
Engine.PostMessage(ent, MT_ValueModification, {
|
||||
"entities": [ent],
|
||||
"component": value.split("/")[0],
|
||||
"valueNames": [value]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
AuraManager.prototype.RemoveTemplateBonus = function(value, player, classes, key)
|
||||
{
|
||||
var v = this.templateModifications.get(value);
|
||||
if (!v)
|
||||
return;
|
||||
var p = v.get(player);
|
||||
if (!p)
|
||||
return;
|
||||
var data = p.get(key);
|
||||
if (!data || !data.count)
|
||||
return;
|
||||
|
||||
data.count--;
|
||||
|
||||
if (data.count > 0)
|
||||
return;
|
||||
|
||||
for (let className of classes)
|
||||
{
|
||||
if (Array.isArray(className))
|
||||
className = className.join("+");
|
||||
|
||||
this.templateModificationsCache.get(value).get(player).get(className).delete(key);
|
||||
|
||||
if (this.templateModificationsCache.get(value).get(player).get(className).size == 0)
|
||||
this.templateModificationsCache.get(value).get(player).delete(className);
|
||||
}
|
||||
|
||||
// clean up the object
|
||||
p.delete(key);
|
||||
if (p.size == 0)
|
||||
v.delete(player);
|
||||
|
||||
Engine.PostMessage(SYSTEM_ENTITY, MT_TemplateModification, {
|
||||
"player": player,
|
||||
"component": value.split("/")[0],
|
||||
"valueNames": [value]
|
||||
});
|
||||
};
|
||||
|
||||
AuraManager.prototype.ApplyModifications = function(valueName, value, ent)
|
||||
{
|
||||
var v = this.modificationsCache.get(valueName);
|
||||
if (!v)
|
||||
return value;
|
||||
var cache = v.get(ent);
|
||||
if (!cache)
|
||||
return value;
|
||||
|
||||
value *= cache.multiply;
|
||||
value += cache.add;
|
||||
return value;
|
||||
};
|
||||
|
||||
AuraManager.prototype.ApplyTemplateModifications = function(valueName, value, player, template)
|
||||
{
|
||||
var v = this.templateModificationsCache.get(valueName);
|
||||
if (!v)
|
||||
return value;
|
||||
var cache = v.get(player);
|
||||
if (!cache)
|
||||
return value;
|
||||
|
||||
if (!template || !template.Identity)
|
||||
return value;
|
||||
var classes = GetIdentityClasses(template.Identity);
|
||||
|
||||
var usedKeys = new Set();
|
||||
var add = 0;
|
||||
var multiply = 1;
|
||||
|
||||
for (let [className, mods] of cache)
|
||||
{
|
||||
if (!MatchesClassList(classes, [className]))
|
||||
continue;
|
||||
|
||||
for (let [key, mod] of mods)
|
||||
{
|
||||
// don't add an aura with the same key twice
|
||||
if (usedKeys.has(key))
|
||||
continue;
|
||||
add += mod.add;
|
||||
multiply *= mod.multiply;
|
||||
usedKeys.add(key);
|
||||
}
|
||||
}
|
||||
return value * multiply + add;
|
||||
};
|
||||
|
||||
AuraManager.prototype.OnGlobalOwnershipChanged = function(msg)
|
||||
{
|
||||
for (let ent of this.globalAuraSources)
|
||||
{
|
||||
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
|
||||
if (cmpAuras)
|
||||
cmpAuras.RegisterGlobalOwnershipChanged(msg);
|
||||
}
|
||||
};
|
||||
|
||||
Engine.RegisterSystemComponentType(IID_AuraManager, "AuraManager", AuraManager);
|
||||
|
|
@ -22,8 +22,8 @@ Auras.prototype.Init = function()
|
|||
Auras.prototype.GetModifierIdentifier = function(name)
|
||||
{
|
||||
if (AuraTemplates.Get(name).stackable)
|
||||
return name + this.entity;
|
||||
return name;
|
||||
return "aura/" + name + this.entity;
|
||||
return "aura/" + name;
|
||||
};
|
||||
|
||||
Auras.prototype.GetDescriptions = function()
|
||||
|
|
@ -218,9 +218,9 @@ Auras.prototype.Clean = function()
|
|||
targetUnitsClone[name] = this[name].targetUnits.slice();
|
||||
|
||||
if (this.IsGlobalAura(name))
|
||||
this.RemoveTemplateBonus(name);
|
||||
this.RemoveTemplateAura(name);
|
||||
|
||||
this.RemoveBonus(name, this[name].targetUnits);
|
||||
this.RemoveAura(name, this[name].targetUnits);
|
||||
|
||||
if (this[name].rangeQuery)
|
||||
cmpRangeManager.DestroyActiveQuery(this[name].rangeQuery);
|
||||
|
|
@ -242,36 +242,38 @@ Auras.prototype.Clean = function()
|
|||
|
||||
if (this.IsGlobalAura(name))
|
||||
{
|
||||
this.ApplyTemplateBonus(name, affectedPlayers);
|
||||
for (let player of affectedPlayers)
|
||||
this.ApplyBonus(name, cmpRangeManager.GetEntitiesByPlayer(player));
|
||||
this.ApplyTemplateAura(name, affectedPlayers);
|
||||
// Only need to call ApplyAura for the aura icons, so skip it if there are none.
|
||||
if (this.GetOverlayIcon(name))
|
||||
for (let player of affectedPlayers)
|
||||
this.ApplyAura(name, cmpRangeManager.GetEntitiesByPlayer(player));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.IsPlayerAura(name))
|
||||
{
|
||||
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
|
||||
this.ApplyBonus(name, affectedPlayers.map(p => cmpPlayerManager.GetPlayerByID(p)));
|
||||
this.ApplyAura(name, affectedPlayers.map(p => cmpPlayerManager.GetPlayerByID(p)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this.IsRangeAura(name))
|
||||
{
|
||||
this.ApplyBonus(name, targetUnitsClone[name]);
|
||||
this.ApplyAura(name, targetUnitsClone[name]);
|
||||
continue;
|
||||
}
|
||||
|
||||
needVisualizationUpdate = true;
|
||||
|
||||
if (this[name].isApplied)
|
||||
if (this[name].isApplied && (this.IsRangeAura(name) || this.IsGlobalAura(name) && !!this.GetOverlayIcon(name)))
|
||||
{
|
||||
this[name].rangeQuery = cmpRangeManager.CreateActiveQuery(
|
||||
this.entity,
|
||||
0,
|
||||
this.GetRange(name),
|
||||
affectedPlayers,
|
||||
IID_Identity,
|
||||
cmpRangeManager.GetEntityFlagMask("normal")
|
||||
this.entity,
|
||||
0,
|
||||
this.GetRange(name),
|
||||
affectedPlayers,
|
||||
IID_Identity,
|
||||
cmpRangeManager.GetEntityFlagMask("normal")
|
||||
);
|
||||
cmpRangeManager.EnableActiveQuery(this[name].rangeQuery);
|
||||
}
|
||||
|
|
@ -301,8 +303,8 @@ Auras.prototype.OnRangeUpdate = function(msg)
|
|||
{
|
||||
for (let name of this.GetAuraNames().filter(n => this[n] && msg.tag == this[n].rangeQuery))
|
||||
{
|
||||
this.ApplyBonus(name, msg.added);
|
||||
this.RemoveBonus(name, msg.removed);
|
||||
this.ApplyAura(name, msg.added);
|
||||
this.RemoveAura(name, msg.removed);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -310,87 +312,86 @@ Auras.prototype.OnGarrisonedUnitsChanged = function(msg)
|
|||
{
|
||||
for (let name of this.GetAuraNames().filter(n => this.IsGarrisonedUnitsAura(n)))
|
||||
{
|
||||
this.ApplyBonus(name, msg.added);
|
||||
this.RemoveBonus(name, msg.removed);
|
||||
this.ApplyAura(name, msg.added);
|
||||
this.RemoveAura(name, msg.removed);
|
||||
}
|
||||
};
|
||||
|
||||
Auras.prototype.RegisterGlobalOwnershipChanged = function(msg)
|
||||
Auras.prototype.ApplyFormationAura = function(memberList)
|
||||
{
|
||||
for (let name of this.GetAuraNames().filter(n => this.IsGlobalAura(n)))
|
||||
for (let name of this.GetAuraNames().filter(n => this.IsFormationAura(n)))
|
||||
this.ApplyAura(name, memberList);
|
||||
};
|
||||
|
||||
Auras.prototype.ApplyGarrisonAura = function(structure)
|
||||
{
|
||||
for (let name of this.GetAuraNames().filter(n => this.IsGarrisonAura(n)))
|
||||
this.ApplyAura(name, [structure]);
|
||||
};
|
||||
|
||||
Auras.prototype.ApplyTemplateAura = function(name, players)
|
||||
{
|
||||
if (!this[name].isApplied)
|
||||
return;
|
||||
|
||||
if (!this.IsGlobalAura(name))
|
||||
return;
|
||||
|
||||
let derivedModifiers = DeriveModificationsFromTech({
|
||||
"modifications": this.GetModifications(name),
|
||||
"affects": this.GetClasses(name)
|
||||
});
|
||||
let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
|
||||
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
|
||||
|
||||
let modifName = this.GetModifierIdentifier(name);
|
||||
for (let player of players)
|
||||
{
|
||||
let affectedPlayers = this.GetAffectedPlayers(name);
|
||||
let wasApplied = affectedPlayers.indexOf(msg.from) != -1;
|
||||
let willBeApplied = affectedPlayers.indexOf(msg.to) != -1;
|
||||
if (wasApplied && !willBeApplied)
|
||||
this.RemoveBonus(name, [msg.entity]);
|
||||
if (willBeApplied && !wasApplied)
|
||||
this.ApplyBonus(name, [msg.entity]);
|
||||
let playerId = cmpPlayerManager.GetPlayerByID(player);
|
||||
for (let modifierPath in derivedModifiers)
|
||||
for (let modifier of derivedModifiers[modifierPath])
|
||||
cmpModifiersManager.AddModifier(modifierPath, modifName, modifier, playerId);
|
||||
}
|
||||
};
|
||||
|
||||
Auras.prototype.ApplyFormationBonus = function(memberList)
|
||||
Auras.prototype.RemoveFormationAura = function(memberList)
|
||||
{
|
||||
for (let name of this.GetAuraNames().filter(n => this.IsFormationAura(n)))
|
||||
this.ApplyBonus(name, memberList);
|
||||
this.RemoveAura(name, memberList);
|
||||
};
|
||||
|
||||
Auras.prototype.ApplyGarrisonBonus = function(structure)
|
||||
Auras.prototype.RemoveGarrisonAura = function(structure)
|
||||
{
|
||||
for (let name of this.GetAuraNames().filter(n => this.IsGarrisonAura(n)))
|
||||
this.ApplyBonus(name, [structure]);
|
||||
this.RemoveAura(name, [structure]);
|
||||
};
|
||||
|
||||
Auras.prototype.ApplyTemplateBonus = function(name, players)
|
||||
Auras.prototype.RemoveTemplateAura = function(name)
|
||||
{
|
||||
if (!this[name].isApplied)
|
||||
return;
|
||||
|
||||
if (!this.IsGlobalAura(name))
|
||||
return;
|
||||
var modifications = this.GetModifications(name);
|
||||
var cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager);
|
||||
var classes = this.GetClasses(name);
|
||||
|
||||
cmpAuraManager.RegisterGlobalAuraSource(this.entity);
|
||||
let derivedModifiers = DeriveModificationsFromTech({
|
||||
"modifications": this.GetModifications(name),
|
||||
"affects": this.GetClasses(name)
|
||||
});
|
||||
let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
|
||||
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
|
||||
|
||||
for (let mod of modifications)
|
||||
for (let player of players)
|
||||
cmpAuraManager.ApplyTemplateBonus(mod.value, player, classes, mod, this.GetModifierIdentifier(name));
|
||||
let modifName = this.GetModifierIdentifier(name);
|
||||
for (let player of this.GetAffectedPlayers(name))
|
||||
{
|
||||
let playerId = cmpPlayerManager.GetPlayerByID(player);
|
||||
for (let modifierPath in derivedModifiers)
|
||||
for (let modifier of derivedModifiers[modifierPath])
|
||||
cmpModifiersManager.RemoveModifier(modifierPath, modifName, playerId);
|
||||
}
|
||||
};
|
||||
|
||||
Auras.prototype.RemoveFormationBonus = function(memberList)
|
||||
{
|
||||
for (let name of this.GetAuraNames().filter(n => this.IsFormationAura(n)))
|
||||
this.RemoveBonus(name, memberList);
|
||||
};
|
||||
|
||||
Auras.prototype.RemoveGarrisonBonus = function(structure)
|
||||
{
|
||||
for (let name of this.GetAuraNames().filter(n => this.IsGarrisonAura(n)))
|
||||
this.RemoveBonus(name, [structure]);
|
||||
};
|
||||
|
||||
Auras.prototype.RemoveTemplateBonus = function(name)
|
||||
{
|
||||
if (!this[name].isApplied)
|
||||
return;
|
||||
if (!this.IsGlobalAura(name))
|
||||
return;
|
||||
|
||||
var cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager);
|
||||
cmpAuraManager.UnregisterGlobalAuraSource(this.entity);
|
||||
|
||||
var modifications = this.GetModifications(name);
|
||||
var classes = this.GetClasses(name);
|
||||
var players = this.GetAffectedPlayers(name);
|
||||
|
||||
for (let mod of modifications)
|
||||
for (let player of players)
|
||||
cmpAuraManager.RemoveTemplateBonus(mod.value, player, classes, this.GetModifierIdentifier(name));
|
||||
};
|
||||
|
||||
Auras.prototype.ApplyBonus = function(name, ents)
|
||||
Auras.prototype.ApplyAura = function(name, ents)
|
||||
{
|
||||
var validEnts = this.GiveMembersWithValidClass(name, ents);
|
||||
if (!validEnts.length)
|
||||
|
|
@ -401,24 +402,36 @@ Auras.prototype.ApplyBonus = function(name, ents)
|
|||
if (!this[name].isApplied)
|
||||
return;
|
||||
|
||||
var modifications = this.GetModifications(name);
|
||||
var cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager);
|
||||
|
||||
for (let mod of modifications)
|
||||
cmpAuraManager.ApplyBonus(mod.value, validEnts, mod, this.GetModifierIdentifier(name));
|
||||
// update status bars if this has an icon
|
||||
if (!this.GetOverlayIcon(name))
|
||||
if (this.GetOverlayIcon(name))
|
||||
for (let ent of validEnts)
|
||||
{
|
||||
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
|
||||
if (cmpStatusBars)
|
||||
cmpStatusBars.AddAuraSource(this.entity, name);
|
||||
}
|
||||
|
||||
// Global aura modifications are handled at the player level by the modification manager,
|
||||
// so stop after icons have been applied.
|
||||
if (this.IsGlobalAura(name))
|
||||
return;
|
||||
|
||||
let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
|
||||
|
||||
let derivedModifiers = DeriveModificationsFromTech({
|
||||
"modifications": this.GetModifications(name),
|
||||
"affects": this.GetClasses(name)
|
||||
});
|
||||
|
||||
let modifName = this.GetModifierIdentifier(name);
|
||||
for (let ent of validEnts)
|
||||
{
|
||||
var cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
|
||||
if (cmpStatusBars)
|
||||
cmpStatusBars.AddAuraSource(this.entity, name);
|
||||
}
|
||||
for (let modifierPath in derivedModifiers)
|
||||
for (let modifier of derivedModifiers[modifierPath])
|
||||
cmpModifiersManager.AddModifier(modifierPath, modifName, modifier, ent);
|
||||
|
||||
};
|
||||
|
||||
Auras.prototype.RemoveBonus = function(name, ents)
|
||||
Auras.prototype.RemoveAura = function(name, ents, skipModifications = false)
|
||||
{
|
||||
var validEnts = this.GiveMembersWithValidClass(name, ents);
|
||||
if (!validEnts.length)
|
||||
|
|
@ -429,22 +442,32 @@ Auras.prototype.RemoveBonus = function(name, ents)
|
|||
if (!this[name].isApplied)
|
||||
return;
|
||||
|
||||
var modifications = this.GetModifications(name);
|
||||
var cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager);
|
||||
|
||||
for (let mod of modifications)
|
||||
cmpAuraManager.RemoveBonus(mod.value, validEnts, this.GetModifierIdentifier(name));
|
||||
|
||||
// update status bars if this has an icon
|
||||
if (!this.GetOverlayIcon(name))
|
||||
if (this.GetOverlayIcon(name))
|
||||
for (let ent of validEnts)
|
||||
{
|
||||
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
|
||||
if (cmpStatusBars)
|
||||
cmpStatusBars.RemoveAuraSource(this.entity, name);
|
||||
}
|
||||
|
||||
// Global aura modifications are handled at the player level by the modification manager,
|
||||
// so stop after icons have been removed.
|
||||
if (this.IsGlobalAura(name))
|
||||
return;
|
||||
|
||||
for (let ent of validEnts)
|
||||
{
|
||||
var cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
|
||||
if (cmpStatusBars)
|
||||
cmpStatusBars.RemoveAuraSource(this.entity, name);
|
||||
}
|
||||
let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
|
||||
|
||||
let derivedModifiers = DeriveModificationsFromTech({
|
||||
"modifications": this.GetModifications(name),
|
||||
"affects": this.GetClasses(name)
|
||||
});
|
||||
|
||||
let modifName = this.GetModifierIdentifier(name);
|
||||
for (let ent of ents)
|
||||
for (let modifierPath in derivedModifiers)
|
||||
for (let modifier of derivedModifiers[modifierPath])
|
||||
cmpModifiersManager.RemoveModifier(modifierPath, modifName, ent);
|
||||
};
|
||||
|
||||
Auras.prototype.OnOwnershipChanged = function(msg)
|
||||
|
|
@ -484,7 +507,7 @@ Auras.prototype.OnGlobalPlayerDefeated = function(msg)
|
|||
{
|
||||
let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
|
||||
if (cmpPlayer && cmpPlayer.GetPlayerID() == msg.playerId ||
|
||||
this.GetAuraNames().some(name => this.GetAffectedPlayers(name).indexOf(msg.playerId) != -1))
|
||||
this.GetAuraNames().some(name => this.GetAffectedPlayers(name).indexOf(msg.playerId) != -1))
|
||||
this.Clean();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -294,7 +294,7 @@ Formation.prototype.SetMembers = function(ents)
|
|||
if (cmpAuras && cmpAuras.HasFormationAura())
|
||||
{
|
||||
this.formationMembersWithAura.push(ent);
|
||||
cmpAuras.ApplyFormationBonus(ents);
|
||||
cmpAuras.ApplyFormationAura(ents);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -326,11 +326,11 @@ Formation.prototype.RemoveMembers = function(ents)
|
|||
for (var ent of this.formationMembersWithAura)
|
||||
{
|
||||
var cmpAuras = Engine.QueryInterface(ent, IID_Auras);
|
||||
cmpAuras.RemoveFormationBonus(ents);
|
||||
cmpAuras.RemoveFormationAura(ents);
|
||||
|
||||
// the unit with the aura is also removed from the formation
|
||||
if (ents.indexOf(ent) !== -1)
|
||||
cmpAuras.RemoveFormationBonus(this.members);
|
||||
cmpAuras.RemoveFormationAura(this.members);
|
||||
}
|
||||
|
||||
this.formationMembersWithAura = this.formationMembersWithAura.filter(function(e) { return ents.indexOf(e) == -1; });
|
||||
|
|
@ -359,7 +359,7 @@ Formation.prototype.AddMembers = function(ents)
|
|||
for (let ent of this.formationMembersWithAura)
|
||||
{
|
||||
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
|
||||
cmpAuras.ApplyFormationBonus(ents);
|
||||
cmpAuras.ApplyFormationAura(ents);
|
||||
}
|
||||
|
||||
this.members = this.members.concat(ents);
|
||||
|
|
@ -373,7 +373,7 @@ Formation.prototype.AddMembers = function(ents)
|
|||
if (cmpAuras && cmpAuras.HasFormationAura())
|
||||
{
|
||||
this.formationMembersWithAura.push(ent);
|
||||
cmpAuras.ApplyFormationBonus(this.members);
|
||||
cmpAuras.ApplyFormationAura(this.members);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -413,7 +413,7 @@ Formation.prototype.Disband = function()
|
|||
for (var ent of this.formationMembersWithAura)
|
||||
{
|
||||
var cmpAuras = Engine.QueryInterface(ent, IID_Auras);
|
||||
cmpAuras.RemoveFormationBonus(this.members);
|
||||
cmpAuras.RemoveFormationAura(this.members);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -266,7 +266,7 @@ GarrisonHolder.prototype.PerformGarrison = function(entity)
|
|||
|
||||
let cmpAura = Engine.QueryInterface(entity, IID_Auras);
|
||||
if (cmpAura && cmpAura.HasGarrisonAura())
|
||||
cmpAura.ApplyGarrisonBonus(this.entity);
|
||||
cmpAura.ApplyGarrisonAura(this.entity);
|
||||
|
||||
Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [entity], "removed": [] });
|
||||
return true;
|
||||
|
|
@ -336,7 +336,7 @@ GarrisonHolder.prototype.Eject = function(entity, forced)
|
|||
|
||||
let cmpEntAura = Engine.QueryInterface(entity, IID_Auras);
|
||||
if (cmpEntAura && cmpEntAura.HasGarrisonAura())
|
||||
cmpEntAura.RemoveGarrisonBonus(this.entity);
|
||||
cmpEntAura.RemoveGarrisonAura(this.entity);
|
||||
|
||||
cmpEntPosition.JumpTo(pos.x, pos.z);
|
||||
cmpEntPosition.SetHeightOffset(0);
|
||||
|
|
|
|||
290
binaries/data/mods/public/simulation/components/ModifiersManager.js
Executable file
290
binaries/data/mods/public/simulation/components/ModifiersManager.js
Executable file
|
|
@ -0,0 +1,290 @@
|
|||
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);
|
||||
else
|
||||
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;
|
||||
return GetTechModifiedProperty(modifs, 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 = 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?
|
||||
playerModifs[propertyName].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);
|
||||
|
|
@ -25,45 +25,57 @@ PlayerManager.prototype.AddPlayer = function(ent)
|
|||
newDiplo[id] = 1;
|
||||
cmpPlayer.SetDiplomacy(newDiplo);
|
||||
|
||||
Engine.BroadcastMessage(MT_PlayerEntityChanged, {
|
||||
"player": id,
|
||||
"from": INVALID_ENTITY,
|
||||
"to": ent
|
||||
});
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
* To avoid possible problems with cached quantities (as in TechnologyManager),
|
||||
* To avoid possible problems,
|
||||
* we first remove all entities from this player, and add them back after the replacement.
|
||||
* Note: This should only be called during setup/init and not during the game
|
||||
*/
|
||||
PlayerManager.prototype.ReplacePlayer = function(id, ent)
|
||||
{
|
||||
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||
var entities = cmpRangeManager.GetEntitiesByPlayer(id);
|
||||
for (var e of entities)
|
||||
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||
let entities = cmpRangeManager.GetEntitiesByPlayer(id);
|
||||
for (let e of entities)
|
||||
{
|
||||
var cmpOwnership = Engine.QueryInterface(e, IID_Ownership);
|
||||
let cmpOwnership = Engine.QueryInterface(e, IID_Ownership);
|
||||
if (cmpOwnership)
|
||||
cmpOwnership.SetOwner(INVALID_PLAYER);
|
||||
}
|
||||
|
||||
var oldent = this.playerEntities[id];
|
||||
var cmpPlayer = Engine.QueryInterface(oldent, IID_Player);
|
||||
var diplo = cmpPlayer.GetDiplomacy();
|
||||
var color = cmpPlayer.GetColor();
|
||||
let oldent = this.playerEntities[id];
|
||||
let oldCmpPlayer = Engine.QueryInterface(oldent, IID_Player);
|
||||
let diplo = oldCmpPlayer.GetDiplomacy();
|
||||
let color = oldCmpPlayer.GetColor();
|
||||
|
||||
var cmpPlayer = Engine.QueryInterface(ent, IID_Player);
|
||||
cmpPlayer.SetPlayerID(id);
|
||||
let newCmpPlayer = Engine.QueryInterface(ent, IID_Player);
|
||||
newCmpPlayer.SetPlayerID(id);
|
||||
this.playerEntities[id] = ent;
|
||||
cmpPlayer.SetColor(color);
|
||||
cmpPlayer.SetDiplomacy(diplo);
|
||||
newCmpPlayer.SetColor(color);
|
||||
newCmpPlayer.SetDiplomacy(diplo);
|
||||
|
||||
Engine.DestroyEntity(oldent);
|
||||
Engine.FlushDestroyedEntities();
|
||||
Engine.BroadcastMessage(MT_PlayerEntityChanged, {
|
||||
"player": id,
|
||||
"from": oldent,
|
||||
"to": ent
|
||||
});
|
||||
|
||||
for (var e of entities)
|
||||
for (let e of entities)
|
||||
{
|
||||
var cmpOwnership = Engine.QueryInterface(e, IID_Ownership);
|
||||
let cmpOwnership = Engine.QueryInterface(e, IID_Ownership);
|
||||
if (cmpOwnership)
|
||||
cmpOwnership.SetOwner(id);
|
||||
}
|
||||
|
||||
Engine.DestroyEntity(oldent);
|
||||
Engine.FlushDestroyedEntities();
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -126,8 +138,15 @@ PlayerManager.prototype.GetActivePlayers = function()
|
|||
PlayerManager.prototype.RemoveAllPlayers = function()
|
||||
{
|
||||
// Destroy existing player entities
|
||||
for (var id of this.playerEntities)
|
||||
Engine.DestroyEntity(id);
|
||||
for (let player in this.playerEntities)
|
||||
{
|
||||
Engine.BroadcastMessage(MT_PlayerEntityChanged, {
|
||||
"player": player,
|
||||
"from": this.playerEntities[player],
|
||||
"to": INVALID_ENTITY
|
||||
});
|
||||
Engine.DestroyEntity(this.playerEntities[player]);
|
||||
}
|
||||
|
||||
this.playerEntities = [];
|
||||
};
|
||||
|
|
@ -138,6 +157,11 @@ PlayerManager.prototype.RemoveLastPlayer = function()
|
|||
return;
|
||||
|
||||
var lastId = this.playerEntities.pop();
|
||||
Engine.BroadcastMessage(MT_PlayerEntityChanged, {
|
||||
"player": this.playerEntities.length + 1,
|
||||
"from": lastId,
|
||||
"to": INVALID_ENTITY
|
||||
});
|
||||
Engine.DestroyEntity(lastId);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,21 +3,6 @@ function TechnologyManager() {}
|
|||
TechnologyManager.prototype.Schema =
|
||||
"<empty/>";
|
||||
|
||||
TechnologyManager.prototype.Serialize = function()
|
||||
{
|
||||
// The modifications cache will be affected by property reads from the GUI and other places so we shouldn't
|
||||
// serialize it.
|
||||
|
||||
var ret = {};
|
||||
for (var i in this)
|
||||
{
|
||||
if (this.hasOwnProperty(i))
|
||||
ret[i] = this[i];
|
||||
}
|
||||
ret.modificationCache = {};
|
||||
return ret;
|
||||
};
|
||||
|
||||
TechnologyManager.prototype.Init = function()
|
||||
{
|
||||
// Holds names of technologies that have been researched.
|
||||
|
|
@ -29,16 +14,6 @@ TechnologyManager.prototype.Init = function()
|
|||
// Holds technologies which are being researched currently (non-queued).
|
||||
this.researchStarted = new Set();
|
||||
|
||||
// This stores the modifications to unit stats from researched technologies
|
||||
// Example data: {"ResourceGatherer/Rates/food.grain": [
|
||||
// {"multiply": 1.15, "affects": ["FemaleCitizen", "Infantry Sword"]},
|
||||
// {"add": 2}
|
||||
// ]}
|
||||
this.modifications = {};
|
||||
this.modificationCache = {}; // Caches the values after technologies have been applied
|
||||
// e.g. { "Attack/Melee/Damage/Hack" : {5: {"origValue": 8, "newValue": 10}, 7: {"origValue": 9, "newValue": 12}, ...}, ...}
|
||||
// where 5 and 7 are entity id's
|
||||
|
||||
this.classCounts = {}; // stores the number of entities of each Class
|
||||
this.typeCountsByClass = {}; // stores the number of entities of each type for each class i.e.
|
||||
// {"someClass": {"unit/spearman": 2, "unit/cav": 5} "someOtherClass":...}
|
||||
|
|
@ -204,31 +179,6 @@ TechnologyManager.prototype.OnGlobalOwnershipChanged = function(msg)
|
|||
this.typeCountsByClass[cls][template] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Newly created entity, check if any researched techs might apply
|
||||
// (only do this for new entities because even if an entity is converted or captured,
|
||||
// we want it to maintain whatever technologies previously applied)
|
||||
if (msg.from == INVALID_PLAYER)
|
||||
{
|
||||
var modifiedComponents = {};
|
||||
for (var name in this.modifications)
|
||||
{
|
||||
// We only need to find one one tech per component for a match
|
||||
var modifications = this.modifications[name];
|
||||
var component = name.split("/")[0];
|
||||
for (let modif of modifications)
|
||||
if (DoesModificationApply(modif, classes))
|
||||
{
|
||||
if (!modifiedComponents[component])
|
||||
modifiedComponents[component] = [];
|
||||
modifiedComponents[component].push(name);
|
||||
}
|
||||
}
|
||||
|
||||
// Send mesage(s) to the entity so it knows about researched techs
|
||||
for (var component in modifiedComponents)
|
||||
Engine.PostMessage(msg.entity, MT_ValueModification, { "entities": [msg.entity], "component": component, "valueNames": modifiedComponents[component] });
|
||||
}
|
||||
}
|
||||
if (msg.from == playerID)
|
||||
{
|
||||
|
|
@ -254,8 +204,6 @@ TechnologyManager.prototype.OnGlobalOwnershipChanged = function(msg)
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.clearModificationCache(msg.entity);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -266,23 +214,16 @@ TechnologyManager.prototype.ResearchTechnology = function(tech)
|
|||
|
||||
var modifiedComponents = {};
|
||||
this.researchedTechs.add(tech);
|
||||
|
||||
// store the modifications in an easy to access structure
|
||||
let template = TechnologyTemplates.Get(tech);
|
||||
if (template.modifications)
|
||||
{
|
||||
let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
|
||||
let derivedModifiers = DeriveModificationsFromTech(template);
|
||||
for (let modifierPath in derivedModifiers)
|
||||
{
|
||||
if (!this.modifications[modifierPath])
|
||||
this.modifications[modifierPath] = [];
|
||||
this.modifications[modifierPath] = this.modifications[modifierPath].concat(derivedModifiers[modifierPath]);
|
||||
|
||||
let component = modifierPath.split("/")[0];
|
||||
if (!modifiedComponents[component])
|
||||
modifiedComponents[component] = [];
|
||||
modifiedComponents[component].push(modifierPath);
|
||||
this.modificationCache[modifierPath] = {};
|
||||
}
|
||||
for (let modifier of derivedModifiers[modifierPath])
|
||||
cmpModifiersManager.AddModifier(modifierPath, "tech/" + tech, modifier, this.entity);
|
||||
}
|
||||
|
||||
if (template.replaces && template.replaces.length > 0)
|
||||
|
|
@ -323,62 +264,6 @@ TechnologyManager.prototype.ResearchTechnology = function(tech)
|
|||
|
||||
// always send research finished message
|
||||
Engine.PostMessage(this.entity, MT_ResearchFinished, {"player": playerID, "tech": tech});
|
||||
|
||||
for (var component in modifiedComponents)
|
||||
{
|
||||
Engine.PostMessage(SYSTEM_ENTITY, MT_TemplateModification, { "player": playerID, "component": component, "valueNames": modifiedComponents[component]});
|
||||
Engine.BroadcastMessage(MT_ValueModification, { "entities": ents, "component": component, "valueNames": modifiedComponents[component]});
|
||||
}
|
||||
|
||||
if (tech.startsWith("phase") && !template.autoResearch)
|
||||
{
|
||||
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
|
||||
cmpGUIInterface.PushNotification({
|
||||
"type": "phase",
|
||||
"players": [playerID],
|
||||
"phaseName": tech,
|
||||
"phaseState": "completed"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Clears the cached data for an entity from the modifications cache
|
||||
TechnologyManager.prototype.clearModificationCache = function(ent)
|
||||
{
|
||||
for (var valueName in this.modificationCache)
|
||||
delete this.modificationCache[valueName][ent];
|
||||
};
|
||||
|
||||
// Caching layer in front of ApplyModificationsWorker
|
||||
// Note: be careful with the type of curValue, if it should be a numerical
|
||||
// value and is derived from template data, you must convert the string
|
||||
// from the template to a number using the + operator, before calling
|
||||
// this function!
|
||||
TechnologyManager.prototype.ApplyModifications = function(valueName, curValue, ent)
|
||||
{
|
||||
if (!this.modificationCache[valueName])
|
||||
this.modificationCache[valueName] = {};
|
||||
|
||||
if (!this.modificationCache[valueName][ent] || this.modificationCache[valueName][ent].origValue != curValue)
|
||||
{
|
||||
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
|
||||
if (!cmpIdentity)
|
||||
return curValue;
|
||||
this.modificationCache[valueName][ent] = {
|
||||
"origValue": curValue,
|
||||
"newValue": GetTechModifiedProperty(this.modifications, cmpIdentity.GetClassesList(), valueName, curValue)
|
||||
};
|
||||
}
|
||||
|
||||
return this.modificationCache[valueName][ent].newValue;
|
||||
};
|
||||
|
||||
// Alternative version of ApplyModifications, applies to templates instead of entities
|
||||
TechnologyManager.prototype.ApplyModificationsTemplate = function(valueName, curValue, template)
|
||||
{
|
||||
if (!template || !template.Identity)
|
||||
return curValue;
|
||||
return GetTechModifiedProperty(this.modifications, GetIdentityClasses(template.Identity), valueName, curValue);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
Engine.RegisterInterface("AuraManager");
|
||||
|
|
@ -0,0 +1 @@
|
|||
Engine.RegisterInterface("ModifiersManager");
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Message of the form { player": number, "from": number, "to": number }
|
||||
* sent from PlayerManager component to warn other components when a player changed entities.
|
||||
* This is also sent when the player gets created or destroyed.
|
||||
*/
|
||||
Engine.RegisterMessageType("PlayerEntityChanged");
|
||||
|
|
@ -3,9 +3,9 @@ Engine.LoadHelperScript("Attacking.js");
|
|||
Engine.LoadHelperScript("Player.js");
|
||||
Engine.LoadHelperScript("ValueModification.js");
|
||||
Engine.LoadComponentScript("interfaces/Attack.js");
|
||||
Engine.LoadComponentScript("interfaces/AuraManager.js");
|
||||
Engine.LoadComponentScript("interfaces/Auras.js");
|
||||
Engine.LoadComponentScript("interfaces/Capturable.js");
|
||||
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
|
||||
Engine.LoadComponentScript("interfaces/Formation.js");
|
||||
Engine.LoadComponentScript("interfaces/Health.js");
|
||||
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
Engine.LoadComponentScript("interfaces/AuraManager.js");
|
||||
Engine.LoadComponentScript("AuraManager.js");
|
||||
|
||||
let value = "Component/Value";
|
||||
let player1 = 1;
|
||||
let player2 = 2;
|
||||
let ents1 = [25, 26, 27];
|
||||
let ents2 = [28, 29, 30];
|
||||
let ents3 = [31];
|
||||
let classes = ["class1", "class2"];
|
||||
let template = { "Identity" : { "Classes" : { "_string" : "class1 class3" } } };
|
||||
|
||||
let cmpAuraManager = ConstructComponent(SYSTEM_ENTITY, "AuraManager", {});
|
||||
|
||||
// Apply and remove a bonus
|
||||
cmpAuraManager.ApplyBonus(value, ents1, { "add": 8 }, "key1");
|
||||
TS_ASSERT_EQUALS(cmpAuraManager.ApplyModifications(value, 10, 25), 18);
|
||||
// It isn't apply to wrong entity
|
||||
TS_ASSERT_EQUALS(cmpAuraManager.ApplyModifications(value, 10, 28), 10);
|
||||
cmpAuraManager.RemoveBonus(value, ents1, "key1");
|
||||
TS_ASSERT_EQUALS(cmpAuraManager.ApplyModifications(value, 10, 25), 10);
|
||||
|
||||
// Apply 2 bonus with two different keys. Bonus should stack
|
||||
cmpAuraManager.ApplyBonus(value, ents2, { "add": 8 }, "key1");
|
||||
cmpAuraManager.ApplyBonus(value, ents2, { "multiply": 3 }, "key2");
|
||||
TS_ASSERT_EQUALS(cmpAuraManager.ApplyModifications(value, 10, 28), 38);
|
||||
|
||||
// With another operation ordering, the result must be the same
|
||||
cmpAuraManager.ApplyBonus(value, ents3, { "multiply": 3 }, "key2");
|
||||
cmpAuraManager.ApplyBonus(value, ents3, { "add": 8 }, "key1");
|
||||
TS_ASSERT_EQUALS(cmpAuraManager.ApplyModifications(value, 10, 31), 38);
|
||||
|
||||
// Apply bonus to templates
|
||||
cmpAuraManager.ApplyTemplateBonus(value, player1, classes, { "add": 10 }, "key3");
|
||||
TS_ASSERT_EQUALS(cmpAuraManager.ApplyTemplateModifications(value, 300, player1, template), 310);
|
||||
cmpAuraManager.RemoveTemplateBonus(value, player1, classes, "key3");
|
||||
TS_ASSERT_EQUALS(cmpAuraManager.ApplyTemplateModifications(value, 300, player1, template), 300);
|
||||
cmpAuraManager.ApplyTemplateBonus(value, player2, classes, { "add": 10 }, "key3");
|
||||
TS_ASSERT_EQUALS(cmpAuraManager.ApplyTemplateModifications(value, 300, player2, template), 310);
|
||||
cmpAuraManager.ApplyTemplateBonus(value, player1, classes, { "add": 10 }, "key3");
|
||||
TS_ASSERT_EQUALS(cmpAuraManager.ApplyTemplateModifications(value, 300, player1, template), 310);
|
||||
cmpAuraManager.RemoveTemplateBonus(value, player2, classes, "key3");
|
||||
TS_ASSERT_EQUALS(cmpAuraManager.ApplyTemplateModifications(value, 300, player2, template), 300);
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
Engine.LoadHelperScript("MultiKeyMap.js");
|
||||
Engine.LoadHelperScript("Player.js");
|
||||
Engine.LoadHelperScript("ValueModification.js");
|
||||
Engine.LoadComponentScript("interfaces/Auras.js");
|
||||
Engine.LoadComponentScript("interfaces/AuraManager.js");
|
||||
Engine.LoadComponentScript("interfaces/RangeOverlayManager.js");
|
||||
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
|
||||
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
|
||||
Engine.LoadComponentScript("Auras.js");
|
||||
Engine.LoadComponentScript("AuraManager.js");
|
||||
Engine.LoadComponentScript("ModifiersManager.js");
|
||||
|
||||
var playerID = [0, 1, 2];
|
||||
var playerEnt = [10, 11, 12];
|
||||
|
|
@ -89,7 +90,9 @@ function testAuras(name, test_function)
|
|||
"GetOwner": () => playerID[1]
|
||||
});
|
||||
|
||||
ConstructComponent(SYSTEM_ENTITY, "AuraManager", {});
|
||||
let cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager", {});
|
||||
cmpModifiersManager.OnGlobalPlayerEntityChanged({ player: playerID[1], from: -1, to: playerEnt[1] });
|
||||
cmpModifiersManager.OnGlobalPlayerEntityChanged({ player: playerID[2], from: -1, to: playerEnt[2] });
|
||||
let cmpAuras = ConstructComponent(sourceEnt, "Auras", { "_string": name });
|
||||
test_function(name, cmpAuras);
|
||||
}
|
||||
|
|
@ -125,17 +128,17 @@ testAuras("garrisonedUnits", (name, cmpAuras) => {
|
|||
|
||||
testAuras("garrison", (name, cmpAuras) => {
|
||||
TS_ASSERT_EQUALS(cmpAuras.HasGarrisonAura(), true);
|
||||
cmpAuras.ApplyGarrisonBonus(targetEnt);
|
||||
cmpAuras.ApplyGarrisonAura(targetEnt);
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15);
|
||||
cmpAuras.RemoveGarrisonBonus(targetEnt);
|
||||
cmpAuras.RemoveGarrisonAura(targetEnt);
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5);
|
||||
});
|
||||
|
||||
testAuras("formation", (name, cmpAuras) => {
|
||||
TS_ASSERT_EQUALS(cmpAuras.HasFormationAura(), true);
|
||||
cmpAuras.ApplyFormationBonus([targetEnt]);
|
||||
cmpAuras.ApplyFormationAura([targetEnt]);
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15);
|
||||
cmpAuras.RemoveFormationBonus([targetEnt]);
|
||||
cmpAuras.RemoveFormationAura([targetEnt]);
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
Engine.LoadHelperScript("Player.js");
|
||||
Engine.LoadHelperScript("ValueModification.js");
|
||||
Engine.LoadComponentScript("interfaces/AuraManager.js");
|
||||
Engine.LoadComponentScript("interfaces/Auras.js");
|
||||
Engine.LoadComponentScript("interfaces/Capturable.js");
|
||||
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
|
||||
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
|
||||
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
|
||||
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
|
||||
Engine.LoadComponentScript("interfaces/TerritoryDecay.js");
|
||||
Engine.LoadComponentScript("interfaces/Timer.js");
|
||||
Engine.LoadComponentScript("Capturable.js");
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ Engine.LoadHelperScript("Sound.js");
|
|||
Engine.LoadHelperScript("ValueModification.js");
|
||||
Engine.LoadComponentScript("interfaces/Attack.js");
|
||||
Engine.LoadComponentScript("interfaces/AttackDetection.js");
|
||||
Engine.LoadComponentScript("interfaces/AuraManager.js");
|
||||
Engine.LoadComponentScript("interfaces/DelayedDamage.js");
|
||||
Engine.LoadComponentScript("interfaces/Resistance.js");
|
||||
Engine.LoadComponentScript("interfaces/Health.js");
|
||||
|
|
@ -13,7 +12,7 @@ Engine.LoadComponentScript("interfaces/Loot.js");
|
|||
Engine.LoadComponentScript("interfaces/Player.js");
|
||||
Engine.LoadComponentScript("interfaces/Promotion.js");
|
||||
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
|
||||
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
|
||||
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
|
||||
Engine.LoadComponentScript("interfaces/Timer.js");
|
||||
Engine.LoadComponentScript("Attack.js");
|
||||
Engine.LoadComponentScript("DelayedDamage.js");
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
Engine.LoadHelperScript("DamageBonus.js");
|
||||
Engine.LoadHelperScript("Attacking.js");
|
||||
Engine.LoadHelperScript("ValueModification.js");
|
||||
Engine.LoadComponentScript("interfaces/AuraManager.js");
|
||||
Engine.LoadComponentScript("interfaces/DeathDamage.js");
|
||||
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
|
||||
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
|
||||
Engine.LoadComponentScript("DeathDamage.js");
|
||||
|
||||
let deadEnt = 60;
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ Engine.LoadHelperScript("Player.js");
|
|||
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
|
||||
Engine.LoadComponentScript("interfaces/Garrisonable.js");
|
||||
Engine.LoadComponentScript("GarrisonHolder.js");
|
||||
Engine.LoadComponentScript("interfaces/AuraManager.js");
|
||||
Engine.LoadComponentScript("interfaces/Auras.js");
|
||||
Engine.LoadComponentScript("interfaces/Health.js");
|
||||
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
|
||||
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
|
||||
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
|
||||
Engine.LoadComponentScript("interfaces/Timer.js");
|
||||
Engine.LoadComponentScript("interfaces/UnitAI.js");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
|
||||
Engine.LoadComponentScript("interfaces/AuraManager.js");
|
||||
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
|
||||
Engine.LoadHelperScript("Player.js");
|
||||
Engine.LoadHelperScript("ValueModification.js");
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
|
||||
Engine.LoadComponentScript("ModifiersManager.js");
|
||||
Engine.LoadHelperScript("MultiKeyMap.js");
|
||||
Engine.LoadHelperScript("Player.js");
|
||||
Engine.LoadHelperScript("ValueModification.js");
|
||||
|
||||
let cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager", {});
|
||||
cmpModifiersManager.Init();
|
||||
|
||||
// These should be different as that is the general case.
|
||||
const PLAYER_ID_FOR_TEST = 2;
|
||||
const PLAYER_ENTITY_ID = 3;
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
||||
"GetEntitiesByPlayer": function(a) { return []; }
|
||||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
|
||||
"GetPlayerByID": (a) => PLAYER_ENTITY_ID
|
||||
});
|
||||
|
||||
AddMock(PLAYER_ENTITY_ID, IID_Player, {
|
||||
"GetPlayerID": () => PLAYER_ID_FOR_TEST
|
||||
});
|
||||
|
||||
let entitiesToTest = [5, 6, 7, 8];
|
||||
for (let ent of entitiesToTest)
|
||||
AddMock(ent, IID_Ownership, {
|
||||
"GetOwner": () => PLAYER_ID_FOR_TEST
|
||||
});
|
||||
|
||||
AddMock(5, IID_Identity, {
|
||||
"GetClassesList": function() { return "Structure";}
|
||||
});
|
||||
AddMock(6, IID_Identity, {
|
||||
"GetClassesList": function() { return "Infantry";}
|
||||
});
|
||||
AddMock(7, IID_Identity, {
|
||||
"GetClassesList": function() { return "Unit";}
|
||||
});
|
||||
AddMock(8, IID_Identity, {
|
||||
"GetClassesList": function() { return "Structure Unit";}
|
||||
});
|
||||
|
||||
// Sprinkle random serialisation cycles.
|
||||
function SerializationCycle()
|
||||
{
|
||||
let data = cmpModifiersManager.Serialize();
|
||||
cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager", {});
|
||||
cmpModifiersManager.Deserialize(data);
|
||||
}
|
||||
|
||||
cmpModifiersManager.OnGlobalPlayerEntityChanged({ player: PLAYER_ID_FOR_TEST, from: -1, to: PLAYER_ENTITY_ID });
|
||||
|
||||
cmpModifiersManager.AddModifier("Test_A", "Test_A_0", { "affects": ["Structure"], "add": 10 }, 10, "testLol");
|
||||
|
||||
cmpModifiersManager.AddModifier("Test_A", "Test_A_0", { "affects": ["Structure"], "add": 10 }, PLAYER_ENTITY_ID);
|
||||
cmpModifiersManager.AddModifier("Test_A", "Test_A_1", { "affects": ["Infantry"], "add": 5 }, PLAYER_ENTITY_ID);
|
||||
cmpModifiersManager.AddModifier("Test_A", "Test_A_2", { "affects": ["Unit"], "add": 3 }, PLAYER_ENTITY_ID);
|
||||
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 5), 15);
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 6), 10);
|
||||
SerializationCycle();
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 5), 15);
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 6), 10);
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 7), 8);
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 8), 18);
|
||||
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_B", 5, 8), 5);
|
||||
|
||||
cmpModifiersManager.RemoveAllModifiers("Test_A_0", PLAYER_ENTITY_ID);
|
||||
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 5), 5);
|
||||
|
||||
cmpModifiersManager.AddModifiers("Test_A_0", {
|
||||
"Test_A": { "affects": ["Structure"], "add": 10 },
|
||||
"Test_B": { "affects": ["Structure"], "add": 8 },
|
||||
}, PLAYER_ENTITY_ID);
|
||||
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 5), 15);
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_B", 5, 8), 13);
|
||||
|
||||
// Add two local modifications, only the first should stick.
|
||||
cmpModifiersManager.AddModifier("Test_C", "Test_C_0", { "affects": ["Structure"], "add": 10 }, 5);
|
||||
cmpModifiersManager.AddModifier("Test_C", "Test_C_1", { "affects": ["Unit"], "add": 5 }, 5);
|
||||
|
||||
SerializationCycle();
|
||||
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, 5), 15);
|
||||
|
||||
// test that local modifications are indeed applied after global managers
|
||||
cmpModifiersManager.AddModifier("Test_C", "Test_C_2", { "affects": ["Structure"], "replace": 0 }, 5);
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, 5), 0);
|
||||
|
||||
TS_ASSERT(!cmpModifiersManager.HasAnyModifier("Test_C_3", PLAYER_ENTITY_ID));
|
||||
|
||||
SerializationCycle();
|
||||
|
||||
// check that things still work properly if we change global modifications
|
||||
cmpModifiersManager.AddModifier("Test_C", "Test_C_3", { "affects": ["Structure"], "add": 10 }, PLAYER_ENTITY_ID);
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, 5), 0);
|
||||
|
||||
TS_ASSERT(cmpModifiersManager.HasAnyModifier("Test_C_3", PLAYER_ENTITY_ID));
|
||||
TS_ASSERT(cmpModifiersManager.HasModifier("Test_C", "Test_C_3", PLAYER_ENTITY_ID));
|
||||
TS_ASSERT(cmpModifiersManager.HasModifier("Test_C", "Test_C_2", 5));
|
||||
|
||||
// test removal
|
||||
cmpModifiersManager.RemoveModifier("Test_C", "Test_C_2", 5);
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, 5), 25);
|
||||
|
||||
SerializationCycle();
|
||||
|
||||
TS_ASSERT(cmpModifiersManager.HasModifier("Test_C", "Test_C_3", PLAYER_ENTITY_ID));
|
||||
TS_ASSERT(!cmpModifiersManager.HasModifier("Test_C", "Test_C_2", 5));
|
||||
|
||||
//////////////////////////////////////////
|
||||
// Test that entities keep local modifications but not global ones when changing owner.
|
||||
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
|
||||
"GetPlayerByID": (a) => a == PLAYER_ID_FOR_TEST ? PLAYER_ENTITY_ID : PLAYER_ENTITY_ID + 1
|
||||
});
|
||||
|
||||
AddMock(PLAYER_ENTITY_ID + 1, IID_Player, {
|
||||
"GetPlayerID": () => PLAYER_ID_FOR_TEST + 1
|
||||
});
|
||||
|
||||
cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager", {});
|
||||
cmpModifiersManager.Init();
|
||||
|
||||
cmpModifiersManager.AddModifier("Test_D", "Test_D_0", { "affects": ["Structure"], "add": 10 }, PLAYER_ENTITY_ID);
|
||||
cmpModifiersManager.AddModifier("Test_D", "Test_D_1", { "affects": ["Structure"], "add": 1 }, PLAYER_ENTITY_ID + 1);
|
||||
cmpModifiersManager.AddModifier("Test_D", "Test_D_2", { "affects": ["Structure"], "add": 5 }, 5);
|
||||
|
||||
cmpModifiersManager.OnGlobalPlayerEntityChanged({ player: PLAYER_ID_FOR_TEST, from: -1, to: PLAYER_ENTITY_ID });
|
||||
cmpModifiersManager.OnGlobalPlayerEntityChanged({ player: PLAYER_ID_FOR_TEST + 1, from: -1, to: PLAYER_ENTITY_ID + 1 });
|
||||
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_D", 10, 5), 25);
|
||||
cmpModifiersManager.OnGlobalOwnershipChanged({ entity: 5, from: PLAYER_ID_FOR_TEST, to: PLAYER_ID_FOR_TEST + 1 });
|
||||
AddMock(5, IID_Ownership, {
|
||||
"GetOwner": () => PLAYER_ID_FOR_TEST + 1
|
||||
});
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_D", 10, 5), 16);
|
||||
|
|
@ -2,8 +2,7 @@ Engine.LoadHelperScript("Player.js");
|
|||
Engine.LoadHelperScript("Sound.js");
|
||||
Engine.LoadHelperScript("Transform.js");
|
||||
Engine.LoadHelperScript("ValueModification.js");
|
||||
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
|
||||
Engine.LoadComponentScript("interfaces/AuraManager.js");
|
||||
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
|
||||
Engine.LoadComponentScript("interfaces/Capturable.js");
|
||||
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
|
||||
Engine.LoadComponentScript("interfaces/Guard.js");
|
||||
|
|
|
|||
|
|
@ -15,9 +15,8 @@ Resources = {
|
|||
};
|
||||
|
||||
Engine.LoadHelperScript("ValueModification.js");
|
||||
Engine.LoadComponentScript("interfaces/AuraManager.js");
|
||||
Engine.LoadComponentScript("interfaces/Player.js");
|
||||
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
|
||||
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
|
||||
Engine.LoadComponentScript("Player.js");
|
||||
|
||||
var cmpPlayer = ConstructComponent(10, "Player", {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
// TODO: Move this to a folder of tests for GlobalScripts (once one is created)
|
||||
|
||||
// This tests the GetTechModifiedProperty function.
|
||||
let add = [{ "add": 10, "affects": "Unit" }];
|
||||
|
||||
let add_add = [{ "add": 10, "affects": "Unit" }, { "add": 5, "affects": "Unit" }];
|
||||
|
||||
let add_mul_add = [{ "add": 10, "affects": "Unit" }, { "multiply": 2, "affects": "Unit" }, { "add": 5, "affects": "Unit" }];
|
||||
|
||||
let add_replace = [{ "add": 10, "affects": "Unit" }, { "replace": 10, "affects": "Unit" }];
|
||||
|
||||
let replace_add = [{ "replace": 10, "affects": "Unit" }, { "add": 10, "affects": "Unit" }];
|
||||
|
||||
let replace_replace = [{ "replace": 10, "affects": "Unit" }, { "replace": 30, "affects": "Unit" }];
|
||||
|
||||
let replace_nonnum = [{ "replace": "alpha", "affects": "Unit" }];
|
||||
|
||||
TS_ASSERT_EQUALS(GetTechModifiedProperty(add, "Unit", 5), 15);
|
||||
TS_ASSERT_EQUALS(GetTechModifiedProperty(add_add, "Unit", 5), 20);
|
||||
TS_ASSERT_EQUALS(GetTechModifiedProperty(add_add, "Other", 5), 5);
|
||||
|
||||
// Technologies work by multiplying then adding all.
|
||||
TS_ASSERT_EQUALS(GetTechModifiedProperty(add_mul_add, "Unit", 5), 25);
|
||||
|
||||
TS_ASSERT_EQUALS(GetTechModifiedProperty(add_replace, "Unit", 5), 10);
|
||||
|
||||
// Only the first replace is taken into account
|
||||
TS_ASSERT_EQUALS(GetTechModifiedProperty(replace_replace, "Unit", 5), 10);
|
||||
|
||||
TS_ASSERT_EQUALS(GetTechModifiedProperty(replace_nonnum, "Unit", "beta"), "alpha");
|
||||
|
|
@ -55,14 +55,14 @@ template.requirements = { "all": [{ "tech": "tech_A" }, { "entity": { "class": "
|
|||
TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["tech_A"], "entities": [{ "class": "class_B", "number": 5, "check": "count" }] }]);
|
||||
|
||||
// Multiple `civ`s
|
||||
template.requirements = { "all": [{ "civ": "civ_A"}, { "civ": "civ_B"}, { "civ": "civ_C"}] };
|
||||
template.requirements = { "all": [{ "civ": "civ_A" }, { "civ": "civ_B" }, { "civ": "civ_C" }] };
|
||||
TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civ_A"), []);
|
||||
TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civ_B"), []);
|
||||
TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civ_C"), []);
|
||||
TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civ_D"), false);
|
||||
|
||||
// Multiple `notciv`s
|
||||
template.requirements = { "all": [{ "notciv": "civ_A"}, { "notciv": "civ_B"}, { "notciv": "civ_C"}] };
|
||||
template.requirements = { "all": [{ "notciv": "civ_A" }, { "notciv": "civ_B" }, { "notciv": "civ_C" }] };
|
||||
TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civ_A"), false);
|
||||
TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civ_B"), false);
|
||||
TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civ_C"), false);
|
||||
|
|
@ -13,8 +13,7 @@ Resources = {
|
|||
return "<interleave>" + schema + "</interleave>";
|
||||
}
|
||||
};
|
||||
Engine.LoadComponentScript("interfaces/AuraManager.js"); // Provides `IID_AuraManager`, tested for in helpers/ValueModification.js.
|
||||
Engine.LoadComponentScript("interfaces/TechnologyManager.js"); // Provides `IID_TechnologyManager`, used below.
|
||||
Engine.LoadComponentScript("interfaces/ModifiersManager.js"); // Provides `IID_ModifiersManager`, used below.
|
||||
Engine.LoadComponentScript("interfaces/Timer.js"); // Provides `IID_Timer`, used below.
|
||||
|
||||
// What we're testing:
|
||||
|
|
@ -89,6 +88,24 @@ AddMock(SYSTEM_ENTITY, IID_Timer, {
|
|||
"CancelTimer": () => {} // Called in components/Upgrade.js::CancelUpgrade().
|
||||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_ModifiersManager, {
|
||||
"ApplyTemplateModifiers": (valueName, curValue, template, player) => {
|
||||
// Called in helpers/ValueModification.js::ApplyValueModificationsToTemplate()
|
||||
// as part of Tests T2 and T5 below.
|
||||
let mods = isResearched ? templateTechModifications.with : templateTechModifications.without;
|
||||
|
||||
if (mods[valueName])
|
||||
return GetTechModifiedProperty(mods[valueName], GetIdentityClasses(template.Identity), curValue);
|
||||
return curValue;
|
||||
},
|
||||
"ApplyModifiers": (valueName, curValue, ent) => {
|
||||
// Called in helpers/ValueModification.js::ApplyValueModificationsToEntity()
|
||||
// as part of Tests T3, T6 and T7 below.
|
||||
let mods = isResearched ? entityTechModifications.with : entityTechModifications.without;
|
||||
return mods[valueName][ent].newValue;
|
||||
}
|
||||
});
|
||||
|
||||
// Init Player:
|
||||
AddMock(10, IID_Player, {
|
||||
"AddResources": () => {}, // Called in components/Upgrade.js::CancelUpgrade().
|
||||
|
|
@ -96,20 +113,6 @@ AddMock(10, IID_Player, {
|
|||
"GetTimeMultiplier": () => 1.0, // Called in components/Upgrade.js::GetUpgradeTime().
|
||||
"TrySubtractResources": () => true // Called in components/Upgrade.js::Upgrade().
|
||||
});
|
||||
AddMock(10, IID_TechnologyManager, {
|
||||
"ApplyModificationsTemplate": (valueName, curValue, template) => {
|
||||
// Called in helpers/ValueModification.js::ApplyValueModificationsToTemplate()
|
||||
// as part of Tests T2 and T5 below.
|
||||
let mods = isResearched ? templateTechModifications.with : templateTechModifications.without;
|
||||
return GetTechModifiedProperty(mods, GetIdentityClasses(template.Identity), valueName, curValue);
|
||||
},
|
||||
"ApplyModifications": (valueName, curValue, ent) => {
|
||||
// Called in helpers/ValueModification.js::ApplyValueModificationsToEntity()
|
||||
// as part of Tests T3, T6 and T7 below.
|
||||
let mods = isResearched ? entityTechModifications.with : entityTechModifications.without;
|
||||
return mods[valueName][ent].newValue;
|
||||
}
|
||||
});
|
||||
|
||||
// Create an entity with an Upgrade component:
|
||||
AddMock(20, IID_Ownership, {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
Engine.LoadHelperScript("Player.js");
|
||||
Engine.LoadHelperScript("ValueModification.js");
|
||||
Engine.LoadComponentScript("interfaces/AuraManager.js");
|
||||
Engine.LoadComponentScript("interfaces/Player.js");
|
||||
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
|
||||
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
|
||||
|
||||
let player = 1;
|
||||
let playerEnt = 10;
|
||||
let ownedEnt = 60;
|
||||
let techKey = "Attack/BigAttack";
|
||||
let otherKey = "Other/Key";
|
||||
|
||||
AddMock(playerEnt, IID_TechnologyManager, {
|
||||
"ApplyModifications": (key, val, ent) => {
|
||||
AddMock(SYSTEM_ENTITY, IID_ModifiersManager, {
|
||||
"ApplyModifiers": (key, val, ent) => {
|
||||
if (key != techKey)
|
||||
return val;
|
||||
if (ent == playerEnt)
|
||||
|
|
@ -21,18 +21,6 @@ AddMock(playerEnt, IID_TechnologyManager, {
|
|||
}
|
||||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_AuraManager, {
|
||||
"ApplyModifications": (key, val, ent) => {
|
||||
if (key != techKey)
|
||||
return val;
|
||||
if (ent == playerEnt)
|
||||
return val * 10;
|
||||
if (ent == ownedEnt)
|
||||
return val * 100;
|
||||
return val;
|
||||
}
|
||||
});
|
||||
|
||||
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
|
||||
"GetPlayerByID": () => 10
|
||||
});
|
||||
|
|
@ -45,6 +33,8 @@ AddMock(ownedEnt, IID_Ownership, {
|
|||
"GetOwner": () => 1
|
||||
});
|
||||
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity(techKey, 2.0, playerEnt), 50.0);
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity(otherKey, 2.0, playerEnt), 2.0);
|
||||
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity(techKey, 2.0, ownedEnt), 900.0);
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity(techKey, 2.0, playerEnt), 5.0);
|
||||
|
||||
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity(techKey, 2.0, ownedEnt), 9.0);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ Engine.LoadHelperScript("ValueModification.js");
|
|||
Engine.LoadHelperScript("Commands.js");
|
||||
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
|
||||
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
|
||||
Engine.LoadComponentScript("interfaces/AuraManager.js");
|
||||
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
|
||||
Engine.LoadComponentScript("interfaces/Timer.js");
|
||||
Engine.LoadComponentScript("interfaces/VisionSharing.js");
|
||||
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
|
||||
|
|
@ -118,7 +118,10 @@ AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
|
|||
|
||||
AddMock(14, IID_TechnologyManager, {
|
||||
"CanProduce": entity => false,
|
||||
"ApplyModificationsTemplate": (valueName, curValue, template) => curValue
|
||||
});
|
||||
|
||||
AddMock(14, IID_ModifiersManager, {
|
||||
"ApplyTemplateModifiers": (valueName, curValue) => curValue
|
||||
});
|
||||
|
||||
TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.shared, new Set([1, 2, 5]));
|
||||
|
|
@ -127,8 +130,12 @@ TS_ASSERT_EQUALS(cmpVisionSharing.spyId, 20);
|
|||
|
||||
AddMock(14, IID_TechnologyManager, {
|
||||
"CanProduce": entity => entity == "special/spy",
|
||||
"ApplyModificationsTemplate": (valueName, curValue, template) => curValue
|
||||
});
|
||||
|
||||
AddMock(14, IID_ModifiersManager, {
|
||||
"ApplyTemplateModifiers": (valueName, curValue) => curValue
|
||||
});
|
||||
|
||||
AddMock(14, IID_Player, {
|
||||
"GetSpyCostMultiplier": () => 1,
|
||||
"TrySubtractResources": costs => false
|
||||
|
|
|
|||
225
binaries/data/mods/public/simulation/helpers/MultiKeyMap.js
Executable file
225
binaries/data/mods/public/simulation/helpers/MultiKeyMap.js
Executable file
|
|
@ -0,0 +1,225 @@
|
|||
// Convenient container abstraction for storing items referenced by a 3-tuple.
|
||||
// Used by the ModifiersManager to store items by (property Name, entity, item ID).
|
||||
// Methods starting with an underscore are private to the storage.
|
||||
// This supports stackable items as it stores count for each 3-tuple.
|
||||
// It is designed to be as fast as can be for a JS container.
|
||||
function MultiKeyMap()
|
||||
{
|
||||
this.items = new Map();
|
||||
// Keys are referred to as 'primaryKey', 'secondaryKey', 'itemID'.
|
||||
}
|
||||
|
||||
MultiKeyMap.prototype.Serialize = function()
|
||||
{
|
||||
let ret = [];
|
||||
for (let primary of this.items.keys())
|
||||
{
|
||||
// Keys of a Map can be arbitrary types whereas objects only support string, so use a list.
|
||||
let vals = [primary, []];
|
||||
ret.push(vals);
|
||||
for (let secondary of this.items.get(primary).keys())
|
||||
vals[1].push([secondary, this.items.get(primary).get(secondary)]);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
MultiKeyMap.prototype.Deserialize = function(data)
|
||||
{
|
||||
for (let primary in data)
|
||||
{
|
||||
this.items.set(data[primary][0], new Map());
|
||||
for (let secondary in data[primary][1])
|
||||
this.items.get(data[primary][0]).set(data[primary][1][secondary][0], data[primary][1][secondary][1]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a single item.
|
||||
* NB: if you add an item with a different value but the same itemID, the original value remains.
|
||||
* @param item - an object.
|
||||
* @param itemID - internal ID of this item, for later removal and/or updating
|
||||
* @param stackable - if stackable, changing the count of items invalides, otherwise not.
|
||||
* @returns true if the items list changed in such a way that cached values are possibly invalidated.
|
||||
*/
|
||||
MultiKeyMap.prototype.AddItem = function(primaryKey, itemID, item, secondaryKey, stackable = false)
|
||||
{
|
||||
if (!this._AddItem(primaryKey, itemID, item, secondaryKey, stackable))
|
||||
return false;
|
||||
|
||||
this._OnItemModified(primaryKey, secondaryKey, itemID);
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add items to multiple properties at once (only one item per property)
|
||||
* @param items - Dictionnary of { primaryKey: item }
|
||||
* @returns true if the items list changed in such a way that cached values are possibly invalidated.
|
||||
*/
|
||||
MultiKeyMap.prototype.AddItems = function(itemID, items, secondaryKey, stackable = false)
|
||||
{
|
||||
let modified = false;
|
||||
for (let primaryKey in items)
|
||||
modified = this.AddItem(primaryKey, itemID, items[primaryKey], secondaryKey, stackable) || modified;
|
||||
return modified;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a item on a property.
|
||||
* @param primaryKey - property to change (e.g. "Health/Max")
|
||||
* @param itemID - internal ID of the item to remove
|
||||
* @param secondaryKey - secondaryKey ID
|
||||
* @returns true if the items list changed in such a way that cached values are possibly invalidated.
|
||||
*/
|
||||
MultiKeyMap.prototype.RemoveItem = function(primaryKey, itemID, secondaryKey, stackable = false)
|
||||
{
|
||||
if (!this._RemoveItem(primaryKey, itemID, secondaryKey, stackable))
|
||||
return false;
|
||||
|
||||
this._OnItemModified(primaryKey, secondaryKey, itemID);
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes items with this ID for any property name.
|
||||
* Naively iterates all property names.
|
||||
* @returns true if the items list changed in such a way that cached values are possibly invalidated.
|
||||
*/
|
||||
MultiKeyMap.prototype.RemoveAllItems = function(itemID, secondaryKey, stackable = false)
|
||||
{
|
||||
let modified = false;
|
||||
// Map doesn't implement some so use a for-loop here.
|
||||
for (let primaryKey of this.items.keys())
|
||||
modified = this.RemoveItem(primaryKey, itemID, secondaryKey, stackable) || modified;
|
||||
return modified;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param itemID - internal ID of the item to try and find.
|
||||
* @returns true if there is at least one item with that itemID
|
||||
*/
|
||||
MultiKeyMap.prototype.HasItem = function(primaryKey, itemID, secondaryKey)
|
||||
{
|
||||
// some() returns false for an empty list which is wanted here.
|
||||
return this._getItems(primaryKey, secondaryKey).some(item => item._ID === itemID);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if we have a item for any property name.
|
||||
* Naively iterates all property names.
|
||||
* @returns true if there is at least one item with that itemID
|
||||
*/
|
||||
MultiKeyMap.prototype.HasAnyItem = function(itemID, secondaryKey)
|
||||
{
|
||||
// Map doesn't implement some so use for loops instead.
|
||||
for (let primaryKey of this.items.keys())
|
||||
if (this.HasItem(primaryKey, itemID, secondaryKey))
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns A list of items (references to stored items to avoid copying)
|
||||
* (these need to be treated as constants to not break the map)
|
||||
*/
|
||||
MultiKeyMap.prototype.GetItems = function(primaryKey, secondaryKey)
|
||||
{
|
||||
return this._getItems(primaryKey, secondaryKey);
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns A dictionary of { Property Name: items } for the secondary Key.
|
||||
* Naively iterates all property names.
|
||||
*/
|
||||
MultiKeyMap.prototype.GetAllItems = function(secondaryKey)
|
||||
{
|
||||
let items = {};
|
||||
|
||||
// Map doesn't implement filter so use a for loop.
|
||||
for (let primaryKey of this.items.keys())
|
||||
{
|
||||
if (!this.items.get(primaryKey).has(secondaryKey))
|
||||
continue;
|
||||
items[primaryKey] = this.GetItems(primaryKey, secondaryKey);
|
||||
}
|
||||
return items;
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns a list of items.
|
||||
* This does not necessarily return a reference to items' list, use _getItemsOrInit for that.
|
||||
*/
|
||||
MultiKeyMap.prototype._getItems = function(primaryKey, secondaryKey)
|
||||
{
|
||||
let cache = this.items.get(primaryKey);
|
||||
if (cache)
|
||||
cache = cache.get(secondaryKey);
|
||||
return cache ? cache : [];
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns a reference to the list of items for that property name and secondaryKey.
|
||||
*/
|
||||
MultiKeyMap.prototype._getItemsOrInit = function(primaryKey, secondaryKey)
|
||||
{
|
||||
let cache = this.items.get(primaryKey);
|
||||
if (!cache)
|
||||
cache = this.items.set(primaryKey, new Map()).get(primaryKey);
|
||||
|
||||
let cache2 = cache.get(secondaryKey);
|
||||
if (!cache2)
|
||||
cache2 = cache.set(secondaryKey, []).get(secondaryKey);
|
||||
return cache2;
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns true if the items list changed in such a way that cached values are possibly invalidated.
|
||||
*/
|
||||
MultiKeyMap.prototype._AddItem = function(primaryKey, itemID, item, secondaryKey, stackable)
|
||||
{
|
||||
let items = this._getItemsOrInit(primaryKey, secondaryKey);
|
||||
for (let it of items)
|
||||
if (it._ID == itemID)
|
||||
{
|
||||
it._count++;
|
||||
return stackable;
|
||||
}
|
||||
items.push(Object.assign({ "_ID": itemID, "_count": 1 }, item));
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns true if the items list changed in such a way that cached values are possibly invalidated.
|
||||
*/
|
||||
MultiKeyMap.prototype._RemoveItem = function(primaryKey, itemID, secondaryKey, stackable)
|
||||
{
|
||||
let items = this._getItems(primaryKey, secondaryKey);
|
||||
|
||||
let existingItem = items.filter(item => { return item._ID == itemID; });
|
||||
if (!existingItem.length)
|
||||
return false;
|
||||
|
||||
if (--existingItem[0]._count > 0)
|
||||
return stackable;
|
||||
|
||||
let stilValidItems = items.filter(item => item._count > 0);
|
||||
|
||||
// Delete entries from the map if necessary to clean up.
|
||||
if (!stilValidItems.length)
|
||||
{
|
||||
this.items.get(primaryKey).delete(secondaryKey);
|
||||
if (!this.items.get(primaryKey).size)
|
||||
this.items.delete(primaryKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
this.items.get(primaryKey).set(secondaryKey, stilValidItems);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Stub method, to overload.
|
||||
*/
|
||||
MultiKeyMap.prototype._OnItemModified = function(primaryKey, secondaryKey, itemID) {};
|
||||
|
||||
Engine.RegisterGlobal("MultiKeyMap", MultiKeyMap);
|
||||
|
|
@ -184,6 +184,31 @@ function GetPlayerTemplateName(civ)
|
|||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id An entity's ID
|
||||
* @returns The entity ID of the owner player (not his player ID) or ent if ent is a player entity.
|
||||
*/
|
||||
function QueryOwnerEntityID(ent)
|
||||
{
|
||||
let cmpPlayer = Engine.QueryInterface(ent, IID_Player);
|
||||
if (cmpPlayer)
|
||||
return ent;
|
||||
|
||||
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
|
||||
if (!cmpOwnership)
|
||||
return null;
|
||||
|
||||
let owner = cmpOwnership.GetOwner();
|
||||
if (owner == INVALID_PLAYER)
|
||||
return null;
|
||||
|
||||
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
|
||||
if (!cmpPlayerManager)
|
||||
return null;
|
||||
|
||||
return cmpPlayerManager.GetPlayerByID(owner);
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to Engine.QueryInterface but applies to the player entity
|
||||
* that owns the given entity.
|
||||
|
|
@ -326,6 +351,7 @@ function IsOwnedByHelper(player, target, check)
|
|||
}
|
||||
|
||||
Engine.RegisterGlobal("LoadPlayerSettings", LoadPlayerSettings);
|
||||
Engine.RegisterGlobal("QueryOwnerEntityID", QueryOwnerEntityID);
|
||||
Engine.RegisterGlobal("QueryOwnerInterface", QueryOwnerInterface);
|
||||
Engine.RegisterGlobal("QueryPlayerIDInterface", QueryPlayerIDInterface);
|
||||
Engine.RegisterGlobal("QueryMiragedInterface", QueryMiragedInterface);
|
||||
|
|
|
|||
|
|
@ -3,29 +3,21 @@
|
|||
function ApplyValueModificationsToEntity(tech_type, current_value, entity)
|
||||
{
|
||||
let value = current_value;
|
||||
// entity can be an owned entity or a player entity.
|
||||
let cmpTechnologyManager = Engine.QueryInterface(entity, IID_Player) ?
|
||||
Engine.QueryInterface(entity, IID_TechnologyManager) : QueryOwnerInterface(entity, IID_TechnologyManager);
|
||||
if (cmpTechnologyManager)
|
||||
value = cmpTechnologyManager.ApplyModifications(tech_type, current_value, entity);
|
||||
|
||||
let cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager);
|
||||
if (!cmpAuraManager)
|
||||
return value;
|
||||
return cmpAuraManager.ApplyModifications(tech_type, value, entity);
|
||||
// entity can be an owned entity or a player entity.
|
||||
let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
|
||||
if (cmpModifiersManager)
|
||||
value = cmpModifiersManager.ApplyModifiers(tech_type, current_value, entity);
|
||||
return value;
|
||||
}
|
||||
|
||||
function ApplyValueModificationsToTemplate(tech_type, current_value, playerID, template)
|
||||
{
|
||||
let value = current_value;
|
||||
let cmpTechnologyManager = QueryPlayerIDInterface(playerID, IID_TechnologyManager);
|
||||
if (cmpTechnologyManager)
|
||||
value = cmpTechnologyManager.ApplyModificationsTemplate(tech_type, current_value, template);
|
||||
|
||||
let cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager);
|
||||
if (!cmpAuraManager)
|
||||
return value;
|
||||
return cmpAuraManager.ApplyTemplateModifications(tech_type, value, playerID, template);
|
||||
let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
|
||||
if (cmpModifiersManager)
|
||||
value = cmpModifiersManager.ApplyTemplateModifiers(tech_type, current_value, template, playerID);
|
||||
return value;
|
||||
}
|
||||
|
||||
Engine.RegisterGlobal("ApplyValueModificationsToEntity", ApplyValueModificationsToEntity);
|
||||
|
|
|
|||
124
binaries/data/mods/public/simulation/helpers/tests/test_MultiKeyMap.js
Executable file
124
binaries/data/mods/public/simulation/helpers/tests/test_MultiKeyMap.js
Executable file
|
|
@ -0,0 +1,124 @@
|
|||
Engine.LoadHelperScript("MultiKeyMap.js");
|
||||
|
||||
function setup_keys(map)
|
||||
{
|
||||
map.AddItem("prim_a", "item_a", null, "sec_a");
|
||||
map.AddItem("prim_a", "item_b", null, "sec_a");
|
||||
map.AddItem("prim_a", "item_c", null, "sec_a");
|
||||
map.AddItem("prim_a", "item_a", null, "sec_b");
|
||||
map.AddItem("prim_b", "item_a", null, "sec_a");
|
||||
map.AddItem("prim_c", "item_a", null, "sec_a");
|
||||
map.AddItem("prim_c", "item_a", null, 5);
|
||||
}
|
||||
|
||||
// Check that key-related operations are correct.
|
||||
function test_keys(map)
|
||||
{
|
||||
TS_ASSERT(map.items.has("prim_a"));
|
||||
TS_ASSERT(map.items.has("prim_b"));
|
||||
TS_ASSERT(map.items.has("prim_c"));
|
||||
|
||||
TS_ASSERT(map.items.get("prim_a").has("sec_a"));
|
||||
TS_ASSERT(map.items.get("prim_a").has("sec_b"));
|
||||
TS_ASSERT(!map.items.get("prim_a").has("sec_c"));
|
||||
TS_ASSERT(map.items.get("prim_b").has("sec_a"));
|
||||
TS_ASSERT(map.items.get("prim_c").has("sec_a"));
|
||||
TS_ASSERT(map.items.get("prim_c").has(5));
|
||||
|
||||
TS_ASSERT(map.items.get("prim_a").get("sec_a").length == 3);
|
||||
TS_ASSERT(map.items.get("prim_a").get("sec_b").length == 1);
|
||||
TS_ASSERT(map.items.get("prim_b").get("sec_a").length == 1);
|
||||
TS_ASSERT(map.items.get("prim_c").get("sec_a").length == 1);
|
||||
TS_ASSERT(map.items.get("prim_c").get(5).length == 1);
|
||||
|
||||
TS_ASSERT(map.GetItems("prim_a", "sec_a").length == 3);
|
||||
TS_ASSERT(map.GetItems("prim_a", "sec_b").length == 1);
|
||||
TS_ASSERT(map.GetItems("prim_b", "sec_a").length == 1);
|
||||
TS_ASSERT(map.GetItems("prim_c", "sec_a").length == 1);
|
||||
TS_ASSERT(map.GetItems("prim_c", 5).length == 1);
|
||||
|
||||
TS_ASSERT(map.HasItem("prim_a", "item_a", "sec_a"));
|
||||
TS_ASSERT(map.HasItem("prim_a", "item_b", "sec_a"));
|
||||
TS_ASSERT(map.HasItem("prim_a", "item_c", "sec_a"));
|
||||
TS_ASSERT(!map.HasItem("prim_a", "item_d", "sec_a"));
|
||||
TS_ASSERT(map.HasItem("prim_a", "item_a", "sec_b"));
|
||||
TS_ASSERT(!map.HasItem("prim_a", "item_b", "sec_b"));
|
||||
TS_ASSERT(!map.HasItem("prim_a", "item_c", "sec_b"));
|
||||
TS_ASSERT(map.HasItem("prim_b", "item_a", "sec_a"));
|
||||
TS_ASSERT(map.HasItem("prim_c", "item_a", "sec_a"));
|
||||
TS_ASSERT(map.HasAnyItem("item_a", "sec_b"));
|
||||
TS_ASSERT(map.HasAnyItem("item_b", "sec_a"));
|
||||
TS_ASSERT(!map.HasAnyItem("item_d", "sec_a"));
|
||||
TS_ASSERT(!map.HasAnyItem("item_b", "sec_b"));
|
||||
|
||||
// Adding the same item increases its count.
|
||||
map.AddItem("prim_a", "item_b", 0, "sec_a");
|
||||
TS_ASSERT_EQUALS(map.items.get("prim_a").get("sec_a").length, 3);
|
||||
TS_ASSERT_EQUALS(map.items.get("prim_a").get("sec_a").filter(item => item._ID == "item_b")[0]._count, 2);
|
||||
TS_ASSERT_EQUALS(map.GetItems("prim_a", "sec_a").length, 3);
|
||||
|
||||
// Adding without stackable doesn't invalidate caches, adding with does.
|
||||
TS_ASSERT(!map.AddItem("prim_a", "item_b", 0, "sec_a"));
|
||||
TS_ASSERT(map.AddItem("prim_a", "item_b", 0, "sec_a", true));
|
||||
|
||||
TS_ASSERT(map.items.get("prim_a").get("sec_a").filter(item => item._ID == "item_b")[0]._count == 4);
|
||||
|
||||
// Likewise removing, unless we now reach 0
|
||||
TS_ASSERT(!map.RemoveItem("prim_a", "item_b", "sec_a"));
|
||||
TS_ASSERT(map.RemoveItem("prim_a", "item_b", "sec_a", true));
|
||||
TS_ASSERT(!map.RemoveItem("prim_a", "item_b", "sec_a"));
|
||||
TS_ASSERT(map.RemoveItem("prim_a", "item_b", "sec_a"));
|
||||
|
||||
// Check that cleanup is done
|
||||
TS_ASSERT(map.items.get("prim_a").get("sec_a").length == 2);
|
||||
TS_ASSERT(map.RemoveItem("prim_a", "item_a", "sec_a"));
|
||||
TS_ASSERT(map.RemoveItem("prim_a", "item_c", "sec_a"));
|
||||
TS_ASSERT(!map.items.get("prim_a").has("sec_a"));
|
||||
TS_ASSERT(map.items.get("prim_a").has("sec_b"));
|
||||
TS_ASSERT(map.RemoveItem("prim_a", "item_a", "sec_b"));
|
||||
TS_ASSERT(!map.items.has("prim_a"));
|
||||
}
|
||||
|
||||
function setup_items(map)
|
||||
{
|
||||
map.AddItem("prim_a", "item_a", { "value": 1 }, "sec_a");
|
||||
map.AddItem("prim_a", "item_b", { "value": 2 }, "sec_a");
|
||||
map.AddItem("prim_a", "item_c", { "value": 3 }, "sec_a");
|
||||
map.AddItem("prim_a", "item_c", { "value": 1000 }, "sec_a");
|
||||
map.AddItem("prim_a", "item_a", { "value": 5 }, "sec_b");
|
||||
map.AddItem("prim_b", "item_a", { "value": 6 }, "sec_a");
|
||||
map.AddItem("prim_c", "item_a", { "value": 7 }, "sec_a");
|
||||
}
|
||||
|
||||
// Check that items returned are correct.
|
||||
function test_items(map)
|
||||
{
|
||||
let items = map.GetAllItems("sec_a");
|
||||
TS_ASSERT("prim_a" in items);
|
||||
TS_ASSERT("prim_b" in items);
|
||||
TS_ASSERT("prim_c" in items);
|
||||
let sum = 0;
|
||||
for (let key in items)
|
||||
items[key].forEach(item => (sum += item.value * item._count));
|
||||
TS_ASSERT(sum == 22);
|
||||
}
|
||||
|
||||
// Test items, and test that deserialised versions still pass test (i.e. test serialisation).
|
||||
let map = new MultiKeyMap();
|
||||
setup_keys(map);
|
||||
test_keys(map);
|
||||
|
||||
map = new MultiKeyMap();
|
||||
let map2 = new MultiKeyMap();
|
||||
setup_keys(map);
|
||||
map2.Deserialize(map.Serialize());
|
||||
test_keys(map2);
|
||||
|
||||
map = new MultiKeyMap();
|
||||
setup_items(map);
|
||||
test_items(map);
|
||||
map = new MultiKeyMap();
|
||||
map2 = new MultiKeyMap();
|
||||
setup_items(map);
|
||||
map2.Deserialize(map.Serialize());
|
||||
test_items(map2);
|
||||
|
|
@ -66,6 +66,7 @@ public:
|
|||
|
||||
VfsPaths paths;
|
||||
TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/components/tests/", L"test_*.js", paths));
|
||||
TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/helpers/tests/", L"test_*.js", paths));
|
||||
paths.push_back(VfsPath(L"simulation/components/tests/setup_test.js"));
|
||||
for (const VfsPath& path : paths)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue