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 @@
+