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,19 @@ } }; +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 entity = 60; + AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetNumPlayers": () => 3 }); @@ -72,3 +80,224 @@ TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 0); // The resource is not available when exhausted TS_ASSERT(!cmpResourceSupply.IsAvailable(1, 70)); + + +/** + * @param {object} temp the resourceSupply template + * @param {number} ent the entity's entity_id_t + * @param {number[]} results the expect results. + */ +function TestChangeModifiers(temp, ent, results) +{ + let turnLength = 500; + let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); + let resourceSupply = ConstructComponent(ent, "ResourceSupply", temp); + TS_ASSERT_EQUALS(resourceSupply.GetMaxAmount(), results[0]); + TS_ASSERT_EQUALS(resourceSupply.GetCurrentAmount(), results[1]); + TS_ASSERT_EQUALS(resourceSupply.GetNumGatherers(), results[2]); + TS_ASSERT_EQUALS(resourceSupply.GetCurrentAmount(), results[3]); + cmpTimer.OnUpdate({ "turnLength": turnLength }); + TS_ASSERT_EQUALS(resourceSupply.GetCurrentAmount(), results[4]); + cmpTimer.OnUpdate({ "turnLength": turnLength }); + TS_ASSERT_EQUALS(resourceSupply.GetCurrentAmount(), results[5]); + TS_ASSERT(resourceSupply.AddGatherer(2, 72)); + cmpTimer.OnUpdate({ "turnLength": turnLength }); + TS_ASSERT_EQUALS(resourceSupply.GetCurrentAmount(), results[6]); + cmpTimer.OnUpdate({ "turnLength": turnLength }); + TS_ASSERT_EQUALS(resourceSupply.GetCurrentAmount(), results[7]); + resourceSupply.RemoveGatherer(72, 2); + cmpTimer.OnUpdate({ "turnLength": turnLength }); + TS_ASSERT_EQUALS(resourceSupply.GetCurrentAmount(), results[8]); + cmpTimer.OnUpdate({ "turnLength": turnLength }); + TS_ASSERT_EQUALS(resourceSupply.GetCurrentAmount(), results[9]); + AddMock(ent, IID_Health, {}); + TS_ASSERT_EQUALS(!!Engine.QueryInterface(ent, IID_Health), true); + cmpTimer.OnUpdate({ "turnLength": turnLength }); + TS_ASSERT_EQUALS(resourceSupply.GetCurrentAmount(), results[10]); + cmpTimer.OnUpdate({ "turnLength": turnLength }); + TS_ASSERT_EQUALS(resourceSupply.GetCurrentAmount(), results[11]); + cmpTimer.OnUpdate({ "turnLength": turnLength }); + DeleteMock(ent, IID_Health); + TS_ASSERT_EQUALS(!!Engine.QueryInterface(ent, IID_Health), false); + TS_ASSERT_EQUALS(resourceSupply.GetCurrentAmount(), results[12]); + cmpTimer.OnUpdate({ "turnLength": turnLength }); + TS_ASSERT_EQUALS(resourceSupply.GetCurrentAmount(), results[13]); + AddMock(SYSTEM_ENTITY, IID_AuraManager, { + "ApplyModifications": (key, val, ent) => { + if (key == "ResourceSupply/Change/Growth/Value") + return val + 2; + if (key == "ResourceSupply/Change/Decay/Value") + return val - 3; + if (key == "ResourceSupply/Change/Regen/Value") + return val + 1; + return val; + } + }); + cmpTimer.OnUpdate({ "turnLength": turnLength }); + TS_ASSERT_EQUALS(resourceSupply.GetCurrentAmount(), results[14]); + DeleteMock(SYSTEM_ENTITY, IID_AuraManager); +} + +// 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 +}; + +TestChangeModifiers(template, 29, [1000, 1000, 0, 1000, 999, 998, 998, 998, 997, 996, 995, 994, 993, 992, 991]); + +// Growing +template = { + "Amount": 1000, + "Type": "food.meat", + "KillBeforeGather": true, + "Change": { + "Growth": { + "Alive": void (0), + "Value": 5, + "Interval": 500, + "ChangeLimit": 1010 + } + }, + "MaxGatherers": 2 +}; + +TestChangeModifiers(template, 28, [1010, 1000, 0, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1005, 1010, 1010, 1010, 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 +}; + +TestChangeModifiers(template, 27, [2000, 1000, 0, 1000, 997, 994, 994, 994, 994, 994, 999, 1004, 1009, 1006, 1000]); + +// 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, + } + }, + "MaxGatherers": 2 +}; + +TestChangeModifiers(template, 26, [Infinity, Infinity, 0, Infinity, Infinity, Infinity, Infinity, Infinity, Infinity, Infinity, Infinity, Infinity, Infinity, Infinity, Infinity]); + +// Test combined changes Alive + Dead +template = { + "Amount": 500, + "Type": "food.meat", + "KillBeforeGather": true, + "Change": { + "Growth": { + "Alive": void (0), + "Dead": void (0), + "Value": 5, + "Interval": 1000, + "ChangeLimit": 2000 + }, + }, + "MaxGatherers": 2 +}; + +TestChangeModifiers(template, 25, [2000, 500, 0, 500, 505, 510, 515, 520, 525, 530, 535, 540, 545, 550, 557]); + +// Test combined changes Alive + NotBeingGathered +template = { + "Amount": 500, + "Type": "food.meat", + "KillBeforeGather": true, + "Change": { + "Growth": { + "Alive": void (0), + "NotBeingGathered": void (0), + "Value": 5, + "Interval": 1000, + "ChangeLimit": 2000 + }, + }, + "MaxGatherers": 2 +}; + +TestChangeModifiers(template, 24, [2000, 500, 0, 500, 500, 500, 500, 500, 500, 500, 505, 510, 515, 515, 515]); + +// Test combined changes Alive + NotBeingGathered and just NotBeingGathered +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 +}; + +TestChangeModifiers(template, 23, [2000, 500, 0, 500, 505, 510, 510, 510, 515, 520, 530, 540, 550, 555, 561]); + +// Test combined changes Dead + NotBeingGathered +template = { + "Amount": 500, + "Type": "food.meat", + "KillBeforeGather": true, + "Change": { + "Decay": { + "Dead": void (0), + "NotBeingGathered": void (0), + "Value": -5, + "Interval": 1000, + "ChangeLimit": 492 + }, + }, + "MaxGatherers": 2 +}; + +TestChangeModifiers(template, 22, [500, 500, 0, 500, 495, 492, 492, 492, 492, 492, 492, 492, 492, 492, 492]); 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 @@ +