Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js
===================================================================
--- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js
+++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js
@@ -496,6 +496,16 @@
"turretPoints": template.TurretHolder.TurretPoints
};
+ if (template.Upkeep)
+ {
+ ret.upkeep = {
+ "interval": +template.Upkeep.Interval,
+ "rates": {}
+ };
+ for (let type in template.Upkeep.Rates)
+ ret.upkeep.rates[type] = getEntityValue("Upkeep/Rates/" + type);
+ }
+
if (template.WallSet)
{
ret.wallSet = {
Index: ps/trunk/binaries/data/mods/public/gui/common/tooltips.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/tooltips.js
+++ ps/trunk/binaries/data/mods/public/gui/common/tooltips.js
@@ -846,6 +846,30 @@
});
}
+function getUpkeepTooltip(template)
+{
+ if (!template.upkeep)
+ return "";
+
+ let resCodes = g_ResourceData.GetCodes().filter(res => !!template.upkeep.rates[res]);
+ if (!resCodes.length)
+ return "";
+
+ return sprintf(translate("%(label)s %(details)s"), {
+ "label": headerFont(translate("Upkeep:")),
+ "details": sprintf(translate("%(resources)s / %(time)s"), {
+ "resources":
+ resCodes.map(
+ res => sprintf(translate("%(resourceIcon)s %(rate)s"), {
+ "resourceIcon": resourceIcon(res),
+ "rate": template.upkeep.rates[res]
+ })
+ ).join(" "),
+ "time": getSecondsString(template.upkeep.interval / 1000)
+ })
+ });
+}
+
/**
* Returns an array of strings for a set of wall pieces. If the pieces share
* resource type requirements, output will be of the form '10 to 30 Stone',
Index: ps/trunk/binaries/data/mods/public/gui/reference/common/ReferencePage.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/reference/common/ReferencePage.js
+++ ps/trunk/binaries/data/mods/public/gui/reference/common/ReferencePage.js
@@ -65,5 +65,6 @@
getTreasureTooltip,
getPopulationBonusTooltip,
getResourceTrickleTooltip,
+ getUpkeepTooltip,
getLootTooltip
];
Index: ps/trunk/binaries/data/mods/public/gui/session/selection_details.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_details.js
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_details.js
@@ -329,6 +329,7 @@
getPopulationBonusTooltip,
getProjectilesTooltip,
getResourceTrickleTooltip,
+ getUpkeepTooltip,
getLootTooltip
].map(func => func(entState)).filter(tip => tip).join("\n");
if (detailedTooltip)
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
@@ -576,6 +576,13 @@
"run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier()
};
+ let cmpUpkeep = Engine.QueryInterface(ent, IID_Upkeep);
+ if (cmpUpkeep)
+ ret.upkeep = {
+ "interval": cmpUpkeep.GetInterval(),
+ "rates": cmpUpkeep.GetRates()
+ };
+
return ret;
};
Index: ps/trunk/binaries/data/mods/public/simulation/components/Upkeep.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Upkeep.js
+++ ps/trunk/binaries/data/mods/public/simulation/components/Upkeep.js
@@ -0,0 +1,150 @@
+function Upkeep() {}
+
+Upkeep.prototype.Schema =
+ "Controls the resource upkeep of an entity." +
+ "" +
+ Resources.BuildSchema("nonNegativeDecimal") +
+ "" +
+ "" +
+ "" +
+ "";
+
+Upkeep.prototype.Init = function()
+{
+ this.upkeepInterval = +this.template.Interval;
+ this.CheckTimer();
+};
+
+/**
+ * @return {number} - The interval between resource subtractions, in ms.
+ */
+Upkeep.prototype.GetInterval = function()
+{
+ return this.upkeepInterval;
+};
+
+/**
+ * @return {Object} - The upkeep rates in the form of { "resourceName": {number} }.
+ */
+Upkeep.prototype.GetRates = function()
+{
+ return this.rates;
+};
+
+/**
+ * @return {boolean} - Whether this entity has at least one non-zero amount of resources to pay.
+ */
+Upkeep.prototype.ComputeRates = function()
+{
+ this.rates = {};
+ let hasUpkeep = false;
+ for (let resource in this.template.Rates)
+ {
+ let rate = ApplyValueModificationsToEntity("Upkeep/Rates/" + resource, +this.template.Rates[resource], this.entity);
+ if (rate)
+ {
+ this.rates[resource] = rate;
+ hasUpkeep = true;
+ }
+ }
+
+ return hasUpkeep;
+};
+
+/**
+ * Try to subtract the needed resources.
+ * Data and lateness are unused.
+ */
+Upkeep.prototype.Pay = function(data, lateness)
+{
+ let cmpPlayer = QueryOwnerInterface(this.entity);
+ if (!cmpPlayer)
+ return;
+
+ if (!cmpPlayer.TrySubtractResources(this.rates))
+ this.HandleInsufficientUpkeep();
+ else
+ this.HandleSufficientUpkeep();
+};
+
+/**
+ * E.g. take a hitpoint, reduce CP.
+ */
+Upkeep.prototype.HandleInsufficientUpkeep = function()
+{
+ if (this.unpayed)
+ return;
+
+ let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
+ if (cmpIdentity)
+ cmpIdentity.SetControllable(false);
+ this.unpayed = true;
+};
+
+/**
+ * Reset to the previous stage.
+ */
+Upkeep.prototype.HandleSufficientUpkeep = function()
+{
+ if (!this.unpayed)
+ return;
+
+ let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
+ if (cmpIdentity)
+ cmpIdentity.SetControllable(true);
+ delete this.unpayed;
+};
+
+Upkeep.prototype.OnValueModification = function(msg)
+{
+ if (msg.component != "Upkeep")
+ return;
+
+ this.CheckTimer();
+};
+
+/**
+ * Recalculate the interval and update the timer accordingly.
+ */
+Upkeep.prototype.CheckTimer = function()
+{
+ if (!this.ComputeRates())
+ {
+ if (!this.timer)
+ return;
+
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ cmpTimer.CancelTimer(this.timer);
+ delete this.timer;
+ return;
+ }
+
+ let oldUpkeepInterval = this.upkeepInterval;
+ this.upkeepInterval = ApplyValueModificationsToEntity("Upkeep/Interval", +this.template.Interval, this.entity);
+ if (this.upkeepInterval < 0)
+ {
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ cmpTimer.CancelTimer(this.timer);
+ delete this.timer;
+ return;
+ }
+
+ if (this.timer)
+ {
+ if (this.upkeepInterval == oldUpkeepInterval)
+ return;
+
+ // If the timer wasn't invalidated before (interval <= 0), just update it.
+ if (oldUpkeepInterval > 0)
+ {
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ cmpTimer.UpdateRepeatTime(this.timer, this.upkeepInterval);
+ return;
+ }
+ }
+
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ this.timer = cmpTimer.SetInterval(this.entity, IID_Upkeep, "Pay", this.upkeepInterval, this.upkeepInterval, undefined);
+};
+
+Engine.RegisterComponentType(IID_Upkeep, "Upkeep", Upkeep);
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Upkeep.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Upkeep.js
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Upkeep.js
@@ -0,0 +1 @@
+Engine.RegisterInterface("Upkeep");
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
@@ -39,6 +39,7 @@
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("interfaces/Upgrade.js");
+Engine.LoadComponentScript("interfaces/Upkeep.js");
Engine.LoadComponentScript("interfaces/BuildingAI.js");
Engine.LoadComponentScript("GuiInterface.js");
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Upkeep.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Upkeep.js
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Upkeep.js
@@ -0,0 +1,221 @@
+Resources = {
+ "GetCodes": () => ["food", "metal"],
+ "GetTradableCodes": () => ["food", "metal"],
+ "GetBarterableCodes": () => ["food", "metal"],
+ "GetResource": () => ({}),
+ "BuildSchema": (type) => {
+ let schema = "";
+ for (let res of Resources.GetCodes())
+ schema +=
+ "" +
+ "" +
+ "" +
+ "" +
+ "";
+ return "" + schema + "";
+ }
+};
+
+Engine.LoadComponentScript("interfaces/Player.js");
+Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
+Engine.LoadComponentScript("interfaces/Timer.js");
+Engine.LoadComponentScript("interfaces/Upkeep.js");
+Engine.LoadComponentScript("Player.js");
+Engine.LoadComponentScript("Timer.js");
+Engine.LoadComponentScript("Upkeep.js");
+
+// Upkeep requires this function to be defined before the component is built.
+let ApplyValueModificationsToEntity = (valueName, currentValue, entity) => currentValue;
+Engine.RegisterGlobal("ApplyValueModificationsToEntity", ApplyValueModificationsToEntity);
+let testedEnt = 10;
+let turnLength = 0.2;
+let playerEnt = 1;
+let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer", {});
+
+let cmpUpkeep = ConstructComponent(testedEnt, "Upkeep", {
+ "Interval": "200",
+ "Rates": {
+ "food": "0",
+ "metal": "0"
+ }
+});
+
+let cmpPlayer = ConstructComponent(playerEnt, "Player", {
+ "SpyCostMultiplier": "1",
+ "BarterMultiplier": {
+ "Buy": {
+ "food": "1",
+ "metal": "1"
+ },
+ "Sell": {
+ "food": "1",
+ "metal": "1"
+ }
+ },
+});
+
+let QueryOwnerInterface = () => cmpPlayer;
+Engine.RegisterGlobal("QueryOwnerInterface", QueryOwnerInterface);
+Engine.RegisterGlobal("QueryPlayerIDInterface", () => null);
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 300, "metal": 300 });
+TS_ASSERT_EQUALS(cmpUpkeep.GetInterval(), 200);
+
+// Since there is no rate > 0, nothing should change.
+TS_ASSERT_UNEVAL_EQUALS(cmpUpkeep.GetRates(), {});
+TS_ASSERT_EQUALS(cmpUpkeep.ComputeRates(), false);
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 300, "metal": 300 });
+
+// Test that only requiring food works.
+ApplyValueModificationsToEntity = (valueName, currentValue, entity) => {
+ if (valueName == "Upkeep/Rates/food")
+ return currentValue + 1;
+
+ return currentValue;
+};
+Engine.RegisterGlobal("ApplyValueModificationsToEntity", ApplyValueModificationsToEntity);
+// Calling OnValueModification will reset the timer, which can then be called, thus decreasing the resources of the player.
+cmpUpkeep.OnValueModification({ "component": "Upkeep" });
+TS_ASSERT_UNEVAL_EQUALS(cmpUpkeep.GetRates(), { "food": 1 });
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 299, "metal": 300 });
+TS_ASSERT_EQUALS(cmpUpkeep.ComputeRates(), true);
+
+// Reset the pay modification.
+ApplyValueModificationsToEntity = (valueName, currentValue, entity) => currentValue;
+Engine.RegisterGlobal("ApplyValueModificationsToEntity", ApplyValueModificationsToEntity);
+cmpUpkeep.OnValueModification({ "component": "Upkeep" });
+TS_ASSERT_UNEVAL_EQUALS(cmpUpkeep.GetRates(), {});
+TS_ASSERT_EQUALS(cmpUpkeep.ComputeRates(), false);
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 299, "metal": 300 });
+
+ApplyValueModificationsToEntity = (valueName, currentValue, entity) => {
+ if (valueName == "Upkeep/Interval")
+ return currentValue + 200;
+ if (valueName == "Upkeep/Rates/food")
+ return currentValue + 1;
+
+ return currentValue;
+};
+Engine.RegisterGlobal("ApplyValueModificationsToEntity", ApplyValueModificationsToEntity);
+cmpUpkeep.OnValueModification({ "component": "Upkeep" });
+TS_ASSERT_EQUALS(cmpUpkeep.GetInterval(), 400);
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 299, "metal": 300 });
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 298, "metal": 300 });
+
+// Interval becomes a normal timer, thus cancelled after the first execution.
+ApplyValueModificationsToEntity = (valueName, currentValue, entity) => {
+ if (valueName == "Upkeep/Interval")
+ return currentValue - 200;
+ if (valueName == "Upkeep/Rates/food")
+ return currentValue + 1;
+
+ return currentValue;
+};
+Engine.RegisterGlobal("ApplyValueModificationsToEntity", ApplyValueModificationsToEntity);
+cmpUpkeep.OnValueModification({ "component": "Upkeep" });
+TS_ASSERT_EQUALS(cmpUpkeep.GetInterval(), 0);
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 298, "metal": 300 });
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 297, "metal": 300 });
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 297, "metal": 300 });
+
+// Timer became invalidated, check whether it's recreated properly after that.
+ApplyValueModificationsToEntity = (valueName, currentValue, entity) => {
+ if (valueName == "Upkeep/Interval")
+ return currentValue - 100;
+ if (valueName == "Upkeep/Rates/food")
+ return currentValue + 1;
+
+ return currentValue;
+};
+Engine.RegisterGlobal("ApplyValueModificationsToEntity", ApplyValueModificationsToEntity);
+cmpUpkeep.OnValueModification({ "component": "Upkeep" });
+TS_ASSERT_EQUALS(cmpUpkeep.GetInterval(), 100);
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 295, "metal": 300 });
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 293, "metal": 300 });
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 291, "metal": 300 });
+
+// Value is now invalid, timer should be cancelled.
+ApplyValueModificationsToEntity = (valueName, currentValue, entity) => {
+ if (valueName == "Upkeep/Interval")
+ return currentValue - 201;
+ if (valueName == "Upkeep/Rates/food")
+ return currentValue + 1;
+
+ return currentValue;
+};
+Engine.RegisterGlobal("ApplyValueModificationsToEntity", ApplyValueModificationsToEntity);
+cmpUpkeep.OnValueModification({ "component": "Upkeep" });
+TS_ASSERT_EQUALS(cmpUpkeep.GetInterval(), -1);
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 291, "metal": 300 });
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 291, "metal": 300 });
+
+// Timer became invalidated, check whether it's recreated properly after that.
+ApplyValueModificationsToEntity = (valueName, currentValue, entity) => {
+ if (valueName == "Upkeep/Rates/food")
+ return currentValue + 1;
+
+ return currentValue;
+};
+Engine.RegisterGlobal("ApplyValueModificationsToEntity", ApplyValueModificationsToEntity);
+cmpUpkeep.OnValueModification({ "component": "Upkeep" });
+TS_ASSERT_EQUALS(cmpUpkeep.GetInterval(), 200);
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 290, "metal": 300 });
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 289, "metal": 300 });
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 288, "metal": 300 });
+
+// Test multiple upkeep resources.
+ApplyValueModificationsToEntity = (valueName, currentValue, entity) => {
+ if (valueName == "Upkeep/Rates/food")
+ return currentValue + 1;
+ if (valueName == "Upkeep/Rates/metal")
+ return currentValue + 2;
+
+ return currentValue;
+};
+Engine.RegisterGlobal("ApplyValueModificationsToEntity", ApplyValueModificationsToEntity);
+cmpUpkeep.OnValueModification({ "component": "Upkeep" });
+TS_ASSERT_EQUALS(cmpUpkeep.GetInterval(), 200);
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 287, "metal": 298 });
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 286, "metal": 296 });
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 285, "metal": 294 });
+
+// Test we don't go into negative resources.
+let cmpGUI = AddMock(SYSTEM_ENTITY, IID_GuiInterface, {
+ "PushNotification": () => {}
+});
+let notificationSpy = new Spy(cmpGUI, "PushNotification");
+ApplyValueModificationsToEntity = (valueName, currentValue, entity) => {
+ if (valueName == "Upkeep/Rates/food")
+ return currentValue + 1;
+
+ return currentValue;
+};
+Engine.RegisterGlobal("ApplyValueModificationsToEntity", ApplyValueModificationsToEntity);
+cmpUpkeep.OnValueModification({ "component": "Upkeep" });
+TS_ASSERT_EQUALS(cmpUpkeep.GetInterval(), 200);
+cmpTimer.OnUpdate({ "turnLength": turnLength * 285 });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 0, "metal": 294 });
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 0, "metal": 294 });
+TS_ASSERT_EQUALS(notificationSpy._called, 1);
+cmpTimer.OnUpdate({ "turnLength": turnLength });
+TS_ASSERT_UNEVAL_EQUALS(cmpPlayer.GetResourceCounts(), { "food": 0, "metal": 294 });
+TS_ASSERT_EQUALS(notificationSpy._called, 2);