Index: ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js (revision 25911) +++ ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js (revision 25912) @@ -1,504 +1,507 @@ function ResourceSupply() {} ResourceSupply.prototype.Schema = "Provides a supply of one particular type of resource." + "" + "1000" + "1000" + "food.meat" + "false" + "25" + "0.8" + "" + "" + "2" + "1000" + "" + "" + "alive" + "2" + "1000" + "500" + "" + "" + "dead notGathered" + "-2" + "1000" + "" + "" + "dead" + "-1" + "1000" + "500" + "" + "" + "" + "" + "" + "" + "" + "Infinity" + "" + "" + "" + "Infinity" + "" + "" + "" + Resources.BuildChoicesSchema(true) + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "alive" + "dead" + "gathered" + "notGathered" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; ResourceSupply.prototype.Init = function() { this.amount = +(this.template.Initial || this.template.Max); // Includes the ones that are tasked but not here yet, i.e. approaching. this.gatherers = []; this.activeGatherers = []; let [type, subtype] = this.template.Type.split('.'); this.cachedType = { "generic": type, "specific": subtype }; if (this.template.Change) { this.timers = {}; this.cachedChanges = {}; } }; ResourceSupply.prototype.IsInfinite = function() { return !isFinite(+this.template.Max); }; ResourceSupply.prototype.GetKillBeforeGather = function() { return this.template.KillBeforeGather == "true"; }; ResourceSupply.prototype.GetMaxAmount = function() { return this.maxAmount; }; ResourceSupply.prototype.GetCurrentAmount = function() { return this.amount; }; ResourceSupply.prototype.GetMaxGatherers = function() { return +this.template.MaxGatherers; }; ResourceSupply.prototype.GetNumGatherers = function() { return this.gatherers.length; }; /** * @return {number} - The number of currently active gatherers. */ ResourceSupply.prototype.GetNumActiveGatherers = function() { return this.activeGatherers.length; }; /** * @return {{ "generic": string, "specific": string }} An object containing the subtype and the generic type. All resources must have both. */ ResourceSupply.prototype.GetType = function() { return this.cachedType; }; /** * @param {number} gathererID - The gatherer's entity id. * @return {boolean} - Whether the ResourceSupply can have this additional gatherer or it is already gathering. */ ResourceSupply.prototype.IsAvailableTo = function(gathererID) { return this.IsAvailable() || this.IsGatheringUs(gathererID); }; /** * @return {boolean} - Whether this entity can have an additional gatherer. */ ResourceSupply.prototype.IsAvailable = function() { return this.amount && this.gatherers.length < this.GetMaxGatherers(); }; /** * @param {number} entity - The entityID to check for. * @return {boolean} - Whether the given entity is already gathering at us. */ ResourceSupply.prototype.IsGatheringUs = function(entity) { return this.gatherers.indexOf(entity) !== -1; }; /** * Each additional gatherer decreases the rate following a geometric sequence, with diminishingReturns as ratio. * @return {number} The diminishing return if any, null otherwise. */ ResourceSupply.prototype.GetDiminishingReturns = function() { if (!this.template.DiminishingReturns) return null; let diminishingReturns = ApplyValueModificationsToEntity("ResourceSupply/DiminishingReturns", +this.template.DiminishingReturns, this.entity); if (!diminishingReturns) return null; let numGatherers = this.GetNumGatherers(); if (numGatherers > 1) return diminishingReturns == 1 ? 1 : (1 - Math.pow(diminishingReturns, numGatherers)) / (1 - diminishingReturns) / numGatherers; return null; }; /** * @param {number} amount The amount of resources that should be taken from the resource supply. The amount must be positive. * @return {{ "amount": number, "exhausted": boolean }} The current resource amount in the entity and whether it's exhausted or not. */ ResourceSupply.prototype.TakeResources = function(amount) { if (this.IsInfinite()) return { "amount": amount, "exhausted": false }; return { "amount": Math.abs(this.Change(-amount)), "exhausted": this.amount == 0 }; }; /** * @param {number} change - The amount to change the resources with (can be negative). * @return {number} - The actual change in resourceSupply. */ ResourceSupply.prototype.Change = function(change) { // Before changing the amount, activate Fogging if necessary to hide changes let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging); if (cmpFogging) cmpFogging.Activate(); let oldAmount = this.amount; this.amount = Math.min(Math.max(oldAmount + change, 0), this.maxAmount); // Remove entities that have been exhausted. if (this.amount == 0) Engine.DestroyEntity(this.entity); let actualChange = this.amount - oldAmount; if (actualChange != 0) { Engine.PostMessage(this.entity, MT_ResourceSupplyChanged, { "from": oldAmount, "to": this.amount }); this.CheckTimers(); } return actualChange; }; /** * @param {number} newValue - The value to set the current amount to. */ ResourceSupply.prototype.SetAmount = function(newValue) { + // We currently don't support changing to or fro Infinity. + if (this.IsInfinite() || newValue === Infinity) + return; this.Change(newValue - this.amount); }; /** * @param {number} gathererID - The gatherer to add. * @return {boolean} - Whether the gatherer was successfully added to the entity's gatherers list * or the entity was already gathering us. */ ResourceSupply.prototype.AddGatherer = function(gathererID) { if (!this.IsAvailable()) return false; if (this.IsGatheringUs(gathererID)) return true; this.gatherers.push(gathererID); Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() }); return true; }; /** * @param {number} player - The playerID owning the gatherer. * @param {number} entity - The entityID gathering. * * @return {boolean} - Whether the gatherer was successfully added to the active-gatherers list * or the entity was already in that list. */ ResourceSupply.prototype.AddActiveGatherer = function(entity) { if (!this.AddGatherer(entity)) return false; if (this.activeGatherers.indexOf(entity) == -1) { this.activeGatherers.push(entity); this.CheckTimers(); } return true; }; /** * @param {number} gathererID - The gatherer's entity id. * @todo: Should this return false if the gatherer didn't gather from said resource? */ ResourceSupply.prototype.RemoveGatherer = function(gathererID) { let index = this.gatherers.indexOf(gathererID); if (index != -1) { this.gatherers.splice(index, 1); Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() }); } index = this.activeGatherers.indexOf(gathererID); if (index == -1) return; this.activeGatherers.splice(index, 1); this.CheckTimers(); }; /** * Checks whether a timer ought to be added or removed. */ ResourceSupply.prototype.CheckTimers = function() { if (!this.template.Change || this.IsInfinite()) return; for (let changeKey in this.template.Change) { if (!this.CheckState(changeKey)) { this.StopTimer(changeKey); continue; } let template = this.template.Change[changeKey]; if (this.amount < +(template.LowerLimit || -1) || this.amount > +(template.UpperLimit || this.GetMaxAmount())) { this.StopTimer(changeKey); continue; } if (this.cachedChanges[changeKey] == 0) { this.StopTimer(changeKey); continue; } if (!this.timers[changeKey]) this.StartTimer(changeKey); } }; /** * This verifies whether the current state of the supply matches the ones needed * for the specific timer to run. * * @param {string} changeKey - The name of the Change to verify the state for. * @return {boolean} - Whether the timer may run. */ ResourceSupply.prototype.CheckState = function(changeKey) { let template = this.template.Change[changeKey]; if (!template.State) return true; let states = template.State; let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); if (states.indexOf("alive") != -1 && !cmpHealth && states.indexOf("dead") == -1 || states.indexOf("dead") != -1 && cmpHealth && states.indexOf("alive") == -1) return false; let activeGatherers = this.GetNumActiveGatherers(); if (states.indexOf("gathered") != -1 && activeGatherers == 0 && states.indexOf("notGathered") == -1 || states.indexOf("notGathered") != -1 && activeGatherers > 0 && states.indexOf("gathered") == -1) return false; return true; }; /** * @param {string} changeKey - The name of the Change to apply to the entity. */ ResourceSupply.prototype.StartTimer = function(changeKey) { if (this.timers[changeKey]) return; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); let interval = ApplyValueModificationsToEntity("ResourceSupply/Change/" + changeKey + "/Interval", +(this.template.Change[changeKey].Interval || 1000), this.entity); this.timers[changeKey] = cmpTimer.SetInterval(this.entity, IID_ResourceSupply, "TimerTick", interval, interval, changeKey); }; /** * @param {string} changeKey - The name of the change to stop the timer for. */ ResourceSupply.prototype.StopTimer = function(changeKey) { if (!this.timers[changeKey]) return; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timers[changeKey]); delete this.timers[changeKey]; }; /** * @param {string} changeKey - The name of the change to apply to the entity. */ ResourceSupply.prototype.TimerTick = function(changeKey) { let template = this.template.Change[changeKey]; if (!template || !this.Change(this.cachedChanges[changeKey])) this.StopTimer(changeKey); }; /** * Since the supposed changes can be affected by modifications, and applying those * are slow, do not calculate them every timer tick. */ ResourceSupply.prototype.RecalculateValues = function() { this.maxAmount = ApplyValueModificationsToEntity("ResourceSupply/Max", +this.template.Max, this.entity); if (!this.template.Change || this.IsInfinite()) return; for (let changeKey in this.template.Change) this.cachedChanges[changeKey] = ApplyValueModificationsToEntity("ResourceSupply/Change/" + changeKey + "/Value", +this.template.Change[changeKey].Value, this.entity); this.CheckTimers(); }; /** * @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; this.RecalculateValues(); }; /** * @param {{ "from": number, "to": number }} msg - Message containing the old new owner. */ ResourceSupply.prototype.OnOwnershipChanged = function(msg) { if (msg.to == INVALID_PLAYER) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); for (let changeKey in this.timers) cmpTimer.CancelTimer(this.timers[changeKey]); } else this.RecalculateValues(); }; /** * @param {{ "entity": number, "newentity": number }} msg - Message to what the entity has been renamed. */ ResourceSupply.prototype.OnEntityRenamed = function(msg) { let cmpResourceSupplyNew = Engine.QueryInterface(msg.newentity, IID_ResourceSupply); if (cmpResourceSupplyNew) cmpResourceSupplyNew.SetAmount(this.GetCurrentAmount()); }; function ResourceSupplyMirage() {} ResourceSupplyMirage.prototype.Init = function(cmpResourceSupply) { this.maxAmount = cmpResourceSupply.GetMaxAmount(); this.amount = cmpResourceSupply.GetCurrentAmount(); this.type = cmpResourceSupply.GetType(); this.isInfinite = cmpResourceSupply.IsInfinite(); this.killBeforeGather = cmpResourceSupply.GetKillBeforeGather(); this.maxGatherers = cmpResourceSupply.GetMaxGatherers(); this.numGatherers = cmpResourceSupply.GetNumGatherers(); }; ResourceSupplyMirage.prototype.GetMaxAmount = function() { return this.maxAmount; }; ResourceSupplyMirage.prototype.GetCurrentAmount = function() { return this.amount; }; ResourceSupplyMirage.prototype.GetType = function() { return this.type; }; ResourceSupplyMirage.prototype.IsInfinite = function() { return this.isInfinite; }; ResourceSupplyMirage.prototype.GetKillBeforeGather = function() { return this.killBeforeGather; }; ResourceSupplyMirage.prototype.GetMaxGatherers = function() { return this.maxGatherers; }; ResourceSupplyMirage.prototype.GetNumGatherers = function() { return this.numGatherers; }; // Apply diminishing returns with more gatherers, for e.g. infinite farms. For most resources this has no effect // (GetDiminishingReturns will return null). We can assume that for resources that are miraged this is the case. ResourceSupplyMirage.prototype.GetDiminishingReturns = function() { return null; }; Engine.RegisterGlobal("ResourceSupplyMirage", ResourceSupplyMirage); ResourceSupply.prototype.Mirage = function() { let mirage = new ResourceSupplyMirage(); mirage.Init(this); return mirage; }; Engine.RegisterComponentType(IID_ResourceSupply, "ResourceSupply", ResourceSupply); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ResourceSupply.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ResourceSupply.js (revision 25911) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ResourceSupply.js (revision 25912) @@ -1,744 +1,761 @@ Resources = { "BuildChoicesSchema": () => { let schema = ""; for (let res of ["food", "metal"]) { for (let subtype in ["meat", "grain"]) schema += "" + res + "." + subtype + ""; schema += " treasure." + res + ""; } return "" + schema + ""; } }; Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/ResourceSupply.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("ResourceSupply.js"); Engine.LoadComponentScript("Timer.js"); let entity = 60; AddMock(entity, IID_Fogging, { "Activate": () => {} }); let template = { "Max": "1001", "Initial": "1000", "Type": "food.meat", "KillBeforeGather": "false", "MaxGatherers": "2" }; let cmpResourceSupply = ConstructComponent(entity, "ResourceSupply", template); cmpResourceSupply.OnOwnershipChanged({ "to": 1 }); TS_ASSERT(!cmpResourceSupply.IsInfinite()); TS_ASSERT(!cmpResourceSupply.GetKillBeforeGather()); TS_ASSERT_EQUALS(cmpResourceSupply.GetMaxAmount(), 1001); TS_ASSERT_EQUALS(cmpResourceSupply.GetMaxGatherers(), 2); TS_ASSERT_EQUALS(cmpResourceSupply.GetDiminishingReturns(), null); TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 0); TS_ASSERT(cmpResourceSupply.IsAvailableTo(70)); TS_ASSERT(cmpResourceSupply.AddGatherer(70)); TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 1); TS_ASSERT(cmpResourceSupply.AddGatherer(71)); TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 2); TS_ASSERT(!cmpResourceSupply.AddGatherer(72)); TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 2); TS_ASSERT(cmpResourceSupply.IsAvailableTo(70)); TS_ASSERT(!cmpResourceSupply.IsAvailableTo(73)); TS_ASSERT(!cmpResourceSupply.AddGatherer(73)); TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 2); cmpResourceSupply.RemoveGatherer(70); TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 1); TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70)); TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 2); cmpResourceSupply.RemoveGatherer(70); TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 1); TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70)); TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 2); cmpResourceSupply.RemoveGatherer(70); TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 1); TS_ASSERT_UNEVAL_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); TS_ASSERT_UNEVAL_EQUALS(cmpResourceSupply.TakeResources(300), { "amount": 300, "exhausted": false }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 700); TS_ASSERT(cmpResourceSupply.IsAvailableTo(70)); TS_ASSERT_UNEVAL_EQUALS(cmpResourceSupply.TakeResources(800), { "amount": 700, "exhausted": true }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 0); // The resource is not available when exhausted TS_ASSERT(!cmpResourceSupply.IsAvailableTo(70)); cmpResourceSupply.RemoveGatherer(71); TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 0); +// #6317 +const infiniteTemplate = { + "Max": "Infinity", + "Type": "food.grain", + "KillBeforeGather": "false", + "MaxGatherers": "1" +}; + +const cmpInfiniteResourceSupply = ConstructComponent(entity, "ResourceSupply", infiniteTemplate); +cmpResourceSupply.OnOwnershipChanged({ "to": 1 }); + +TS_ASSERT(cmpInfiniteResourceSupply.IsInfinite()); +TS_ASSERT_UNEVAL_EQUALS(cmpInfiniteResourceSupply.TakeResources(300), { "amount": 300, "exhausted": false }); + +cmpInfiniteResourceSupply.OnEntityRenamed({ "newentity": entity }); +TS_ASSERT(cmpInfiniteResourceSupply.IsAvailable()); + // Test Changes. let cmpTimer; function reset(newTemplate) { cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); cmpResourceSupply = ConstructComponent(entity, "ResourceSupply", newTemplate); cmpResourceSupply.OnOwnershipChanged({ "to": 1 }); } // Decay. template = { "Max": "1000", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Rotting": { "Value": "-1", "Interval": "1000" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 999); cmpTimer.OnUpdate({ "turnLength": 5 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 994); // Decay with minimum. template = { "Max": "1000", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Rotting": { "Value": "-1", "Interval": "1000", "LowerLimit": "997" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); cmpTimer.OnUpdate({ "turnLength": 3 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 997); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996); // Decay with maximum. template = { "Max": "1000", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Rotting": { "Value": "-1", "Interval": "1000", "UpperLimit": "995" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); // Decay with minimum and maximum. template = { "Max": "1000", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Rotting": { "Value": "-1", "Interval": "1000", "UpperLimit": "995", "LowerLimit": "990" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); cmpResourceSupply.TakeResources(6); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 994); cmpTimer.OnUpdate({ "turnLength": 10 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 989); // Growth. template = { "Initial": "995", "Max": "1000", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Growth": { "Value": "1", "Interval": "1000" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 995); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996); cmpTimer.OnUpdate({ "turnLength": 5 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); // Growth with minimum. template = { "Initial": "995", "Max": "1000", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Growth": { "Value": "1", "Interval": "1000", "LowerLimit": "997" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 995); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 995); // Growth with maximum. template = { "Initial": "994", "Max": "1000", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Growth": { "Value": "1", "Interval": "1000", "UpperLimit": 995 } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 994); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 995); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996); // Growth with minimum and maximum. template = { "Initial": "990", "Max": "1000", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Growth": { "Value": "1", "Interval": "1000", "UpperLimit": "995", "LowerLimit": "990" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 990); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 991); cmpTimer.OnUpdate({ "turnLength": 8 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996); // Growth when resources are taken again. template = { "Initial": "995", "Max": "1000", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Growth": { "Value": "1", "Interval": "1000" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 995); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996); cmpResourceSupply.TakeResources(6); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 990); cmpTimer.OnUpdate({ "turnLength": 5 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 995); // Decay when dead. template = { "Max": "10", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Rotting": { "Value": "-1", "Interval": "1000", "State": "dead" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 9); // No growth when dead. template = { "Max": "10", "Initial": "5", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Growth": { "Value": "1", "Interval": "1000", "State": "alive" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); // Decay when dead or alive. template = { "Max": "10", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Rotting": { "Value": "-1", "Interval": "1000", "State": "dead alive" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 9); AddMock(entity, IID_Health, {}); // Bring the entity to life. cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 8); // No decay when alive. template = { "Max": "10", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Rotting": { "Value": "-1", "Interval": "1000", "State": "dead" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10); // Growth when alive. template = { "Max": "10", "Initial": "5", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Growth": { "Value": "1", "Interval": "1000", "State": "alive" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6); // Growth when dead or alive. template = { "Max": "10", "Initial": "5", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Growth": { "Value": "1", "Interval": "1000", "State": "dead alive" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6); DeleteMock(entity, IID_Health); // "Kill" the entity. cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); // Decay *and* growth. template = { "Max": "10", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Rotting": { "Value": "-1", "Interval": "1000" }, "Growth": { "Value": "1", "Interval": "1000" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10); // Decay *and* growth with different health states. template = { "Max": "10", "Initial": "5", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Rotting": { "Value": "-1", "Interval": "1000", "State": "dead" }, "Growth": { "Value": "1", "Interval": "1000", "State": "alive" } }, "MaxGatherers": "2" }; AddMock(entity, IID_Health, { }); // Bring the entity to life. reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6); DeleteMock(entity, IID_Health); // "Kill" the entity. // We overshoot one due to lateness. cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); // Two effects with different limits. template = { "Max": "20", "Initial": "5", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "SuperGrowth": { "Value": "2", "Interval": "1000", "UpperLimit": "8" }, "Growth": { "Value": "1", "Interval": "1000", "UpperLimit": "12" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 8); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 11); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 12); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 13); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 13); // Two effects with different limits. // This in an interesting case, where the order of the changes matters. template = { "Max": "20", "Initial": "5", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Growth": { "Value": "1", "Interval": "1000", "UpperLimit": "12" }, "SuperGrowth": { "Value": "2", "Interval": "1000", "UpperLimit": "8" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 8); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 9); cmpTimer.OnUpdate({ "turnLength": 5 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 13); // Infinity with growth. template = { "Max": "Infinity", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Growth": { "Value": "1", "Interval": "1000" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), Infinity); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), Infinity); // Infinity with decay. template = { "Max": "Infinity", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Decay": { "Value": "-1", "Interval": "1000" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), Infinity); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), Infinity); // Decay when not gathered. template = { "Max": "10", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Decay": { "Value": "-1", "Interval": "1000", "State": "notGathered" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 9); TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70)); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 9); cmpResourceSupply.RemoveGatherer(70); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 8); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); // Grow when gathered. template = { "Max": "10", "Initial": "5", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Growth": { "Value": "1", "Interval": "1000", "State": "gathered" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70)); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); cmpResourceSupply.RemoveGatherer(70, 1); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); // Grow when gathered or not. template = { "Max": "10", "Initial": "5", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Growth": { "Value": "1", "Interval": "1000", "State": "notGathered gathered" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6); TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70)); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 8); cmpResourceSupply.RemoveGatherer(70); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 9); // Grow when gathered and alive. template = { "Max": "10", "Initial": "5", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Growth": { "Value": "1", "Interval": "1000", "State": "alive gathered" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70)); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); AddMock(entity, IID_Health, { }); // Bring the entity to life. cmpResourceSupply.CheckTimers(); // No other way to tell we've come to life. cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); cmpResourceSupply.RemoveGatherer(70); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); DeleteMock(entity, IID_Health); // "Kill" the entity. cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); // Decay when dead and not gathered. template = { "Max": "10", "Initial": "5", "Type": "food.meat", "KillBeforeGather": "false", "Change": { "Decay": { "Value": "-1", "Interval": "1000", "State": "dead notGathered" } }, "MaxGatherers": "2" }; reset(template); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 4); TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70)); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 4); AddMock(entity, IID_Health, {}); // Bring the entity to life. cmpResourceSupply.CheckTimers(); // No other way to tell we've come to life. cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 4); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 4); cmpResourceSupply.RemoveGatherer(70); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 4); DeleteMock(entity, IID_Health); // "Kill" the entity. cmpResourceSupply.CheckTimers(); // No other way to tell we've died. cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 3); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 2);