Index: binaries/data/mods/public/simulation/components/Health.js =================================================================== --- binaries/data/mods/public/simulation/components/Health.js +++ binaries/data/mods/public/simulation/components/Health.js @@ -297,15 +297,23 @@ // persistent corpse retaining the ResourceSupply element of the parent. let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let templateName = cmpTemplateManager.GetCurrentTemplateName(this.entity); - let corpse; + let entCorpse; if (leaveResources) - corpse = Engine.AddEntity("resource|" + templateName); + { + let cmpResourceSupply = Engine.QueryInterface(this.entity, IID_ResourceSupply); + if (cmpResourceSupply) + { + entCorpse = Engine.AddEntity("resource|" + templateName); + let cmpResourceSupplyCorpse = Engine.QueryInterface(entCorpse, IID_ResourceSupply); + cmpResourceSupplyCorpse.SetAmount(cmpResourceSupply.GetCurrentAmount()); + } + } else - corpse = Engine.AddLocalEntity("corpse|" + templateName); + entCorpse = Engine.AddLocalEntity("corpse|" + templateName); // Copy various parameters so it looks just like us - let cmpCorpsePosition = Engine.QueryInterface(corpse, IID_Position); + let cmpCorpsePosition = Engine.QueryInterface(entCorpse, IID_Position); let pos = cmpPosition.GetPosition(); cmpCorpsePosition.JumpTo(pos.x, pos.z); let rot = cmpPosition.GetRotation(); @@ -313,17 +321,17 @@ cmpCorpsePosition.SetXZRotation(rot.x, rot.z); let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); - let cmpCorpseOwnership = Engine.QueryInterface(corpse, IID_Ownership); + let cmpCorpseOwnership = Engine.QueryInterface(entCorpse, IID_Ownership); cmpCorpseOwnership.SetOwner(cmpOwnership.GetOwner()); let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); - let cmpCorpseVisual = Engine.QueryInterface(corpse, IID_Visual); + let cmpCorpseVisual = Engine.QueryInterface(entCorpse, IID_Visual); cmpCorpseVisual.SetActorSeed(cmpVisual.GetActorSeed()); // Make it fall over cmpCorpseVisual.SelectAnimation("death", true, 1.0); - return corpse; + return entCorpse; }; Health.prototype.CreateDeathSpawnedEntity = function() Index: binaries/data/mods/public/simulation/components/ResourceSupply.js =================================================================== --- binaries/data/mods/public/simulation/components/ResourceSupply.js +++ binaries/data/mods/public/simulation/components/ResourceSupply.js @@ -8,6 +8,29 @@ "false" + "25" + "0.8" + + "" + + "" + + "2" + + "1000" + + "" + + "" + + "" + + "2" + + "1000" + + "" + + "" + + "" + + "" + + "2" + + "1000" + + "" + + "" + + "" + + "-1" + + "1000" + + "500" + + "" + + "" + "" + "" + "" + @@ -25,13 +48,54 @@ "" + "" + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + ""; ResourceSupply.prototype.Init = function() { // Current resource amount (non-negative) - this.amount = this.GetMaxAmount(); + this.amount = +this.template.Amount; + if (this.template.Change && !this.IsInfinite()) + { + for (let changeKey in this.template.Change) + if ("ChangeLimit" in this.template.Change[changeKey] && +this.template.Change[changeKey].ChangeLimit > this.amount) + this.maxAmount = Math.max(this.amount, +this.template.Change[changeKey].ChangeLimit); + + for (let changeKey in this.template.Change) + this.AddTimer(changeKey); + } + // List of IDs for each player this.gatherers = []; let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); @@ -54,9 +118,16 @@ ResourceSupply.prototype.GetMaxAmount = function() { - return +this.template.Amount; + return this.maxAmount !== undefined ? this.maxAmount : +this.template.Amount; }; +ResourceSupply.prototype.SetAmount = function(newAmount) +{ + let oldAmount = this.amount; + this.amount = Math.min(Math.max(newAmount, 0), this.GetMaxAmount()); + this.UpdateSupplyStatus(oldAmount); +}; + ResourceSupply.prototype.GetCurrentAmount = function() { return this.amount; @@ -124,17 +195,10 @@ if (this.IsInfinite()) return { "amount": amount, "exhausted": false }; - let oldAmount = this.GetCurrentAmount(); - this.amount = Math.max(0, oldAmount - amount); + let oldAmount = this.amount; + this.SetAmount(oldAmount - amount); - let isExhausted = this.GetCurrentAmount() == 0; - // Remove entities that have been exhausted - if (isExhausted) - Engine.DestroyEntity(this.entity); - - Engine.PostMessage(this.entity, MT_ResourceSupplyChanged, { "from": oldAmount, "to": this.GetCurrentAmount() }); - - return { "amount": oldAmount - this.GetCurrentAmount(), "exhausted": isExhausted }; + return { "amount": oldAmount - this.amount, "exhausted": this.amount == 0 }; }; /** @@ -154,10 +218,33 @@ Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() }); } + if (this.template.Change && !this.IsInfinite()) + { + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + for (let changeKey in this.template.Change) + if ("NotBeingGathered" in this.template.Change[changeKey]) + cmpTimer.CancelTimer(this.timers[changeKey]); + } + return true; }; /** + * @param {{ "component": string, "valueNames": string[] }} msg - message containing a list of values that were changed. + */ +ResourceSupply.prototype.OnValueModification = function(msg) +{ + if (msg.component != "ResourceSupply") + return; + + if (!this.template.Change || this.IsInfinite()) + return; + + for (let changeKey in this.template.Change) + this.AddTimer(changeKey); +} + +/** * @param {number} gathererID - The gatherer's entity id. * @param {number} player - The gatherer's player id. * @todo: Should this return false if the gatherer didn't gather from said resource? @@ -180,6 +267,91 @@ this.gatherers[player].splice(index, 1); // Broadcast message, mainly useful for the AIs. Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() }); + + if (this.GetNumGatherers() != 0 || !this.template.Change || this.IsInfinite()) + return; + + for (let changeKey in this.template.Change) + if ("NotBeingGathered" in this.template.Change[changeKey]) + this.AddTimer(changeKey); }; +/** + * @param {string} changeKey the name of the Change to apply to the entity. + */ +ResourceSupply.prototype.AddTimer = function(changeKey) +{ + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + if (!this.timers) + this.timers = {}; + + if (this.timers[changeKey]) + { + cmpTimer.CancelTimer(this.timers[changeKey]); + delete this.timers[changeKey]; + } + + let change = this.template.Change[changeKey]; + let interval = ApplyValueModificationsToEntity("ResourceSupply/Change/" + changeKey + "/Interval", +change.Interval, this.entity); + this.timers[changeKey] = cmpTimer.SetTimeout(this.entity, IID_ResourceSupply, "ApplyChanges", interval, changeKey); +}; + +/** + * @param {string} changeKey the name of the change to apply to the entity. + * @param {number} lateness how late the timer was executed after the specified time. + */ +ResourceSupply.prototype.ApplyChanges = function(changeKey, lateness) +{ + let change = this.template.Change[changeKey]; + + if (!change) + return; + + let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); + if ("Alive" in change && !cmpHealth && !("Dead" in change) || "Dead" in change && cmpHealth && !("Alive" in change)) + { + this.AddTimer(changeKey); + return; + } + + let newAmount = ApplyValueModificationsToEntity("ResourceSupply/Change/" + changeKey + "/Value", +change.Value, this.entity); + let finalAmount = this.amount + newAmount; + + if ("ChangeLimit" in change) + if (newAmount > 0 && finalAmount > +change.ChangeLimit || + newAmount < 0 && finalAmount < +change.ChangeLimit) + finalAmount = +change.ChangeLimit; + + this.SetAmount(finalAmount); + this.AddTimer(changeKey); +}; + +/** + * Notify the other components the resource amoun has changed. + * @param {number} oldAmount the previous amount in the resource supply. + */ +ResourceSupply.prototype.UpdateSupplyStatus = function(oldAmount) +{ + // Remove entities that have been exhausted. + if (this.amount == 0) + Engine.DestroyEntity(this.entity); + + // Do not send messages if the resource count didn't change. + if (oldAmount != this.amount) + Engine.PostMessage(this.entity, MT_ResourceSupplyChanged, { + "from": oldAmount, + "to": this.amount + }); +}; + +ResourceSupply.prototype.OnDestroy = function() +{ + if (!this.timers) + return; + + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + for (let changeKey in this.timers) + cmpTimer.CancelTimer(this.timers[changeKey]); +}; + Engine.RegisterComponentType(IID_ResourceSupply, "ResourceSupply", ResourceSupply); Index: binaries/data/mods/public/simulation/components/tests/test_ResourceSupply.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_ResourceSupply.js +++ binaries/data/mods/public/simulation/components/tests/test_ResourceSupply.js @@ -11,11 +11,20 @@ } }; +Engine.LoadHelperScript("Player.js"); +Engine.LoadHelperScript("ValueModification.js"); +Engine.LoadComponentScript("interfaces/TechnologyManager.js"); +Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/ResourceSupply.js"); +Engine.LoadComponentScript("interfaces/Health.js"); +Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("ResourceSupply.js"); +Engine.LoadComponentScript("Timer.js"); -const entity = 60; +let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); +let entity = 60; + AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetNumPlayers": () => 3 }); @@ -72,3 +81,297 @@ TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 0); // The resource is not available when exhausted TS_ASSERT(!cmpResourceSupply.IsAvailable(1, 70)); + +// Decay when it has no gatherers +template = { + "Amount": 1000, + "Type": "food.meat", + "KillBeforeGather": false, + "Change": { + "Rotting": { + "NotBeingGathered": void(0), + "Value": -1, + "Interval": 500 + } + }, + "MaxGatherers": 2 +}; +entity = 28; +cmpResourceSupply = ConstructComponent(entity, "ResourceSupply", template); + +TS_ASSERT_EQUALS(cmpResourceSupply.GetMaxAmount(), 1000); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); +TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 0); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 999); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 998); +TS_ASSERT(cmpResourceSupply.AddGatherer(2, 72)); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 998); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 998); +cmpResourceSupply.RemoveGatherer(72, 2); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 997); + +// Growing +template = { + "Amount": 1000, + "Type": "food.meat", + "KillBeforeGather": true, + "Change": { + "Growth": { + "Alive": void(0), + "Value": 5, + "Interval": 500, + "ChangeLimit": 1010 + } + }, + "MaxGatherers": 2 +}; + + +entity = 28; +AddMock(entity, IID_Health, { + +}); +cmpResourceSupply = ConstructComponent(entity, "ResourceSupply", template); +// We can reach higher than the initial max amount, because the timer can make it reach that value. +TS_ASSERT_EQUALS(cmpResourceSupply.GetMaxAmount(), 1010); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1005); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1010); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +// Since we already reached that value, it won't grow anymore. +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1010); +DeleteMock(entity, IID_Health, { + +}); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1010); + + +// Decaying and growing. +template = { + "Amount": 1000, + "Type": "food.meat", + "KillBeforeGather": {}, + "Change": { + "Growth": { + "Alive": void(0), + "Value": 5, + "Interval": 500, + "ChangeLimit": 2000 + }, + "Decay": { + "Dead": void(0), + "Value": -3, + "Interval": 500, + "ChangeLimit": 994 + } + }, + "MaxGatherers": 2 +}; + +entity = 27; +cmpResourceSupply = ConstructComponent(entity, "ResourceSupply", template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetMaxAmount(), 2000); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 997); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 994); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +// Can't go below the Limit +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 994); +AddMock(entity, IID_Health, { + +}); +// Now the unit became alive the death timer no longer applies. +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 994); +TS_ASSERT_EQUALS(!!Engine.QueryInterface(entity, IID_Health), true); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 999); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1004); +DeleteMock(entity, IID_Health, { + +}); +TS_ASSERT_EQUALS(!!Engine.QueryInterface(entity, IID_Health), false); +// Now the health component was removed so it starts decaying again and stops growing. +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1001); + +// Test infinity +template = { + "Amount": Infinity, + "Type": "food.meat", + "KillBeforeGather": true, + "Change": { + "Growth": { + "Alive": void(0), + "Value": 5, + "Interval": 500, + "ChangeLimit": 2000 + }, + "Decay": { + "Dead": void(0), + "Value": -3, + "Interval": 500, + "ChangeLimit": 994 + } + }, + "MaxGatherers": 2 +}; + +entity = 26; +cmpResourceSupply = ConstructComponent(entity, "ResourceSupply", template); +AddMock(entity, IID_Health, { + +}); +TS_ASSERT_EQUALS(cmpResourceSupply.GetMaxAmount(), Infinity); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), Infinity); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +// No growing or decaying when the resource is infinite +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), Infinity); + +// Test combined changes 1 +template = { + "Amount": 500, + "Type": "food.meat", + "KillBeforeGather": true, + "Change": { + "Growth": { + "Alive": void(0), + "Dead": void(0), + "Value": 5, + "Interval": 1000, + "ChangeLimit": 2000 + }, + }, + "MaxGatherers": 2 +}; + +entity = 25; +cmpResourceSupply = ConstructComponent(entity, "ResourceSupply", template); +AddMock(entity, IID_Health, { + +}); +TS_ASSERT_EQUALS(cmpResourceSupply.GetMaxAmount(), 2000); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 500); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 505); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 510); +DeleteMock(entity, IID_Health, { + +}); +cmpTimer.OnUpdate({ "turnLength": 500 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 515); + + +template = { + "Amount": 500, + "Type": "food.meat", + "KillBeforeGather": true, + "Change": { + "Growth": { + "Alive": void(0), + "NotBeingGathered": void(0), + "Value": 5, + "Interval": 1000, + "ChangeLimit": 2000 + }, + }, + "MaxGatherers": 2 +}; + +entity = 25; +cmpResourceSupply = ConstructComponent(entity, "ResourceSupply", template); +AddMock(entity, IID_Health, { + +}); +TS_ASSERT_EQUALS(cmpResourceSupply.GetMaxAmount(), 2000); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 500); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 505); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 510); +DeleteMock(entity, IID_Health, { + +}); +cmpTimer.OnUpdate({ "turnLength": 500 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 510); + +template = { + "Amount": 500, + "Type": "food.meat", + "KillBeforeGather": true, + "Change": { + "Growth": { + "Alive": void(0), + "NotBeingGathered": void(0), + "Value": 5, + "Interval": 1000, + "ChangeLimit": 2000 + }, + "Regen": { + "NotBeingGathered": void(0), + "Value": 5, + "Interval": 1000, + "ChangeLimit": 2000 + }, + }, + "MaxGatherers": 2 +}; + +entity = 24; +cmpResourceSupply = ConstructComponent(entity, "ResourceSupply", template); +AddMock(entity, IID_Health, { + +}); +TS_ASSERT_EQUALS(cmpResourceSupply.GetMaxAmount(), 2000); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 500); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 510); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 520); +DeleteMock(entity, IID_Health, { + +}); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 525); + +template = { + "Amount": 500, + "Type": "food.meat", + "KillBeforeGather": true, + "Change": { + "Decay": { + "Dead": void(0), + "NotBeingGathered": void(0), + "Value": -5, + "Interval": 1000, + "ChangeLimit": 492 + }, + }, + "MaxGatherers": 2 +}; + +entity = 23; +cmpResourceSupply = ConstructComponent(entity, "ResourceSupply", template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetMaxAmount(), 500); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 500); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 495); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 492); +AddMock(entity, IID_Health, { + +}); +cmpTimer.OnUpdate({ "turnLength": 1000 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 492); \ No newline at end of file Index: binaries/data/mods/public/simulation/templates/special/filter/resource.xml =================================================================== --- binaries/data/mods/public/simulation/templates/special/filter/resource.xml +++ binaries/data/mods/public/simulation/templates/special/filter/resource.xml @@ -2,6 +2,7 @@ +