Index: binaries/data/mods/public/gui/session/selection_details.js
===================================================================
--- binaries/data/mods/public/gui/session/selection_details.js
+++ binaries/data/mods/public/gui/session/selection_details.js
@@ -189,7 +189,7 @@
{
let experienceBar = Engine.GetGUIObjectByName("experienceBar");
let experienceSize = experienceBar.size;
- experienceSize.rtop = 100 - (100 * Math.max(0, Math.min(1, 1.0 * +entState.promotion.curr / +entState.promotion.req)));
+ experienceSize.rtop = 100 - (100 * Math.max(0, Math.min(1, 1.0 * +entState.promotion.curr / +(entState.promotion.req || 1))));
experienceBar.size = experienceSize;
if (entState.promotion.curr < entState.promotion.req)
Index: binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- binaries/data/mods/public/simulation/components/GuiInterface.js
+++ binaries/data/mods/public/simulation/components/GuiInterface.js
@@ -501,7 +501,8 @@
if (cmpPromotion)
ret.promotion = {
"curr": cmpPromotion.GetCurrentXp(),
- "req": cmpPromotion.GetRequiredXp()
+ "req": cmpPromotion.GetRequiredXp(),
+ "level": cmpPromotion.GetCurrentLevel()
};
if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket"))
Index: binaries/data/mods/public/simulation/components/ProductionQueue.js
===================================================================
--- binaries/data/mods/public/simulation/components/ProductionQueue.js
+++ binaries/data/mods/public/simulation/components/ProductionQueue.js
@@ -108,7 +108,7 @@
let upgradeTemplate = function(templateName)
{
let template = cmpTemplateManager.GetTemplate(templateName);
- while (template && template.Promotion !== undefined)
+ while (template && template.Promotion && !!template.Promotion.Entity)
{
let requiredXp = ApplyValueModificationsToTemplate(
"Promotion/RequiredXp",
@@ -277,7 +277,7 @@
let template = cmpTemplateManager.GetTemplate(templateName);
if (!template)
return;
- if (template.Promotion &&
+ if (template.Promotion && template.Promotion.Entity &&
!ApplyValueModificationsToTemplate(
"Promotion/RequiredXp",
+template.Promotion.RequiredXp,
Index: binaries/data/mods/public/simulation/components/Promotion.js
===================================================================
--- binaries/data/mods/public/simulation/components/Promotion.js
+++ binaries/data/mods/public/simulation/components/Promotion.js
@@ -1,132 +1,186 @@
-function Promotion() {}
+class Promotion
+{
+ Init()
+ {
+ this.currentXp = 0;
+ this.level = 0;
+ this.ComputeTrickleRate();
+ }
-Promotion.prototype.Schema =
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "";
+ /**
+ * @return {boolean} - Whether this entity promotes to a new entity.
+ */
+ PromotesToEntity()
+ {
+ return this.template.Entity !== undefined;
+ }
-Promotion.prototype.Init = function()
-{
- this.currentXp = 0;
- this.ComputeTrickleRate();
-};
+ /**
+ * Gets the current modification template.
+ * This corrects for the current level of the entity, if applicable.
+ */
+ GetCurrentTemplate()
+ {
+ if (this.PromotesToEntity())
+ return this.template;
-Promotion.prototype.GetRequiredXp = function()
-{
- return ApplyValueModificationsToEntity("Promotion/RequiredXp", +this.template.RequiredXp, this.entity);
-};
+ for (let level in this.template.Levels)
+ if (this.level >= this.template.Levels[level]["@from"] &&
+ (!this.template.Levels[level]["@until"] || this.level < this.template.Levels[level]["@until"]))
+ return this.template.Levels[level];
+ return undefined;
+ }
-Promotion.prototype.GetCurrentXp = function()
-{
- return this.currentXp;
-};
+ /**
+ * @return {number} - The required XP to promote, or undefined if the entity
+ * cannot promote (e.g. is at its maximum level).
+ */
+ GetRequiredXp()
+ {
+ let template = this.GetCurrentTemplate();
+ if (!template)
+ return undefined;
+ return ApplyValueModificationsToEntity("Promotion/RequiredXp", +template.RequiredXp, this.entity);
+ }
-Promotion.prototype.GetPromotedTemplateName = function()
-{
- return this.template.Entity;
-};
+ /**
+ * @return {number} - The current amount of experience points.
+ */
+ GetCurrentXp()
+ {
+ return this.currentXp;
+ }
-Promotion.prototype.Promote = function(promotedTemplateName)
-{
- // If the unit is dead, don't promote it
- let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
- if (cmpHealth && cmpHealth.GetHitpoints() == 0)
+ /**
+ * @return {number} - The current level of this entity.
+ */
+ GetCurrentLevel()
{
- this.promotedUnitEntity = INVALID_ENTITY;
- return;
+ return this.level;
}
- // Save the entity id.
- this.promotedUnitEntity = ChangeEntityTemplate(this.entity, promotedTemplateName);
+ /**
+ * @return {string} - The template this entity promotes to.
+ */
+ GetPromotedTemplateName()
+ {
+ return this.template.Entity;
+ }
- let cmpPosition = Engine.QueryInterface(this.promotedUnitEntity, IID_Position);
- let cmpUnitAI = Engine.QueryInterface(this.promotedUnitEntity, IID_UnitAI);
+ Promote()
+ {
+ // If the unit is dead, don't promote it.
+ let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
+ if (cmpHealth && cmpHealth.GetHitpoints() == 0)
+ {
+ this.promotedUnitEntity = INVALID_ENTITY;
+ return;
+ }
- if (cmpPosition && cmpPosition.IsInWorld() && cmpUnitAI)
- cmpUnitAI.Cheer();
-};
+ if (this.PromotesToEntity())
+ {
+ let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
+ let playerID = QueryOwnerInterface(this.entity, IID_Player).GetPlayerID();
+ let promotedTemplateName = this.GetPromotedTemplateName();
+
+ this.currentXp -= this.GetRequiredXp();
+ // Check if we can upgrade a second time (or even more).
+ while (true)
+ {
+ let template = cmpTemplateManager.GetTemplate(promotedTemplateName);
+ if (!template.Promotion || !template.Promotion.Entity)
+ break;
+ let requiredXp = ApplyValueModificationsToTemplate("Promotion/RequiredXp", +template.Promotion.RequiredXp, playerID, template);
+ // Compare the current XP to the required XP of the promoted entity.
+ if (this.currentXp < requiredXp)
+ break;
+ this.currentXp -= requiredXp;
+ promotedTemplateName = template.Promotion.Entity;
+ }
+
+ // Save the new entity ID.
+ this.promotedUnitEntity = ChangeEntityTemplate(this.entity, promotedTemplateName);
+
+ let cmpPromotion = Engine.QueryInterface(this.promotedUnitEntity, IID_Promotion);
+ if (cmpPromotion && this.currentXp > 0 && this.promotedUnitEntity != this.entity)
+ cmpPromotion.IncreaseXp(this.currentXp);
+ }
+ else
+ {
+ let cmpStatusEffectReceiver = Engine.QueryInterface(this.entity, IID_StatusEffectsReceiver);
+ if (cmpStatusEffectReceiver)
+ // False if GetRequiredXp returns undefined.
+ while (this.currentXp >= this.GetRequiredXp())
+ {
+ this.currentXp -= this.GetRequiredXp();
+ cmpStatusEffectReceiver.ApplyStatus(this.GetCurrentTemplate().ApplyStatus, INVALID_PLAYER, INVALID_ENTITY);
+ this.level++;
+ }
+ this.promotedUnitEntity = this.entity;
+ }
-Promotion.prototype.IncreaseXp = function(amount)
-{
- // if the unit was already promoted, but is waiting for the engine to be destroyed
- // transfer the gained xp to the promoted unit if applicable
- if (this.promotedUnitEntity)
- {
- let cmpPromotion = Engine.QueryInterface(this.promotedUnitEntity, IID_Promotion);
- if (cmpPromotion)
- cmpPromotion.IncreaseXp(amount);
- return;
- }
+ let cmpPosition = Engine.QueryInterface(this.promotedUnitEntity, IID_Position);
+ let cmpUnitAI = Engine.QueryInterface(this.promotedUnitEntity, IID_UnitAI);
- this.currentXp += +(amount);
- var requiredXp = this.GetRequiredXp();
+ if (cmpPosition && cmpPosition.IsInWorld() && cmpUnitAI)
+ cmpUnitAI.Cheer();
- if (this.currentXp >= requiredXp)
+ if (this.promotedUnitEntity == this.entity)
+ delete this.promotedUnitEntity;
+ }
+
+ IncreaseXp(amount)
{
- var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
- var playerID = QueryOwnerInterface(this.entity, IID_Player).GetPlayerID();
- this.currentXp -= requiredXp;
- var promotedTemplateName = this.GetPromotedTemplateName();
- // check if we can upgrade a second time (or even more)
- while (true)
+ // If the unit was already promoted, but is waiting for the engine to be destroyed
+ // transfer the gained XP to the promoted unit, if applicable.
+ if (this.promotedUnitEntity)
{
- var template = cmpTemplateManager.GetTemplate(promotedTemplateName);
- if (!template.Promotion)
- break;
- requiredXp = ApplyValueModificationsToTemplate("Promotion/RequiredXp", +template.Promotion.RequiredXp, playerID, template);
- // compare the current xp to the required xp of the promoted entity
- if (this.currentXp < requiredXp)
- break;
- this.currentXp -= requiredXp;
- promotedTemplateName = template.Promotion.Entity;
+ let cmpPromotion = Engine.QueryInterface(this.promotedUnitEntity, IID_Promotion);
+ if (cmpPromotion)
+ cmpPromotion.IncreaseXp(amount);
+ return;
}
- this.Promote(promotedTemplateName);
- let cmpPromotion = Engine.QueryInterface(this.promotedUnitEntity, IID_Promotion);
- if (cmpPromotion)
- cmpPromotion.IncreaseXp(this.currentXp);
- }
- Engine.PostMessage(this.entity, MT_ExperienceChanged, {});
-};
+ this.currentXp += +amount;
-Promotion.prototype.ComputeTrickleRate = function()
-{
- this.trickleRate = ApplyValueModificationsToEntity("Promotion/TrickleRate", +(this.template.TrickleRate || 0), this.entity);
- this.CheckTrickleTimer();
-};
+ if (this.currentXp >= this.GetRequiredXp())
+ this.Promote();
-Promotion.prototype.CheckTrickleTimer = function()
-{
- if (!this.trickleRate)
+ if (amount)
+ Engine.PostMessage(this.entity, MT_ExperienceChanged, {});
+ }
+
+ ComputeTrickleRate()
{
- if (this.trickleTimer)
+ this.trickleRate = ApplyValueModificationsToEntity("Promotion/TrickleRate", +(this.template.TrickleRate || 0), this.entity);
+ this.CheckTrickleTimer();
+ }
+
+ CheckTrickleTimer()
+ {
+ if (!this.trickleRate)
{
- let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
- cmpTimer.CancelTimer(this.trickleTimer);
- delete this.trickleTimer;
+ if (this.trickleTimer)
+ {
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ cmpTimer.CancelTimer(this.trickleTimer);
+ delete this.trickleTimer;
+ }
+ return;
}
- return;
- }
- if (this.trickleTimer)
- return;
+ if (this.trickleTimer)
+ return;
- let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
- this.trickleTimer = cmpTimer.SetInterval(this.entity, IID_Promotion, "TrickleTick", 1000, 1000, null);
-};
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ this.trickleTimer = cmpTimer.SetInterval(this.entity, IID_Promotion, "TrickleTick", 1000, 1000, null);
+ }
-Promotion.prototype.TrickleTick = function()
-{
- this.IncreaseXp(this.trickleRate);
-};
+ TrickleTick()
+ {
+ this.IncreaseXp(this.trickleRate);
+ }
+}
Promotion.prototype.OnValueModification = function(msg)
{
@@ -137,4 +191,42 @@
this.IncreaseXp(0);
};
+Promotion.prototype.Schema =
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ Attacking.StatusEffectsSchema +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "";
+
Engine.RegisterComponentType(IID_Promotion, "Promotion", Promotion);
Index: binaries/data/mods/public/simulation/components/tests/test_Promotion.js
===================================================================
--- binaries/data/mods/public/simulation/components/tests/test_Promotion.js
+++ binaries/data/mods/public/simulation/components/tests/test_Promotion.js
@@ -1,14 +1,17 @@
+Engine.LoadHelperScript("Attacking.js");
+Engine.LoadHelperScript("MultiKeyMap.js");
+Engine.LoadHelperScript("ValueModification.js");
Engine.LoadComponentScript("interfaces/Health.js");
+Engine.LoadComponentScript("interfaces/ModifiersManager.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
+Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
+Engine.LoadComponentScript("ModifiersManager.js");
Engine.LoadComponentScript("Promotion.js");
+Engine.LoadComponentScript("StatusEffectsReceiver.js");
(function testMultipleXPIncrease()
{
-let ApplyValueModificationsToEntity = (_, val) => val;
-Engine.RegisterGlobal("ApplyValueModificationsToEntity", ApplyValueModificationsToEntity);
-Engine.RegisterGlobal("ApplyValueModificationsToTemplate", ApplyValueModificationsToEntity);
-
let QueryOwnerInterface = () => ({ "GetPlayerID": () => 2 });
Engine.RegisterGlobal("QueryOwnerInterface", QueryOwnerInterface);
@@ -16,7 +19,7 @@
let entTemplates = {
"60": "template_b",
- "61": "template_f",
+ "61": "template_c",
"62": "end",
};
@@ -75,3 +78,115 @@
cmpPromotion.IncreaseXp(1000);
})();
+
+(function testRankPromotion()
+{
+let ownerId = 2;
+let entId = 4;
+
+let cmpPromotion = ConstructComponent(entId, "Promotion", {
+ "Levels": {
+ "firstPromotion": {
+ "@from": 0,
+ "@until": 2,
+ "RequiredXp": 100,
+ "ApplyStatus": {
+ "name": {
+ "Modifiers": [
+ ]
+ }
+ }
+ }
+ }
+});
+let cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager");
+let cmpStatusReceiver = ConstructComponent(entId, "StatusEffectsReceiver");
+
+let QueryOwnerEntityID = () => null;
+Engine.RegisterGlobal("QueryOwnerEntityID", QueryOwnerEntityID);
+
+AddMock(entId, IID_Identity, {
+ "GetClassesList": () => ["class_a"]
+});
+
+TS_ASSERT_EQUALS(cmpPromotion.GetCurrentXp(), 0);
+TS_ASSERT_EQUALS(cmpPromotion.GetRequiredXp(), 100);
+TS_ASSERT_EQUALS(cmpPromotion.GetCurrentLevel(), 0);
+
+// Promote.
+cmpPromotion.IncreaseXp(150);
+TS_ASSERT_EQUALS(cmpPromotion.GetCurrentXp(), 50);
+TS_ASSERT_EQUALS(cmpPromotion.GetCurrentLevel(), 1);
+TS_ASSERT_EQUALS(cmpPromotion.GetRequiredXp(), 100);
+// Promote.
+cmpPromotion.IncreaseXp(150);
+TS_ASSERT_EQUALS(cmpPromotion.GetCurrentLevel(), 2);
+TS_ASSERT_EQUALS(cmpPromotion.GetRequiredXp(), undefined);
+// Check that increasing more does nothing.
+cmpPromotion.IncreaseXp(150);
+TS_ASSERT_EQUALS(cmpPromotion.GetCurrentLevel(), 2);
+TS_ASSERT_EQUALS(cmpPromotion.GetRequiredXp(), undefined);
+
+cmpPromotion = ConstructComponent(entId, "Promotion", {
+ "Levels": {
+ "firstPromotion": {
+ "@from": 0,
+ "@until": 2,
+ "RequiredXp": 100,
+ "ApplyStatus": {
+ "name": {
+ "Modifiers": [
+ ]
+ }
+ }
+ },
+ "secondPromotion": {
+ "@from": 2,
+ "@until": 5,
+ "RequiredXp": 200,
+ "ApplyStatus": {
+ "name2": {
+ "Modifiers": {
+ "Test-Modifier": {
+ "Paths": {
+ "_string": "Health/Max"
+ },
+ "Multiply": 2,
+ "Affects": {
+ "_string": "class_a class_b",
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+});
+
+TS_ASSERT_EQUALS(cmpPromotion.GetCurrentXp(), 0);
+TS_ASSERT_EQUALS(cmpPromotion.GetRequiredXp(), 100);
+TS_ASSERT_EQUALS(cmpPromotion.GetCurrentLevel(), 0);
+
+// Promote.
+cmpPromotion.IncreaseXp(150);
+TS_ASSERT_EQUALS(cmpPromotion.GetCurrentXp(), 50);
+TS_ASSERT_EQUALS(cmpPromotion.GetCurrentLevel(), 1);
+TS_ASSERT_EQUALS(cmpPromotion.GetRequiredXp(), 100);
+
+TS_ASSERT("name" in cmpStatusReceiver.GetActiveStatuses());
+
+// Promote twice.
+cmpPromotion.IncreaseXp(250);
+TS_ASSERT_EQUALS(cmpPromotion.GetCurrentLevel(), 3);
+TS_ASSERT_EQUALS(cmpPromotion.GetRequiredXp(), 200);
+
+cmpPromotion.IncreaseXp(200);
+TS_ASSERT_EQUALS(cmpPromotion.GetCurrentLevel(), 4);
+TS_ASSERT("name" in cmpStatusReceiver.GetActiveStatuses() &&
+ "name2" in cmpStatusReceiver.GetActiveStatuses());
+cmpPromotion.IncreaseXp(200);
+TS_ASSERT_EQUALS(cmpPromotion.GetCurrentLevel(), 5);
+TS_ASSERT_EQUALS(cmpPromotion.GetRequiredXp(), undefined);
+TS_ASSERT("name" in cmpStatusReceiver.GetActiveStatuses() &&
+ "name2" in cmpStatusReceiver.GetActiveStatuses());
+})();
Index: binaries/data/mods/public/simulation/helpers/Attacking.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/Attacking.js
+++ binaries/data/mods/public/simulation/helpers/Attacking.js
@@ -19,7 +19,7 @@
"" +
"";
-const StatusEffectsSchema =
+Attacking.prototype.StatusEffectsSchema =
"" +
"" +
"" +
@@ -66,7 +66,7 @@
"" +
"" +
DirectEffectsSchema +
- StatusEffectsSchema +
+ this.StatusEffectsSchema +
"" +
"" +
"" +
Index: binaries/data/mods/public/simulation/templates/units/athen_hero_themistocles.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/units/athen_hero_themistocles.xml
+++ binaries/data/mods/public/simulation/templates/units/athen_hero_themistocles.xml
@@ -11,6 +11,40 @@
Themistoklēs
units/athen_hero_themistocles.png
+
+
+
+ 50
+
+
+ Basic promotion
+
+
+ Health/Max
+ Unit
+ 100
+
+
+
+
+
+
+ 100
+
+
+ Advanced promotion
+
+
+ Health/Max
+ Unit
+ 200
+
+
+
+
+
+
+
units/athenians/hero_infantry_swordsman_themistocles.xml
Index: binaries/data/mods/public/simulation/templates/units/athen_infantry_spearman_b.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/units/athen_infantry_spearman_b.xml
+++ binaries/data/mods/public/simulation/templates/units/athen_infantry_spearman_b.xml
@@ -17,8 +17,49 @@
Hoplítēs Athēnaîos
units/athen_infantry_spearman.png
-
- units/athen_infantry_spearman_a
+
+
+
+ 100
+
+
+ Basic promotion
+
+
+ Health/Max
+ Unit
+ 100
+
+
+ VisualActor/Actor
+ Unit
+ units/athenians/infantry_spearman_a.xml
+
+
+
+
+
+
+ 100
+
+
+ Advanced promotion
+
+
+ Health/Max
+ Unit
+ 200
+
+
+ VisualActor/Actor
+ Unit
+ units/athenians/infantry_spearman_e.xml
+
+
+
+
+
+
units/athenians/infantry_spearman_b.xml