Index: binaries/data/mods/public/simulation/components/ModificationsManager.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/ModificationsManager.js @@ -0,0 +1,418 @@ +function ModificationsManager() {} + +ModificationsManager.prototype.Schema = + ""; + +ModificationsManager.prototype.Serialize = function() +{ + // The cache will be affected by property reads from the GUI and other places so we shouldn't serialize it. + let ret = {}; + for (let i in this) + if (this.hasOwnProperty(i)) + ret[i] = this[i]; + + ret.cachedValues = new Map(); + return ret; +}; + +ModificationsManager.prototype.Init = function() +{ + // TODO: + // - add a way to show an icon for a given modification ID (convenient for auras) + + // The two following variables hold the list of modifications in an array. + // See GetTechModifiedProperty for what a Modification must be. It's transparent to this component. + this.globalModifications = new Map(); // Keyed by property name. + this.localModifications = new Map(); // Keyed by property name and entity ID. + + // 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. +}; + +/** + * Wrapper for AddGlobalModification, to set multiple modifications with the same ID + */ +ModificationsManager.prototype.AddGlobalModifications = function(modificationID, modificationArray) +{ + for (let propertyName in modificationArray) + this.AddGlobalModification(propertyName, modificationID, modificationArray[propertyName]); +}; + +/** + * Add a global modification. + * @param propertyName - Handle of a technology property (eg "Attack/Ranged/Pierce") + * @param modificationID - internal ID of this modification, for later removal and/or updating + * @param modification - Object describing the change. + */ +ModificationsManager.prototype.AddGlobalModification = function(propertyName, modificationID, modification) +{ + if (!this.globalModifications.get(propertyName)) + this.globalModifications.set(propertyName, []); + + if (this._AddModification(this.globalModifications.get(propertyName), propertyName, modificationID, modification)) + this._InvalidateCache(propertyName); + + this.SendGlobalModificationMessages(propertyName); +}; + +/** + * Wrapper for AddLocalModification, to set multiple modifications with the same ID + */ +ModificationsManager.prototype.AddLocalModifications = function(entity, modificationID, modificationArray) +{ + for (let propertyName in modificationArray) + this.AddLocalModification(entity, propertyName, modificationID, modificationArray[propertyName]); +}; + +/** + * Add a local modification. Local modifications are applied to a specific entity. + * @param entity - ID of the target entity + * @param propertyName - Handle of a technology property (eg "Attack/Ranged/Pierce") + * @param modificationID - internal ID of this modification, for later removal and/or updating + * @param modification - Object describing the change. + */ +ModificationsManager.prototype.AddLocalModification = function(entity, propertyName, modificationID, modification) +{ + if (!this.localModifications.get(propertyName)) + this.localModifications.set(propertyName, new Map()); + if (!this.localModifications.get(propertyName).get(entity)) + this.localModifications.get(propertyName).set(entity, []); + + if (this._AddModification(this.localModifications.get(propertyName).get(entity), propertyName, modificationID, modification)) + this._InvalidateCache(propertyName, entity); + + this.SendLocalModificationMessages(entity, propertyName); +}; + + +/** + * Wrapper for RemoveGlobalModification, to remove any modification with this ID + * Not extremely efficient but that should be fine in general. + * @param modificationID - internal ID of the modification to remove + */ +ModificationsManager.prototype.RemoveGlobalModifications = function(modificationID) +{ + for (let propertyName of this.globalModifications.keys()) + this.RemoveGlobalModification(propertyName, modificationID); +}; + +ModificationsManager.prototype.RemoveGlobalModification = function(propertyName, modificationID) +{ + if (!this.globalModifications.get(propertyName)) + return; + + if (this._RemoveModification(propertyName, modificationID)) + this._InvalidateCache(propertyName); + + this.SendGlobalModificationMessages(propertyName); +}; + +ModificationsManager.prototype.RemoveLocalModifications = function(entity, modificationID) +{ + for (let propertyName of this.localModifications.keys()) + this.RemoveLocalModification(entity, propertyName, modificationID); +}; + +ModificationsManager.prototype.RemoveLocalModification = function(entity, propertyName, modificationID) +{ + if (!this.localModifications.has(propertyName)) + return; + if (!this.localModifications.get(propertyName).has(entity)) + return; + + if (this._RemoveModification(propertyName, modificationID, entity)) + this._InvalidateCache(propertyName, entity); + + this.SendLocalModificationMessages(entity, propertyName); +}; + +/** + * Wrapper for HasGlobalModification. Probably rather inefficient as it checks all property names we have. + */ +ModificationsManager.prototype.HasAnyGlobalModificationWithID = function(modificationID) +{ + // Map doesn't implement someā€¦ + for (let propertyName of this.globalModifications.keys()) + if (this.HasGlobalModification(propertyName, modificationID)) + return true; + return false; +}; + +/** + * Check if we have a global modification. + * @param propertyName - Handle of a technology property (eg "Attack/Ranged/Pierce") + * @param modificationID - internal ID of the modification to try and find. + * @returns true if there is at least one modification with that modificationID + */ +ModificationsManager.prototype.HasGlobalModification = function(propertyName, modificationID) +{ + if (!this.globalModifications.get(propertyName)) + return false; + + return this._HasModification(this.globalModifications.get(propertyName), modificationID); +}; + +ModificationsManager.prototype.HasLocalModification = function(entity, propertyName, modificationID) +{ + if (!this.localModifications.get(propertyName)) + return false; + if (!this.localModifications.get(propertyName).get(entity)) + return false; + + return this._HasModification(this.localModifications.get(propertyName).get(entity), modificationID); +}; + +/** + * Internal use. Call HasLocalModification or HasGlobalModification instead of this helper + * @param modificationsList - the internal object for the modifications. See callers. + * @param modificationID - internal ID of the modification to add. + * @returns true if the modificationsList contains the modification ID for this property name. + */ +ModificationsManager.prototype._HasModification = function(modificationsList, modificationID) +{ + return modificationsList.some(modification => { return modification.ID === modificationID; }); +}; + +/** + * Internal use. Call AddLocalModification or AddGlobalModification instead of this helper + * @param modificationsList - the internal object for the modifications. See callers. + * @param propertyName - Handle of a technology property (eg Attack/Ranged/Pierce) + * @param modificationID - internal ID of the modification to add. + * @param modification - new modification + * @returns true if the modifications list changed, i.e. the cache is invalidated. Returns false otherwise. + */ +ModificationsManager.prototype._AddModification = function(modificationsList, propertyName, modificationID, modification) +{ + // Check whether we already have that modification + let existing = modificationsList.filter(modif => { return modif.ID == modificationID; }); + + if (existing.length > 1) + { + error("Two modifications of " + propertyName + " have the same modificationID " + modificationID); + return false; + } + + // If this is not the first time we are applying this modification, the cached value won't change + // as we don't stack modifications at the moment. + if (existing.length) + { + existing[0].count++; + return false; + } + + // Create a new modification at the end. + modificationsList.push({ "ID": modificationID, "count": 1, "effect": modification }); + return true; +}; + +/** + * Internal use. Call RemoveLocalModification or RemoveGlobalModification instead of this helper + * @returns true if the modifications list changed, i.e. the cache is invalidated. Returns false otherwise. + */ +ModificationsManager.prototype._RemoveModification = function(propertyName, modificationID, entity = undefined) +{ + let modificationsList = entity === undefined ? this.globalModifications.get(propertyName) : this.localModifications.get(propertyName).get(entity); + + let modification = modificationsList.filter(modification => { return modification.ID == modificationID; }); + if (!modification.length) + return false; + + // If we still want to apply this modification, no changes to the cache (no stacking) so return false. + if (--modification[0].count > 0) + return false; + + modificationsList = modificationsList.filter(modification => { return modification.count > 0; }); + + // Update the map, deleting entries if necessary + if (!entity) + { + if (modificationsList.length) + this.globalModifications.set(propertyName, modificationsList); + else + this.globalModifications.delete(propertyName); + } + else + { + if (modificationsList.length) + this.localModifications.get(propertyName).set(entity, modificationsList); + else + { + this.localModifications.get(propertyName).delete(entity); + if (!this.localModifications.get(propertyName).length) + this.localModifications.delete(propertyName); + } + } + + return true; +}; + + +ModificationsManager.prototype._InvalidateCache = function(propertyName, entity = undefined) +{ + if (entity && this.cachedValues.has(propertyName)) + this.cachedValues.get(propertyName).delete(entity); + else if (!entity) + this.cachedValues.delete(propertyName); +}; + +/** + * Inform entities that we have changed possibly all values affected by that property. + * It's not hugely efficient and would be nice to batch. + * @param propertyName - Handle of a technology property (eg Attack/Ranged/Pierce) that was changed. + */ +ModificationsManager.prototype.SendGlobalModificationMessages = function(propertyName) +{ + let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); + if (!cmpPlayer) + return; + + let playerID = cmpPlayer.GetPlayerID(); + if (playerId === undefined) + return; + + // 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": playerID, "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(playerID); + Engine.BroadcastMessage(MT_ValueModification, { "entities": ents, "component": propertyName.split("/")[0], "valueNames": [propertyName] }); +}; + +/** + * Inform entities that we have changed a value from a specific entity + * It's not hugely efficient and would be nice to batch. + * @param entity - ID of the target entity + * @param propertyName - Handle of a technology property (eg "Attack/Ranged/Pierce") that was changed. + */ +ModificationsManager.prototype.SendLocalModificationMessages = function(entity, propertyName) +{ + // TODO: if this is slow, it might be more efficient to keep track of who wants our info and post them directly + Engine.BroadcastMessage(MT_ValueModification, { "entities": [entity], "component": propertyName.split("/")[0], "valueNames": [propertyName] }); +}; + +/** + * Caching system in front of GetTechModifiedProperty() and such, as calling that every time is quite slow. + * This recomputes lazily. + * @param propertyName - Handle of a technology property (eg Attack/Ranged/Pierce) that was changed. + * @param origValue - template/raw/before modifications 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 origValue after the modifications + */ +ModificationsManager.prototype.ApplyModifications = function(propertyName, origValue, ent) +{ + let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); + if (!cmpIdentity) + return origValue; + + let originalValue = origValue; + // Some code bits pass an array instead of a single value + // Those bits should probably be changed, but it is supported in the meantime. + if (Array.isArray(origValue)) + value = origValue.slice(); + else if (typeof origValue == "object") + { + error("ModificationsManager ApplyModifications() called with an object instead of a number."); + return undefined; + } + + if (this.cachedValues.has(propertyName) && + this.cachedValues.get(propertyName).has(ent) && + this.cachedValues.get(propertyName).get(ent).has(originalValue)) + return this.cachedValues.get(propertyName).get(ent).get(originalValue); + + // Initialise the cache + if (!this.cachedValues.get(propertyName)) + this.cachedValues.set(propertyName, new Map()); + if (!this.cachedValues.get(propertyName).get(ent)) + this.cachedValues.get(propertyName).set(ent, new Map()); + if (!this.cachedValues.get(propertyName).get(ent).get(originalValue)) + this.cachedValues.get(propertyName).get(ent).set(originalValue, originalValue); + + let entityCache = this.cachedValues.get(propertyName).get(ent); + let temporaryValue = entityCache.get(originalValue); + + // Apply global modifications first, local modifications are treated as higher priority. + if (this.globalModifications.has(propertyName)) + temporaryValue = this._FetchModifiedProperty(this.globalModifications.get(propertyName), cmpIdentity.GetClassesList(), propertyName, temporaryValue); + + if (this.localModifications.has(propertyName) && this.localModifications.get(propertyName).has(ent)) + temporaryValue = this._FetchModifiedProperty(this.localModifications.get(propertyName).get(ent), cmpIdentity.GetClassesList(), propertyName, temporaryValue); + + entityCache.set(originalValue, temporaryValue); + return temporaryValue; +}; + +/** + * Alternative version of ApplyModifications, applies to templates instead of entities + * Only needs to handle global modifications + */ +ModificationsManager.prototype.ApplyModificationsTemplate = function(propertyName, originalValue, template) +{ + if (!template || !template.Identity || !this.globalModifications.has(propertyName)) + return originalValue; + + return this._FetchModifiedProperty(this.globalModifications.get(propertyName), GetIdentityClasses(template.Identity), propertyName, originalValue); +}; + +/** + * Internal use only. + * @returns referenceValue after modifications + */ +ModificationsManager.prototype._FetchModifiedProperty = function(modificationsList, classesList, propertyName, referenceValue) +{ + let modifications = { [propertyName]: [] }; + for (let modification of modificationsList) + modifications[propertyName].push(modification.effect); + + return GetTechModifiedProperty(modifications, classesList, propertyName, referenceValue); +}; + +/** + * Handle modifications when a unit is created. + */ +ModificationsManager.prototype.OnGlobalOwnershipChanged = function(msg) +{ + // Only do this for created entities, not captures/conversions as we want + // captured/converted entities to retain their former technologies. + if (msg.from != -1) + return; + + let playerID = Engine.QueryInterface(this.entity, IID_Player).GetPlayerID(); + if (msg.to != playerID) + return; + + let cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); + if (!cmpIdentity) + return; + + let classes = cmpIdentity.GetClassesList(); + + let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetCurrentTemplateName(msg.entity); + + // Local modifications will be added by the relevant components, so no need to check for them here. + let modifiedComponents = {}; + for (let property of this.globalModifications) + { + // We only need to find one one tech per component for a match + let modifications = property[1]; + let component = property[0].split("/")[0]; + for (let modif of modifications) + { + if (DoesModificationApply(modif.effect, classes)) + { + if (!modifiedComponents[component]) + modifiedComponents[component] = []; + modifiedComponents[component].push(property[0]); + } + } + } + + // Send message(s) to the entity so it knows about researched techs + for (let component in modifiedComponents) + Engine.PostMessage(msg.entity, MT_ValueModification, { "entities": [msg.entity], "component": component, "valueNames": modifiedComponents[component] }); +}; + +Engine.RegisterComponentType(IID_ModificationsManager, "ModificationsManager", ModificationsManager); Index: binaries/data/mods/public/simulation/components/TechnologyManager.js =================================================================== --- binaries/data/mods/public/simulation/components/TechnologyManager.js +++ binaries/data/mods/public/simulation/components/TechnologyManager.js @@ -3,21 +3,6 @@ TechnologyManager.prototype.Schema = ""; -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 @@ // 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/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 @@ 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 @@ } } } - - this.clearModificationCache(msg.entity); } }; @@ -266,23 +214,16 @@ var modifiedComponents = {}; this.researchedTechs.add(tech); + // store the modifications in an easy to access structure let template = TechnologyTemplates.Get(tech); if (template.modifications) { + let cmpModificationsManager = Engine.QueryInterface(this.entity, IID_ModificationsManager); 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]) + cmpModificationsManager.AddGlobalModification(modifierPath, "tech/" + tech, modifier); } if (template.replaces && template.replaces.length > 0) @@ -323,62 +264,6 @@ // 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); }; /** Index: binaries/data/mods/public/simulation/components/interfaces/ModificationsManager.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/interfaces/ModificationsManager.js @@ -0,0 +1 @@ +Engine.RegisterInterface("ModificationsManager"); Index: binaries/data/mods/public/simulation/components/tests/test_Attack.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Attack.js +++ binaries/data/mods/public/simulation/components/tests/test_Attack.js @@ -6,6 +6,7 @@ Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Index: binaries/data/mods/public/simulation/components/tests/test_Auras.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Auras.js +++ binaries/data/mods/public/simulation/components/tests/test_Auras.js @@ -4,6 +4,7 @@ Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/RangeOverlayManager.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); Engine.LoadComponentScript("Auras.js"); Engine.LoadComponentScript("AuraManager.js"); Index: binaries/data/mods/public/simulation/components/tests/test_Capturable.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Capturable.js +++ binaries/data/mods/public/simulation/components/tests/test_Capturable.js @@ -5,7 +5,7 @@ Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); -Engine.LoadComponentScript("interfaces/TechnologyManager.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); Engine.LoadComponentScript("interfaces/TerritoryDecay.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("Capturable.js"); Index: binaries/data/mods/public/simulation/components/tests/test_Damage.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Damage.js +++ binaries/data/mods/public/simulation/components/tests/test_Damage.js @@ -12,7 +12,7 @@ Engine.LoadComponentScript("interfaces/Loot.js"); Engine.LoadComponentScript("interfaces/Player.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); -Engine.LoadComponentScript("interfaces/TechnologyManager.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("Attack.js"); Engine.LoadComponentScript("Damage.js"); Index: binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js +++ binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js @@ -7,7 +7,7 @@ Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/ProductionQueue.js"); -Engine.LoadComponentScript("interfaces/TechnologyManager.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Index: binaries/data/mods/public/simulation/components/tests/test_ModificationsManager.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/tests/test_ModificationsManager.js @@ -0,0 +1,91 @@ +Engine.LoadComponentScript("interfaces/AuraManager.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); +Engine.LoadComponentScript("ModificationsManager.js"); +Engine.LoadHelperScript("Player.js"); +Engine.LoadHelperScript("ValueModification.js"); + +let cmpModificationsManager = ConstructComponent(2, "ModificationsManager", {}); + +cmpModificationsManager.Init(); + +AddMock(2, IID_AuraManager, { + "ApplyModifications": function(a, value, b) { return value; }, + "ApplyTemplateModifications": function(a, value, b) { return value; }, +}); +AddMock(SYSTEM_ENTITY, IID_PlayerManager, { + "GetPlayerByID": () => 2, +}); +// create ownership, set to "1" for any entity used below, +// otherwise QueryOwnerInterface fails +let entitiesToTest = [5, 6, 7, 8]; +for (let ent of entitiesToTest) + AddMock(ent, IID_Ownership, { + "GetOwner": () => 1 + }); + +// hack + +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";} +}); + +cmpModificationsManager.AddGlobalModification("Test_A", "Test_A_0", { "affects": ["Structure"], "add": 10 }); +cmpModificationsManager.AddGlobalModification("Test_A", "Test_A_1", { "affects": ["Infantry"], "add": 5 }); +cmpModificationsManager.AddGlobalModification("Test_A", "Test_A_2", { "affects": ["Unit"], "add": 3 }); + +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); + +cmpModificationsManager.RemoveGlobalModifications("Test_A_0"); + +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 5), 5); + +cmpModificationsManager.AddGlobalModifications("Test_A_0", { + "Test_A": { "affects": ["Structure"], "add": 10 }, + "Test_B": { "affects": ["Structure"], "add": 8 }, +}); + +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. +cmpModificationsManager.AddLocalModification(5, "Test_C", "Test_C_0", { "affects": ["Structure"], "add": 10 }); +cmpModificationsManager.AddLocalModification(5, "Test_C", "Test_C_1", { "affects": ["Unit"], "add": 5 }); + +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, 5), 15); + +// test that local modifications are indeed applied after global managers +cmpModificationsManager.AddLocalModification(5, "Test_C", "Test_C_2", { "affects": ["Structure"], "replace": 0 }); +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, 5), 0); + +TS_ASSERT(!cmpModificationsManager.HasAnyGlobalModificationWithID("Test_C_3")); + +// check that things still work properly if we change global modifications +cmpModificationsManager.AddGlobalModification("Test_C", "Test_C_3", { "affects": ["Structure"], "add": 10 }); +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, 5), 0); + +TS_ASSERT(cmpModificationsManager.HasAnyGlobalModificationWithID("Test_C_3")); +TS_ASSERT(cmpModificationsManager.HasGlobalModification("Test_C", "Test_C_3")); +TS_ASSERT(cmpModificationsManager.HasLocalModification(5, "Test_C", "Test_C_2")); + +// test removal +cmpModificationsManager.RemoveLocalModification(5, "Test_C", "Test_C_2"); +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, 5), 25); + +TS_ASSERT(cmpModificationsManager.HasGlobalModification("Test_C", "Test_C_3")); +TS_ASSERT(!cmpModificationsManager.HasLocalModification(5, "Test_C", "Test_C_2")); + +// TODO: test ownership changes, updating. Index: binaries/data/mods/public/simulation/components/tests/test_Pack.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Pack.js +++ binaries/data/mods/public/simulation/components/tests/test_Pack.js @@ -2,7 +2,7 @@ Engine.LoadHelperScript("Sound.js"); Engine.LoadHelperScript("Transform.js"); Engine.LoadHelperScript("ValueModification.js"); -Engine.LoadComponentScript("interfaces/TechnologyManager.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Index: binaries/data/mods/public/simulation/components/tests/test_Player.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Player.js +++ binaries/data/mods/public/simulation/components/tests/test_Player.js @@ -17,7 +17,7 @@ Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/Player.js"); -Engine.LoadComponentScript("interfaces/TechnologyManager.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); Engine.LoadComponentScript("Player.js"); var cmpPlayer = ConstructComponent(10, "Player", { Index: binaries/data/mods/public/simulation/components/tests/test_Technologies_effects.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/tests/test_Technologies_effects.js @@ -0,0 +1,45 @@ +// TODO: Move this to a folder of tests for GlobalScripts (once one is created) + +// This tests the GetTechModifiedProperty function. + +let add = { + "Test_A": [{ "add": 10, "affects": "Unit" }] +}; + +let add_add = { + "Test_A": [{ "add": 10, "affects": "Unit" }, { "add": 5, "affects": "Unit" }] +}; + +let add_mul_add = { + "Test_A": [{ "add": 10, "affects": "Unit" }, { "multiply": 2, "affects": "Unit" }, { "add": 5, "affects": "Unit" }] +}; + +let add_replace = { + "Test_A": [{ "add": 10, "affects": "Unit" }, { "replace": 10, "affects": "Unit" }] +}; + +let replace_add = { + "Test_A": [{ "replace": 10, "affects": "Unit" }, { "add": 10, "affects": "Unit" }] +}; + +let replace_replace = { + "Test_A": [{ "replace": 10, "affects": "Unit" }, { "replace": 30, "affects": "Unit" }] +}; + +let replace_nonnum = { + "Test_A": [{ "replace": "alpha", "affects": "Unit" }] +}; + +TS_ASSERT_EQUALS(GetTechModifiedProperty(add, "Unit", "Test_A", 5), 15); +TS_ASSERT_EQUALS(GetTechModifiedProperty(add_add, "Unit", "Test_A", 5), 20); +TS_ASSERT_EQUALS(GetTechModifiedProperty(add_add, "Other", "Test_A", 5), 5); + +// Technologies work by multiplying then adding all. +TS_ASSERT_EQUALS(GetTechModifiedProperty(add_mul_add, "Unit", "Test_A", 5), 25); + +TS_ASSERT_EQUALS(GetTechModifiedProperty(add_replace, "Unit", "Test_A", 5), 10); + +// Only the first replace is taken into account +TS_ASSERT_EQUALS(GetTechModifiedProperty(replace_replace, "Unit", "Test_A", 5), 10); + +TS_ASSERT_EQUALS(GetTechModifiedProperty(replace_nonnum, "Unit", "Test_A", "beta"), "alpha"); Index: binaries/data/mods/public/simulation/components/tests/test_Technologies_reqs.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Technologies_reqs.js +++ binaries/data/mods/public/simulation/components/tests/test_Technologies_reqs.js @@ -55,14 +55,14 @@ 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); Index: binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js +++ binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js @@ -14,7 +14,7 @@ } }; 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/ModificationsManager.js"); // Provides `IID_ModificationsManager`, used below. Engine.LoadComponentScript("interfaces/Timer.js"); // Provides `IID_Timer`, used below. // What we're testing: @@ -96,7 +96,7 @@ "GetTimeMultiplier": () => 1.0, // Called in components/Upgrade.js::GetUpgradeTime(). "TrySubtractResources": () => true // Called in components/Upgrade.js::Upgrade(). }); -AddMock(10, IID_TechnologyManager, { +AddMock(10, IID_ModificationsManager, { "ApplyModificationsTemplate": (valueName, curValue, template) => { // Called in helpers/ValueModification.js::ApplyValueModificationsToTemplate() // as part of Tests T2 and T5 below. Index: binaries/data/mods/public/simulation/components/tests/test_ValueModificationHelper.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_ValueModificationHelper.js +++ binaries/data/mods/public/simulation/components/tests/test_ValueModificationHelper.js @@ -2,14 +2,14 @@ Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/Player.js"); -Engine.LoadComponentScript("interfaces/TechnologyManager.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); let player = 1; let playerEnt = 10; let ownedEnt = 60; let techKey = "Attack/BigAttack"; -AddMock(playerEnt, IID_TechnologyManager, { +AddMock(playerEnt, IID_ModificationsManager, { "ApplyModifications": (key, val, ent) => { if (key != techKey) return val; Index: binaries/data/mods/public/simulation/components/tests/test_VisionSharing.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_VisionSharing.js +++ binaries/data/mods/public/simulation/components/tests/test_VisionSharing.js @@ -3,6 +3,7 @@ Engine.LoadHelperScript("Commands.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/VisionSharing.js"); @@ -118,6 +119,9 @@ AddMock(14, IID_TechnologyManager, { "CanProduce": entity => false, +}); + +AddMock(14, IID_ModificationsManager, { "ApplyModificationsTemplate": (valueName, curValue, template) => curValue }); @@ -127,8 +131,12 @@ AddMock(14, IID_TechnologyManager, { "CanProduce": entity => entity == "special/spy", +}); + +AddMock(14, IID_ModificationsManager, { "ApplyModificationsTemplate": (valueName, curValue, template) => curValue }); + AddMock(14, IID_Player, { "GetSpyCostMultiplier": () => 1, "TrySubtractResources": costs => false Index: binaries/data/mods/public/simulation/helpers/ValueModification.js =================================================================== --- binaries/data/mods/public/simulation/helpers/ValueModification.js +++ binaries/data/mods/public/simulation/helpers/ValueModification.js @@ -3,11 +3,12 @@ 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 cmpModificationsManager = Engine.QueryInterface(entity, IID_Player) ? + Engine.QueryInterface(entity, IID_ModificationsManager) : QueryOwnerInterface(entity, IID_ModificationsManager); + if (cmpModificationsManager) + value = cmpModificationsManager.ApplyModifications(tech_type, current_value, entity); let cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager); if (!cmpAuraManager) @@ -18,9 +19,9 @@ 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 cmpModificationsManager = QueryPlayerIDInterface(playerID, IID_ModificationsManager); + if (cmpModificationsManager) + value = cmpModificationsManager.ApplyModificationsTemplate(tech_type, current_value, template); let cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager); if (!cmpAuraManager) Index: binaries/data/mods/public/simulation/templates/special/player.xml =================================================================== --- binaries/data/mods/public/simulation/templates/special/player.xml +++ binaries/data/mods/public/simulation/templates/special/player.xml @@ -34,9 +34,6 @@ 2 2 5 - 4 - 2 - 1 @@ -59,8 +56,8 @@ Player Player - true + Index: binaries/data/mods/public/simulation/templates/special/player/player.xml =================================================================== --- binaries/data/mods/public/simulation/templates/special/player/player.xml +++ binaries/data/mods/public/simulation/templates/special/player/player.xml @@ -61,6 +61,7 @@ Player true +