Index: ps/trunk/binaries/data/mods/public/simulation/components/Promotion.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Promotion.js (revision 25355) +++ ps/trunk/binaries/data/mods/public/simulation/components/Promotion.js (revision 25356) @@ -1,138 +1,135 @@ function Promotion() {} Promotion.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; Promotion.prototype.Init = function() { this.currentXp = 0; this.ComputeTrickleRate(); }; Promotion.prototype.GetRequiredXp = function() { return ApplyValueModificationsToEntity("Promotion/RequiredXp", +this.template.RequiredXp, this.entity); }; Promotion.prototype.GetCurrentXp = function() { return this.currentXp; }; Promotion.prototype.GetPromotedTemplateName = function() { return this.template.Entity; }; 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) { this.promotedUnitEntity = INVALID_ENTITY; return; } // Save the entity id. this.promotedUnitEntity = ChangeEntityTemplate(this.entity, promotedTemplateName); }; 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; let requiredXp = this.GetRequiredXp(); if (this.currentXp >= requiredXp) { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); if (!cmpPlayer) return; let playerID = cmpPlayer.GetPlayerID(); this.currentXp -= requiredXp; let promotedTemplateName = this.GetPromotedTemplateName(); // check if we can upgrade a second time (or even more) while (true) { let 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.Promote(promotedTemplateName); - let cmpPromotion = Engine.QueryInterface(this.promotedUnitEntity, IID_Promotion); - if (cmpPromotion) - cmpPromotion.IncreaseXp(this.currentXp); } Engine.PostMessage(this.entity, MT_ExperienceChanged, {}); }; Promotion.prototype.ComputeTrickleRate = function() { this.trickleRate = ApplyValueModificationsToEntity("Promotion/TrickleRate", +(this.template.TrickleRate || 0), this.entity); this.CheckTrickleTimer(); }; Promotion.prototype.CheckTrickleTimer = function() { if (!this.trickleRate) { if (this.trickleTimer) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.trickleTimer); delete 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); }; Promotion.prototype.TrickleTick = function() { this.IncreaseXp(this.trickleRate); }; Promotion.prototype.OnValueModification = function(msg) { if (msg.component != "Promotion") return; this.ComputeTrickleRate(); this.IncreaseXp(0); }; Engine.RegisterComponentType(IID_Promotion, "Promotion", Promotion); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Promotion.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Promotion.js (revision 25355) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Promotion.js (revision 25356) @@ -1,120 +1,122 @@ Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("Promotion.js"); Engine.LoadComponentScript("Timer.js"); let cmpPromotion; const entity = 60; let modifier = 0; let ApplyValueModificationsToEntity = (_, val) => val + modifier; Engine.RegisterGlobal("ApplyValueModificationsToEntity", ApplyValueModificationsToEntity); Engine.RegisterGlobal("ApplyValueModificationsToTemplate", ApplyValueModificationsToEntity); let QueryOwnerInterface = () => ({ "GetPlayerID": () => 1 }); Engine.RegisterGlobal("QueryOwnerInterface", QueryOwnerInterface); let entTemplates = { "60": "template_b", "61": "template_f", "62": "end" }; let promote = { "template_b": "template_c", "template_c": "template_d", "template_d": "template_e", "template_e": "template_f" }; AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "GetTemplate": (t) => ({ "Promotion": { "Entity": promote[t], "RequiredXp": 1000 }, }), }); let ChangeEntityTemplate = function(ent, template) { let newEnt = ent + 1; + let exp = cmpPromotion.GetCurrentXp(); cmpPromotion = ConstructComponent(newEnt, "Promotion", { "Entity": entTemplates[newEnt], "RequiredXp": 1000 }); + cmpPromotion.IncreaseXp(exp); return newEnt; }; Engine.RegisterGlobal("ChangeEntityTemplate", ChangeEntityTemplate); cmpPromotion = ConstructComponent(entity, "Promotion", { "Entity": "template_b", "RequiredXp": 1000 }); // Test getters/setters. TS_ASSERT_EQUALS(cmpPromotion.GetRequiredXp(), 1000); TS_ASSERT_EQUALS(cmpPromotion.GetCurrentXp(), 0); TS_ASSERT_EQUALS(cmpPromotion.GetPromotedTemplateName(), "template_b"); modifier = 111; TS_ASSERT_EQUALS(cmpPromotion.GetRequiredXp(), 1111); modifier = 0; cmpPromotion.IncreaseXp(200); TS_ASSERT_EQUALS(cmpPromotion.GetCurrentXp(), 200); // Test promotion itself. cmpPromotion.IncreaseXp(800); TS_ASSERT_EQUALS(cmpPromotion.entity, 61); TS_ASSERT_EQUALS(cmpPromotion.GetCurrentXp(), 0); TS_ASSERT_EQUALS(cmpPromotion.GetRequiredXp(), 1000); TS_ASSERT_EQUALS(cmpPromotion.GetPromotedTemplateName(), "template_f"); // Test multiple promotions at once. cmpPromotion.IncreaseXp(4200); TS_ASSERT_EQUALS(cmpPromotion.entity, 62); TS_ASSERT_EQUALS(cmpPromotion.template.Entity, "end"); TS_ASSERT_EQUALS(cmpPromotion.GetCurrentXp(), 200); TS_ASSERT_EQUALS(cmpPromotion.GetPromotedTemplateName(), "end"); // Test a dead entity can't promote. cmpPromotion = ConstructComponent(entity, "Promotion", { "Entity": "template_b", "RequiredXp": 1000 }); let cmpHealth = AddMock(entity, IID_Health, { "GetHitpoints": () => 0, }); cmpPromotion.IncreaseXp(1000); TS_ASSERT_EQUALS(cmpPromotion.entity, entity); TS_ASSERT_EQUALS(cmpPromotion.GetCurrentXp(), 0); DeleteMock(entity, IID_Health); // Test XP trickle. let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer", {}); cmpPromotion = ConstructComponent(entity, "Promotion", { "Entity": "template_b", "RequiredXp": "100", "TrickleRate": "10" }); TS_ASSERT_EQUALS(cmpPromotion.GetCurrentXp(), 0); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpPromotion.GetCurrentXp(), 10); cmpTimer.OnUpdate({ "turnLength": 2 }); TS_ASSERT_EQUALS(cmpPromotion.GetCurrentXp(), 30); // Test promoted due to trickle. cmpTimer.OnUpdate({ "turnLength": 8 }); TS_ASSERT_EQUALS(cmpPromotion.GetCurrentXp(), 10); TS_ASSERT_EQUALS(cmpPromotion.entity, 61); // Test valuemodification applies. modifier = 10; cmpPromotion.OnValueModification({ "component": "Promotion" }); cmpTimer.OnUpdate({ "turnLength": 4 }); TS_ASSERT_EQUALS(cmpPromotion.GetCurrentXp(), 90);