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);
}
}
};