Index: ps/trunk/binaries/data/mods/public/simulation/components/AuraManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/AuraManager.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/AuraManager.js (nonexistent) @@ -1,275 +0,0 @@ -function AuraManager() {} - -AuraManager.prototype.Schema = - ""; - -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); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/AuraManager.js ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/AuraManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/AuraManager.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/AuraManager.js (nonexistent) @@ -1 +0,0 @@ -Engine.RegisterInterface("AuraManager"); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/AuraManager.js ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Technologies.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Technologies.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Technologies.js (nonexistent) @@ -1,583 +0,0 @@ -// TODO: Move this to a folder of tests for GlobalScripts (once one is created) - -// No requirements set in template -let template = {}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), []); - -/** - * First, the basics: - */ - -// Technology Requirement -template.requirements = { "tech": "expected_tech" }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["expected_tech"] }]); - -// Entity Requirement: Count of entities matching given class -template.requirements = { "entity": { "class": "Village", "number": 5 } }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "entities": [{ "class": "Village", "number": 5, "check": "count" }] }]); - -// Entity Requirement: Count of entities matching given class -template.requirements = { "entity": { "class": "Village", "numberOfTypes": 5 } }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "entities": [{ "class": "Village", "number": 5, "check": "variants" }] }]); - -// Single `civ` -template.requirements = { "civ": "athen" }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), []); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), false); - -// Single `notciv` -template.requirements = { "notciv": "athen" }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), []); - - -/** - * Basic `all`s: - */ - -// Multiple techs -template.requirements = { "all": [{ "tech": "tech_A" }, { "tech": "tech_B" }, { "tech": "tech_C" }] }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["tech_A", "tech_B", "tech_C"] }]); - -// Multiple entity definitions -template.requirements = { - "all": [ - { "entity": { "class": "class_A", "number": 5 } }, - { "entity": { "class": "class_B", "number": 5 } }, - { "entity": { "class": "class_C", "number": 5 } } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), - [{ "entities": [{ "class": "class_A", "number": 5, "check": "count" }, { "class": "class_B", "number": 5, "check": "count" }, { "class": "class_C", "number": 5, "check": "count" }] }]); - -// A `tech` and an `entity` -template.requirements = { "all": [{ "tech": "tech_A" }, { "entity": { "class": "class_B", "number": 5, "check": "count" } }] }; -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"}] }; -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"}] }; -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); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civ_D"), []); - -// A `civ` with a tech/entity -template.requirements = { "all": [{ "civ": "athen" }, { "tech": "expected_tech" }] }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["expected_tech"] }]); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), false); - -template.requirements = { "all": [{ "civ": "athen" }, { "entity": { "class": "Village", "number": 5 } }] }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "entities": [{ "class": "Village", "number": 5, "check": "count" }] }]); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), false); - -template.requirements = { "all": [{ "civ": "athen" }, { "entity": { "class": "Village", "numberOfTypes": 5 } }] }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "entities": [{ "class": "Village", "number": 5, "check": "variants" }] }]); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), false); - -// A `notciv` with a tech/entity -template.requirements = { "all": [{ "notciv": "athen" }, { "tech": "expected_tech" }] }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), [{ "techs": ["expected_tech"] }]); - -template.requirements = { "all": [{ "notciv": "athen" }, { "entity": { "class": "Village", "number": 5 } }] }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), [{ "entities": [{ "class": "Village", "number": 5, "check": "count" }] }]); - -template.requirements = { "all": [{ "notciv": "athen" }, { "entity": { "class": "Village", "numberOfTypes": 5 } }] }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), [{ "entities": [{ "class": "Village", "number": 5, "check": "variants" }] }]); - - -/** - * Basic `any`s: - */ - -// Multiple techs -template.requirements = { "any": [{ "tech": "tech_A" }, { "tech": "tech_B" }, { "tech": "tech_C" }] }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["tech_A"] }, { "techs": ["tech_B"] }, { "techs": ["tech_C"] }]); - -// Multiple entity definitions -template.requirements = { - "any": [ - { "entity": { "class": "class_A", "number": 5 } }, - { "entity": { "class": "class_B", "number": 5 } }, - { "entity": { "class": "class_C", "number": 5 } } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [ - { "entities": [{ "class": "class_A", "number": 5, "check": "count" }] }, - { "entities": [{ "class": "class_B", "number": 5, "check": "count" }] }, - { "entities": [{ "class": "class_C", "number": 5, "check": "count" }] } -]); - -// A tech or an entity -template.requirements = { "any": [{ "tech": "tech_A" }, { "entity": { "class": "class_B", "number": 5, "check": "count" } }] }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["tech_A"] }, { "entities": [{ "class": "class_B", "number": 5, "check": "count" }] }]); - -// Multiple `civ`s -template.requirements = { "any": [{ "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 = { "any": [{ "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); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civ_D"), []); - -// A `civ` or a tech/entity -template.requirements = { "any": [{ "civ": "athen" }, { "tech": "expected_tech" }] }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), []); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), [{ "techs": ["expected_tech"] }]); - -template.requirements = { "any": [{ "civ": "athen" }, { "entity": { "class": "Village", "number": 5 } }] }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), []); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), [{ "entities": [{ "class": "Village", "number": 5, "check": "count" }] }]); - -template.requirements = { "any": [{ "civ": "athen" }, { "entity": { "class": "Village", "numberOfTypes": 5 } }] }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), []); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), [{ "entities": [{ "class": "Village", "number": 5, "check": "variants" }] }]); - -// A `notciv` or a tech -template.requirements = { "any": [{ "notciv": "athen" }, { "tech": "expected_tech" }] }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), [{ "techs": ["expected_tech"] }]); - -template.requirements = { "any": [{ "notciv": "athen" }, { "entity": { "class": "Village", "number": 5 } }] }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), [{ "entities": [{ "class": "Village", "number": 5, "check": "count" }] }]); - -template.requirements = { "any": [{ "notciv": "athen" }, { "entity": { "class": "Village", "numberOfTypes": 5 } }] }; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), [{ "entities": [{ "class": "Village", "number": 5, "check": "variants" }] }]); - - -/** - * Complicated `all`s, part 1 - an `all` inside an `all`: - */ - -// Techs -template.requirements = { - "all": [ - { "all": [{ "tech": "tech_A" }, { "tech": "tech_B" }] }, - { "all": [{ "tech": "tech_C" }, { "tech": "tech_D" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["tech_A", "tech_B", "tech_C", "tech_D"] }]); - -// Techs and entities -template.requirements = { - "all": [ - { "all": [{ "tech": "tech_A" }, { "entity": { "class": "class_A", "number": 5 } }] }, - { "all": [{ "entity": { "class": "class_B", "numberOfTypes": 5 } }, { "tech": "tech_B" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ - "techs": ["tech_A", "tech_B"], - "entities": [{ "class": "class_A", "number": 5, "check": "count" }, { "class": "class_B", "number": 5, "check": "variants" }] -}]); - -// Two `civ`s, without and with a tech -template.requirements = { - "all": [ - { "all": [{ "civ": "athen" }, { "civ": "spart" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), []); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), false); - -template.requirements = { - "all": [ - { "tech": "required_tech" }, - { "all": [{ "civ": "athen" }, { "civ": "spart" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["required_tech"] }]); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), false); - -// Two `notciv`s, without and with a tech -template.requirements = { - "all": [ - { "all": [{ "notciv": "athen" }, { "notciv": "spart" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), []); - -template.requirements = { - "all": [ - { "tech": "required_tech" }, - { "all": [{ "notciv": "athen" }, { "notciv": "spart" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), [{ "techs": ["required_tech"] }]); - -// Inner `all` has a tech and a `civ`/`notciv` -template.requirements = { - "all": [ - { "all": [{ "tech": "tech_A" }, { "civ": "maur" }] }, - { "tech": "tech_B" } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["tech_B"] }]); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), [{ "techs": ["tech_A", "tech_B"] }]); - -template.requirements = { - "all": [ - { "all": [{ "tech": "tech_A" }, { "notciv": "maur" }] }, - { "tech": "tech_B" } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["tech_A", "tech_B"] }]); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), [{ "techs": ["tech_B"] }]); - - -/** - * Complicated `all`s, part 2 - an `any` inside an `all`: - */ - -// Techs -template.requirements = { - "all": [ - { "any": [{ "tech": "tech_A" }, { "tech": "tech_B" }] }, - { "any": [{ "tech": "tech_C" }, { "tech": "tech_D" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [ - { "techs": ["tech_A", "tech_C"] }, - { "techs": ["tech_A", "tech_D"] }, - { "techs": ["tech_B", "tech_C"] }, - { "techs": ["tech_B", "tech_D"] } -]); - -// Techs and entities -template.requirements = { - "all": [ - { "any": [{ "tech": "tech_A" }, { "entity": { "class": "class_A", "number": 5 } }] }, - { "any": [{ "entity": { "class": "class_B", "numberOfTypes": 5 } }, { "tech": "tech_B" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [ - { "techs": ["tech_A"], "entities": [{ "class": "class_B", "number": 5, "check": "variants" }] }, - { "techs": ["tech_A", "tech_B"] }, - { "entities": [{ "class": "class_A", "number": 5, "check": "count" }, { "class": "class_B", "number": 5, "check": "variants" }] }, - { "entities": [{ "class": "class_A", "number": 5, "check": "count" }], "techs": ["tech_B"] } -]); - -// Two `civ`s, without and with a tech -template.requirements = { - "all": [ - { "any": [{ "civ": "athen" }, { "civ": "spart" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), []); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), false); - -template.requirements = { - "all": [ - { "tech": "required_tech" }, - { "any": [{ "civ": "athen" }, { "civ": "spart" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["required_tech"] }]); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), false); - -// Two `notciv`s, without and with a tech -template.requirements = { - "all": [ - { "any": [{ "notciv": "athen" }, { "notciv": "spart" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), []); - -template.requirements = { - "all": [ - { "tech": "required_tech" }, - { "any": [{ "notciv": "athen" }, { "notciv": "spart" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), [{ "techs": ["required_tech"] }]); - - -/** - * Complicated `any`s, part 1 - an `all` inside an `any`: - */ - -// Techs -template.requirements = { - "any": [ - { "all": [{ "tech": "tech_A" }, { "tech": "tech_B" }] }, - { "all": [{ "tech": "tech_C" }, { "tech": "tech_D" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [ - { "techs": ["tech_A", "tech_B"] }, - { "techs": ["tech_C", "tech_D"] } -]); - -// Techs and entities -template.requirements = { - "any": [ - { "all": [{ "tech": "tech_A" }, { "entity": { "class": "class_A", "number": 5 } }] }, - { "all": [{ "entity": { "class": "class_B", "numberOfTypes": 5 } }, { "tech": "tech_B" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [ - { "techs": ["tech_A"], "entities": [{ "class": "class_A", "number": 5, "check": "count" }] }, - { "entities": [{ "class": "class_B", "number": 5, "check": "variants" }], "techs": ["tech_B"] } -]); - -// Two `civ`s, without and with a tech -template.requirements = { - "any": [ - { "all": [{ "civ": "athen" }, { "civ": "spart" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), []); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), false); - -template.requirements = { - "any": [ - { "tech": "required_tech" }, - { "all": [{ "civ": "athen" }, { "civ": "spart" }] } - ] -}; -// Note: these requirements don't really make sense, as the `any` makes the `civ`s in the the inner `all` irrelevant. -// We test it anyway as a precursor to later tests. -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["required_tech"] }]); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), [{ "techs": ["required_tech"] }]); - -// Two `notciv`s, without and with a tech -template.requirements = { - "any": [ - { "all": [{ "notciv": "athen" }, { "notciv": "spart" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), []); - -template.requirements = { - "any": [ - { "tech": "required_tech" }, - { "all": [{ "notciv": "athen" }, { "notciv": "spart" }] } - ] -}; -// Note: these requirements have a result that might seen unexpected at first glance. -// This is because the `notciv`s are rendered irrelevant by the `any`, and they have nothing else to operate on. -// We test it anyway as a precursor for later tests. -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["required_tech"] }]); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), [{ "techs": ["required_tech"] }]); - -// Inner `all` has a tech and a `civ`/`notciv` -template.requirements = { - "any": [ - { "all": [{ "civ": "civA" }, { "tech": "tech1" }] }, - { "tech": "tech2" } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civA"), [{ "techs": ["tech1"] }, { "techs": ["tech2"] }]); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civB"), [{ "techs": ["tech2"] }]); - -template.requirements = { - "any": [ - { "all": [{ "notciv": "civA" }, { "tech": "tech1" }] }, - { "tech": "tech2" } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civA"), [{ "techs": ["tech2"] }]); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civB"), [{ "techs": ["tech1"] }, { "techs": ["tech2"] }]); - - -/** - * Complicated `any`s, part 2 - an `any` inside an `any`: - */ - -// Techs -template.requirements = { - "any": [ - { "any": [{ "tech": "tech_A" }, { "tech": "tech_B" }] }, - { "any": [{ "tech": "tech_C" }, { "tech": "tech_D" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [ - { "techs": ["tech_A"] }, - { "techs": ["tech_B"] }, - { "techs": ["tech_C"] }, - { "techs": ["tech_D"] } -]); - -// Techs and entities -template.requirements = { - "any": [ - { "any": [{ "tech": "tech_A" }, { "entity": { "class": "class_A", "number": 5 } }] }, - { "any": [{ "entity": { "class": "class_B", "numberOfTypes": 5 } }, { "tech": "tech_B" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [ - { "techs": ["tech_A"] }, - { "entities": [{ "class": "class_A", "number": 5, "check": "count" }] }, - { "entities": [{ "class": "class_B", "number": 5, "check": "variants" }] }, - { "techs": ["tech_B"] } -]); - -// Two `civ`s, without and with a tech -template.requirements = { - "any": [ - { "any": [{ "civ": "athen" }, { "civ": "spart" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), []); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), false); - -template.requirements = { - "any": [ - { "tech": "required_tech" }, - { "any": [{ "civ": "athen" }, { "civ": "spart" }] } - ] -}; -// These requirements may not make sense, as the `civ`s are unable to restrict the requirements due to the outer `any` -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["required_tech"] }]); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), [{ "techs": ["required_tech"] }]); - -// Two `notciv`s, without and with a tech -template.requirements = { - "any": [ - { "any": [{ "notciv": "athen" }, { "notciv": "spart" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), []); - -template.requirements = { - "any": [ - { "tech": "required_tech" }, - { "any": [{ "notciv": "athen" }, { "notciv": "spart" }] } - ] -}; -// These requirements may not make sense, as the `notciv`s are made irrelevant by the outer `any` -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["required_tech"] }]); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), [{ "techs": ["required_tech"] }]); - - -/** - * Further tests - */ - -template.requirements = { - "all": [ - { "tech": "tech1" }, - { "any": [{ "civ": "civA" }, { "civ": "civB" }] }, - { "notciv": "civC" } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civA"), [{ "techs": ["tech1"] }]); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civC"), false); - -template.requirements = { - "any": [ - { "all": [{ "civ": "civA" }, { "tech": "tech1" }] }, - { "all": [{ "civ": "civB" }, { "tech": "tech2" }] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civA"), [{ "techs": ["tech1"] }]); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civB"), [{ "techs": ["tech2"] }]); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civC"), false); - -template.requirements = { - "any": [ - { "all": [{ "civ": "civA" }, { "tech": "tech1" }] }, - { "all": [ - { "any": [{ "civ": "civB" }, { "civ": "civC" }] }, - { "tech": "tech2" } - ] } - ] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civA"), [{ "techs": ["tech1"] }]); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civC"), [{ "techs": ["tech2"] }]); -TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civD"), false); - -// Test DeriveModificationsFromTech -template = { - "modifications": [{ - "value": "ResourceGatherer/Rates/food.grain", - "multiply": 15, - "affects": "Spear Sword" - }, - { - "value": "ResourceGatherer/Rates/food.meat", - "multiply": 10 - }], - "affects": ["Female", "CitizenSoldier Melee"] -}; -let techMods = { - "ResourceGatherer/Rates/food.grain": [{ - "affects": [ - ["Female", "Spear", "Sword"], - ["CitizenSoldier", "Melee", "Spear", "Sword"] - ], - "multiply": 15 - }], - "ResourceGatherer/Rates/food.meat": [{ - "affects": [ - ["Female"], - ["CitizenSoldier", "Melee"] - ], - "multiply": 10 - }] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveModificationsFromTech(template), techMods); - -template = { - "modifications": [{ - "value": "ResourceGatherer/Rates/food.grain", - "multiply": 15, - "affects": "Spear" - }, - { - "value": "ResourceGatherer/Rates/food.grain", - "multiply": 15, - "affects": "Sword" - }, - { - "value": "ResourceGatherer/Rates/food.meat", - "multiply": 10 - }], - "affects": ["Female", "CitizenSoldier Melee"] -}; -techMods = { - "ResourceGatherer/Rates/food.grain": [{ - "affects": [ - ["Female", "Spear"], - ["CitizenSoldier", "Melee", "Spear"] - ], - "multiply": 15 - }, - { - "affects": [ - ["Female", "Sword"], - ["CitizenSoldier", "Melee", "Sword"] - ], - "multiply": 15 - }], - "ResourceGatherer/Rates/food.meat": [{ - "affects": [ - ["Female"], - ["CitizenSoldier", "Melee"] - ], - "multiply": 10 - }] -}; -TS_ASSERT_UNEVAL_EQUALS(DeriveModificationsFromTech(template), techMods); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Technologies.js ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_AuraManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_AuraManager.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_AuraManager.js (nonexistent) @@ -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); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_AuraManager.js ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/globalscripts/Technologies.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Technologies.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/globalscripts/Technologies.js (revision 22767) @@ -1,386 +1,382 @@ /** * This file contains shared logic for applying tech modifications in GUI, AI, * and simulation scripts. As such it must be fully deterministic and not store * any global state, but each context should do its own caching as needed. * Also it cannot directly access the simulation and requires data passed to it. */ /** * 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; for (let modification of modifications) { if (!DoesModificationApply(modification, classes)) continue; if (modification.replace !== undefined) return modification.replace; if (modification.multiply) multiply *= modification.multiply; 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; } /** * Derives modifications (to be applied to entities) from a given technology. * * @param {Object} techTemplate - The technology template to derive the modifications from. * @return {Object} containing the relevant modifications. */ function DeriveModificationsFromTech(techTemplate) { if (!techTemplate.modifications) return {}; let techMods = {}; let techAffects = []; if (techTemplate.affects && techTemplate.affects.length) for (let affected of techTemplate.affects) techAffects.push(affected.split(/\s+/)); else techAffects.push([]); for (let mod of techTemplate.modifications) { let affects = techAffects.slice(); if (mod.affects) { let specAffects = mod.affects.split(/\s+/); for (let a in affects) affects[a] = affects[a].concat(specAffects); } let newModifier = { "affects": affects }; for (let idx in mod) if (idx !== "value" && idx !== "affects") newModifier[idx] = mod[idx]; if (!techMods[mod.value]) techMods[mod.value] = []; techMods[mod.value].push(newModifier); } return techMods; } /** * Derives modifications (to be applied to entities) from a provided array * of technology template data. * * @param {array} techsDataArray * @return {object} containing the combined relevant modifications of all * the technologies. */ function DeriveModificationsFromTechnologies(techsDataArray) { let derivedModifiers = {}; for (let technology of techsDataArray) { if (!technology.reqs) continue; let modifiers = DeriveModificationsFromTech(technology); for (let modPath in modifiers) { if (!derivedModifiers[modPath]) derivedModifiers[modPath] = []; derivedModifiers[modPath] = derivedModifiers[modPath].concat(modifiers[modPath]); } } return derivedModifiers; } /** * Returns whether the given modification applies to the entity containing the given class list */ function DoesModificationApply(modification, classes) { return MatchesClassList(classes, modification.affects); } /** * Derives the technology requirements from a given technology template. * Takes into account the `supersedes` attribute. * * @param {object} template - The template object. Loading of the template must have already occured. * * @return Derived technology requirements. See `InterpretTechRequirements` for object's syntax. */ function DeriveTechnologyRequirements(template, civ) { let requirements = []; if (template.requirements) { let op = Object.keys(template.requirements)[0]; let val = template.requirements[op]; requirements = InterpretTechRequirements(civ, op, val); } if (template.supersedes && requirements) { if (!requirements.length) requirements.push({}); for (let req of requirements) { if (!req.techs) req.techs = []; req.techs.push(template.supersedes); } } return requirements; } /** * Interprets the prerequisite requirements of a technology. * * Takes the initial { key: value } from the short-form requirements object in entity templates, * and parses it into an object that can be more easily checked by simulation and gui. * * Works recursively if needed. * * The returned object is in the form: * ``` * { "techs": ["tech1", "tech2"] }, * { "techs": ["tech3"] } * ``` * or * ``` * { "entities": [[{ * "class": "human", * "number": 2, * "check": "count" * } * or * ``` * false; * ``` * (Or, to translate: * 1. need either both `tech1` and `tech2`, or `tech3` * 2. need 2 entities with the `human` class * 3. cannot research this tech at all) * * @param {string} civ - The civ code * @param {string} operator - The base operation. Can be "civ", "notciv", "tech", "entity", "all" or "any". * @param {mixed} value - The value associated with the above operation. * * @return Object containing the requirements for the given civ, or false if the civ cannot research the tech. */ function InterpretTechRequirements(civ, operator, value) { let requirements = []; switch (operator) { case "civ": return !civ || civ == value ? [] : false; case "notciv": return civ == value ? false : []; case "entity": { let number = value.number || value.numberOfTypes || 0; if (number > 0) requirements.push({ "entities": [{ "class": value.class, "number": number, "check": value.number ? "count" : "variants" }] }); break; } case "tech": requirements.push({ "techs": [value] }); break; case "all": { let civPermitted = undefined; // tri-state (undefined, false, or true) for (let subvalue of value) { let newOper = Object.keys(subvalue)[0]; let newValue = subvalue[newOper]; let result = InterpretTechRequirements(civ, newOper, newValue); switch (newOper) { case "civ": if (result) civPermitted = true; else if (civPermitted !== true) civPermitted = false; break; case "notciv": if (!result) return false; break; case "any": if (!result) return false; // else, fall through case "all": if (!result) { let nullcivreqs = InterpretTechRequirements(null, newOper, newValue); if (!nullcivreqs || !nullcivreqs.length) civPermitted = false; continue; } // else, fall through case "tech": case "entity": { if (result.length) { if (!requirements.length) requirements.push({}); let newRequirements = []; for (let currReq of requirements) for (let res of result) { let newReq = {}; for (let subtype in currReq) newReq[subtype] = currReq[subtype]; for (let subtype in res) { if (!newReq[subtype]) newReq[subtype] = []; newReq[subtype] = newReq[subtype].concat(res[subtype]); } newRequirements.push(newReq); } requirements = newRequirements; } break; } } } if (civPermitted === false) // if and only if false return false; break; } case "any": { let civPermitted = false; for (let subvalue of value) { let newOper = Object.keys(subvalue)[0]; let newValue = subvalue[newOper]; let result = InterpretTechRequirements(civ, newOper, newValue); switch (newOper) { case "civ": if (result) return []; break; case "notciv": if (!result) return false; civPermitted = true; break; case "any": if (!result) { let nullcivreqs = InterpretTechRequirements(null, newOper, newValue); if (!nullcivreqs || !nullcivreqs.length) continue; return false; } // else, fall through case "all": if (!result) continue; civPermitted = true; // else, fall through case "tech": case "entity": for (let res of result) requirements.push(res); break; } } if (!civPermitted && !requirements.length) return false; break; } default: warn("Unknown requirement operator: "+operator); } return requirements; } /** * Determine order of phases. * * @param {object} phases - The current available store of phases. * @return {array} List of phases */ function UnravelPhases(phases) { let phaseMap = {}; for (let phaseName in phases) { let phaseData = phases[phaseName]; if (!phaseData.reqs.length || !phaseData.reqs[0].techs || !phaseData.replaces) continue; let myPhase = phaseData.replaces[0]; let reqPhase = phaseData.reqs[0].techs[0]; if (phases[reqPhase] && phases[reqPhase].replaces) reqPhase = phases[reqPhase].replaces[0]; phaseMap[myPhase] = reqPhase; if (!phaseMap[reqPhase]) phaseMap[reqPhase] = undefined; } let phaseList = Object.keys(phaseMap); phaseList.sort((a, b) => phaseList.indexOf(a) - phaseList.indexOf(phaseMap[b])); return phaseList; } Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 22767) @@ -1,551 +1,551 @@ /** * Loads history and gameplay data of all civs. * * @param selectableOnly {boolean} - Only load civs that can be selected * in the gamesetup. Scenario maps might set non-selectable civs. */ function loadCivFiles(selectableOnly) { let propertyNames = [ "Code", "Culture", "Name", "Emblem", "History", "Music", "Factions", "CivBonuses", "TeamBonuses", "Structures", "StartEntities", "Formations", "AINames", "SkirmishReplacements", "SelectableInGameSetup"]; let civData = {}; for (let filename of Engine.ListDirectoryFiles("simulation/data/civs/", "*.json", false)) { let data = Engine.ReadJSONFile(filename); for (let prop of propertyNames) if (data[prop] === undefined) throw new Error(filename + " doesn't contain " + prop); if (!selectableOnly || data.SelectableInGameSetup) civData[data.Code] = data; } return civData; } /** * Gets an array of all classes for this identity template */ function GetIdentityClasses(template) { var classList = []; if (template.Classes && template.Classes._string) classList = classList.concat(template.Classes._string.split(/\s+/)); if (template.VisibleClasses && template.VisibleClasses._string) classList = classList.concat(template.VisibleClasses._string.split(/\s+/)); if (template.Rank) classList = classList.concat(template.Rank); return classList; } /** * Gets an array with all classes for this identity template * that should be shown in the GUI */ function GetVisibleIdentityClasses(template) { if (template.VisibleClasses && template.VisibleClasses._string) return template.VisibleClasses._string.split(/\s+/); return []; } /** * Check if a given list of classes matches another list of classes. * Useful f.e. for checking identity classes. * * @param classes - List of the classes to check against. * @param match - Either a string in the form * "Class1 Class2+Class3" * where spaces are handled as OR and '+'-signs as AND, * and ! is handled as NOT, thus Class1+!Class2 = Class1 AND NOT Class2. * Or a list in the form * [["Class1"], ["Class2", "Class3"]] * where the outer list is combined as OR, and the inner lists are AND-ed. * Or a hybrid format containing a list of strings, where the list is * combined as OR, and the strings are split by space and '+' and AND-ed. * * @return undefined if there are no classes or no match object * true if the the logical combination in the match object matches the classes * false otherwise. */ function MatchesClassList(classes, match) { if (!match || !classes) return undefined; // Transform the string to an array if (typeof match == "string") match = match.split(/\s+/); for (let sublist of match) { // If the elements are still strings, split them by space or by '+' if (typeof sublist == "string") sublist = sublist.split(/[+\s]+/); if (sublist.every(c => (c[0] == "!" && classes.indexOf(c.substr(1)) == -1) || (c[0] != "!" && classes.indexOf(c) != -1))) return true; } return false; } /** * Gets the value originating at the value_path as-is, with no modifiers applied. * * @param {object} template - A valid template as returned from a template loader. * @param {string} value_path - Route to value within the xml template structure. * @return {number} */ function GetBaseTemplateDataValue(template, value_path) { let current_value = template; for (let property of value_path.split("/")) current_value = current_value[property] || 0; return +current_value; } /** * Gets the value originating at the value_path with the modifiers dictated by the mod_key applied. * * @param {object} template - A valid template as returned from a template loader. * @param {string} value_path - Route to value within the xml template structure. * @param {string} mod_key - Tech modification key, if different from value_path. * @param {number} player - Optional player id. * @param {object} modifiers - Value modifiers from auto-researched techs, unit upgrades, * etc. Optional as only used if no player id provided. * @return {number} Modifier altered value. */ function GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers={}) { let current_value = GetBaseTemplateDataValue(template, value_path); mod_key = mod_key || value_path; 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); } /** * Get information about a template with or without technology modifications. * * NOTICE: The data returned here should have the same structure as * the object returned by GetEntityState and GetExtendedEntityState! * * @param {object} template - A valid template as returned by the template loader. * @param {number} player - An optional player id to get the technology modifications * of properties. * @param {object} auraTemplates - In the form of { key: { "auraName": "", "auraDescription": "" } }. * @param {object} resources - An instance of the Resources prototype. * @param {object} damageTypes - An instance of the DamageTypes prototype. * @param {object} modifiers - Modifications from auto-researched techs, unit upgrades * etc. Optional as only used if there's no player * id provided. */ function GetTemplateDataHelper(template, player, auraTemplates, resources, damageTypes, modifiers={}) { // Return data either from template (in tech tree) or sim state (ingame). // @param {string} value_path - Route to the value within the template. // @param {string} mod_key - Modification key, if not the same as the value_path. let getEntityValue = function(value_path, mod_key) { return GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers); }; let ret = {}; if (template.Armour) { ret.armour = {}; for (let damageType in template.Armour) if (damageType != "Foundation") ret.armour[damageType] = getEntityValue("Armour/" + damageType); } let getAttackEffects = (temp, path) => { let effects = {}; if (temp.Capture) effects.Capture = getEntityValue(path + "/Capture"); if (temp.Damage) { effects.Damage = {}; for (let damageType in temp.Damage) effects.Damage[damageType] = getEntityValue(path + "/Damage/" + damageType); } // TODO: status effects return effects; }; if (template.Attack) { ret.attack = {}; for (let type in template.Attack) { let getAttackStat = function(stat) { return getEntityValue("Attack/" + type + "/" + stat); }; ret.attack[type] = { "minRange": getAttackStat("MinRange"), "maxRange": getAttackStat("MaxRange"), "elevationBonus": getAttackStat("ElevationBonus"), }; ret.attack[type].elevationAdaptedRange = Math.sqrt(ret.attack[type].maxRange * (2 * ret.attack[type].elevationBonus + ret.attack[type].maxRange)); ret.attack[type].repeatTime = getAttackStat("RepeatTime"); Object.assign(ret.attack[type], getAttackEffects(template.Attack[type], "Attack/" + type)); if (template.Attack[type].Splash) { ret.attack[type].splash = { // true if undefined "friendlyFire": template.Attack[type].Splash.FriendlyFire != "false", "shape": template.Attack[type].Splash.Shape, }; Object.assign(ret.attack[type].splash, getAttackEffects(template.Attack[type].Splash, "Attack/" + type + "/Splash")); } } } if (template.DeathDamage) { ret.deathDamage = { "friendlyFire": template.DeathDamage.FriendlyFire != "false", }; Object.assign(ret.deathDamage, getAttackEffects(template.DeathDamage, "DeathDamage")); } if (template.Auras && auraTemplates) { ret.auras = {}; for (let auraID of template.Auras._string.split(/\s+/)) { let aura = auraTemplates[auraID]; ret.auras[auraID] = { "name": aura.auraName, "description": aura.auraDescription || null, "radius": aura.radius || null }; } } if (template.BuildingAI) ret.buildingAI = { "defaultArrowCount": Math.round(getEntityValue("BuildingAI/DefaultArrowCount")), "garrisonArrowMultiplier": getEntityValue("BuildingAI/GarrisonArrowMultiplier"), "maxArrowCount": Math.round(getEntityValue("BuildingAI/MaxArrowCount")) }; if (template.BuildRestrictions) { // required properties ret.buildRestrictions = { "placementType": template.BuildRestrictions.PlacementType, "territory": template.BuildRestrictions.Territory, "category": template.BuildRestrictions.Category, }; // optional properties if (template.BuildRestrictions.Distance) { ret.buildRestrictions.distance = { "fromClass": template.BuildRestrictions.Distance.FromClass, }; if (template.BuildRestrictions.Distance.MinDistance) ret.buildRestrictions.distance.min = getEntityValue("BuildRestrictions/Distance/MinDistance"); if (template.BuildRestrictions.Distance.MaxDistance) ret.buildRestrictions.distance.max = getEntityValue("BuildRestrictions/Distance/MaxDistance"); } } if (template.TrainingRestrictions) ret.trainingRestrictions = { "category": template.TrainingRestrictions.Category, }; if (template.Cost) { ret.cost = {}; for (let resCode in template.Cost.Resources) ret.cost[resCode] = getEntityValue("Cost/Resources/" + resCode); if (template.Cost.Population) ret.cost.population = getEntityValue("Cost/Population"); if (template.Cost.PopulationBonus) ret.cost.populationBonus = getEntityValue("Cost/PopulationBonus"); if (template.Cost.BuildTime) ret.cost.time = getEntityValue("Cost/BuildTime"); } if (template.Footprint) { ret.footprint = { "height": template.Footprint.Height }; if (template.Footprint.Square) ret.footprint.square = { "width": +template.Footprint.Square["@width"], "depth": +template.Footprint.Square["@depth"] }; else if (template.Footprint.Circle) ret.footprint.circle = { "radius": +template.Footprint.Circle["@radius"] }; else warn("GetTemplateDataHelper(): Unrecognized Footprint type"); } if (template.GarrisonHolder) { ret.garrisonHolder = { "buffHeal": getEntityValue("GarrisonHolder/BuffHeal") }; if (template.GarrisonHolder.Max) ret.garrisonHolder.capacity = getEntityValue("GarrisonHolder/Max"); } if (template.Heal) ret.heal = { "hp": getEntityValue("Heal/HP"), "range": getEntityValue("Heal/Range"), "rate": getEntityValue("Heal/Rate") }; if (template.ResourceGatherer) { ret.resourceGatherRates = {}; let baseSpeed = getEntityValue("ResourceGatherer/BaseSpeed"); for (let type in template.ResourceGatherer.Rates) ret.resourceGatherRates[type] = getEntityValue("ResourceGatherer/Rates/"+ type) * baseSpeed; } if (template.ResourceTrickle) { ret.resourceTrickle = { "interval": +template.ResourceTrickle.Interval, "rates": {} }; for (let type in template.ResourceTrickle.Rates) ret.resourceTrickle.rates[type] = getEntityValue("ResourceTrickle/Rates/" + type); } if (template.Loot) { ret.loot = {}; for (let type in template.Loot) ret.loot[type] = getEntityValue("Loot/"+ type); } if (template.Obstruction) { ret.obstruction = { "active": ("" + template.Obstruction.Active == "true"), "blockMovement": ("" + template.Obstruction.BlockMovement == "true"), "blockPathfinding": ("" + template.Obstruction.BlockPathfinding == "true"), "blockFoundation": ("" + template.Obstruction.BlockFoundation == "true"), "blockConstruction": ("" + template.Obstruction.BlockConstruction == "true"), "disableBlockMovement": ("" + template.Obstruction.DisableBlockMovement == "true"), "disableBlockPathfinding": ("" + template.Obstruction.DisableBlockPathfinding == "true"), "shape": {} }; if (template.Obstruction.Static) { ret.obstruction.shape.type = "static"; ret.obstruction.shape.width = +template.Obstruction.Static["@width"]; ret.obstruction.shape.depth = +template.Obstruction.Static["@depth"]; } else if (template.Obstruction.Unit) { ret.obstruction.shape.type = "unit"; ret.obstruction.shape.radius = +template.Obstruction.Unit["@radius"]; } else ret.obstruction.shape.type = "cluster"; } if (template.Pack) ret.pack = { "state": template.Pack.State, "time": getEntityValue("Pack/Time"), }; if (template.Health) ret.health = Math.round(getEntityValue("Health/Max")); if (template.Identity) { ret.selectionGroupName = template.Identity.SelectionGroupName; ret.name = { "specific": (template.Identity.SpecificName || template.Identity.GenericName), "generic": template.Identity.GenericName }; ret.icon = template.Identity.Icon; ret.tooltip = template.Identity.Tooltip; ret.requiredTechnology = template.Identity.RequiredTechnology; ret.visibleIdentityClasses = GetVisibleIdentityClasses(template.Identity); ret.nativeCiv = template.Identity.Civ; } if (template.UnitMotion) { ret.speed = { "walk": getEntityValue("UnitMotion/WalkSpeed"), }; ret.speed.run = getEntityValue("UnitMotion/WalkSpeed"); if (template.UnitMotion.RunMultiplier) ret.speed.run *= getEntityValue("UnitMotion/RunMultiplier"); } if (template.Upgrade) { ret.upgrades = []; for (let upgradeName in template.Upgrade) { let upgrade = template.Upgrade[upgradeName]; let cost = {}; if (upgrade.Cost) for (let res in upgrade.Cost) cost[res] = getEntityValue("Upgrade/" + upgradeName + "/Cost/" + res, "Upgrade/Cost/" + res); if (upgrade.Time) cost.time = getEntityValue("Upgrade/" + upgradeName + "/Time", "Upgrade/Time"); ret.upgrades.push({ "entity": upgrade.Entity, "tooltip": upgrade.Tooltip, "cost": cost, "icon": upgrade.Icon || undefined, "requiredTechnology": upgrade.RequiredTechnology || undefined }); } } if (template.ProductionQueue) { ret.techCostMultiplier = {}; for (let res in template.ProductionQueue.TechCostMultiplier) ret.techCostMultiplier[res] = getEntityValue("ProductionQueue/TechCostMultiplier/" + res); } if (template.Trader) ret.trader = { "GainMultiplier": getEntityValue("Trader/GainMultiplier") }; if (template.WallSet) { ret.wallSet = { "templates": { "tower": template.WallSet.Templates.Tower, "gate": template.WallSet.Templates.Gate, "fort": template.WallSet.Templates.Fort || "structures/" + template.Identity.Civ + "_fortress", "long": template.WallSet.Templates.WallLong, "medium": template.WallSet.Templates.WallMedium, "short": template.WallSet.Templates.WallShort }, "maxTowerOverlap": +template.WallSet.MaxTowerOverlap, "minTowerOverlap": +template.WallSet.MinTowerOverlap }; if (template.WallSet.Templates.WallEnd) ret.wallSet.templates.end = template.WallSet.Templates.WallEnd; if (template.WallSet.Templates.WallCurves) ret.wallSet.templates.curves = template.WallSet.Templates.WallCurves.split(" "); } if (template.WallPiece) ret.wallPiece = { "length": +template.WallPiece.Length, "angle": +(template.WallPiece.Orientation || 1) * Math.PI, "indent": +(template.WallPiece.Indent || 0), "bend": +(template.WallPiece.Bend || 0) * Math.PI }; return ret; } /** * Get basic information about a technology template. * @param {object} template - A valid template as obtained by loading the tech JSON file. * @param {string} civ - Civilization for which the tech requirements should be calculated. */ function GetTechnologyBasicDataHelper(template, civ) { return { "name": { "generic": template.genericName }, "icon": template.icon ? "technologies/" + template.icon : undefined, "description": template.description, "reqs": DeriveTechnologyRequirements(template, civ), "modifications": template.modifications, "affects": template.affects, "replaces": template.replaces }; } /** * Get information about a technology template. * @param {object} template - A valid template as obtained by loading the tech JSON file. * @param {string} civ - Civilization for which the specific name and tech requirements should be returned. */ function GetTechnologyDataHelper(template, civ, resources) { let ret = GetTechnologyBasicDataHelper(template, civ); if (template.specificName) ret.name.specific = template.specificName[civ] || template.specificName.generic; ret.cost = { "time": template.researchTime ? +template.researchTime : 0 }; for (let type of resources.GetCodes()) ret.cost[type] = +(template.cost && template.cost[type] || 0); ret.tooltip = template.tooltip; ret.requirementsTooltip = template.requirementsTooltip || ""; return ret; } function calculateCarriedResources(carriedResources, tradingGoods) { var resources = {}; if (carriedResources) for (let resource of carriedResources) resources[resource.type] = (resources[resource.type] || 0) + resource.amount; if (tradingGoods && tradingGoods.amount) resources[tradingGoods.type] = (resources[tradingGoods.type] || 0) + (tradingGoods.amount.traderGain || 0) + (tradingGoods.amount.market1Gain || 0) + (tradingGoods.amount.market2Gain || 0); return resources; } /** * Remove filter prefix (mirage, corpse, etc) from template name. * * ie. filter|dir/to/template -> dir/to/template */ function removeFiltersFromTemplateName(templateName) { return templateName.split("|").pop(); } Index: ps/trunk/binaries/data/mods/public/simulation/components/Auras.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Auras.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/Auras.js (revision 22767) @@ -1,491 +1,514 @@ function Auras() {} Auras.prototype.Schema = "" + "tokens" + "" + ""; Auras.prototype.Init = function() { this.affectedPlayers = {}; for (let name of this.GetAuraNames()) this.affectedPlayers[name] = []; // In case of autogarrisoning, this component can be called before ownership is set. // So it needs to be completely initialised from the start. this.Clean(); }; // We can modify identifier if we want stackable auras in some case. 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() { var ret = {}; for (let auraID of this.GetAuraNames()) { let aura = AuraTemplates.Get(auraID); ret[auraID] = { "name": aura.auraName, "description": aura.auraDescription || null, "radius": this.GetRange(auraID) || null }; } return ret; }; Auras.prototype.GetAuraNames = function() { return this.template._string.split(/\s+/); }; Auras.prototype.GetOverlayIcon = function(name) { return AuraTemplates.Get(name).overlayIcon || ""; }; Auras.prototype.GetAffectedEntities = function(name) { return this[name].targetUnits; }; Auras.prototype.GetRange = function(name) { if (this.IsRangeAura(name)) return +AuraTemplates.Get(name).radius; return undefined; }; Auras.prototype.GetClasses = function(name) { return AuraTemplates.Get(name).affects; }; Auras.prototype.GetModifications = function(name) { return AuraTemplates.Get(name).modifications; }; Auras.prototype.GetAffectedPlayers = function(name) { return this.affectedPlayers[name]; }; Auras.prototype.GetRangeOverlays = function() { let rangeOverlays = []; for (let name of this.GetAuraNames()) { if (!this.IsRangeAura(name) || !this[name].isApplied) continue; let rangeOverlay = AuraTemplates.Get(name).rangeOverlay; rangeOverlays.push( rangeOverlay ? { "radius": this.GetRange(name), "texture": rangeOverlay.lineTexture, "textureMask": rangeOverlay.lineTextureMask, "thickness": rangeOverlay.lineThickness } : // Specify default in order not to specify it in about 40 auras { "radius": this.GetRange(name), "texture": "outline_border.png", "textureMask": "outline_border_mask.png", "thickness": 0.2 }); } return rangeOverlays; }; Auras.prototype.CalculateAffectedPlayers = function(name) { var affectedPlayers = AuraTemplates.Get(name).affectedPlayers || ["Player"]; this.affectedPlayers[name] = []; var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!cmpPlayer) cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer || cmpPlayer.GetState() == "defeated") return; let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); for (let i of cmpPlayerManager.GetAllPlayers()) { let cmpAffectedPlayer = QueryPlayerIDInterface(i); if (!cmpAffectedPlayer || cmpAffectedPlayer.GetState() == "defeated") continue; if (affectedPlayers.some(p => p == "Player" ? cmpPlayer.GetPlayerID() == i : cmpPlayer["Is" + p](i))) this.affectedPlayers[name].push(i); } }; Auras.prototype.CanApply = function(name) { if (!AuraTemplates.Get(name).requiredTechnology) return true; let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.IsTechnologyResearched(AuraTemplates.Get(name).requiredTechnology); }; Auras.prototype.HasFormationAura = function() { return this.GetAuraNames().some(n => this.IsFormationAura(n)); }; Auras.prototype.HasGarrisonAura = function() { return this.GetAuraNames().some(n => this.IsGarrisonAura(n)); }; Auras.prototype.HasGarrisonedUnitsAura = function() { return this.GetAuraNames().some(n => this.IsGarrisonedUnitsAura(n)); }; Auras.prototype.GetType = function(name) { return AuraTemplates.Get(name).type; }; Auras.prototype.IsFormationAura = function(name) { return this.GetType(name) == "formation"; }; Auras.prototype.IsGarrisonAura = function(name) { return this.GetType(name) == "garrison"; }; Auras.prototype.IsGarrisonedUnitsAura = function(name) { return this.GetType(name) == "garrisonedUnits"; }; Auras.prototype.IsRangeAura = function(name) { return this.GetType(name) == "range"; }; Auras.prototype.IsGlobalAura = function(name) { return this.GetType(name) == "global"; }; Auras.prototype.IsPlayerAura = function(name) { return this.GetType(name) == "player"; }; /** * clean all bonuses. Remove the old ones and re-apply the new ones */ Auras.prototype.Clean = function() { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var auraNames = this.GetAuraNames(); let targetUnitsClone = {}; let needVisualizationUpdate = false; // remove all bonuses for (let name of auraNames) { targetUnitsClone[name] = []; if (!this[name]) continue; if (this.IsRangeAura(name)) needVisualizationUpdate = true; if (this[name].targetUnits) 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); } for (let name of auraNames) { // only calculate the affected players on re-applying the bonuses // this makes sure the template bonuses are removed from the correct players this.CalculateAffectedPlayers(name); // initialise range query this[name] = {}; this[name].targetUnits = []; this[name].isApplied = this.CanApply(name); var affectedPlayers = this.GetAffectedPlayers(name); if (!affectedPlayers.length) continue; 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); } } if (needVisualizationUpdate) { let cmpRangeOverlayManager = Engine.QueryInterface(this.entity, IID_RangeOverlayManager); if (cmpRangeOverlayManager) { cmpRangeOverlayManager.UpdateRangeOverlays("Auras"); cmpRangeOverlayManager.RegenerateRangeOverlays(false); } } }; Auras.prototype.GiveMembersWithValidClass = function(auraName, entityList) { var match = this.GetClasses(auraName); return entityList.filter(ent => { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), match); }); }; 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); } }; 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) -{ - for (let name of this.GetAuraNames().filter(n => this.IsGlobalAura(n))) - { - 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]); - } -}; - -Auras.prototype.ApplyFormationBonus = function(memberList) +Auras.prototype.ApplyFormationAura = function(memberList) { for (let name of this.GetAuraNames().filter(n => this.IsFormationAura(n))) - this.ApplyBonus(name, memberList); + this.ApplyAura(name, memberList); }; -Auras.prototype.ApplyGarrisonBonus = function(structure) +Auras.prototype.ApplyGarrisonAura = function(structure) { for (let name of this.GetAuraNames().filter(n => this.IsGarrisonAura(n))) - this.ApplyBonus(name, [structure]); + this.ApplyAura(name, [structure]); }; -Auras.prototype.ApplyTemplateBonus = function(name, players) +Auras.prototype.ApplyTemplateAura = function(name, players) { 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 players) + { + let playerId = cmpPlayerManager.GetPlayerByID(player); + for (let modifierPath in derivedModifiers) + for (let modifier of derivedModifiers[modifierPath]) + cmpModifiersManager.AddModifier(modifierPath, modifName, modifier, playerId); + } }; -Auras.prototype.RemoveFormationBonus = function(memberList) +Auras.prototype.RemoveFormationAura = function(memberList) { for (let name of this.GetAuraNames().filter(n => this.IsFormationAura(n))) - this.RemoveBonus(name, memberList); + this.RemoveAura(name, memberList); }; -Auras.prototype.RemoveGarrisonBonus = function(structure) +Auras.prototype.RemoveGarrisonAura = function(structure) { for (let name of this.GetAuraNames().filter(n => this.IsGarrisonAura(n))) - this.RemoveBonus(name, [structure]); + this.RemoveAura(name, [structure]); }; -Auras.prototype.RemoveTemplateBonus = function(name) +Auras.prototype.RemoveTemplateAura = 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); + 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.RemoveTemplateBonus(mod.value, player, classes, 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.ApplyBonus = function(name, ents) +Auras.prototype.ApplyAura = function(name, ents) { var validEnts = this.GiveMembersWithValidClass(name, ents); if (!validEnts.length) return; this[name].targetUnits = this[name].targetUnits.concat(validEnts); 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) return; this[name].targetUnits = this[name].targetUnits.filter(v => validEnts.indexOf(v) == -1); 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) { this.Clean(); }; Auras.prototype.OnDiplomacyChanged = function(msg) { var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (cmpPlayer && (cmpPlayer.GetPlayerID() == msg.player || cmpPlayer.GetPlayerID() == msg.otherPlayer) || IsOwnedByPlayer(msg.player, this.entity) || IsOwnedByPlayer(msg.otherPlayer, this.entity)) this.Clean(); }; Auras.prototype.OnGlobalResearchFinished = function(msg) { var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if ((!cmpPlayer || cmpPlayer.GetPlayerID() != msg.player) && !IsOwnedByPlayer(msg.player, this.entity)) return; for (let name of this.GetAuraNames()) { let requiredTech = AuraTemplates.Get(name).requiredTechnology; if (requiredTech && requiredTech == msg.tech) { this.Clean(); return; } } }; /** * Update auras of the player entity and entities affecting player entities that didn't change ownership. */ 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(); }; Engine.RegisterComponentType(IID_Auras, "Auras", Auras); Index: ps/trunk/binaries/data/mods/public/simulation/components/Formation.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Formation.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/Formation.js (revision 22767) @@ -1,995 +1,995 @@ function Formation() {} Formation.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; var g_ColumnDistanceThreshold = 128; // distance at which we'll switch between column/box formations Formation.prototype.Init = function() { this.formationShape = this.template.FormationShape; this.sortingClasses = this.template.SortingClasses.split(/\s+/g); this.sortingOrder = this.template.SortingOrder; this.shiftRows = this.template.ShiftRows == "true"; this.separationMultiplier = { "width": +this.template.UnitSeparationWidthMultiplier, "depth": +this.template.UnitSeparationDepthMultiplier }; this.sloppyness = +this.template.Sloppyness; this.widthDepthRatio = +this.template.WidthDepthRatio; this.minColumns = +(this.template.MinColumns || 0); this.maxColumns = +(this.template.MaxColumns || 0); this.maxRows = +(this.template.MaxRows || 0); this.centerGap = +(this.template.CenterGap || 0); this.animations = []; if (this.template.Animations) { let differentAnimations = this.template.Animations.split(/\s*;\s*/); // loop over the different rectangulars that will map to different animations for (var rectAnimation of differentAnimations) { var rect, replacementAnimationName; [rect, replacementAnimationName] = rectAnimation.split(/\s*:\s*/); var rows, columns; [rows, columns] = rect.split(/\s*,\s*/); var minRow, maxRow, minColumn, maxColumn; [minRow, maxRow] = rows.split(/\s*\.\.\s*/); [minColumn, maxColumn] = columns.split(/\s*\.\.\s*/); this.animations.push({ "minRow": +minRow, "maxRow": +maxRow, "minColumn": +minColumn, "maxColumn": +maxColumn, "animation": replacementAnimationName }); } } this.members = []; // entity IDs currently belonging to this formation this.memberPositions = {}; this.maxRowsUsed = 0; this.maxColumnsUsed = []; this.inPosition = []; // entities that have reached their final position this.columnar = false; // whether we're travelling in column (vs box) formation this.rearrange = true; // whether we should rearrange all formation members this.formationMembersWithAura = []; // Members with a formation aura this.width = 0; this.depth = 0; this.oldOrientation = {"sin": 0, "cos": 0}; this.twinFormations = []; // distance from which two twin formations will merge into one. this.formationSeparation = 0; Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer) .SetInterval(this.entity, IID_Formation, "ShapeUpdate", 1000, 1000, null); }; /** * Set the value from which two twin formations will become one. */ Formation.prototype.SetFormationSeparation = function(value) { this.formationSeparation = value; }; Formation.prototype.GetSize = function() { return {"width": this.width, "depth": this.depth}; }; Formation.prototype.GetSpeedMultiplier = function() { return +this.template.SpeedMultiplier; }; Formation.prototype.GetMemberCount = function() { return this.members.length; }; Formation.prototype.GetMembers = function() { return this.members; }; Formation.prototype.GetClosestMember = function(ent, filter) { var cmpEntPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpEntPosition || !cmpEntPosition.IsInWorld()) return INVALID_ENTITY; var entPosition = cmpEntPosition.GetPosition2D(); var closestMember = INVALID_ENTITY; var closestDistance = Infinity; for (var member of this.members) { if (filter && !filter(ent)) continue; var cmpPosition = Engine.QueryInterface(member, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) continue; var pos = cmpPosition.GetPosition2D(); var dist = entPosition.distanceToSquared(pos); if (dist < closestDistance) { closestMember = member; closestDistance = dist; } } return closestMember; }; /** * Returns the 'primary' member of this formation (typically the most * important unit type), for e.g. playing a representative sound. * Returns undefined if no members. * TODO: actually implement something like that; currently this just returns * the arbitrary first one. */ Formation.prototype.GetPrimaryMember = function() { return this.members[0]; }; /** * Get the formation animation for a certain member of this formation * @param entity The entity ID to get the animation for * @return The name of the transformed animation as defined in the template * E.g. "testudo_row1" */ Formation.prototype.GetFormationAnimation = function(entity) { var animationGroup = this.animations; if (!animationGroup.length || this.columnar || !this.memberPositions[entity]) return "formation"; var row = this.memberPositions[entity].row; var column = this.memberPositions[entity].column; for (var i = 0; i < animationGroup.length; ++i) { var minRow = animationGroup[i].minRow; if (minRow < 0) minRow += this.maxRowsUsed + 1; if (row < minRow) continue; var maxRow = animationGroup[i].maxRow; if (maxRow < 0) maxRow += this.maxRowsUsed + 1; if (row > maxRow) continue; var minColumn = animationGroup[i].minColumn; if (minColumn < 0) minColumn += this.maxColumnsUsed[row] + 1; if (column < minColumn) continue; var maxColumn = animationGroup[i].maxColumn; if (maxColumn < 0) maxColumn += this.maxColumnsUsed[row] + 1; if (column > maxColumn) continue; return animationGroup[i].animation; } return "formation"; }; /** * Permits formation members to register that they've reached their destination. */ Formation.prototype.SetInPosition = function(ent) { if (this.inPosition.indexOf(ent) != -1) return; // Rotate the entity to the right angle var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); var cmpEntPosition = Engine.QueryInterface(ent, IID_Position); if (cmpEntPosition && cmpEntPosition.IsInWorld() && cmpPosition && cmpPosition.IsInWorld()) cmpEntPosition.TurnTo(cmpPosition.GetRotation().y); this.inPosition.push(ent); }; /** * Called by formation members upon entering non-walking states. */ Formation.prototype.UnsetInPosition = function(ent) { var ind = this.inPosition.indexOf(ent); if (ind != -1) this.inPosition.splice(ind, 1); }; /** * Set whether we should rearrange formation members if * units are removed from the formation. */ Formation.prototype.SetRearrange = function(rearrange) { this.rearrange = rearrange; }; /** * Initialise the members of this formation. * Must only be called once. * All members must implement UnitAI. */ Formation.prototype.SetMembers = function(ents) { this.members = ents; for (var ent of this.members) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); cmpUnitAI.SetFormationController(this.entity); var cmpAuras = Engine.QueryInterface(ent, IID_Auras); if (cmpAuras && cmpAuras.HasFormationAura()) { this.formationMembersWithAura.push(ent); - cmpAuras.ApplyFormationBonus(ents); + cmpAuras.ApplyFormationAura(ents); } } this.offsets = undefined; // Locate this formation controller in the middle of its members this.MoveToMembersCenter(); // Compute the speed etc. of the formation this.ComputeMotionParameters(); }; /** * Remove the given list of entities. * The entities must already be members of this formation. */ Formation.prototype.RemoveMembers = function(ents) { this.offsets = undefined; this.members = this.members.filter(function(e) { return ents.indexOf(e) == -1; }); this.inPosition = this.inPosition.filter(function(e) { return ents.indexOf(e) == -1; }); for (var ent of ents) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); cmpUnitAI.UpdateWorkOrders(); cmpUnitAI.SetFormationController(INVALID_ENTITY); } 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; }); // If there's nobody left, destroy the formation if (this.members.length == 0) { Engine.DestroyEntity(this.entity); return; } if (!this.rearrange) return; this.ComputeMotionParameters(); // Rearrange the remaining members this.MoveMembersIntoFormation(true, true); }; Formation.prototype.AddMembers = function(ents) { this.offsets = undefined; this.inPosition = []; 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); for (let ent of ents) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); cmpUnitAI.SetFormationController(this.entity); let cmpAuras = Engine.QueryInterface(ent, IID_Auras); if (cmpAuras && cmpAuras.HasFormationAura()) { this.formationMembersWithAura.push(ent); - cmpAuras.ApplyFormationBonus(this.members); + cmpAuras.ApplyFormationAura(this.members); } } this.MoveMembersIntoFormation(true, true); }; /** * Called when the formation stops moving in order to detect * units that have already reached their final positions. */ Formation.prototype.FindInPosition = function() { for (var i = 0; i < this.members.length; ++i) { var cmpUnitMotion = Engine.QueryInterface(this.members[i], IID_UnitMotion); if (!cmpUnitMotion.IsMoveRequested()) { // Verify that members are stopped in FORMATIONMEMBER.WALKING var cmpUnitAI = Engine.QueryInterface(this.members[i], IID_UnitAI); if (cmpUnitAI.IsWalking()) this.SetInPosition(this.members[i]); } } }; /** * Remove all members and destroy the formation. */ Formation.prototype.Disband = function() { for (var ent of this.members) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); cmpUnitAI.SetFormationController(INVALID_ENTITY); } for (var ent of this.formationMembersWithAura) { var cmpAuras = Engine.QueryInterface(ent, IID_Auras); - cmpAuras.RemoveFormationBonus(this.members); + cmpAuras.RemoveFormationAura(this.members); } this.members = []; this.inPosition = []; this.formationMembersWithAura = []; this.offsets = undefined; Engine.DestroyEntity(this.entity); }; /** * Set all members to form up into the formation shape. * If moveCenter is true, the formation center will be reinitialised * to the center of the units. * If force is true, all individual orders of the formation units are replaced, * otherwise the order to walk into formation is just pushed to the front. */ Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force) { if (!this.members.length) return; var active = []; var positions = []; for (var ent of this.members) { var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) continue; active.push(ent); // query the 2D position as exact hight calculation isn't needed // but bring the position to the right coordinates var pos = cmpPosition.GetPosition2D(); positions.push(pos); } var avgpos = Vector2D.average(positions); // Reposition the formation if we're told to or if we don't already have a position var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); var inWorld = cmpPosition.IsInWorld(); if (moveCenter || !inWorld) { cmpPosition.JumpTo(avgpos.x, avgpos.y); // Don't make the formation controller entity show up in range queries if (!inWorld) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.SetEntityFlag(this.entity, "normal", false); } } // Switch between column and box if necessary var cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); var walkingDistance = cmpUnitAI.ComputeWalkingDistance(); var columnar = walkingDistance > g_ColumnDistanceThreshold; if (columnar != this.columnar) { this.columnar = columnar; this.offsets = undefined; } var newOrientation = this.GetEstimatedOrientation(avgpos); var dSin = Math.abs(newOrientation.sin - this.oldOrientation.sin); var dCos = Math.abs(newOrientation.cos - this.oldOrientation.cos); // If the formation existed, only recalculate positions if the turning agle is somewhat biggish if (!this.offsets || dSin > 1 || dCos > 1) this.offsets = this.ComputeFormationOffsets(active, positions); this.oldOrientation = newOrientation; var xMax = 0; var yMax = 0; var xMin = 0; var yMin = 0; for (var i = 0; i < this.offsets.length; ++i) { var offset = this.offsets[i]; var cmpUnitAI = Engine.QueryInterface(offset.ent, IID_UnitAI); if (!cmpUnitAI) continue; var data = { "target": this.entity, "x": offset.x, "z": offset.y }; cmpUnitAI.AddOrder("FormationWalk", data, !force); xMax = Math.max(xMax, offset.x); yMax = Math.max(yMax, offset.y); xMin = Math.min(xMin, offset.x); yMin = Math.min(yMin, offset.y); } this.width = xMax - xMin; this.depth = yMax - yMin; }; Formation.prototype.MoveToMembersCenter = function() { var positions = []; for (var ent of this.members) { var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) continue; positions.push(cmpPosition.GetPosition2D()); } var avgpos = Vector2D.average(positions); var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); var inWorld = cmpPosition.IsInWorld(); cmpPosition.JumpTo(avgpos.x, avgpos.y); // Don't make the formation controller show up in range queries if (!inWorld) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.SetEntityFlag(this.entity, "normal", false); } }; Formation.prototype.GetAvgFootprint = function(active) { var footprints = []; for (var ent of active) { var cmpFootprint = Engine.QueryInterface(ent, IID_Footprint); if (cmpFootprint) footprints.push(cmpFootprint.GetShape()); } if (!footprints.length) return {"width":1, "depth": 1}; var r = {"width": 0, "depth": 0}; for (var shape of footprints) { if (shape.type == "circle") { r.width += shape.radius * 2; r.depth += shape.radius * 2; } else if (shape.type == "square") { r.width += shape.width; r.depth += shape.depth; } } r.width /= footprints.length; r.depth /= footprints.length; return r; }; Formation.prototype.ComputeFormationOffsets = function(active, positions) { var separation = this.GetAvgFootprint(active); separation.width *= this.separationMultiplier.width; separation.depth *= this.separationMultiplier.depth; if (this.columnar) var sortingClasses = ["Cavalry","Infantry"]; else var sortingClasses = this.sortingClasses.slice(); sortingClasses.push("Unknown"); // the entities will be assigned to positions in the formation in // the same order as the types list is ordered var types = {}; for (var i = 0; i < sortingClasses.length; ++i) types[sortingClasses[i]] = []; for (var i in active) { var cmpIdentity = Engine.QueryInterface(active[i], IID_Identity); var classes = cmpIdentity.GetClassesList(); var done = false; for (var c = 0; c < sortingClasses.length; ++c) { if (classes.indexOf(sortingClasses[c]) > -1) { types[sortingClasses[c]].push({"ent": active[i], "pos": positions[i]}); done = true; break; } } if (!done) types["Unknown"].push({"ent": active[i], "pos": positions[i]}); } var count = active.length; var shape = this.formationShape; var shiftRows = this.shiftRows; var centerGap = this.centerGap; var sortingOrder = this.sortingOrder; var offsets = []; // Choose a sensible size/shape for the various formations, depending on number of units var cols; if (this.columnar) { shape = "square"; cols = Math.min(count,3); shiftRows = false; centerGap = 0; sortingOrder = null; } else { var depth = Math.sqrt(count / this.widthDepthRatio); if (this.maxRows && depth > this.maxRows) depth = this.maxRows; cols = Math.ceil(count / Math.ceil(depth) + (this.shiftRows ? 0.5 : 0)); if (cols < this.minColumns) cols = Math.min(count, this.minColumns); if (this.maxColumns && cols > this.maxColumns && this.maxRows != depth) cols = this.maxColumns; } // define special formations here if (this.template.FormationName == "Scatter") { var width = Math.sqrt(count) * (separation.width + separation.depth) * 2.5; for (var i = 0; i < count; ++i) { var obj = new Vector2D(randFloat(0, width), randFloat(0, width)); obj.row = 1; obj.column = i + 1; offsets.push(obj); } } // For non-special formations, calculate the positions based on the number of entities this.maxColumnsUsed = []; this.maxRowsUsed = 0; if (shape != "special") { offsets = []; var r = 0; var left = count; // while there are units left, start a new row in the formation while (left > 0) { // save the position of the row var z = -r * separation.depth; // switch between the left and right side of the center to have a symmetrical distribution var side = 1; // determine the number of entities in this row of the formation if (shape == "square") { var n = cols; if (shiftRows) n -= r%2; } else if (shape == "triangle") { if (shiftRows) var n = r + 1; else var n = r * 2 + 1; } if (!shiftRows && n > left) n = left; for (var c = 0; c < n && left > 0; ++c) { // switch sides for the next entity side *= -1; if (n%2 == 0) var x = side * (Math.floor(c/2) + 0.5) * separation.width; else var x = side * Math.ceil(c/2) * separation.width; if (centerGap) { if (x == 0) // don't use the center position with a center gap continue; x += side * centerGap / 2; } var column = Math.ceil(n/2) + Math.ceil(c/2) * side; var r1 = randFloat(-1, 1) * this.sloppyness; var r2 = randFloat(-1, 1) * this.sloppyness; offsets.push(new Vector2D(x + r1, z + r2)); offsets[offsets.length - 1].row = r+1; offsets[offsets.length - 1].column = column; left--; } ++r; this.maxColumnsUsed[r] = n; } this.maxRowsUsed = r; } // make sure the average offset is zero, as the formation is centered around that // calculating offset distances without a zero average makes no sense, as the formation // will jump to a different position any time var avgoffset = Vector2D.average(offsets); offsets.forEach(function (o) {o.sub(avgoffset);}); // sort the available places in certain ways // the places first in the list will contain the heaviest units as defined by the order // of the types list if (this.sortingOrder == "fillFromTheSides") offsets.sort(function(o1, o2) { return Math.abs(o1.x) < Math.abs(o2.x);}); else if (this.sortingOrder == "fillToTheCenter") offsets.sort(function(o1, o2) { return Math.max(Math.abs(o1.x), Math.abs(o1.y)) < Math.max(Math.abs(o2.x), Math.abs(o2.y)); }); // query the 2D position of the formation var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); var formationPos = cmpPosition.GetPosition2D(); // use realistic place assignment, // every soldier searches the closest available place in the formation var newOffsets = []; var realPositions = this.GetRealOffsetPositions(offsets, formationPos); for (var i = sortingClasses.length; i; --i) { var t = types[sortingClasses[i-1]]; if (!t.length) continue; var usedOffsets = offsets.splice(-t.length); var usedRealPositions = realPositions.splice(-t.length); for (var entPos of t) { var closestOffsetId = this.TakeClosestOffset(entPos, usedRealPositions, usedOffsets); usedRealPositions.splice(closestOffsetId, 1); newOffsets.push(usedOffsets.splice(closestOffsetId, 1)[0]); newOffsets[newOffsets.length - 1].ent = entPos.ent; } } return newOffsets; }; /** * Search the closest position in the realPositions list to the given entity * @param ent, the queried entity * @param realPositions, the world coordinates of the available offsets * @return the index of the closest offset position */ Formation.prototype.TakeClosestOffset = function(entPos, realPositions, offsets) { var pos = entPos.pos; var closestOffsetId = -1; var offsetDistanceSq = Infinity; for (var i = 0; i < realPositions.length; i++) { var distSq = pos.distanceToSquared(realPositions[i]); if (distSq < offsetDistanceSq) { offsetDistanceSq = distSq; closestOffsetId = i; } } this.memberPositions[entPos.ent] = {"row": offsets[closestOffsetId].row, "column":offsets[closestOffsetId].column}; return closestOffsetId; }; /** * Get the world positions for a list of offsets in this formation */ Formation.prototype.GetRealOffsetPositions = function(offsets, pos) { var offsetPositions = []; var {sin, cos} = this.GetEstimatedOrientation(pos); // calculate the world positions for (var o of offsets) offsetPositions.push(new Vector2D(pos.x + o.y * sin + o.x * cos, pos.y + o.y * cos - o.x * sin)); return offsetPositions; }; /** * calculate the estimated rotation of the formation * based on the first unitAI target position when ordered to walk, * based on the current rotation in other cases * Return the sine and cosine of the angle */ Formation.prototype.GetEstimatedOrientation = function(pos) { var cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); var r = {"sin": 0, "cos": 1}; var unitAIState = cmpUnitAI.GetCurrentState(); if (unitAIState == "FORMATIONCONTROLLER.WALKING" || unitAIState == "FORMATIONCONTROLLER.COMBAT.APPROACHING") { var targetPos = cmpUnitAI.GetTargetPositions(); if (!targetPos.length) return r; var d = targetPos[0].sub(pos).normalize(); if (!d.x && !d.y) return r; r.cos = d.y; r.sin = d.x; } else { var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition) return r; var rot = cmpPosition.GetRotation().y; r.sin = Math.sin(rot); r.cos = Math.cos(rot); } return r; }; /** * Set formation controller's speed based on its current members. */ Formation.prototype.ComputeMotionParameters = function() { var maxRadius = 0; var minSpeed = Infinity; for (var ent of this.members) { var cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) minSpeed = Math.min(minSpeed, cmpUnitMotion.GetWalkSpeed()); } minSpeed *= this.GetSpeedMultiplier(); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpUnitMotion.SetSpeedMultiplier(minSpeed / cmpUnitMotion.GetWalkSpeed()); }; Formation.prototype.ShapeUpdate = function() { // Check the distance to twin formations, and merge if when // the formations could collide for (var i = this.twinFormations.length - 1; i >= 0; --i) { // only do the check on one side if (this.twinFormations[i] <= this.entity) continue; var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); var cmpOtherPosition = Engine.QueryInterface(this.twinFormations[i], IID_Position); var cmpOtherFormation = Engine.QueryInterface(this.twinFormations[i], IID_Formation); if (!cmpPosition || !cmpOtherPosition || !cmpOtherFormation) continue; var thisPosition = cmpPosition.GetPosition2D(); var otherPosition = cmpOtherPosition.GetPosition2D(); var dx = thisPosition.x - otherPosition.x; var dy = thisPosition.y - otherPosition.y; var dist = Math.sqrt(dx * dx + dy * dy); var thisSize = this.GetSize(); var otherSize = cmpOtherFormation.GetSize(); var minDist = Math.max(thisSize.width / 2, thisSize.depth / 2) + Math.max(otherSize.width / 2, otherSize.depth / 2) + this.formationSeparation; if (minDist < dist) continue; // merge the members from the twin formation into this one // twin formations should always have exactly the same orders let otherMembers = cmpOtherFormation.members; cmpOtherFormation.RemoveMembers(otherMembers); this.AddMembers(otherMembers); Engine.DestroyEntity(this.twinFormations[i]); this.twinFormations.splice(i,1); } // Switch between column and box if necessary var cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); var walkingDistance = cmpUnitAI.ComputeWalkingDistance(); var columnar = walkingDistance > g_ColumnDistanceThreshold; if (columnar != this.columnar) { this.offsets = undefined; this.columnar = columnar; this.MoveMembersIntoFormation(false, true); // (disable moveCenter so we can't get stuck in a loop of switching // shape causing center to change causing shape to switch back) } }; Formation.prototype.OnGlobalOwnershipChanged = function(msg) { // When an entity is captured or destroyed, it should no longer be // controlled by this formation if (this.members.indexOf(msg.entity) != -1) this.RemoveMembers([msg.entity]); }; Formation.prototype.OnGlobalEntityRenamed = function(msg) { if (this.members.indexOf(msg.entity) != -1) { this.offsets = undefined; var cmpNewUnitAI = Engine.QueryInterface(msg.newentity, IID_UnitAI); if (cmpNewUnitAI) { this.members[this.members.indexOf(msg.entity)] = msg.newentity; this.memberPositions[msg.newentity] = this.memberPositions[msg.entity]; } var cmpOldUnitAI = Engine.QueryInterface(msg.entity, IID_UnitAI); cmpOldUnitAI.SetFormationController(INVALID_ENTITY); if (cmpNewUnitAI) cmpNewUnitAI.SetFormationController(this.entity); // Because the renamed entity might have different characteristics, // (e.g. packed vs. unpacked siege), we need to recompute motion parameters this.ComputeMotionParameters(); } }; Formation.prototype.RegisterTwinFormation = function(entity) { var cmpFormation = Engine.QueryInterface(entity, IID_Formation); if (!cmpFormation) return; this.twinFormations.push(entity); cmpFormation.twinFormations.push(this.entity); }; Formation.prototype.DeleteTwinFormations = function() { for (var ent of this.twinFormations) { var cmpFormation = Engine.QueryInterface(ent, IID_Formation); if (cmpFormation) cmpFormation.twinFormations.splice(cmpFormation.twinFormations.indexOf(this.entity), 1); } this.twinFormations = []; }; Formation.prototype.LoadFormation = function(newTemplate) { // get the old formation info var members = this.members.slice(); var cmpThisUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); var orders = cmpThisUnitAI.GetOrders().slice(); this.Disband(); var newFormation = Engine.AddEntity(newTemplate); // Apply the info from the old formation to the new one let cmpNewOwnership = Engine.QueryInterface(newFormation, IID_Ownership); let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership && cmpNewOwnership) cmpNewOwnership.SetOwner(cmpOwnership.GetOwner()); var cmpNewPosition = Engine.QueryInterface(newFormation, IID_Position); var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition && cmpPosition.IsInWorld() && cmpNewPosition) cmpNewPosition.TurnTo(cmpPosition.GetRotation().y); var cmpFormation = Engine.QueryInterface(newFormation, IID_Formation); var cmpNewUnitAI = Engine.QueryInterface(newFormation, IID_UnitAI); cmpFormation.SetMembers(members); if (orders.length) cmpNewUnitAI.AddOrders(orders); else cmpNewUnitAI.MoveIntoFormation(); Engine.PostMessage(this.entity, MT_EntityRenamed, { "entity": this.entity, "newentity": newFormation }); }; Engine.RegisterComponentType(IID_Formation, "Formation", Formation); Index: ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 22767) @@ -1,725 +1,725 @@ function GarrisonHolder() {} GarrisonHolder.prototype.Schema = "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; /** * Initialize GarrisonHolder Component * Garrisoning when loading a map is set in the script of the map, by setting initGarrison * which should contain the array of garrisoned entities. */ GarrisonHolder.prototype.Init = function() { // Garrisoned Units this.entities = []; this.timer = undefined; this.allowGarrisoning = new Map(); this.visibleGarrisonPoints = []; if (this.template.VisibleGarrisonPoints) { let points = this.template.VisibleGarrisonPoints; for (let point in points) this.visibleGarrisonPoints.push({ "offset": { "x": +points[point].X, "y": +points[point].Y, "z": +points[point].Z }, "angle": points[point].Angle ? +points[point].Angle * Math.PI / 180 : null, "entity": null }); } }; /** * @return {Object} max and min range at which entities can garrison the holder. */ GarrisonHolder.prototype.GetLoadingRange = function() { return { "max": +this.template.LoadingRange, "min": 0 }; }; GarrisonHolder.prototype.CanPickup = function(ent) { if (!this.template.Pickup || this.IsFull()) return false; let cmpOwner = Engine.QueryInterface(this.entity, IID_Ownership); return !!cmpOwner && IsOwnedByPlayer(cmpOwner.GetOwner(), ent); }; GarrisonHolder.prototype.GetEntities = function() { return this.entities; }; /** * @return {Array} unit classes which can be garrisoned inside this * particular entity. Obtained from the entity's template. */ GarrisonHolder.prototype.GetAllowedClasses = function() { return this.template.List._string; }; GarrisonHolder.prototype.GetCapacity = function() { return ApplyValueModificationsToEntity("GarrisonHolder/Max", +this.template.Max, this.entity); }; GarrisonHolder.prototype.IsFull = function() { return this.GetGarrisonedEntitiesCount() >= this.GetCapacity(); }; GarrisonHolder.prototype.GetHealRate = function() { return ApplyValueModificationsToEntity("GarrisonHolder/BuffHeal", +this.template.BuffHeal, this.entity); }; /** * Set this entity to allow or disallow garrisoning in the entity. * Every component calling this function should do it with its own ID, and as long as one * component doesn't allow this entity to garrison, it can't be garrisoned * When this entity already contains garrisoned soldiers, * these will not be able to ungarrison until the flag is set to true again. * * This more useful for modern-day features. For example you can't garrison or ungarrison * a driving vehicle or plane. * @param {boolean} allow - Whether the entity should be garrisonable. */ GarrisonHolder.prototype.AllowGarrisoning = function(allow, callerID) { this.allowGarrisoning.set(callerID, allow); }; GarrisonHolder.prototype.IsGarrisoningAllowed = function() { return Array.from(this.allowGarrisoning.values()).every(allow => allow); }; GarrisonHolder.prototype.GetGarrisonedEntitiesCount = function() { let count = this.entities.length; for (let ent of this.entities) { let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); if (cmpGarrisonHolder) count += cmpGarrisonHolder.GetGarrisonedEntitiesCount(); } return count; }; GarrisonHolder.prototype.IsAllowedToGarrison = function(ent) { if (!this.IsGarrisoningAllowed()) return false; if (!IsOwnedByMutualAllyOfEntity(ent, this.entity)) return false; let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity) return false; let entityClasses = cmpIdentity.GetClassesList(); return MatchesClassList(entityClasses, this.template.List._string) && !!Engine.QueryInterface(ent, IID_Garrisonable); }; /** * Garrison a unit inside. The timer for AutoHeal is started here. * @param {number} vgpEntity - The visual garrison point that will be used. * If vgpEntity is given, this visualGarrisonPoint will be used for the entity. * @return {boolean} Whether the entity was garrisonned. */ GarrisonHolder.prototype.Garrison = function(entity, vgpEntity) { let cmpPosition = Engine.QueryInterface(entity, IID_Position); if (!cmpPosition) return false; if (!this.PerformGarrison(entity)) return false; let visibleGarrisonPoint = vgpEntity; if (!visibleGarrisonPoint) for (let vgp of this.visibleGarrisonPoints) { if (vgp.entity) continue; visibleGarrisonPoint = vgp; break; } if (visibleGarrisonPoint) { visibleGarrisonPoint.entity = entity; // Angle of turrets: // Renamed entities (vgpEntity != undefined) should keep their angle. // Otherwise if an angle is given in the visibleGarrisonPoint, use it. // If no such angle given (usually walls for which outside/inside not well defined), we keep // the current angle as it was used for garrisoning and thus quite often was from inside to // outside, except when garrisoning from outWorld where we take as default PI. let cmpTurretPosition = Engine.QueryInterface(this.entity, IID_Position); if (!vgpEntity && visibleGarrisonPoint.angle != null) cmpPosition.SetYRotation(cmpTurretPosition.GetRotation().y + visibleGarrisonPoint.angle); else if (!vgpEntity && !cmpPosition.IsInWorld()) cmpPosition.SetYRotation(cmpTurretPosition.GetRotation().y + Math.PI); let cmpUnitMotion = Engine.QueryInterface(entity, IID_UnitMotion); if (cmpUnitMotion) cmpUnitMotion.SetFacePointAfterMove(false); cmpPosition.SetTurretParent(this.entity, visibleGarrisonPoint.offset); let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.SetTurretStance(); } else cmpPosition.MoveOutOfWorld(); return true; }; /** * @return {boolean} Whether the entity was garrisonned. */ GarrisonHolder.prototype.PerformGarrison = function(entity) { if (!this.HasEnoughHealth()) return false; // Check if the unit is allowed to be garrisoned inside the building if (!this.IsAllowedToGarrison(entity)) return false; // Check the capacity let extraCount = 0; let cmpGarrisonHolder = Engine.QueryInterface(entity, IID_GarrisonHolder); if (cmpGarrisonHolder) extraCount += cmpGarrisonHolder.GetGarrisonedEntitiesCount(); if (this.GetGarrisonedEntitiesCount() + extraCount >= this.GetCapacity()) return false; if (!this.timer && this.GetHealRate() > 0) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {}); } // Actual garrisoning happens here this.entities.push(entity); this.UpdateGarrisonFlag(); let cmpProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue); if (cmpProductionQueue) cmpProductionQueue.PauseProduction(); 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; }; /** * Simply eject the unit from the garrisoning entity without moving it * @param {number} entity - Id of the entity to be ejected. * @param {boolean} forced - Whether eject is forced (i.e. if building is destroyed). * @return {boolean} Whether the entity was ejected. */ GarrisonHolder.prototype.Eject = function(entity, forced) { let entityIndex = this.entities.indexOf(entity); // Error: invalid entity ID, usually it's already been ejected if (entityIndex == -1) return false; // Find spawning location let cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint); let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); // If the garrisonHolder is a sinking ship, restrict the location to the intersection of both passabilities // TODO: should use passability classes to be more generic let pos; if ((!cmpHealth || cmpHealth.GetHitpoints() == 0) && cmpIdentity && cmpIdentity.HasClass("Ship")) pos = cmpFootprint.PickSpawnPointBothPass(entity); else pos = cmpFootprint.PickSpawnPoint(entity); if (pos.y < 0) { // Error: couldn't find space satisfying the unit's passability criteria if (!forced) return false; // If ejection is forced, we need to continue, so use center of the building let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); pos = cmpPosition.GetPosition(); } this.entities.splice(entityIndex, 1); let cmpEntPosition = Engine.QueryInterface(entity, IID_Position); let cmpEntUnitAI = Engine.QueryInterface(entity, IID_UnitAI); for (let vgp of this.visibleGarrisonPoints) { if (vgp.entity != entity) continue; cmpEntPosition.SetTurretParent(INVALID_ENTITY, new Vector3D()); let cmpEntUnitMotion = Engine.QueryInterface(entity, IID_UnitMotion); if (cmpEntUnitMotion) cmpEntUnitMotion.SetFacePointAfterMove(true); if (cmpEntUnitAI) cmpEntUnitAI.ResetTurretStance(); vgp.entity = null; break; } if (cmpEntUnitAI) cmpEntUnitAI.Ungarrison(); let cmpEntProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue); if (cmpEntProductionQueue) cmpEntProductionQueue.UnpauseProduction(); 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); let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition) cmpEntPosition.SetYRotation(cmpPosition.GetPosition().horizAngleTo(pos)); Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": [entity] }); return true; }; /** * Ejects units and orders them to move to the rally point. If an ejection * with a given obstruction radius has failed, we won't try anymore to eject * entities with a bigger obstruction as that is compelled to also fail. * @param {Array} entities - An array containing the ids of the entities to eject. * @param {boolean} forced - Whether eject is forced (ie: if building is destroyed). * @return {boolean} Whether the entities were ejected. */ GarrisonHolder.prototype.PerformEject = function(entities, forced) { if (!this.IsGarrisoningAllowed() && !forced) return false; let ejectedEntities = []; let success = true; let failedRadius; let radius; let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); for (let entity of entities) { if (failedRadius !== undefined) { let cmpObstruction = Engine.QueryInterface(entity, IID_Obstruction); radius = cmpObstruction ? cmpObstruction.GetUnitRadius() : 0; if (radius >= failedRadius) continue; } if (this.Eject(entity, forced)) { let cmpEntOwnership = Engine.QueryInterface(entity, IID_Ownership); if (cmpOwnership && cmpEntOwnership && cmpOwnership.GetOwner() == cmpEntOwnership.GetOwner()) ejectedEntities.push(entity); } else { success = false; if (failedRadius !== undefined) failedRadius = Math.min(failedRadius, radius); else { let cmpObstruction = Engine.QueryInterface(entity, IID_Obstruction); failedRadius = cmpObstruction ? cmpObstruction.GetUnitRadius() : 0; } } } this.OrderWalkToRallyPoint(ejectedEntities); this.UpdateGarrisonFlag(); return success; }; /** * Order entities to walk to the rally point. * @param {Array} entities - An array containing all the ids of the entities. */ GarrisonHolder.prototype.OrderWalkToRallyPoint = function(entities) { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); let cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint); if (!cmpRallyPoint || !cmpRallyPoint.GetPositions()[0]) return; let commands = GetRallyPointCommands(cmpRallyPoint, entities); // Ignore the rally point if it is autogarrison if (commands[0].type == "garrison" && commands[0].target == this.entity) return; for (let command of commands) ProcessCommand(cmpOwnership.GetOwner(), command); }; /** * Unload unit from the garrisoning entity and order them * to move to the rally point. * @return {boolean} Whether the command was successful. */ GarrisonHolder.prototype.Unload = function(entity, forced) { return this.PerformEject([entity], forced); }; /** * Unload one or all units that match a template and owner from * the garrisoning entity and order them to move to the rally point. * @param {string} template - Type of units that should be ejected. * @param {number} owner - Id of the player whose units should be ejected. * @param {boolean} all - Whether all units should be ejected. * @param {boolean} forced - Whether unload is forced. * @return {boolean} Whether the unloading was successful. */ GarrisonHolder.prototype.UnloadTemplate = function(template, owner, all, forced) { let entities = []; let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); for (let entity of this.entities) { let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); // Units with multiple ranks are grouped together. let name = cmpIdentity.GetSelectionGroupName() || cmpTemplateManager.GetCurrentTemplateName(entity); if (name != template || owner != Engine.QueryInterface(entity, IID_Ownership).GetOwner()) continue; entities.push(entity); // If 'all' is false, only ungarrison the first matched unit. if (!all) break; } return this.PerformEject(entities, forced); }; /** * Unload all units, that belong to certain player * and order all own units to move to the rally point. * @param {boolean} forced - Whether unload is forced. * @param {number} owner - Id of the player whose units should be ejected. * @return {boolean} Whether the unloading was successful. */ GarrisonHolder.prototype.UnloadAllByOwner = function(owner, forced) { let entities = this.entities.filter(ent => { let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); return cmpOwnership && cmpOwnership.GetOwner() == owner; }); return this.PerformEject(entities, forced); }; /** * Unload all units from the entity and order them to move to the rally point. * @param {boolean} forced - Whether unload is forced. * @return {boolean} Whether the unloading was successful. */ GarrisonHolder.prototype.UnloadAll = function(forced) { return this.PerformEject(this.entities.slice(), forced); }; /** * Used to check if the garrisoning entity's health has fallen below * a certain limit after which all garrisoned units are unloaded. */ GarrisonHolder.prototype.OnHealthChanged = function(msg) { if (!this.HasEnoughHealth() && this.entities.length) this.EjectOrKill(this.entities.slice()); }; GarrisonHolder.prototype.HasEnoughHealth = function() { let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); return cmpHealth.GetHitpoints() > Math.floor(+this.template.EjectHealth * cmpHealth.GetMaxHitpoints()); }; /** * Called every second. Heals garrisoned units. */ GarrisonHolder.prototype.HealTimeout = function(data) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); if (!this.entities.length) { cmpTimer.CancelTimer(this.timer); this.timer = undefined; return; } for (let entity of this.entities) { let cmpHealth = Engine.QueryInterface(entity, IID_Health); if (cmpHealth && !cmpHealth.IsUnhealable()) cmpHealth.Increase(this.GetHealRate()); } this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {}); }; /** * Updates the garrison flag depending whether something is garrisoned in the entity. */ GarrisonHolder.prototype.UpdateGarrisonFlag = function() { let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SetVariant("garrison", this.entities.length ? "garrisoned" : "ungarrisoned"); }; /** * Cancel timer when destroyed. */ GarrisonHolder.prototype.OnDestroy = function() { if (this.timer) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); } }; /** * If a garrisoned entity is captured, or about to be killed (so its owner changes to '-1'), * remove it from the building so we only ever contain valid entities. */ GarrisonHolder.prototype.OnGlobalOwnershipChanged = function(msg) { // The ownership change may be on the garrisonholder if (this.entity == msg.entity) { let entities = this.entities.filter(ent => msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, ent)); if (entities.length) this.EjectOrKill(entities); return; } // or on some of its garrisoned units let entityIndex = this.entities.indexOf(msg.entity); if (entityIndex != -1) { // If the entity is dead, remove it directly instead of ejecting the corpse let cmpHealth = Engine.QueryInterface(msg.entity, IID_Health); if (cmpHealth && cmpHealth.GetHitpoints() == 0) { this.entities.splice(entityIndex, 1); Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": [msg.entity] }); this.UpdateGarrisonFlag(); for (let point of this.visibleGarrisonPoints) if (point.entity == msg.entity) point.entity = null; } else if (msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, msg.entity)) this.EjectOrKill([msg.entity]); } }; /** * Update list of garrisoned entities if one gets renamed (e.g. by promotion). */ GarrisonHolder.prototype.OnGlobalEntityRenamed = function(msg) { let entityIndex = this.entities.indexOf(msg.entity); if (entityIndex != -1) { let vgpRenamed; for (let vgp of this.visibleGarrisonPoints) { if (vgp.entity != msg.entity) continue; vgpRenamed = vgp; break; } this.Eject(msg.entity, true); this.Garrison(msg.newentity, vgpRenamed); } if (!this.initGarrison) return; // Update the pre-game garrison because of SkirmishReplacement if (msg.entity == this.entity) { let cmpGarrisonHolder = Engine.QueryInterface(msg.newentity, IID_GarrisonHolder); if (cmpGarrisonHolder) cmpGarrisonHolder.initGarrison = this.initGarrison; } else { entityIndex = this.initGarrison.indexOf(msg.entity); if (entityIndex != -1) this.initGarrison[entityIndex] = msg.newentity; } }; /** * Eject all foreign garrisoned entities which are no more allied. */ GarrisonHolder.prototype.OnDiplomacyChanged = function() { this.EjectOrKill(this.entities.filter(ent => !IsOwnedByMutualAllyOfEntity(this.entity, ent))); }; /** * Eject or kill a garrisoned unit which can no more be garrisoned * (garrisonholder's health too small or ownership changed). */ GarrisonHolder.prototype.EjectOrKill = function(entities) { let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); // Eject the units which can be ejected (if not in world, it generally means this holder // is inside a holder which kills its entities, so do not eject) if (cmpPosition && cmpPosition.IsInWorld()) { let ejectables = entities.filter(ent => this.IsEjectable(ent)); if (ejectables.length) this.PerformEject(ejectables, false); } // And destroy all remaining entities let killedEntities = []; for (let entity of entities) { let entityIndex = this.entities.indexOf(entity); if (entityIndex == -1) continue; let cmpHealth = Engine.QueryInterface(entity, IID_Health); if (cmpHealth) cmpHealth.Kill(); this.entities.splice(entityIndex, 1); killedEntities.push(entity); } if (killedEntities.length) Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": killedEntities }); this.UpdateGarrisonFlag(); }; GarrisonHolder.prototype.IsEjectable = function(entity) { if (!this.entities.find(ent => ent == entity)) return false; let ejectableClasses = this.template.EjectClassesOnDestroy._string; ejectableClasses = ejectableClasses ? ejectableClasses.split(/\s+/) : []; let entityClasses = Engine.QueryInterface(entity, IID_Identity).GetClassesList(); return ejectableClasses.some(ejectableClass => entityClasses.indexOf(ejectableClass) != -1); }; /** * Initialise the garrisoned units. */ GarrisonHolder.prototype.OnGlobalInitGame = function(msg) { if (!this.initGarrison) return; for (let ent of this.initGarrison) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI && cmpUnitAI.CanGarrison(this.entity) && this.Garrison(ent)) cmpUnitAI.Autogarrison(this.entity); } this.initGarrison = undefined; }; GarrisonHolder.prototype.OnValueModification = function(msg) { if (msg.component != "GarrisonHolder" || msg.valueNames.indexOf("GarrisonHolder/BuffHeal") == -1) return; if (this.timer && this.GetHealRate() == 0) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; } else if (!this.timer && this.GetHealRate() > 0) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {}); } }; Engine.RegisterComponentType(IID_GarrisonHolder, "GarrisonHolder", GarrisonHolder); Index: ps/trunk/binaries/data/mods/public/simulation/components/ModifiersManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ModifiersManager.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/ModifiersManager.js (revision 22767) @@ -0,0 +1,290 @@ +function ModifiersManager() {} + +ModifiersManager.prototype.Schema = + ""; + +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); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/ModifiersManager.js ___________________________________________________________________ Added: svn:executable ## -0,0 +1 ## +* \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/PlayerManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/PlayerManager.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/PlayerManager.js (revision 22767) @@ -1,144 +1,168 @@ function PlayerManager() {} PlayerManager.prototype.Schema = ""; PlayerManager.prototype.Init = function() { this.playerEntities = []; // list of player entity IDs }; PlayerManager.prototype.AddPlayer = function(ent) { var id = this.playerEntities.length; var cmpPlayer = Engine.QueryInterface(ent, IID_Player); cmpPlayer.SetPlayerID(id); this.playerEntities.push(ent); // initialize / update the diplomacy arrays var newDiplo = []; for (var i = 0; i < id; i++) { var cmpOtherPlayer = Engine.QueryInterface(this.GetPlayerByID(i), IID_Player); cmpOtherPlayer.diplomacy[id] = -1; newDiplo[i] = -1; } 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(); }; /** * Returns the player entity ID for the given player ID. * The player ID must be valid (else there will be an error message). */ PlayerManager.prototype.GetPlayerByID = function(id) { if (id in this.playerEntities) return this.playerEntities[id]; // All players at or below ID 0 get gaia-level data. (Observers for example) if (id <= 0) return this.playerEntities[0]; var stack = new Error().stack.trimRight().replace(/^/mg, ' '); // indent each line warn("GetPlayerByID: no player defined for id '"+id+"'\n"+stack); return INVALID_ENTITY; }; /** * Returns the number of players including gaia. */ PlayerManager.prototype.GetNumPlayers = function() { return this.playerEntities.length; }; /** * Returns IDs of all players including gaia. */ PlayerManager.prototype.GetAllPlayers = function() { let players = []; for (let i = 0; i < this.playerEntities.length; ++i) players.push(i); return players; }; /** * Returns IDs of all players excluding gaia. */ PlayerManager.prototype.GetNonGaiaPlayers = function() { let players = []; for (let i = 1; i < this.playerEntities.length; ++i) players.push(i); return players; }; /** * Returns IDs of all players excluding gaia that are not defeated nor have won. */ PlayerManager.prototype.GetActivePlayers = function() { return this.GetNonGaiaPlayers().filter(playerID => QueryPlayerIDInterface(playerID).GetState() == "active"); }; 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 = []; }; PlayerManager.prototype.RemoveLastPlayer = function() { if (this.playerEntities.length == 0) return; var lastId = this.playerEntities.pop(); + Engine.BroadcastMessage(MT_PlayerEntityChanged, { + "player": this.playerEntities.length + 1, + "from": lastId, + "to": INVALID_ENTITY + }); Engine.DestroyEntity(lastId); }; Engine.RegisterSystemComponentType(IID_PlayerManager, "PlayerManager", PlayerManager); Index: ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 22767) @@ -1,481 +1,366 @@ function TechnologyManager() {} 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. this.researchedTechs = new Set(); // Maps from technolgy name to the entityID of the researcher. this.researchQueued = new Map(); // 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":...} // Some technologies are automatically researched when their conditions are met. They have no cost and are // researched instantly. This allows civ bonuses and more complicated technologies. this.unresearchedAutoResearchTechs = new Set(); let allTechs = TechnologyTemplates.GetAll(); for (let key in allTechs) if (allTechs[key].autoResearch || allTechs[key].top) this.unresearchedAutoResearchTechs.add(key); }; TechnologyManager.prototype.OnUpdate = function() { this.UpdateAutoResearch(); }; // This function checks if the requirements of any autoresearch techs are met and if they are it researches them TechnologyManager.prototype.UpdateAutoResearch = function() { for (let key of this.unresearchedAutoResearchTechs) { let tech = TechnologyTemplates.Get(key); if ((tech.autoResearch && this.CanResearch(key)) || (tech.top && (this.IsTechnologyResearched(tech.top) || this.IsTechnologyResearched(tech.bottom)))) { this.unresearchedAutoResearchTechs.delete(key); this.ResearchTechnology(key); return; // We will have recursively handled any knock-on effects so can just return } } }; // Checks an entity template to see if its technology requirements have been met TechnologyManager.prototype.CanProduce = function (templateName) { var cmpTempManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempManager.GetTemplate(templateName); if (template.Identity && template.Identity.RequiredTechnology) return this.IsTechnologyResearched(template.Identity.RequiredTechnology); // If there is no required technology then this entity can be produced return true; }; TechnologyManager.prototype.IsTechnologyQueued = function(tech) { return this.researchQueued.has(tech); }; TechnologyManager.prototype.IsTechnologyResearched = function(tech) { return this.researchedTechs.has(tech); }; TechnologyManager.prototype.IsTechnologyStarted = function(tech) { return this.researchStarted.has(tech); }; // Checks the requirements for a technology to see if it can be researched at the current time TechnologyManager.prototype.CanResearch = function(tech) { let template = TechnologyTemplates.Get(tech); if (!template) { warn("Technology \"" + tech + "\" does not exist"); return false; } if (template.top && this.IsInProgress(template.top) || template.bottom && this.IsInProgress(template.bottom)) return false; if (template.pair && !this.CanResearch(template.pair)) return false; if (this.IsInProgress(tech)) return false; if (this.IsTechnologyResearched(tech)) return false; return this.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, Engine.QueryInterface(this.entity, IID_Player).GetCiv())); }; /** * Private function for checking a set of requirements is met * @param {object} reqs - Technology requirements as derived from the technology template by globalscripts * @param {boolean} civonly - True if only the civ requirement is to be checked * * @return true if the requirements pass, false otherwise */ TechnologyManager.prototype.CheckTechnologyRequirements = function(reqs, civonly = false) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!reqs) return false; if (civonly || !reqs.length) return true; return reqs.some(req => { return Object.keys(req).every(type => { switch (type) { case "techs": return req[type].every(this.IsTechnologyResearched, this); case "entities": return req[type].every(this.DoesEntitySpecPass, this); } return false; }); }); }; TechnologyManager.prototype.DoesEntitySpecPass = function(entity) { switch (entity.check) { case "count": if (!this.classCounts[entity.class] || this.classCounts[entity.class] < entity.number) return false; break; case "variants": if (!this.typeCountsByClass[entity.class] || Object.keys(this.typeCountsByClass[entity.class]).length < entity.number) return false; break; } return true; }; TechnologyManager.prototype.OnGlobalOwnershipChanged = function(msg) { // This automatically updates classCounts and typeCountsByClass var playerID = (Engine.QueryInterface(this.entity, IID_Player)).GetPlayerID(); if (msg.to == playerID) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity); var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (!cmpIdentity) return; var classes = cmpIdentity.GetClassesList(); // don't use foundations for the class counts but check if techs apply (e.g. health increase) if (!Engine.QueryInterface(msg.entity, IID_Foundation)) { for (let cls of classes) { this.classCounts[cls] = this.classCounts[cls] || 0; this.classCounts[cls] += 1; this.typeCountsByClass[cls] = this.typeCountsByClass[cls] || {}; this.typeCountsByClass[cls][template] = this.typeCountsByClass[cls][template] || 0; 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) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity); // don't use foundations for the class counts if (!Engine.QueryInterface(msg.entity, IID_Foundation)) { var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (cmpIdentity) { var classes = cmpIdentity.GetClassesList(); for (let cls of classes) { this.classCounts[cls] -= 1; if (this.classCounts[cls] <= 0) delete this.classCounts[cls]; this.typeCountsByClass[cls][template] -= 1; if (this.typeCountsByClass[cls][template] <= 0) delete this.typeCountsByClass[cls][template]; } } } - - this.clearModificationCache(msg.entity); } }; // Marks a technology as researched. Note that this does not verify that the requirements are met. TechnologyManager.prototype.ResearchTechnology = function(tech) { this.StoppedResearch(tech, false); 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) { for (var i of template.replaces) { if (!i || this.IsTechnologyResearched(i)) continue; this.researchedTechs.add(i); // Change the EntityLimit if any let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (cmpPlayer && cmpPlayer.GetPlayerID() !== undefined) { let playerID = cmpPlayer.GetPlayerID(); let cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits); if (cmpPlayerEntityLimits) cmpPlayerEntityLimits.UpdateLimitsFromTech(i); } } } this.UpdateAutoResearch(); var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!cmpPlayer || cmpPlayer.GetPlayerID() === undefined) return; var playerID = cmpPlayer.GetPlayerID(); var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var ents = cmpRangeManager.GetEntitiesByPlayer(playerID); ents.push(this.entity); // Change the EntityLimit if any var cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits); if (cmpPlayerEntityLimits) cmpPlayerEntityLimits.UpdateLimitsFromTech(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); }; /** * Marks a technology as being queued for research at the given entityID. */ TechnologyManager.prototype.QueuedResearch = function(tech, researcher) { this.researchQueued.set(tech, researcher); }; // Marks a technology as actively being researched TechnologyManager.prototype.StartedResearch = function(tech, notification) { this.researchStarted.add(tech); if (notification && tech.startsWith("phase")) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "phase", "players": [cmpPlayer.GetPlayerID()], "phaseName": tech, "phaseState": "started" }); } }; /** * Marks a technology as not being currently researched and optionally sends a GUI notification. */ TechnologyManager.prototype.StoppedResearch = function(tech, notification) { if (notification && tech.startsWith("phase") && this.researchStarted.has(tech)) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "phase", "players": [cmpPlayer.GetPlayerID()], "phaseName": tech, "phaseState": "aborted" }); } this.researchQueued.delete(tech); this.researchStarted.delete(tech); }; /** * Checks whether a technology is set to be researched. */ TechnologyManager.prototype.IsInProgress = function(tech) { return this.researchQueued.has(tech); }; /** * Returns the names of technologies that are currently being researched (non-queued). */ TechnologyManager.prototype.GetStartedTechs = function() { return this.researchStarted; }; /** * Gets the entity currently researching the technology. */ TechnologyManager.prototype.GetResearcher = function(tech) { return this.researchQueued.get(tech); }; /** * Called by GUIInterface for PlayerData. AI use. */ TechnologyManager.prototype.GetQueuedResearch = function() { return this.researchQueued; }; /** * Returns the names of technologies that have already been researched. */ TechnologyManager.prototype.GetResearchedTechs = function() { return this.researchedTechs; }; TechnologyManager.prototype.GetClassCounts = function() { return this.classCounts; }; TechnologyManager.prototype.GetTypeCountsByClass = function() { return this.typeCountsByClass; }; Engine.RegisterComponentType(IID_TechnologyManager, "TechnologyManager", TechnologyManager); Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ModifiersManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ModifiersManager.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ModifiersManager.js (revision 22767) @@ -0,0 +1 @@ +Engine.RegisterInterface("ModifiersManager"); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ModifiersManager.js ___________________________________________________________________ Added: svn:executable ## -0,0 +1 ## +* \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/PlayerManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/PlayerManager.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/PlayerManager.js (revision 22767) @@ -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"); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/PlayerManager.js ___________________________________________________________________ Added: svn:executable ## -0,0 +1 ## +* \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js (revision 22767) @@ -1,323 +1,323 @@ Engine.LoadHelperScript("DamageBonus.js"); 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"); Engine.LoadComponentScript("Attack.js"); let entityID = 903; function attackComponentTest(defenderClass, isEnemy, test_function) { ResetState(); { let playerEnt1 = 5; AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": () => playerEnt1 }); AddMock(playerEnt1, IID_Player, { "GetPlayerID": () => 1, "IsEnemy": () => isEnemy }); } let attacker = entityID; AddMock(attacker, IID_Position, { "IsInWorld": () => true, "GetHeightOffset": () => 5, "GetPosition2D": () => new Vector2D(1, 2) }); AddMock(attacker, IID_Ownership, { "GetOwner": () => 1 }); let cmpAttack = ConstructComponent(attacker, "Attack", { "Melee": { "Damage": { "Hack": 11, "Pierce": 5, "Crush": 0 }, "MinRange": 3, "MaxRange": 5, "PreferredClasses": { "_string": "FemaleCitizen" }, "RestrictedClasses": { "_string": "Elephant Archer" }, "Bonuses": { "BonusCav": { "Classes": "Cavalry", "Multiplier": 2 } } }, "Ranged": { "Damage": { "Hack": 0, "Pierce": 10, "Crush": 0 }, "MinRange": 10, "MaxRange": 80, "PrepareTime": 300, "RepeatTime": 500, "Projectile": { "Speed": 10, "Spread": 2, "Gravity": 1 }, "PreferredClasses": { "_string": "Archer" }, "RestrictedClasses": { "_string": "Elephant" }, "Splash": { "Shape": "Circular", "Range": 10, "FriendlyFire": "false", "Damage": { "Hack": 0.0, "Pierce": 15.0, "Crush": 35.0 }, "Bonuses": { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } } } }, "Capture": { "Capture": 8, "MaxRange": 10, }, "Slaughter": {} }); let defender = ++entityID; AddMock(defender, IID_Identity, { "GetClassesList": () => [defenderClass], "HasClass": className => className == defenderClass }); AddMock(defender, IID_Ownership, { "GetOwner": () => 1 }); AddMock(defender, IID_Position, { "IsInWorld": () => true, "GetHeightOffset": () => 0 }); AddMock(defender, IID_Health, { "GetHitpoints": () => 100 }); test_function(attacker, cmpAttack, defender); } // Validate template getter functions attackComponentTest(undefined, true, (attacker, cmpAttack, defender) => { TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(), ["Melee", "Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes([]), ["Melee", "Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "Ranged", "Capture"]), ["Melee", "Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "Ranged"]), ["Melee", "Ranged"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture"]), ["Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "!Melee"]), []); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["!Melee"]), ["Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["!Melee", "!Ranged"]), ["Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "!Ranged"]), ["Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "Melee", "!Ranged"]), ["Melee", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetPreferredClasses("Melee"), ["FemaleCitizen"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRestrictedClasses("Melee"), ["Elephant", "Archer"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetFullAttackRange(), { "min": 0, "max": 80 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Capture"), { "Capture": 8 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged"), { "Damage": { "Hack": 0, "Pierce": 10, "Crush": 0 } }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged", true), { "Damage": { "Hack": 0.0, "Pierce": 15.0, "Crush": 35.0 }, "Bonuses": { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } } }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Ranged"), { "prepare": 300, "repeat": 500 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Capture"), { "prepare": 0, "repeat": 1000 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetSplashDamage("Ranged"), { "attackData": { "Damage": { "Hack": 0, "Pierce": 15, "Crush": 35, }, "Bonuses": { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } } }, "friendlyFire": false, "shape": "Circular" }); }); for (let className of ["Infantry", "Cavalry"]) attackComponentTest(className, true, (attacker, cmpAttack, defender) => { TS_ASSERT_EQUALS(cmpAttack.GetAttackEffectsData("Melee").Bonuses.BonusCav.Multiplier, 2); TS_ASSERT_EQUALS(cmpAttack.GetAttackEffectsData("Capture").Bonuses || null, null); let getAttackBonus = (s, t, e, splash) => GetAttackBonus(s, e, t, cmpAttack.GetAttackEffectsData(t, splash).Bonuses || null); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Melee", defender), className == "Cavalry" ? 2 : 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged", defender), 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged", defender, true), className == "Cavalry" ? 3 : 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Capture", defender), 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Slaughter", defender), 1); }); // CanAttack rejects elephant attack due to RestrictedClasses attackComponentTest("Elephant", true, (attacker, cmpAttack, defender) => { TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), false); }); function testGetBestAttackAgainst(defenderClass, bestAttack, bestAllyAttack, isBuilding = false) { attackComponentTest(defenderClass, true, (attacker, cmpAttack, defender) => { if (isBuilding) AddMock(defender, IID_Capturable, { "CanCapture": playerID => { TS_ASSERT_EQUALS(playerID, 1); return true; } }); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, []), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged"]), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Melee"]), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "Capture"]), defenderClass != "Archer"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged", "Capture"]), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Ranged", "!Melee"]), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "!Melee"]), false); let allowCapturing = [true]; if (!isBuilding) allowCapturing.push(false); for (let ac of allowCapturing) TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAttack); }); attackComponentTest(defenderClass, false, (attacker, cmpAttack, defender) => { if (isBuilding) AddMock(defender, IID_Capturable, { "CanCapture": playerID => { TS_ASSERT_EQUALS(playerID, 1); return true; } }); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, []), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged"]), false); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Melee"]), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged", "Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Ranged", "!Melee"]), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "!Melee"]), false); let allowCapturing = [true]; if (!isBuilding) allowCapturing.push(false); for (let ac of allowCapturing) TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAllyAttack); }); } testGetBestAttackAgainst("FemaleCitizen", "Melee", undefined); testGetBestAttackAgainst("Archer", "Ranged", undefined); testGetBestAttackAgainst("Domestic", "Slaughter", "Slaughter"); testGetBestAttackAgainst("Structure", "Capture", "Capture", true); testGetBestAttackAgainst("Structure", "Ranged", undefined, false); function testPredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity) { ResetState(); let cmpAttack = ConstructComponent(1, "Attack", {}); let timeToTarget = cmpAttack.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity); if (timeToTarget === false) return; // Position of the target after that time. let targetPos = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition); // Time that the projectile need to reach it. let time = targetPos.horizDistanceTo(selfPosition) / horizSpeed; TS_ASSERT_EQUALS(timeToTarget.toFixed(1), time.toFixed(1)); } testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(0, 0, 0), new Vector3D(0, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(1, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(4, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(16, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-1, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-4, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-16, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 1)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 4)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 16)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(1, 0, 1)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(2, 0, 2)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(8, 0, 8)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-1, 0, 1)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-2, 0, 2)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-8, 0, 8)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(4, 0, 2)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-4, 0, 2)); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Auras.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Auras.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Auras.js (revision 22767) @@ -1,159 +1,162 @@ +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]; var playerState = ["active", "active", "active"]; var sourceEnt = 20; var targetEnt = 30; var auraRange = 40; var template = { "Identity": { "Classes": { "_string": "CorrectClass OtherClass" } } }; global.AuraTemplates = { "Get": name => { let template = { "type": name, "affectedPlayers": ["Ally"], "affects": ["CorrectClass"], "modifications": [{ "value": "Component/Value", "add": 10 }], "auraName": "name", "auraDescription": "description" }; if (name == "range") template.radius = auraRange; return template; } }; function testAuras(name, test_function) { ResetState(); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": idx => playerEnt[idx], "GetNumPlayers": () => 3, "GetAllPlayers": () => playerID }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "CreateActiveQuery": (ent, minRange, maxRange, players, iid, flags) => 1, "EnableActiveQuery": id => {}, "ResetActiveQuery": id => {}, "DisableActiveQuery": id => {}, "DestroyActiveQuery": id => {}, "GetEntityFlagMask": identifier => {}, "GetEntitiesByPlayer": id => [30, 31, 32] }); AddMock(playerEnt[1], IID_Player, { "IsAlly": id => id == playerID[1] || id == playerID[2], "IsEnemy": id => id != playerID[1] || id != playerID[2], "GetPlayerID": () => playerID[1], "GetState": () => playerState[1] }); AddMock(playerEnt[2], IID_Player, { "IsAlly": id => id == playerID[1] || id == playerID[2], "IsEnemy": id => id != playerID[1] || id != playerID[2], "GetPlayerID": () => playerID[2], "GetState": () => playerState[2] }); AddMock(targetEnt, IID_Identity, { "GetClassesList": () => ["CorrectClass", "OtherClass"] }); AddMock(sourceEnt, IID_Position, { "GetPosition2D": () => new Vector2D() }); if (name != "player" || playerEnt.indexOf(targetEnt) == -1) { AddMock(targetEnt, IID_Position, { "GetPosition2D": () => new Vector2D() }); AddMock(targetEnt, IID_Ownership, { "GetOwner": () => playerID[1] }); } if (playerEnt.indexOf(sourceEnt) == -1) AddMock(sourceEnt, IID_Ownership, { "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); } targetEnt = playerEnt[playerID[2]]; testAuras("player", (name, cmpAuras) => { TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); }); targetEnt = 30; // Test the case when the aura source is a player entity. sourceEnt = 11; testAuras("global", (name, cmpAuras) => { TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[1], template), 15); }); sourceEnt = 20; testAuras("range", (name, cmpAuras) => { cmpAuras.OnRangeUpdate({ "tag": 1, "added": [targetEnt], "removed": [] }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[1], template), 5); cmpAuras.OnRangeUpdate({ "tag": 1, "added": [], "removed": [targetEnt] }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5); }); testAuras("garrisonedUnits", (name, cmpAuras) => { cmpAuras.OnGarrisonedUnitsChanged({ "added": [targetEnt], "removed": [] }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); cmpAuras.OnGarrisonedUnitsChanged({ "added": [], "removed": [targetEnt] }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5); }); 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); }); testAuras("global", (name, cmpAuras) => { TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[1], template), 15); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[2], template), 15); AddMock(sourceEnt, IID_Ownership, { "GetOwner": () => -1 }); cmpAuras.OnOwnershipChanged({ "from": sourceEnt, "to": -1 }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[1], template), 5); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[2], template), 5); }); playerState[1] = "defeated"; testAuras("global", (name, cmpAuras) => { cmpAuras.OnGlobalPlayerDefeated({ "playerId": playerID[1] }); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[2], template), 5); }); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Capturable.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Capturable.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Capturable.js (revision 22767) @@ -1,200 +1,199 @@ 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"); var testData = { "structure": 20, "playerID": 1, "regenRate": 2, "garrisonedEntities": [30, 31, 32, 33], "garrisonRegenRate": 5, "decay": false, "decayRate": 30, "maxCp": 3000, "neighbours": [20, 0, 20, 10] }; function testCapturable(testData, test_function) { ResetState(); AddMock(SYSTEM_ENTITY, IID_Timer, { "SetInterval": (ent, iid, funcname, time, repeattime, data) => {}, "CancelTimer": timer => {} }); AddMock(testData.structure, IID_Ownership, { "GetOwner": () => testData.playerID, "SetOwner": id => {} }); AddMock(testData.structure, IID_GarrisonHolder, { "GetEntities": () => testData.garrisonedEntities }); AddMock(testData.structure, IID_Fogging, { "Activate": () => {} }); AddMock(10, IID_Player, { "IsEnemy": id => id != 0 }); AddMock(11, IID_Player, { "IsEnemy": id => id != 1 && id != 2 }); AddMock(12, IID_Player, { "IsEnemy": id => id != 1 && id != 2 }); AddMock(13, IID_Player, { "IsEnemy": id => id != 3 }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetNumPlayers": () => 4, "GetPlayerByID": id => 10 + id }); AddMock(testData.structure, IID_StatisticsTracker, { "LostEntity": () => {}, "CapturedBuilding": () => {} }); let cmpCapturable = ConstructComponent(testData.structure, "Capturable", { "CapturePoints": testData.maxCp, "RegenRate": testData.regenRate, "GarrisonRegenRate": testData.garrisonRegenRate }); AddMock(testData.structure, IID_TerritoryDecay, { "IsDecaying": () => testData.decay, "GetDecayRate": () => testData.decayRate, "GetConnectedNeighbours": () => testData.neighbours }); TS_ASSERT_EQUALS(cmpCapturable.GetRegenRate(), testData.regenRate + testData.garrisonRegenRate * testData.garrisonedEntities.length); test_function(cmpCapturable); Engine.PostMessage = (ent, iid, message) => {}; } // Tests initialisation of the capture points when the entity is created testCapturable(testData, cmpCapturable => { Engine.PostMessage = function(ent, iid, message) { TS_ASSERT_UNEVAL_EQUALS(message, { "regenerating": true, "regenRate": cmpCapturable.GetRegenRate(), "territoryDecay": 0 }); }; cmpCapturable.OnOwnershipChanged({ "from": INVALID_PLAYER, "to": testData.playerID }); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), [0, 3000, 0, 0]); }); // Tests if the message is sent when capture points change testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints([0, 2000, 0 , 1000]); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), [0, 2000, 0, 1000]); Engine.PostMessage = function(ent, iid, message) { TS_ASSERT_UNEVAL_EQUALS(message, { "capturePoints": [0, 2000, 0, 1000] }); }; cmpCapturable.RegisterCapturePointsChanged(); }); // Tests reducing capture points (after a capture attack or a decay) testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints([0, 2000, 0, 1000]); cmpCapturable.CheckTimer(); Engine.PostMessage = function(ent, iid, message) { if (iid == MT_CapturePointsChanged) TS_ASSERT_UNEVAL_EQUALS(message, { "capturePoints": [0, 2000 - 100, 0, 1000 + 100] }); if (iid == MT_CaptureRegenStateChanged) TS_ASSERT_UNEVAL_EQUALS(message, { "regenerating": true, "regenRate": cmpCapturable.GetRegenRate(), "territoryDecay": 0 }); }; TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.Reduce(100, 3), 100); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), [0, 2000 - 100, 0, 1000 + 100]); }); // Tests reducing capture points (after a capture attack or a decay) testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints([0, 2000, 0, 1000]); cmpCapturable.CheckTimer(); TS_ASSERT_EQUALS(cmpCapturable.Reduce(2500, 3), 2000); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), [0, 0, 0, 3000]); }); function testRegen(testData, cpIn, cpOut, regenerating) { testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints(cpIn); cmpCapturable.CheckTimer(); Engine.PostMessage = function(ent, iid, message) { if (iid == MT_CaptureRegenStateChanged) TS_ASSERT_UNEVAL_EQUALS(message.regenerating, regenerating); }; cmpCapturable.TimerTick(cpIn); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), cpOut); }); } // With our testData, the total regen rate is 22. That should be taken from the ennemies testRegen(testData, [12, 2950, 2, 36], [1, 2972, 2, 25], true); testRegen(testData, [0, 2994, 2, 4], [0, 2998, 2, 0], true); testRegen(testData, [0, 2998, 2, 0], [0, 2998, 2, 0], false); // If the regeneration rate becomes negative, capture points are given in favour of gaia testData.regenRate = -32; // With our testData, the total regen rate is -12. That should be taken from all players to gaia testRegen(testData, [100, 2800, 50, 50], [112, 2796, 46, 46], true); testData.regenRate = 2; function testDecay(testData, cpIn, cpOut) { testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints(cpIn); cmpCapturable.CheckTimer(); Engine.PostMessage = function(ent, iid, message) { if (iid == MT_CaptureRegenStateChanged) TS_ASSERT_UNEVAL_EQUALS(message.territoryDecay, testData.decayRate); }; cmpCapturable.TimerTick(); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), cpOut); }); } testData.decay = true; // With our testData, the decay rate is 30, that should be given to all neighbours with weights [20/50, 0, 20/50, 10/50], then it regens. testDecay(testData, [2900, 35, 10, 55], [2901, 27, 22, 50]); testData.decay = false; // Tests Reduce function testReduce(testData, amount, player, taken) { testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints([0, 2000, 0, 1000]); cmpCapturable.CheckTimer(); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.Reduce(amount, player), taken); }); } testReduce(testData, 50, 3, 50); testReduce(testData, 50, 2, 50); testReduce(testData, 50, 1, 50); testReduce(testData, -50, 3, 0); testReduce(testData, 50, 0, 50); testReduce(testData, 0, 3, 0); testReduce(testData, 1500, 3, 1500); testReduce(testData, 2000, 3, 2000); testReduce(testData, 3000, 3, 2000); // Test defeated player testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints([500, 1000, 0, 250]); cmpCapturable.OnGlobalPlayerDefeated({ "playerId": 3 }); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), [750, 1000, 0, 0]); }); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js (revision 22767) @@ -1,548 +1,547 @@ Engine.LoadHelperScript("DamageBonus.js"); Engine.LoadHelperScript("Attacking.js"); Engine.LoadHelperScript("Player.js"); 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"); 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"); Engine.LoadComponentScript("Timer.js"); function Test_Generic() { ResetState(); let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); cmpTimer.OnUpdate({ "turnLength": 1 }); let attacker = 11; let atkPlayerEntity = 1; let attackerOwner = 6; let cmpAttack = ConstructComponent(attacker, "Attack", { "Ranged": { "Damage": { "Crush": 5, }, "MaxRange": 50, "MinRange": 0, "Delay": 0, "Projectile": { "Speed": 75.0, "Spread": 0.5, "Gravity": 9.81, "LaunchPoint": { "@y": 3 } } } }); let damage = 5; let target = 21; let targetOwner = 7; let targetPos = new Vector3D(3, 0, 3); let type = "Melee"; let damageTaken = false; cmpAttack.GetAttackStrengths = attackType => ({ "Hack": 0, "Pierce": 0, "Crush": damage }); let data = { "type": "Melee", "attackData": { "Damage": { "Hack": 0, "Pierce": 0, "Crush": damage }, }, "target": target, "attacker": attacker, "attackerOwner": attackerOwner, "position": targetPos, "projectileId": 9, "direction": new Vector3D(1, 0, 0) }; AddMock(atkPlayerEntity, IID_Player, { "GetEnemies": () => [targetOwner] }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => atkPlayerEntity, "GetAllPlayers": () => [0, 1, 2, 3, 4] }); AddMock(SYSTEM_ENTITY, IID_ProjectileManager, { "RemoveProjectile": () => {}, "LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {}, }); AddMock(target, IID_Position, { "GetPosition": () => targetPos, "GetPreviousPosition": () => targetPos, "GetPosition2D": () => Vector2D.From(targetPos), "IsInWorld": () => true, }); AddMock(target, IID_Health, { "TakeDamage": (effectData, __, ___, bonusMultiplier) => { damageTaken = true; return { "killed": false, "HPchange": -bonusMultiplier * effectData.Crush }; }, }); AddMock(SYSTEM_ENTITY, IID_DelayedDamage, { "MissileHit": () => { damageTaken = true; }, }); Engine.PostMessage = function(ent, iid, message) { TS_ASSERT_UNEVAL_EQUALS({ "type": type, "target": target, "attacker": attacker, "attackerOwner": attackerOwner, "damage": damage, "capture": 0, "statusEffects": [] }, message); }; AddMock(target, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); AddMock(attacker, IID_Ownership, { "GetOwner": () => attackerOwner, }); AddMock(attacker, IID_Position, { "GetPosition": () => new Vector3D(2, 0, 3), "GetRotation": () => new Vector3D(1, 2, 3), "IsInWorld": () => true, }); function TestDamage() { cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT(damageTaken); damageTaken = false; } Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attacker, data.attackerOwner); TestDamage(); data.type = "Ranged"; type = data.type; Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attacker, data.attackerOwner); TestDamage(); // Check for damage still being dealt if the attacker dies cmpAttack.PerformAttack("Ranged", target); Engine.DestroyEntity(attacker); TestDamage(); atkPlayerEntity = 1; AddMock(atkPlayerEntity, IID_Player, { "GetEnemies": () => [2, 3] }); TS_ASSERT_UNEVAL_EQUALS(Attacking.GetPlayersToDamage(atkPlayerEntity, true), [0, 1, 2, 3, 4]); TS_ASSERT_UNEVAL_EQUALS(Attacking.GetPlayersToDamage(atkPlayerEntity, false), [2, 3]); } Test_Generic(); function TestLinearSplashDamage() { ResetState(); Engine.PostMessage = (ent, iid, message) => {}; const attacker = 50; const attackerOwner = 1; const origin = new Vector2D(0, 0); let data = { "type": "Ranged", "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } }, "attacker": attacker, "attackerOwner": attackerOwner, "origin": origin, "radius": 10, "shape": "Linear", "direction": new Vector3D(1, 747, 0), "playersToDamage": [2], }; let fallOff = function(x, y) { return (1 - x * x / (data.radius * data.radius)) * (1 - 25 * y * y / (data.radius * data.radius)); }; let hitEnts = new Set(); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [60, 61, 62], }); AddMock(60, IID_Position, { "GetPosition2D": () => new Vector2D(2.2, -0.4), }); AddMock(61, IID_Position, { "GetPosition2D": () => new Vector2D(0, 0), }); AddMock(62, IID_Position, { "GetPosition2D": () => new Vector2D(5, 2), }); AddMock(60, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(60); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(2.2, -0.4)); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(61, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(61); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(0, 0)); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(62, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(62); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 0); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); Attacking.CauseDamageOverArea(data); TS_ASSERT(hitEnts.has(60)); TS_ASSERT(hitEnts.has(61)); TS_ASSERT(hitEnts.has(62)); hitEnts.clear(); data.direction = new Vector3D(0.6, 747, 0.8); AddMock(60, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(60); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(1, 2)); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); Attacking.CauseDamageOverArea(data); TS_ASSERT(hitEnts.has(60)); TS_ASSERT(hitEnts.has(61)); TS_ASSERT(hitEnts.has(62)); hitEnts.clear(); } TestLinearSplashDamage(); function TestCircularSplashDamage() { ResetState(); Engine.PostMessage = (ent, iid, message) => {}; const radius = 10; let fallOff = function(r) { return 1 - r * r / (radius * radius); }; AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [60, 61, 62, 64], }); AddMock(60, IID_Position, { "GetPosition2D": () => new Vector2D(3, 4), }); AddMock(61, IID_Position, { "GetPosition2D": () => new Vector2D(0, 0), }); AddMock(62, IID_Position, { "GetPosition2D": () => new Vector2D(3.6, 3.2), }); AddMock(63, IID_Position, { "GetPosition2D": () => new Vector2D(10, -10), }); // Target on the frontier of the shape AddMock(64, IID_Position, { "GetPosition2D": () => new Vector2D(9, -4), }); AddMock(60, IID_Resistance, { "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(0)); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(61, IID_Resistance, { "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(5)); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(62, IID_Resistance, { "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(1)); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(63, IID_Resistance, { "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT(false); } }); AddMock(64, IID_Resistance, { "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 0); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); Attacking.CauseDamageOverArea({ "type": "Ranged", "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } }, "attacker": 50, "attackerOwner": 1, "origin": new Vector2D(3, 4), "radius": radius, "shape": "Circular", "playersToDamage": [2], }); } TestCircularSplashDamage(); function Test_MissileHit() { ResetState(); Engine.PostMessage = (ent, iid, message) => {}; let cmpDelayedDamage = ConstructComponent(SYSTEM_ENTITY, "DelayedDamage"); let target = 60; let targetOwner = 1; let targetPos = new Vector3D(3, 10, 0); let hitEnts = new Set(); AddMock(SYSTEM_ENTITY, IID_Timer, { "GetLatestTurnLength": () => 500 }); const radius = 10; let data = { "type": "Ranged", "attackData": { "Damage": { "Hack": 0, "Pierce": 100, "Crush": 0 } }, "target": 60, "attacker": 70, "attackerOwner": 1, "position": targetPos, "direction": new Vector3D(1, 0, 0), "projectileId": 9, }; AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => id == 1 ? 10 : 11, "GetAllPlayers": () => [0, 1] }); AddMock(SYSTEM_ENTITY, IID_ProjectileManager, { "RemoveProjectile": () => {}, "LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {}, }); AddMock(60, IID_Position, { "GetPosition": () => targetPos, "GetPreviousPosition": () => targetPos, "GetPosition2D": () => Vector2D.From(targetPos), "IsInWorld": () => true, }); AddMock(60, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(60); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(60, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); AddMock(70, IID_Ownership, { "GetOwner": () => 1, }); AddMock(70, IID_Position, { "GetPosition": () => new Vector3D(0, 0, 0), "GetRotation": () => new Vector3D(0, 0, 0), "IsInWorld": () => true, }); AddMock(10, IID_Player, { "GetEnemies": () => [2] }); cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(60)); hitEnts.clear(); // The main target is not hit but another one is hit. AddMock(60, IID_Position, { "GetPosition": () => new Vector3D(900, 10, 0), "GetPreviousPosition": () => new Vector3D(900, 10, 0), "GetPosition2D": () => new Vector2D(900, 0), "IsInWorld": () => true, }); AddMock(60, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT_EQUALS(false); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [61] }); AddMock(61, IID_Position, { "GetPosition": () => targetPos, "GetPreviousPosition": () => targetPos, "GetPosition2D": () => Vector2D.from3D(targetPos), "IsInWorld": () => true, }); AddMock(61, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(61); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(61, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); hitEnts.clear(); // Add a splash damage. data.splash = {}; data.splash.friendlyFire = false; data.splash.radius = 10; data.splash.shape = "Circular"; data.splash.attackData = { "Damage": { "Hack": 0, "Pierce": 0, "Crush": 200 } }; AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [61, 62] }); let dealtDamage = 0; AddMock(61, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(61); dealtDamage += mult * (effectData.Hack + effectData.Pierce + effectData.Crush); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(62, IID_Position, { "GetPosition": () => new Vector3D(8, 10, 0), "GetPreviousPosition": () => new Vector3D(8, 10, 0), "GetPosition2D": () => new Vector2D(8, 0), "IsInWorld": () => true, }); AddMock(62, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(62); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 200 * 0.75); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(62, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 200); dealtDamage = 0; TS_ASSERT(hitEnts.has(62)); hitEnts.clear(); // Add some hard counters bonus. Engine.DestroyEntity(62); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [61] }); let bonus= { "BonusCav": { "Classes": "Cavalry", "Multiplier": 400 } }; let splashBonus = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 10000 } }; AddMock(61, IID_Identity, { "HasClass": cl => cl == "Cavalry" }); data.attackData.Bonuses = bonus; cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 200); dealtDamage = 0; hitEnts.clear(); data.splash.attackData.Bonuses = splashBonus; cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); data.attackData.Bonuses = undefined; cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); data.attackData.Bonuses = null; cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); data.attackData.Bonuses = {}; cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); } Test_MissileHit(); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js (revision 22767) @@ -1,75 +1,74 @@ 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; let player = 1; ApplyValueModificationsToEntity = function(value, stat, ent) { if (value == "DeathDamage/Damage/Pierce" && ent == deadEnt) return stat + 200; return stat; }; let template = { "Shape": "Circular", "Range": 10.7, "FriendlyFire": "false", "Damage": { "Hack": 0.0, "Pierce": 15.0, "Crush": 35.0 } }; let effects = { "Damage": { "Hack": 0.0, "Pierce": 215.0, "Crush": 35.0 } }; let cmpDeathDamage = ConstructComponent(deadEnt, "DeathDamage", template); let playersToDamage = [2, 3, 7]; let pos = new Vector2D(3, 4.2); let result = { "type": "Death", "attackData": effects, "attacker": deadEnt, "attackerOwner": player, "origin": pos, "radius": template.Range, "shape": template.Shape, "playersToDamage": playersToDamage }; Attacking.CauseDamageOverArea = data => TS_ASSERT_UNEVAL_EQUALS(data, result); Attacking.GetPlayersToDamage = () => playersToDamage; AddMock(deadEnt, IID_Position, { "GetPosition2D": () => pos, "IsInWorld": () => true }); AddMock(deadEnt, IID_Ownership, { "GetOwner": () => player }); TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageEffects(), effects); cmpDeathDamage.CauseDeathDamage(); // Test splash damage bonus effects.Bonuses = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } }; template.Bonuses = effects.Bonuses; cmpDeathDamage = ConstructComponent(deadEnt, "DeathDamage", template); result.attackData.Bonuses = effects.Bonuses; TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageEffects(), effects); cmpDeathDamage.CauseDeathDamage(); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js (revision 22767) @@ -1,171 +1,170 @@ Engine.LoadHelperScript("ValueModification.js"); 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"); const garrisonedEntitiesList = [25, 26, 27, 28, 29, 30, 31, 32, 33]; const garrisonHolderId = 15; const unitToGarrisonId = 24; const enemyUnitId = 34; const player = 1; const friendlyPlayer = 2; const enemyPlayer = 3; AddMock(garrisonHolderId, IID_Footprint, { "PickSpawnPointBothPass": entity => new Vector3D(4, 3, 30), "PickSpawnPoint": entity => new Vector3D(4, 3, 30) }); AddMock(garrisonHolderId, IID_Ownership, { "GetOwner": () => player }); AddMock(garrisonHolderId, IID_Health, { "GetHitpoints": () => 50, "GetMaxHitpoints": () => 600 }); AddMock(player, IID_Player, { "IsAlly": id => true, "IsMutualAlly": id => true, "GetPlayerID": () => player }); AddMock(friendlyPlayer, IID_Player, { "IsAlly": id => true, "IsMutualAlly": id => true, "GetPlayerID": () => friendlyPlayer }); AddMock(SYSTEM_ENTITY, IID_Timer, { "SetTimeout": (ent, iid, funcname, time, data) => 1 }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => id }); for (let i = 24; i <= 34; ++i) { AddMock(i, IID_Identity, { "GetClassesList": () => "Infantry+Cavalry", "GetSelectionGroupName": () => "mace_infantry_archer_a" }); if (i < 28) AddMock(i, IID_Ownership, { "GetOwner": () => player }); else if (i == 34) AddMock(i, IID_Ownership, { "GetOwner": () => enemyPlayer }); else AddMock(i, IID_Ownership, { "GetOwner": () => friendlyPlayer }); AddMock(i, IID_Garrisonable, {}); AddMock(i, IID_Position, { "GetHeightOffset": () => 0, "GetPosition": () => new Vector3D(4, 3, 25), "GetRotation": () => new Vector3D(4, 0, 6), "GetTurretParent": () => INVALID_ENTITY, "IsInWorld": () => true, "JumpTo": (posX, posZ) => {}, "MoveOutOfWorld": () => {}, "SetTurretParent": (entity, offset) => {}, "SetHeightOffset": height => {} }); } AddMock(33, IID_Identity, { "GetClassesList": () => "Infantry+Cavalry", "GetSelectionGroupName": () => "spart_infantry_archer_a" }); let cmpGarrisonHolder = ConstructComponent(garrisonHolderId, "GarrisonHolder", { "Max": 10, "List": { "_string": "Infantry+Cavalry" }, "EjectHealth": 0.1, "EjectClassesOnDestroy": { "_string": "Infantry" }, "BuffHeal": 1, "LoadingRange": 2.1, "Pickup": false, "VisibleGarrisonPoints": { "archer1": { "X": 12, "Y": 5, "Z": 6 }, "archer2": { "X": 15, "Y": 5, "Z": 6 } } }); cmpGarrisonHolder.AllowGarrisoning(true, "callerID1"); cmpGarrisonHolder.AllowGarrisoning(false, 5); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(unitToGarrisonId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(unitToGarrisonId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsGarrisoningAllowed(), false); cmpGarrisonHolder.AllowGarrisoning(true, 5); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsGarrisoningAllowed(), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetLoadingRange(), { "max": 2.1, "min": 0 }); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetHealRate(), 1); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetAllowedClasses(), "Infantry+Cavalry"); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetCapacity(), 10); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 0); TS_ASSERT_EQUALS(cmpGarrisonHolder.CanPickup(unitToGarrisonId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.CanPickup(enemyUnitId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsAllowedToGarrison(enemyUnitId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsAllowedToGarrison(unitToGarrisonId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.HasEnoughHealth(), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.PerformGarrison(unitToGarrisonId), false); AddMock(garrisonHolderId, IID_Health, { "GetHitpoints": () => 600, "GetMaxHitpoints": () => 600 }); TS_ASSERT_EQUALS(cmpGarrisonHolder.HasEnoughHealth(), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(enemyUnitId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(unitToGarrisonId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Eject(unitToGarrisonId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(unitToGarrisonId), true); for (let entity of garrisonedEntitiesList) TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(entity), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.CanPickup(unitToGarrisonId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadTemplate("spart_infantry_archer_a", 2, false, false), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [24, 25, 26, 27, 28, 29, 30, 31, 32]); TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadAllByOwner(friendlyPlayer, false), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [24, 25, 26, 27]); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 4); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsEjectable(25), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(25), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsEjectable(25), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.PerformEject([25], false), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.PerformEject([], false), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [24, 26, 27]); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 3); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadAll(), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Health.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Health.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Health.js (revision 22767) @@ -1,150 +1,149 @@ -Engine.LoadComponentScript("interfaces/TechnologyManager.js"); -Engine.LoadComponentScript("interfaces/AuraManager.js"); +Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadHelperScript("Sound.js"); Engine.LoadComponentScript("interfaces/DeathDamage.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("Health.js"); const entity_id = 5; const corpse_id = entity_id + 1; const health_template = { "Max": 50, "RegenRate": 0, "IdleRegenRate": 0, "DeathType": "corpse", "Unhealable": false }; var injured_flag = false; var corpse_entity; function setEntityUp() { let cmpHealth = ConstructComponent(entity_id, "Health", health_template); AddMock(entity_id, IID_DeathDamage, { "CauseDeathDamage": () => {} }); AddMock(entity_id, IID_Position, { "IsInWorld": () => true, "GetPosition": () => ({ "x": 0, "z": 0 }), "GetRotation": () => ({ "x": 0, "y": 0, "z": 0 }) }); AddMock(entity_id, IID_Ownership, { "GetOwner": () => 1 }); AddMock(entity_id, IID_Visual, { "GetActorSeed": () => 1 }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "GetCurrentTemplateName": () => "test" }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "SetEntityFlag": (ent, flag, value) => (injured_flag = value) }); return cmpHealth; } var cmpHealth = setEntityUp(); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false); TS_ASSERT_EQUALS(cmpHealth.IsUnhealable(), true); var change = cmpHealth.Reduce(25); TS_ASSERT_EQUALS(injured_flag, true); TS_ASSERT_EQUALS(change.killed, false); TS_ASSERT_EQUALS(change.HPchange, -25); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 25); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), true); TS_ASSERT_EQUALS(cmpHealth.IsUnhealable(), false); change = cmpHealth.Increase(25); TS_ASSERT_EQUALS(injured_flag, false); TS_ASSERT_EQUALS(change.new, 50); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false); TS_ASSERT_EQUALS(cmpHealth.IsUnhealable(), true); // Check death. Engine.AddLocalEntity = function(template) { corpse_entity = template; AddMock(corpse_id, IID_Position, { "JumpTo": () => {}, "SetYRotation": () => {}, "SetXZRotation": () => {}, }); AddMock(corpse_id, IID_Ownership, { "SetOwner": () => {}, }); AddMock(corpse_id, IID_Visual, { "SetActorSeed": () => {}, "SelectAnimation": () => {}, }); return corpse_id; }; change = cmpHealth.Reduce(50); // Assert we create a corpse with the proper template. TS_ASSERT_EQUALS(corpse_entity, "corpse|test"); // Check that we are not marked as injured. TS_ASSERT_EQUALS(injured_flag, false); TS_ASSERT_EQUALS(change.killed, true); TS_ASSERT_EQUALS(change.HPchange, -50); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 0); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false); // Check that we can't be revived once dead. change = cmpHealth.Increase(25); TS_ASSERT_EQUALS(change.new, 0); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 0); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false); // Check that we can't die twice. change = cmpHealth.Reduce(50); TS_ASSERT_EQUALS(change.killed, false); TS_ASSERT_EQUALS(change.HPchange, 0); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 0); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false); cmpHealth = setEntityUp(); // Check that we still die with > Max HP of damage. change = cmpHealth.Reduce(60); TS_ASSERT_EQUALS(change.killed, true); TS_ASSERT_EQUALS(change.HPchange, -50); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 0); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false); cmpHealth = setEntityUp(); // Check that increasing by more than required puts us at the max HP change = cmpHealth.Reduce(30); change = cmpHealth.Increase(30); TS_ASSERT_EQUALS(injured_flag, false); TS_ASSERT_EQUALS(change.new, 50); TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50); TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false); TS_ASSERT_EQUALS(cmpHealth.IsUnhealable(), true); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ModificationsManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ModificationsManager.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ModificationsManager.js (revision 22767) @@ -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); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ModificationsManager.js ___________________________________________________________________ Added: svn:executable ## -0,0 +1 ## +* \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Pack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Pack.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Pack.js (revision 22767) @@ -1,160 +1,159 @@ 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"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Pack.js"); Engine.LoadComponentScript("interfaces/Player.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); Engine.LoadComponentScript("interfaces/ResourceGatherer.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("Pack.js"); Engine.RegisterGlobal("MT_EntityRenamed", "entityRenamed"); const ent = 170; const newEnt = 171; const PACKING_INTERVAL = 250; let timerActivated = false; AddMock(ent, IID_Visual, { "SelectAnimation": (name, once, speed) => name }); AddMock(ent, IID_Ownership, { "GetOwner": () => 1 }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => 11 }); AddMock(11, IID_Player, { "GetTimeMultiplier": () => 1 }); AddMock(ent, IID_Sound, { "PlaySoundGroup": name => {} }); AddMock(SYSTEM_ENTITY, IID_Timer, { "CancelTimer": id => { timerActivated = false; return; }, "SetInterval": (ent, iid, funcname, time, repeattime, data) => { timerActivated = true; return 7; } }); Engine.AddEntity = function(template) { TS_ASSERT_EQUALS(template, "finalTemplate"); return true; }; // Test Packing let template = { "Entity": "finalTemplate", "Time": "2000", "State": "unpacked" }; let cmpPack = ConstructComponent(ent, "Pack", template); // Check internals TS_ASSERT(!cmpPack.packed); TS_ASSERT(!cmpPack.packing); TS_ASSERT_EQUALS(cmpPack.elapsedTime, 0); TS_ASSERT_EQUALS(cmpPack.timer, undefined); TS_ASSERT(!cmpPack.IsPacked()); TS_ASSERT(!cmpPack.IsPacking()); TS_ASSERT_EQUALS(cmpPack.GetProgress(), 0); // Pack cmpPack.Pack(); TS_ASSERT_EQUALS(cmpPack.timer, 7); TS_ASSERT(cmpPack.IsPacking()); // Listen to destroy message cmpPack.OnDestroy(); TS_ASSERT(!cmpPack.timer); TS_ASSERT(!timerActivated); // Test UnPacking template = { "Entity": "finalTemplate", "Time": "2000", "State": "packed" }; cmpPack = ConstructComponent(ent, "Pack", template); // Check internals TS_ASSERT(cmpPack.packed); TS_ASSERT(!cmpPack.packing); TS_ASSERT_EQUALS(cmpPack.elapsedTime, 0); TS_ASSERT_EQUALS(cmpPack.timer, undefined); TS_ASSERT(cmpPack.IsPacked()); TS_ASSERT(!cmpPack.IsPacking()); TS_ASSERT_EQUALS(cmpPack.GetProgress(), 0); // Unpack cmpPack.Unpack(); TS_ASSERT(cmpPack.IsPacking()); TS_ASSERT_EQUALS(cmpPack.timer, 7); // Unpack progress cmpPack.elapsedTime = 400; cmpPack.PackProgress({}, 100); TS_ASSERT(cmpPack.IsPacking()); TS_ASSERT_EQUALS(cmpPack.GetElapsedTime(), 400 + 100 + PACKING_INTERVAL); TS_ASSERT_EQUALS(cmpPack.GetProgress(), (400 + 100 + PACKING_INTERVAL) / 2000); // Try to Pack or Unpack while packing, nothing happen cmpPack.elapsedTime = 400; cmpPack.Unpack(); TS_ASSERT(cmpPack.IsPacking()); TS_ASSERT_EQUALS(cmpPack.GetElapsedTime(), 400); TS_ASSERT_EQUALS(cmpPack.timer, 7); TS_ASSERT(timerActivated); cmpPack.Pack(); TS_ASSERT(cmpPack.IsPacking()); TS_ASSERT_EQUALS(cmpPack.GetElapsedTime(), 400); TS_ASSERT_EQUALS(cmpPack.timer, 7); TS_ASSERT(timerActivated); // Cancel cmpPack.CancelPack(); TS_ASSERT(!cmpPack.IsPacking()); TS_ASSERT_EQUALS(cmpPack.GetElapsedTime(), 0); TS_ASSERT_EQUALS(cmpPack.GetProgress(), 0); TS_ASSERT_EQUALS(cmpPack.timer, undefined); TS_ASSERT(!timerActivated); // Progress until completing cmpPack.Unpack(); cmpPack.elapsedTime = 1800; cmpPack.PackProgress({}, 100); TS_ASSERT(cmpPack.IsPacking()); TS_ASSERT_EQUALS(cmpPack.GetElapsedTime(), 1800 + 100 + PACKING_INTERVAL); // Cap progress at 100% TS_ASSERT_EQUALS(cmpPack.GetProgress(), 1); TS_ASSERT_EQUALS(cmpPack.timer, 7); TS_ASSERT(timerActivated); // Unpack completing cmpPack.Unpack(); cmpPack.elapsedTime = 2100; cmpPack.PackProgress({}, 100); TS_ASSERT(!cmpPack.IsPacking()); TS_ASSERT(!cmpPack.timer); TS_ASSERT(!timerActivated); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Player.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Player.js (revision 22767) @@ -1,67 +1,66 @@ Resources = { "GetCodes": () => ["food", "metal", "stone", "wood"], "GetResource": () => ({}), "BuildSchema": (type) => { let schema = ""; for (let res of Resources.GetCodes()) schema += "" + "" + "" + "" + ""; return "" + schema + ""; } }; 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", { "SpyCostMultiplier": 1, "BarterMultiplier": { "Buy": { "wood": 1.0, "stone": 1.0, "metal": 1.0 }, "Sell": { "wood": 1.0, "stone": 1.0, "metal": 1.0 } }, }); TS_ASSERT_EQUALS(cmpPlayer.GetPopulationCount(), 0); TS_ASSERT_EQUALS(cmpPlayer.GetPopulationLimit(), 0); cmpPlayer.SetDiplomacy([-1, 1, 0, 1, -1]); TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetAllies(), [1, 3]); TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetEnemies(), [0, 4]); var diplo = cmpPlayer.GetDiplomacy(); diplo[0] = 1; TS_ASSERT(cmpPlayer.IsEnemy(0)); diplo = [1, 1, 0]; cmpPlayer.SetDiplomacy(diplo); diplo[1] = -1; TS_ASSERT(cmpPlayer.IsAlly(1)); TS_ASSERT_EQUALS(cmpPlayer.GetSpyCostMultiplier(), 1); TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetBarterMultiplier(), { "buy": { "wood": 1.0, "stone": 1.0, "metal": 1.0 }, "sell": { "wood": 1.0, "stone": 1.0, "metal": 1.0 } }); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Technologies_effects.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Technologies_effects.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Technologies_effects.js (revision 22767) @@ -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"); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Technologies_effects.js ___________________________________________________________________ Added: svn:executable ## -0,0 +1 ## +* \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Technologies_reqs.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Technologies_reqs.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Technologies_reqs.js (revision 22767) @@ -0,0 +1,583 @@ +// TODO: Move this to a folder of tests for GlobalScripts (once one is created) + +// No requirements set in template +let template = {}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), []); + +/** + * First, the basics: + */ + +// Technology Requirement +template.requirements = { "tech": "expected_tech" }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["expected_tech"] }]); + +// Entity Requirement: Count of entities matching given class +template.requirements = { "entity": { "class": "Village", "number": 5 } }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "entities": [{ "class": "Village", "number": 5, "check": "count" }] }]); + +// Entity Requirement: Count of entities matching given class +template.requirements = { "entity": { "class": "Village", "numberOfTypes": 5 } }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "entities": [{ "class": "Village", "number": 5, "check": "variants" }] }]); + +// Single `civ` +template.requirements = { "civ": "athen" }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), []); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), false); + +// Single `notciv` +template.requirements = { "notciv": "athen" }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), []); + + +/** + * Basic `all`s: + */ + +// Multiple techs +template.requirements = { "all": [{ "tech": "tech_A" }, { "tech": "tech_B" }, { "tech": "tech_C" }] }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["tech_A", "tech_B", "tech_C"] }]); + +// Multiple entity definitions +template.requirements = { + "all": [ + { "entity": { "class": "class_A", "number": 5 } }, + { "entity": { "class": "class_B", "number": 5 } }, + { "entity": { "class": "class_C", "number": 5 } } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), + [{ "entities": [{ "class": "class_A", "number": 5, "check": "count" }, { "class": "class_B", "number": 5, "check": "count" }, { "class": "class_C", "number": 5, "check": "count" }] }]); + +// A `tech` and an `entity` +template.requirements = { "all": [{ "tech": "tech_A" }, { "entity": { "class": "class_B", "number": 5, "check": "count" } }] }; +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" }] }; +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" }] }; +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); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civ_D"), []); + +// A `civ` with a tech/entity +template.requirements = { "all": [{ "civ": "athen" }, { "tech": "expected_tech" }] }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["expected_tech"] }]); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), false); + +template.requirements = { "all": [{ "civ": "athen" }, { "entity": { "class": "Village", "number": 5 } }] }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "entities": [{ "class": "Village", "number": 5, "check": "count" }] }]); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), false); + +template.requirements = { "all": [{ "civ": "athen" }, { "entity": { "class": "Village", "numberOfTypes": 5 } }] }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "entities": [{ "class": "Village", "number": 5, "check": "variants" }] }]); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), false); + +// A `notciv` with a tech/entity +template.requirements = { "all": [{ "notciv": "athen" }, { "tech": "expected_tech" }] }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), [{ "techs": ["expected_tech"] }]); + +template.requirements = { "all": [{ "notciv": "athen" }, { "entity": { "class": "Village", "number": 5 } }] }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), [{ "entities": [{ "class": "Village", "number": 5, "check": "count" }] }]); + +template.requirements = { "all": [{ "notciv": "athen" }, { "entity": { "class": "Village", "numberOfTypes": 5 } }] }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), [{ "entities": [{ "class": "Village", "number": 5, "check": "variants" }] }]); + + +/** + * Basic `any`s: + */ + +// Multiple techs +template.requirements = { "any": [{ "tech": "tech_A" }, { "tech": "tech_B" }, { "tech": "tech_C" }] }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["tech_A"] }, { "techs": ["tech_B"] }, { "techs": ["tech_C"] }]); + +// Multiple entity definitions +template.requirements = { + "any": [ + { "entity": { "class": "class_A", "number": 5 } }, + { "entity": { "class": "class_B", "number": 5 } }, + { "entity": { "class": "class_C", "number": 5 } } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [ + { "entities": [{ "class": "class_A", "number": 5, "check": "count" }] }, + { "entities": [{ "class": "class_B", "number": 5, "check": "count" }] }, + { "entities": [{ "class": "class_C", "number": 5, "check": "count" }] } +]); + +// A tech or an entity +template.requirements = { "any": [{ "tech": "tech_A" }, { "entity": { "class": "class_B", "number": 5, "check": "count" } }] }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["tech_A"] }, { "entities": [{ "class": "class_B", "number": 5, "check": "count" }] }]); + +// Multiple `civ`s +template.requirements = { "any": [{ "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 = { "any": [{ "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); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civ_D"), []); + +// A `civ` or a tech/entity +template.requirements = { "any": [{ "civ": "athen" }, { "tech": "expected_tech" }] }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), []); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), [{ "techs": ["expected_tech"] }]); + +template.requirements = { "any": [{ "civ": "athen" }, { "entity": { "class": "Village", "number": 5 } }] }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), []); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), [{ "entities": [{ "class": "Village", "number": 5, "check": "count" }] }]); + +template.requirements = { "any": [{ "civ": "athen" }, { "entity": { "class": "Village", "numberOfTypes": 5 } }] }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), []); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), [{ "entities": [{ "class": "Village", "number": 5, "check": "variants" }] }]); + +// A `notciv` or a tech +template.requirements = { "any": [{ "notciv": "athen" }, { "tech": "expected_tech" }] }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), [{ "techs": ["expected_tech"] }]); + +template.requirements = { "any": [{ "notciv": "athen" }, { "entity": { "class": "Village", "number": 5 } }] }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), [{ "entities": [{ "class": "Village", "number": 5, "check": "count" }] }]); + +template.requirements = { "any": [{ "notciv": "athen" }, { "entity": { "class": "Village", "numberOfTypes": 5 } }] }; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "spart"), [{ "entities": [{ "class": "Village", "number": 5, "check": "variants" }] }]); + + +/** + * Complicated `all`s, part 1 - an `all` inside an `all`: + */ + +// Techs +template.requirements = { + "all": [ + { "all": [{ "tech": "tech_A" }, { "tech": "tech_B" }] }, + { "all": [{ "tech": "tech_C" }, { "tech": "tech_D" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["tech_A", "tech_B", "tech_C", "tech_D"] }]); + +// Techs and entities +template.requirements = { + "all": [ + { "all": [{ "tech": "tech_A" }, { "entity": { "class": "class_A", "number": 5 } }] }, + { "all": [{ "entity": { "class": "class_B", "numberOfTypes": 5 } }, { "tech": "tech_B" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ + "techs": ["tech_A", "tech_B"], + "entities": [{ "class": "class_A", "number": 5, "check": "count" }, { "class": "class_B", "number": 5, "check": "variants" }] +}]); + +// Two `civ`s, without and with a tech +template.requirements = { + "all": [ + { "all": [{ "civ": "athen" }, { "civ": "spart" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), []); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), false); + +template.requirements = { + "all": [ + { "tech": "required_tech" }, + { "all": [{ "civ": "athen" }, { "civ": "spart" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["required_tech"] }]); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), false); + +// Two `notciv`s, without and with a tech +template.requirements = { + "all": [ + { "all": [{ "notciv": "athen" }, { "notciv": "spart" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), []); + +template.requirements = { + "all": [ + { "tech": "required_tech" }, + { "all": [{ "notciv": "athen" }, { "notciv": "spart" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), [{ "techs": ["required_tech"] }]); + +// Inner `all` has a tech and a `civ`/`notciv` +template.requirements = { + "all": [ + { "all": [{ "tech": "tech_A" }, { "civ": "maur" }] }, + { "tech": "tech_B" } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["tech_B"] }]); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), [{ "techs": ["tech_A", "tech_B"] }]); + +template.requirements = { + "all": [ + { "all": [{ "tech": "tech_A" }, { "notciv": "maur" }] }, + { "tech": "tech_B" } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["tech_A", "tech_B"] }]); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), [{ "techs": ["tech_B"] }]); + + +/** + * Complicated `all`s, part 2 - an `any` inside an `all`: + */ + +// Techs +template.requirements = { + "all": [ + { "any": [{ "tech": "tech_A" }, { "tech": "tech_B" }] }, + { "any": [{ "tech": "tech_C" }, { "tech": "tech_D" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [ + { "techs": ["tech_A", "tech_C"] }, + { "techs": ["tech_A", "tech_D"] }, + { "techs": ["tech_B", "tech_C"] }, + { "techs": ["tech_B", "tech_D"] } +]); + +// Techs and entities +template.requirements = { + "all": [ + { "any": [{ "tech": "tech_A" }, { "entity": { "class": "class_A", "number": 5 } }] }, + { "any": [{ "entity": { "class": "class_B", "numberOfTypes": 5 } }, { "tech": "tech_B" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [ + { "techs": ["tech_A"], "entities": [{ "class": "class_B", "number": 5, "check": "variants" }] }, + { "techs": ["tech_A", "tech_B"] }, + { "entities": [{ "class": "class_A", "number": 5, "check": "count" }, { "class": "class_B", "number": 5, "check": "variants" }] }, + { "entities": [{ "class": "class_A", "number": 5, "check": "count" }], "techs": ["tech_B"] } +]); + +// Two `civ`s, without and with a tech +template.requirements = { + "all": [ + { "any": [{ "civ": "athen" }, { "civ": "spart" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), []); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), false); + +template.requirements = { + "all": [ + { "tech": "required_tech" }, + { "any": [{ "civ": "athen" }, { "civ": "spart" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["required_tech"] }]); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), false); + +// Two `notciv`s, without and with a tech +template.requirements = { + "all": [ + { "any": [{ "notciv": "athen" }, { "notciv": "spart" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), []); + +template.requirements = { + "all": [ + { "tech": "required_tech" }, + { "any": [{ "notciv": "athen" }, { "notciv": "spart" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), [{ "techs": ["required_tech"] }]); + + +/** + * Complicated `any`s, part 1 - an `all` inside an `any`: + */ + +// Techs +template.requirements = { + "any": [ + { "all": [{ "tech": "tech_A" }, { "tech": "tech_B" }] }, + { "all": [{ "tech": "tech_C" }, { "tech": "tech_D" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [ + { "techs": ["tech_A", "tech_B"] }, + { "techs": ["tech_C", "tech_D"] } +]); + +// Techs and entities +template.requirements = { + "any": [ + { "all": [{ "tech": "tech_A" }, { "entity": { "class": "class_A", "number": 5 } }] }, + { "all": [{ "entity": { "class": "class_B", "numberOfTypes": 5 } }, { "tech": "tech_B" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [ + { "techs": ["tech_A"], "entities": [{ "class": "class_A", "number": 5, "check": "count" }] }, + { "entities": [{ "class": "class_B", "number": 5, "check": "variants" }], "techs": ["tech_B"] } +]); + +// Two `civ`s, without and with a tech +template.requirements = { + "any": [ + { "all": [{ "civ": "athen" }, { "civ": "spart" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), []); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), false); + +template.requirements = { + "any": [ + { "tech": "required_tech" }, + { "all": [{ "civ": "athen" }, { "civ": "spart" }] } + ] +}; +// Note: these requirements don't really make sense, as the `any` makes the `civ`s in the the inner `all` irrelevant. +// We test it anyway as a precursor to later tests. +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["required_tech"] }]); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), [{ "techs": ["required_tech"] }]); + +// Two `notciv`s, without and with a tech +template.requirements = { + "any": [ + { "all": [{ "notciv": "athen" }, { "notciv": "spart" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), []); + +template.requirements = { + "any": [ + { "tech": "required_tech" }, + { "all": [{ "notciv": "athen" }, { "notciv": "spart" }] } + ] +}; +// Note: these requirements have a result that might seen unexpected at first glance. +// This is because the `notciv`s are rendered irrelevant by the `any`, and they have nothing else to operate on. +// We test it anyway as a precursor for later tests. +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["required_tech"] }]); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), [{ "techs": ["required_tech"] }]); + +// Inner `all` has a tech and a `civ`/`notciv` +template.requirements = { + "any": [ + { "all": [{ "civ": "civA" }, { "tech": "tech1" }] }, + { "tech": "tech2" } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civA"), [{ "techs": ["tech1"] }, { "techs": ["tech2"] }]); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civB"), [{ "techs": ["tech2"] }]); + +template.requirements = { + "any": [ + { "all": [{ "notciv": "civA" }, { "tech": "tech1" }] }, + { "tech": "tech2" } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civA"), [{ "techs": ["tech2"] }]); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civB"), [{ "techs": ["tech1"] }, { "techs": ["tech2"] }]); + + +/** + * Complicated `any`s, part 2 - an `any` inside an `any`: + */ + +// Techs +template.requirements = { + "any": [ + { "any": [{ "tech": "tech_A" }, { "tech": "tech_B" }] }, + { "any": [{ "tech": "tech_C" }, { "tech": "tech_D" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [ + { "techs": ["tech_A"] }, + { "techs": ["tech_B"] }, + { "techs": ["tech_C"] }, + { "techs": ["tech_D"] } +]); + +// Techs and entities +template.requirements = { + "any": [ + { "any": [{ "tech": "tech_A" }, { "entity": { "class": "class_A", "number": 5 } }] }, + { "any": [{ "entity": { "class": "class_B", "numberOfTypes": 5 } }, { "tech": "tech_B" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [ + { "techs": ["tech_A"] }, + { "entities": [{ "class": "class_A", "number": 5, "check": "count" }] }, + { "entities": [{ "class": "class_B", "number": 5, "check": "variants" }] }, + { "techs": ["tech_B"] } +]); + +// Two `civ`s, without and with a tech +template.requirements = { + "any": [ + { "any": [{ "civ": "athen" }, { "civ": "spart" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), []); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), false); + +template.requirements = { + "any": [ + { "tech": "required_tech" }, + { "any": [{ "civ": "athen" }, { "civ": "spart" }] } + ] +}; +// These requirements may not make sense, as the `civ`s are unable to restrict the requirements due to the outer `any` +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["required_tech"] }]); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), [{ "techs": ["required_tech"] }]); + +// Two `notciv`s, without and with a tech +template.requirements = { + "any": [ + { "any": [{ "notciv": "athen" }, { "notciv": "spart" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), false); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), []); + +template.requirements = { + "any": [ + { "tech": "required_tech" }, + { "any": [{ "notciv": "athen" }, { "notciv": "spart" }] } + ] +}; +// These requirements may not make sense, as the `notciv`s are made irrelevant by the outer `any` +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["required_tech"] }]); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "maur"), [{ "techs": ["required_tech"] }]); + + +/** + * Further tests + */ + +template.requirements = { + "all": [ + { "tech": "tech1" }, + { "any": [{ "civ": "civA" }, { "civ": "civB" }] }, + { "notciv": "civC" } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civA"), [{ "techs": ["tech1"] }]); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civC"), false); + +template.requirements = { + "any": [ + { "all": [{ "civ": "civA" }, { "tech": "tech1" }] }, + { "all": [{ "civ": "civB" }, { "tech": "tech2" }] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civA"), [{ "techs": ["tech1"] }]); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civB"), [{ "techs": ["tech2"] }]); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civC"), false); + +template.requirements = { + "any": [ + { "all": [{ "civ": "civA" }, { "tech": "tech1" }] }, + { "all": [ + { "any": [{ "civ": "civB" }, { "civ": "civC" }] }, + { "tech": "tech2" } + ] } + ] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civA"), [{ "techs": ["tech1"] }]); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civC"), [{ "techs": ["tech2"] }]); +TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civD"), false); + +// Test DeriveModificationsFromTech +template = { + "modifications": [{ + "value": "ResourceGatherer/Rates/food.grain", + "multiply": 15, + "affects": "Spear Sword" + }, + { + "value": "ResourceGatherer/Rates/food.meat", + "multiply": 10 + }], + "affects": ["Female", "CitizenSoldier Melee"] +}; +let techMods = { + "ResourceGatherer/Rates/food.grain": [{ + "affects": [ + ["Female", "Spear", "Sword"], + ["CitizenSoldier", "Melee", "Spear", "Sword"] + ], + "multiply": 15 + }], + "ResourceGatherer/Rates/food.meat": [{ + "affects": [ + ["Female"], + ["CitizenSoldier", "Melee"] + ], + "multiply": 10 + }] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveModificationsFromTech(template), techMods); + +template = { + "modifications": [{ + "value": "ResourceGatherer/Rates/food.grain", + "multiply": 15, + "affects": "Spear" + }, + { + "value": "ResourceGatherer/Rates/food.grain", + "multiply": 15, + "affects": "Sword" + }, + { + "value": "ResourceGatherer/Rates/food.meat", + "multiply": 10 + }], + "affects": ["Female", "CitizenSoldier Melee"] +}; +techMods = { + "ResourceGatherer/Rates/food.grain": [{ + "affects": [ + ["Female", "Spear"], + ["CitizenSoldier", "Melee", "Spear"] + ], + "multiply": 15 + }, + { + "affects": [ + ["Female", "Sword"], + ["CitizenSoldier", "Melee", "Sword"] + ], + "multiply": 15 + }], + "ResourceGatherer/Rates/food.meat": [{ + "affects": [ + ["Female"], + ["CitizenSoldier", "Melee"] + ], + "multiply": 10 + }] +}; +TS_ASSERT_UNEVAL_EQUALS(DeriveModificationsFromTech(template), techMods); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Technologies_reqs.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js (revision 22767) @@ -1,159 +1,162 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Resources = { "BuildSchema": type => { let schema = ""; for (let res of ["food", "metal", "stone", "wood"]) schema += "" + "" + "" + "" + ""; return "" + schema + ""; } }; -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: Engine.LoadComponentScript("interfaces/Upgrade.js"); Engine.LoadComponentScript("Upgrade.js"); // Input (bare minimum needed for tests): let techs = { "alter_tower_upgrade_cost": { "modifications": [ { "value": "Upgrade/Cost/stone", "add": 60.0 }, { "value": "Upgrade/Cost/wood", "multiply": 0.5 }, { "value": "Upgrade/Time", "replace": 90 } ], "affects": ["Tower"] } }; let template = { "Identity": { "Classes": { '@datatype': "tokens", "_string": "Tower" }, "VisibleClasses": { '@datatype': "tokens", "_string": "" } }, "Upgrade": { "Tower": { "Cost": { "stone": "100", "wood": "50" }, "Entity": "structures/{civ}_defense_tower", "Time": "100" } } }; let civCode = "pony"; let playerID = 1; // Usually, the tech modifications would be worked out by the TechnologyManager // with assistance from globalscripts. This test is not about testing the // TechnologyManager, so the modifications (both with and without the technology // researched) are worked out before hand and placed here. let isResearched = false; let templateTechModifications = { "without": {}, "with": { "Upgrade/Cost/stone": [{ "affects": [["Tower"]], "add": 60 }], "Upgrade/Cost/wood": [{ "affects": [["Tower"]], "multiply": 0.5 }], "Upgrade/Time": [{ "affects": [["Tower"]], "replace": 90 }] } }; let entityTechModifications = { "without": { 'Upgrade/Cost/stone': { "20": { "origValue": 100, "newValue": 100 } }, 'Upgrade/Cost/wood': { "20": { "origValue": 50, "newValue": 50 } }, 'Upgrade/Time': { "20": { "origValue": 100, "newValue": 100 } } }, "with": { 'Upgrade/Cost/stone': { "20": { "origValue": 100, "newValue": 160 } }, 'Upgrade/Cost/wood': { "20": { "origValue": 50, "newValue": 25 } }, 'Upgrade/Time': { "20": { "origValue": 100, "newValue": 90 } } } }; /** * Initialise various bits. */ // System Entities: AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": pID => 10 // Called in helpers/player.js::QueryPlayerIDInterface(), as part of Tests T2 and T5. }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "GetTemplate": () => template // Called in components/Upgrade.js::ChangeUpgradedEntityCount(). }); AddMock(SYSTEM_ENTITY, IID_Timer, { "SetInterval": () => 1, // Called in components/Upgrade.js::Upgrade(). "CancelTimer": () => {} // Called in components/Upgrade.js::CancelUpgrade(). }); -// Init Player: -AddMock(10, IID_Player, { - "AddResources": () => {}, // Called in components/Upgrade.js::CancelUpgrade(). - "GetPlayerID": () => playerID, // Called in helpers/Player.js::QueryOwnerInterface() (and several times below). - "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) => { +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; - return GetTechModifiedProperty(mods, GetIdentityClasses(template.Identity), valueName, curValue); + + if (mods[valueName]) + return GetTechModifiedProperty(mods[valueName], GetIdentityClasses(template.Identity), curValue); + return curValue; }, - "ApplyModifications": (valueName, curValue, ent) => { + "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(). + "GetPlayerID": () => playerID, // Called in helpers/Player.js::QueryOwnerInterface() (and several times below). + "GetTimeMultiplier": () => 1.0, // Called in components/Upgrade.js::GetUpgradeTime(). + "TrySubtractResources": () => true // Called in components/Upgrade.js::Upgrade(). +}); + // Create an entity with an Upgrade component: AddMock(20, IID_Ownership, { "GetOwner": () => playerID // Called in helpers/Player.js::QueryOwnerInterface(). }); AddMock(20, IID_Identity, { "GetCiv": () => civCode // Called in components/Upgrade.js::init(). }); let cmpUpgrade = ConstructComponent(20, "Upgrade", template.Upgrade); cmpUpgrade.owner = playerID; /** * Now to start the test proper * To start with, no techs are researched... */ // T1: Check the cost of the upgrade without a player value being passed (as it would be in the structree). let parsed_template = GetTemplateDataHelper(template, null, {}, Resources); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 }); // T2: Check the value, with a player ID (as it would be in-session). parsed_template = GetTemplateDataHelper(template, playerID, {}, Resources); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 }); // T3: Check that the value is correct within the Update Component. TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 100, "wood": 50, "time": 100 }); /** * Tell the Upgrade component to start the Upgrade, * then mark the technology that alters the upgrade cost as researched. */ cmpUpgrade.Upgrade("structures/"+civCode+"_defense_tower"); isResearched = true; // T4: Check that the player-less value hasn't increased... parsed_template = GetTemplateDataHelper(template, null, {}, Resources); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 }); // T5: ...but the player-backed value has. parsed_template = GetTemplateDataHelper(template, playerID, {}, Resources); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 160, "wood": 25, "time": 90 }); // T6: The upgrade component should still be using the old resource cost (but new time cost) for the upgrade in progress... TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 100, "wood": 50, "time": 90 }); // T7: ...but with the upgrade cancelled, it now uses the modified value. cmpUpgrade.CancelUpgrade(playerID); TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 160, "wood": 25, "time": 90 }); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ValueModificationHelper.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ValueModificationHelper.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ValueModificationHelper.js (revision 22767) @@ -1,50 +1,40 @@ 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) return val + 3; if (ent == ownedEnt) return val + 7; return val; } }); -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 }); AddMock(playerEnt, IID_Player, { "GetPlayerID": () => 1 }); 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, playerEnt), 5.0); -TS_ASSERT_EQUALS(ApplyValueModificationsToEntity(techKey, 2.0, ownedEnt), 900.0); +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity(techKey, 2.0, ownedEnt), 9.0); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_VisionSharing.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_VisionSharing.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_VisionSharing.js (revision 22767) @@ -1,182 +1,189 @@ Engine.LoadHelperScript("Player.js"); 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"); Engine.LoadComponentScript("VisionSharing.js"); const ent = 170; let template = { "Bribable": "true" }; AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "GetTemplate": (name) => name == "special/spy" ? ({ "Cost": { "Resources": { "wood": 1000 } }, "VisionSharing": { "Duration": 15 } }) : ({}) }); AddMock(ent, IID_GarrisonHolder, { "GetEntities": () => [] }); AddMock(ent, IID_Ownership, { "GetOwner": () => 1 }); let cmpVisionSharing = ConstructComponent(ent, "VisionSharing", template); // Add some entities AddMock(180, IID_Ownership, { "GetOwner": () => 2 }); AddMock(181, IID_Ownership, { "GetOwner": () => 1 }); AddMock(182, IID_Ownership, { "GetOwner": () => 8 }); AddMock(183, IID_Ownership, { "GetOwner": () => 2 }); TS_ASSERT_EQUALS(cmpVisionSharing.activated, false); // Test Activate cmpVisionSharing.activated = false; cmpVisionSharing.Activate(); TS_ASSERT_EQUALS(cmpVisionSharing.activated, true); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.shared, new Set([1])); // Test CheckVisionSharings cmpVisionSharing.activated = true; cmpVisionSharing.shared = new Set([1]); AddMock(ent, IID_GarrisonHolder, { "GetEntities": () => [181] }); Engine.PostMessage = function(id, iid, message) { TS_ASSERT(false); // One doesn't send message }; cmpVisionSharing.CheckVisionSharings(); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.shared, new Set([1])); cmpVisionSharing.shared = new Set([1, 2, 8]); AddMock(ent, IID_GarrisonHolder, { "GetEntities": () => [180] }); Engine.PostMessage = function(id, iid, message) { TS_ASSERT_UNEVAL_EQUALS({ "entity": ent, "player": 8, "add": false }, message); }; cmpVisionSharing.CheckVisionSharings(); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.shared, new Set([1, 2])); cmpVisionSharing.shared = new Set([1, 8]); AddMock(ent, IID_GarrisonHolder, { "GetEntities": () => [181, 182, 183] }); Engine.PostMessage = function(id, iid, message) { TS_ASSERT_UNEVAL_EQUALS({ "entity": ent, "player": 2, "add": true }, message); }; cmpVisionSharing.CheckVisionSharings(); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.shared, new Set([1, 8, 2])); // take care of order or sort them // Test IsBribable TS_ASSERT(cmpVisionSharing.IsBribable()); // Test RemoveSpy AddMock(ent, IID_GarrisonHolder, { "GetEntities": () => [] }); cmpVisionSharing.spies = new Map([[5, 2], [17, 5]]); cmpVisionSharing.shared = new Set([1, 2, 5]); Engine.PostMessage = function(id, iid, message) { TS_ASSERT_UNEVAL_EQUALS({ "entity": ent, "player": 2, "add": false }, message); }; cmpVisionSharing.RemoveSpy({ "id": 5 }); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.shared, new Set([1, 5])); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.spies, new Map([[17, 5]])); Engine.PostMessage = function(id, iid, message) {}; // Test AddSpy cmpVisionSharing.spies = new Map([[5, 2], [17, 5]]); cmpVisionSharing.shared = new Set([1, 2, 5]); cmpVisionSharing.spyId = 20; AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => 14 }); 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])); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.spies, new Map([[5, 2], [17, 5]])); 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 }); AddMock(4, IID_StatisticsTracker, { "IncreaseSuccessfulBribesCounter": () => {}, "IncreaseFailedBribesCounter": () => {} }); cmpVisionSharing.AddSpy(4, 25); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.shared, new Set([1, 2, 5])); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.spies, new Map([[5, 2], [17, 5]])); TS_ASSERT_EQUALS(cmpVisionSharing.spyId, 20); AddMock(14, IID_Player, { "GetSpyCostMultiplier": () => 1, "TrySubtractResources": costs => true }); AddMock(SYSTEM_ENTITY, IID_Timer, { "SetTimeout": (ent, iid, funcname, time, data) => TS_ASSERT_EQUALS(time, 25 * 1000) }); cmpVisionSharing.AddSpy(4, 25); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.shared, new Set([1, 2, 5, 4])); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.spies, new Map([[5, 2], [17, 5], [21, 4]])); TS_ASSERT_EQUALS(cmpVisionSharing.spyId, 21); cmpVisionSharing.spies = new Map([[5, 2], [17, 5]]); cmpVisionSharing.shared = new Set([1, 2, 5]); cmpVisionSharing.spyId = 20; AddMock(ent, IID_Vision, { "GetRange": () => 48 }); AddMock(SYSTEM_ENTITY, IID_Timer, { "SetTimeout": (ent, iid, funcname, time, data) => TS_ASSERT_EQUALS(time, 15 * 1000 * 60 / 48) }); cmpVisionSharing.AddSpy(4); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.shared, new Set([1, 2, 5, 4])); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.spies, new Map([[5, 2], [17, 5], [21, 4]])); TS_ASSERT_EQUALS(cmpVisionSharing.spyId, 21); // Test ShareVisionWith cmpVisionSharing.activated = false; cmpVisionSharing.shared = undefined; TS_ASSERT(cmpVisionSharing.ShareVisionWith(1)); TS_ASSERT(!cmpVisionSharing.ShareVisionWith(2)); cmpVisionSharing.activated = true; cmpVisionSharing.shared = new Set([1, 2, 8]); TS_ASSERT(cmpVisionSharing.ShareVisionWith(1)); TS_ASSERT(cmpVisionSharing.ShareVisionWith(2)); TS_ASSERT(!cmpVisionSharing.ShareVisionWith(3)); TS_ASSERT(!cmpVisionSharing.ShareVisionWith(0)); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js (revision 22767) @@ -1,340 +1,366 @@ /** * Used to create player entities prior to reading the rest of a map, * all other initialization must be done after loading map (terrain/entities). * DO NOT use other components here, as they may fail unpredictably. * settings is the object containing settings for this map. * newPlayers if true will remove old player entities or add new ones until * the new number of player entities is obtained * (used when loading a map or when Atlas changes the number of players). */ function LoadPlayerSettings(settings, newPlayers) { var playerDefaults = Engine.ReadJSONFile("simulation/data/settings/player_defaults.json").PlayerData; // Default settings if (!settings) settings = {}; // Add gaia to simplify iteration // (if gaia is not already the first civ such as when called from Atlas' ActorViewer) if (settings.PlayerData && settings.PlayerData[0] && (!settings.PlayerData[0].Civ || settings.PlayerData[0].Civ != "gaia")) settings.PlayerData.unshift(null); var playerData = settings.PlayerData; // Disable the AIIinterface when no AI players are present if (playerData && !playerData.some(v => v && !!v.AI)) Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface).Disable(); var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var numPlayers = cmpPlayerManager.GetNumPlayers(); var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); // Remove existing players or add new ones if (newPlayers) { var settingsNumPlayers = 9; // default 8 players + gaia if (playerData) settingsNumPlayers = playerData.length; // includes gaia (see above) else warn("Player.js: Setup has no player data - using defaults"); while (settingsNumPlayers > numPlayers) { // Add player entity to engine var entID = Engine.AddEntity(GetPlayerTemplateName(getSetting(playerData, playerDefaults, numPlayers, "Civ"))); var cmpPlayer = Engine.QueryInterface(entID, IID_Player); if (!cmpPlayer) throw new Error("Player.js: Error creating player entity " + numPlayers); cmpPlayerManager.AddPlayer(entID); ++numPlayers; } while (settingsNumPlayers < numPlayers) { cmpPlayerManager.RemoveLastPlayer(); --numPlayers; } } // Even when no new player, we must check the template compatibility as player template may be civ dependent for (var i = 0; i < numPlayers; ++i) { var template = GetPlayerTemplateName(getSetting(playerData, playerDefaults, i, "Civ")); var entID = cmpPlayerManager.GetPlayerByID(i); if (cmpTemplateManager.GetCurrentTemplateName(entID) === template) continue; // We need to recreate this player to have the right template entID = Engine.AddEntity(template); cmpPlayerManager.ReplacePlayer(i, entID); } // Initialize the player data for (var i = 0; i < numPlayers; ++i) { let cmpPlayer = QueryPlayerIDInterface(i); cmpPlayer.SetName(getSetting(playerData, playerDefaults, i, "Name")); cmpPlayer.SetCiv(getSetting(playerData, playerDefaults, i, "Civ")); var color = getSetting(playerData, playerDefaults, i, "Color"); cmpPlayer.SetColor(color.r, color.g, color.b); // Special case for gaia if (i == 0) { // Gaia should be its own ally. cmpPlayer.SetAlly(0); // Gaia is everyone's enemy for (var j = 1; j < numPlayers; ++j) cmpPlayer.SetEnemy(j); continue; } // Note: this is not yet implemented but I leave it commented to highlight it's easy // If anyone ever adds handicap. //if (getSetting(playerData, playerDefaults, i, "GatherRateMultiplier") !== undefined) // cmpPlayer.SetGatherRateMultiplier(getSetting(playerData, playerDefaults, i, "GatherRateMultiplier")); if (getSetting(playerData, playerDefaults, i, "PopulationLimit") !== undefined) cmpPlayer.SetMaxPopulation(getSetting(playerData, playerDefaults, i, "PopulationLimit")); if (getSetting(playerData, playerDefaults, i, "Resources") !== undefined) cmpPlayer.SetResourceCounts(getSetting(playerData, playerDefaults, i, "Resources")); if (getSetting(playerData, playerDefaults, i, "StartingTechnologies") !== undefined) cmpPlayer.SetStartingTechnologies(getSetting(playerData, playerDefaults, i, "StartingTechnologies")); if (getSetting(playerData, playerDefaults, i, "DisabledTechnologies") !== undefined) cmpPlayer.SetDisabledTechnologies(getSetting(playerData, playerDefaults, i, "DisabledTechnologies")); let disabledTemplates = []; if (settings.DisabledTemplates !== undefined) disabledTemplates = settings.DisabledTemplates; if (getSetting(playerData, playerDefaults, i, "DisabledTemplates") !== undefined) disabledTemplates = disabledTemplates.concat(getSetting(playerData, playerDefaults, i, "DisabledTemplates")); if (disabledTemplates.length) cmpPlayer.SetDisabledTemplates(disabledTemplates); if (settings.DisableSpies) { cmpPlayer.AddDisabledTechnology("unlock_spies"); cmpPlayer.AddDisabledTemplate("special/spy"); } // If diplomacy explicitly defined, use that; otherwise use teams if (getSetting(playerData, playerDefaults, i, "Diplomacy") !== undefined) cmpPlayer.SetDiplomacy(getSetting(playerData, playerDefaults, i, "Diplomacy")); else { // Init diplomacy var myTeam = getSetting(playerData, playerDefaults, i, "Team"); // Set all but self as enemies as SetTeam takes care of allies for (var j = 0; j < numPlayers; ++j) { if (i == j) cmpPlayer.SetAlly(j); else cmpPlayer.SetEnemy(j); } cmpPlayer.SetTeam(myTeam === undefined ? -1 : myTeam); } cmpPlayer.SetFormations( getSetting(playerData, playerDefaults, i, "Formations") || Engine.ReadJSONFile("simulation/data/civs/" + cmpPlayer.GetCiv() + ".json").Formations); var startCam = getSetting(playerData, playerDefaults, i, "StartingCamera"); if (startCam !== undefined) cmpPlayer.SetStartingCamera(startCam.Position, startCam.Rotation); } // NOTE: We need to do the team locking here, as otherwise // SetTeam can't ally the players. if (settings.LockTeams) for (let i = 0; i < numPlayers; ++i) QueryPlayerIDInterface(i).SetLockTeams(true); } // Get a setting if it exists or return default function getSetting(settings, defaults, idx, property) { if (settings && settings[idx] && (property in settings[idx])) return settings[idx][property]; // Use defaults if (defaults && defaults[idx] && (property in defaults[idx])) return defaults[idx][property]; return undefined; } function GetPlayerTemplateName(civ) { let path = "special/player/player"; if (Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).TemplateExists(path + "_" + civ)) return path + "_" + 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. * iid is typically IID_Player. */ function QueryOwnerInterface(ent, iid = IID_Player) { var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!cmpOwnership) return null; var owner = cmpOwnership.GetOwner(); if (owner == INVALID_PLAYER) return null; return QueryPlayerIDInterface(owner, iid); } /** * Similar to Engine.QueryInterface but applies to the player entity * with the given ID number. * iid is typically IID_Player. */ function QueryPlayerIDInterface(id, iid = IID_Player) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var playerEnt = cmpPlayerManager.GetPlayerByID(id); if (!playerEnt) return null; return Engine.QueryInterface(playerEnt, iid); } /** * Similar to Engine.QueryInterface but first checks if the entity * mirages the interface. */ function QueryMiragedInterface(ent, iid) { var cmp = Engine.QueryInterface(ent, IID_Mirage); if (cmp && !cmp.Mirages(iid)) return null; else if (!cmp) cmp = Engine.QueryInterface(ent, iid); return cmp; } /** * Similar to Engine.QueryInterface, but checks for all interfaces * implementing a builder list (currently Foundation and Repairable) * TODO Foundation and Repairable could both implement a BuilderList component */ function QueryBuilderListInterface(ent) { return Engine.QueryInterface(ent, IID_Foundation) || Engine.QueryInterface(ent, IID_Repairable); } /** * Returns true if the entity 'target' is owned by an ally of * the owner of 'entity'. */ function IsOwnedByAllyOfEntity(entity, target) { return IsOwnedByEntityHelper(entity, target, "IsAlly"); } function IsOwnedByMutualAllyOfEntity(entity, target) { return IsOwnedByEntityHelper(entity, target, "IsMutualAlly"); } function IsOwnedByEntityHelper(entity, target, check) { // Figure out which player controls us let owner = 0; let cmpOwnership = Engine.QueryInterface(entity, IID_Ownership); if (cmpOwnership) owner = cmpOwnership.GetOwner(); // Figure out which player controls the target entity let targetOwner = 0; let cmpOwnershipTarget = Engine.QueryInterface(target, IID_Ownership); if (cmpOwnershipTarget) targetOwner = cmpOwnershipTarget.GetOwner(); let cmpPlayer = QueryPlayerIDInterface(owner); return cmpPlayer && cmpPlayer[check](targetOwner); } /** * Returns true if the entity 'target' is owned by player */ function IsOwnedByPlayer(player, target) { var cmpOwnershipTarget = Engine.QueryInterface(target, IID_Ownership); return cmpOwnershipTarget && player == cmpOwnershipTarget.GetOwner(); } function IsOwnedByGaia(target) { return IsOwnedByPlayer(0, target); } /** * Returns true if the entity 'target' is owned by an ally of player */ function IsOwnedByAllyOfPlayer(player, target) { return IsOwnedByHelper(player, target, "IsAlly"); } function IsOwnedByMutualAllyOfPlayer(player, target) { return IsOwnedByHelper(player, target, "IsMutualAlly"); } function IsOwnedByNeutralOfPlayer(player,target) { return IsOwnedByHelper(player, target, "IsNeutral"); } function IsOwnedByEnemyOfPlayer(player, target) { return IsOwnedByHelper(player, target, "IsEnemy"); } function IsOwnedByHelper(player, target, check) { let targetOwner = 0; let cmpOwnershipTarget = Engine.QueryInterface(target, IID_Ownership); if (cmpOwnershipTarget) targetOwner = cmpOwnershipTarget.GetOwner(); let cmpPlayer = QueryPlayerIDInterface(player); return cmpPlayer && cmpPlayer[check](targetOwner); } Engine.RegisterGlobal("LoadPlayerSettings", LoadPlayerSettings); +Engine.RegisterGlobal("QueryOwnerEntityID", QueryOwnerEntityID); Engine.RegisterGlobal("QueryOwnerInterface", QueryOwnerInterface); Engine.RegisterGlobal("QueryPlayerIDInterface", QueryPlayerIDInterface); Engine.RegisterGlobal("QueryMiragedInterface", QueryMiragedInterface); Engine.RegisterGlobal("QueryBuilderListInterface", QueryBuilderListInterface); Engine.RegisterGlobal("IsOwnedByAllyOfEntity", IsOwnedByAllyOfEntity); Engine.RegisterGlobal("IsOwnedByMutualAllyOfEntity", IsOwnedByMutualAllyOfEntity); Engine.RegisterGlobal("IsOwnedByPlayer", IsOwnedByPlayer); Engine.RegisterGlobal("IsOwnedByGaia", IsOwnedByGaia); Engine.RegisterGlobal("IsOwnedByAllyOfPlayer", IsOwnedByAllyOfPlayer); Engine.RegisterGlobal("IsOwnedByMutualAllyOfPlayer", IsOwnedByMutualAllyOfPlayer); Engine.RegisterGlobal("IsOwnedByNeutralOfPlayer", IsOwnedByNeutralOfPlayer); Engine.RegisterGlobal("IsOwnedByEnemyOfPlayer", IsOwnedByEnemyOfPlayer); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_MultiKeyMap.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_MultiKeyMap.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_MultiKeyMap.js (revision 22767) @@ -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); Property changes on: ps/trunk/binaries/data/mods/public/simulation/helpers/tests/test_MultiKeyMap.js ___________________________________________________________________ Added: svn:executable ## -0,0 +1 ## +* \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/helpers/MultiKeyMap.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/MultiKeyMap.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/MultiKeyMap.js (revision 22767) @@ -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); Property changes on: ps/trunk/binaries/data/mods/public/simulation/helpers/MultiKeyMap.js ___________________________________________________________________ Added: svn:executable ## -0,0 +1 ## +* \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/helpers/ValueModification.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/ValueModification.js (revision 22766) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/ValueModification.js (revision 22767) @@ -1,32 +1,24 @@ // Little helper functions to make applying technology and auras more convenient 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); Engine.RegisterGlobal("ApplyValueModificationsToTemplate", ApplyValueModificationsToTemplate); Index: ps/trunk/source/simulation2/components/tests/test_scripts.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_scripts.h (revision 22766) +++ ps/trunk/source/simulation2/components/tests/test_scripts.h (revision 22767) @@ -1,86 +1,87 @@ /* Copyright (C) 2017 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "simulation2/system/ComponentTest.h" #include "ps/Filesystem.h" class TestComponentScripts : public CxxTest::TestSuite { public: void setUp() { g_VFS = CreateVfs(); g_VFS->Mount(L"", DataDir()/"mods"/"mod", VFS_MOUNT_MUST_EXIST); g_VFS->Mount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST, 1); // ignore directory-not-found errors CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); } static void load_script(const ScriptInterface& scriptInterface, const VfsPath& pathname) { CVFSFile file; TS_ASSERT_EQUALS(file.Load(g_VFS, pathname), PSRETURN_OK); CStr content = file.DecodeUTF8(); // assume it's UTF-8 TSM_ASSERT(L"Running script "+pathname.string(), scriptInterface.LoadScript(pathname, content)); } static void Script_LoadComponentScript(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& pathname) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/components") / pathname)); } static void Script_LoadHelperScript(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& pathname) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/helpers") / pathname)); } void test_scripts() { if (!VfsFileExists(L"simulation/components/tests/setup.js")) { debug_printf("WARNING: Skipping component scripts tests (can't find binaries/data/mods/public/simulation/components/tests/setup.js)\n"); return; } 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) { CSimContext context; CComponentManager componentManager(context, g_ScriptRuntime, true); ScriptTestSetup(componentManager.GetScriptInterface()); componentManager.GetScriptInterface().RegisterFunction ("LoadComponentScript"); componentManager.GetScriptInterface().RegisterFunction ("LoadHelperScript"); componentManager.LoadComponentTypes(); load_script(componentManager.GetScriptInterface(), L"simulation/components/tests/setup.js"); load_script(componentManager.GetScriptInterface(), path); } } };