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,95 +1,150 @@ -function Promotion() {} - -Promotion.prototype.Schema = - "" + - "" + - "" + - "" + - "" + - ""; - -Promotion.prototype.Init = function() +class Promotion { - this.currentXp = 0; -}; + Init() + { + this.currentXp = 0; + this.level = 0; + } -Promotion.prototype.GetRequiredXp = function() -{ - return ApplyValueModificationsToEntity("Promotion/RequiredXp", +this.template.RequiredXp, this.entity); -}; + /** + * @return {boolean} - Whether this entity promotes to a new entity. + */ + PromotesToEntity() + { + return this.template.Entity !== undefined; + } -Promotion.prototype.GetCurrentXp = function() -{ - return this.currentXp; -}; + /** + * 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.GetPromotedTemplateName = function() -{ - return this.template.Entity; -}; + for (let level in this.template) + if (this.level >= this.template[level]["@from"] && + (!this.template[level]["@until"] || this.level < this.template[level]["@until"])) + return this.template[level]; + return undefined; + } -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 required XP to promote, or undefined if the entity + * cannot promote (e.g. is at its maximum level). + */ + GetRequiredXp() { - this.promotedUnitEntity = INVALID_ENTITY; - return; + let template = this.GetCurrentTemplate(); + if (!template) + return undefined; + return ApplyValueModificationsToEntity("Promotion/RequiredXp", +template.RequiredXp, this.entity); } - // Save the entity id. - this.promotedUnitEntity = ChangeEntityTemplate(this.entity, promotedTemplateName); + /** + * @return {number} - The current amount of experience points. + */ + GetCurrentXp() + { + return this.currentXp; + } - let cmpPosition = Engine.QueryInterface(this.promotedUnitEntity, IID_Position); - let cmpUnitAI = Engine.QueryInterface(this.promotedUnitEntity, IID_UnitAI); + /** + * @return {number} - The current level of this entity. + */ + GetCurrentLevel() + { + return this.level; + } - if (cmpPosition && cmpPosition.IsInWorld() && cmpUnitAI) - cmpUnitAI.Cheer(); -}; + GetPromotedTemplateName() + { + return this.template.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; - } - - this.currentXp += +(amount); - var requiredXp = this.GetRequiredXp(); - - if (this.currentXp >= requiredXp) - { - 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) + Promote() + { + // If the unit is dead, don't promote it. + let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); + if (cmpHealth && cmpHealth.GetHitpoints() == 0) { - 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; + this.promotedUnitEntity = INVALID_ENTITY; + return; } - this.Promote(promotedTemplateName); - let cmpPromotion = Engine.QueryInterface(this.promotedUnitEntity, IID_Promotion); - if (cmpPromotion) - cmpPromotion.IncreaseXp(this.currentXp); + + 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 + { + // False if GetRequiredXp returns undefined. + let cmpStatusEffectReceiver = Engine.QueryInterface(this.entity, IID_StatusEffectsReceiver); + while (this.currentXp >= this.GetRequiredXp()) + { + this.currentXp -= this.GetRequiredXp(); + if (cmpStatusEffectReceiver) + cmpStatusEffectReceiver.ApplyStatus(this.GetCurrentTemplate().ApplyStatus, INVALID_PLAYER, INVALID_ENTITY); + this.level++; + } + this.promotedUnitEntity = this.entity; + } + + let cmpPosition = Engine.QueryInterface(this.promotedUnitEntity, IID_Position); + let cmpUnitAI = Engine.QueryInterface(this.promotedUnitEntity, IID_UnitAI); + + if (cmpPosition && cmpPosition.IsInWorld() && cmpUnitAI) + cmpUnitAI.Cheer(); + + if (this.promotedUnitEntity == this.entity) + delete this.promotedUnitEntity; } - Engine.PostMessage(this.entity, MT_ExperienceChanged, {}); -}; + IncreaseXp(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; + } + + this.currentXp += +amount; + + if (this.currentXp >= this.GetRequiredXp()) + this.Promote(); + + Engine.PostMessage(this.entity, MT_ExperienceChanged, {}); + } +} Promotion.prototype.OnValueModification = function(msg) { @@ -97,4 +152,35 @@ 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,19 @@ +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/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); +Engine.LoadComponentScript("ModifiersManager.js"); Engine.LoadComponentScript("Promotion.js"); +Engine.LoadComponentScript("StatusEffectsReceiver.js"); +Engine.LoadComponentScript("Timer.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 +21,7 @@ let entTemplates = { "60": "template_b", - "61": "template_f", + "61": "template_c", "62": "end", }; @@ -75,3 +80,117 @@ cmpPromotion.IncreaseXp(1000); })(); + +(function testRankPromotion() +{ +let ownerId = 2; +let entId = 4; + +let cmpPromotion = ConstructComponent(entId, "Promotion", { + "firstPromotion": { + "@from": 0, + "@until": 2, + "RequiredXp": 100, + "ApplyStatus": { + "name": { + "Modifiers": [ + ] + } + } + }, +}); +let cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager"); +let cmpStatusReceiver = ConstructComponent(entId, "StatusEffectsReceiver"); +let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); + +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); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpPromotion.GetCurrentXp(), 50); +TS_ASSERT_EQUALS(cmpPromotion.GetCurrentLevel(), 1); +TS_ASSERT_EQUALS(cmpPromotion.GetRequiredXp(), 100); +// Promote +cmpPromotion.IncreaseXp(150); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpPromotion.GetCurrentLevel(), 2); +TS_ASSERT_EQUALS(cmpPromotion.GetRequiredXp(), undefined); +// Check that increasing more does nothing. +cmpPromotion.IncreaseXp(150); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpPromotion.GetCurrentLevel(), 2); +TS_ASSERT_EQUALS(cmpPromotion.GetRequiredXp(), undefined); + +cmpPromotion = ConstructComponent(entId, "Promotion", { + "firstPromotion": { + "@from": 0, + "@until": 2, + "RequiredXp": 100, + "ApplyStatus": { + "name": { + "Modifiers": [ + ] + } + } + }, + "secondPromotion": { + "@from": 2, + "@until": 5, + "RequiredXp": 200, + "ApplyStatus": { + "name": { + "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); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpPromotion.GetCurrentXp(), 50); +TS_ASSERT_EQUALS(cmpPromotion.GetCurrentLevel(), 1); +TS_ASSERT_EQUALS(cmpPromotion.GetRequiredXp(), 100); + +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Health/Max", 5, entId), 5); + +// Promote twice +cmpPromotion.IncreaseXp(250); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpPromotion.GetCurrentLevel(), 3); +TS_ASSERT_EQUALS(cmpPromotion.GetRequiredXp(), 200); + +cmpPromotion.IncreaseXp(200); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpPromotion.GetCurrentLevel(), 4); +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Health/Max", 5, entId), 20); +cmpPromotion.IncreaseXp(200); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpPromotion.GetCurrentLevel(), 5); +TS_ASSERT_EQUALS(cmpPromotion.GetRequiredXp(), undefined); +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Health/Max", 5, entId), 40); +})(); 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,23 @@ Themistoklēs units/athen_hero_themistocles.png + + + 50 + + + Basic promotion + + + Health/Max + Unit + 100 + + + + + + units/athenians/hero_infantry_swordsman_themistocles.xml