Index: binaries/data/mods/public/simulation/components/ResourceGatherer.js =================================================================== --- binaries/data/mods/public/simulation/components/ResourceGatherer.js +++ binaries/data/mods/public/simulation/components/ResourceGatherer.js @@ -31,6 +31,12 @@ Resources.BuildSchema("positiveDecimal") + ""; +/* + * Call interval will be determined by gather rate, + * so always gather integer amount when gathering. + */ +ResourceGatherer.prototype.GATHER_AMOUNT = 1; + ResourceGatherer.prototype.Init = function() { this.carrying = {}; // { generic type: integer amount currently carried } @@ -40,6 +46,8 @@ // The last exact type gathered, so we can render appropriate props this.lastCarriedType = undefined; // { generic, specific } + + this.target = INVALID_ENTITY; }; /** @@ -155,23 +163,25 @@ }; /** - * Try to gather treasure - * @return 'true' if treasure is successfully gathered, otherwise 'false' + * Take all of the resources of the supply. + * Used currently solely by treasures. */ -ResourceGatherer.prototype.TryInstantGather = function(target) +ResourceGatherer.prototype.InstantGather = function(target) { let cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply); - let type = cmpResourceSupply.GetType(); - - if (type.generic != "treasure") - return false; + if (!cmpResourceSupply) + return; + let type = cmpResourceSupply.GetType(); let status = cmpResourceSupply.TakeResources(cmpResourceSupply.GetCurrentAmount()); let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); if (cmpPlayer) cmpPlayer.AddResource(type.specific, status.amount); + if (type.generic != "treasure") + return; + let cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); if (cmpStatisticsTracker) cmpStatisticsTracker.IncreaseTreasuresCollectedCounter(); @@ -179,33 +189,115 @@ let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); if (cmpTrigger && cmpPlayer) cmpTrigger.CallEvent("TreasureCollected", { "player": cmpPlayer.GetPlayerID(), "type": type.specific, "amount": status.amount }); +}; + +/** + * Starts gathering on the specified target. + * @param {number} target - The target to gather from. + * @param {Object} callback - An object with the functions to call on specific events. + * @return {number} - The gathering rate. + */ +ResourceGatherer.prototype.StartGathering = function(target, callback) +{ + if (this.target != INVALID_ENTITY) + this.StopGathering(); + + let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership) + return false; + + // Check if the resource is full. + // We'll only be added if we're not already in. + let cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply); + if (!cmpResourceSupply || !cmpResourceSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity)) + return false; + + let rate = this.GetTargetGatherRate(target); + if (!rate) + return false; + + // Calculate timing based on gather rates. + // This allows the gather rate to control how often we gather, instead of how much. + let timing = 1000 / rate; + + this.target = target; + this.callback = callback; + + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + this.gatherTimer = cmpTimer.SetInterval(this.entity, IID_ResourceGatherer, "PerformGather", timing, timing, null); + + return rate; +}; + +/** + * Stop gathering, cancel the timer and remove us from the ResourceSupply. + */ +ResourceGatherer.prototype.StopGathering = function(reason) +{ + if (this.gatherTimer) + { + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.gatherTimer); + delete this.gatherTimer; + } - return true; + if (this.target != INVALID_ENTITY) + { + let cmpResourceSupply = Engine.QueryInterface(this.target, IID_ResourceSupply); + if (cmpResourceSupply) + cmpResourceSupply.RemoveGatherer(this.entity); + this.target = INVALID_ENTITY; + } + + if (!reason) + return; + + let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); + if (cmpUnitAI) + cmpUnitAI[this.callback[reason]].apply(cmpUnitAI); }; /** * Gather from the target entity. This should only be called after a successful range check, * and if the target has a compatible ResourceSupply. - * Call interval will be determined by gather rate, so always gather 1 amount when called. */ -ResourceGatherer.prototype.PerformGather = function(target) +ResourceGatherer.prototype.PerformGather = function() { - if (!this.GetTargetGatherRate(target)) - return { "exhausted": true }; + if (!this.CanGather(this.target)) + { + this.StopGathering("targetInvalidated"); + return; + } - let gatherAmount = 1; + if (!this.IsTargetInRange(this.target)) + { + this.StopGathering("outOfRange"); + return; + } - let cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply); + let cmpResourceSupply = Engine.QueryInterface(this.target, IID_ResourceSupply); let type = cmpResourceSupply.GetType(); - // Initialise the carried count if necessary + if (type.generic == "treasure") + { + this.InstantGather(this.target); + this.StopGathering("targetInvalidated"); + return; + } + + // If we've already got some resources but they're the wrong type, + // drop them first to ensure we're only ever carrying one type. + if (this.IsCarryingAnythingExcept(type.generic)) + this.DropResources(); + + // Initialise the carried count if necessary. if (!this.carrying[type.generic]) this.carrying[type.generic] = 0; - // Find the maximum so we won't exceed our capacity + // Find the maximum so we won't exceed our capacity. let maxGathered = this.GetCapacity(type.generic) - this.carrying[type.generic]; - let status = cmpResourceSupply.TakeResources(Math.min(gatherAmount, maxGathered)); + let status = cmpResourceSupply.TakeResources(Math.min(this.GATHER_AMOUNT, maxGathered)); this.carrying[type.generic] += status.amount; @@ -213,19 +305,23 @@ // Update stats of how much the player collected. // (We have to do it here rather than at the dropsite, because we - // need to know what subtype it was) + // need to know what subtype it was.) let cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); if (cmpStatisticsTracker) cmpStatisticsTracker.IncreaseResourceGatheredCounter(type.generic, status.amount, type.specific); Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() }); + let filled = !this.CanCarryMore(type.generic); + if (!status.exhausted && !filled) + return; + - return { - "amount": status.amount, - "exhausted": status.exhausted, - "filled": this.carrying[type.generic] >= this.GetCapacity(type.generic) - }; + + if (filled) + this.StopGathering("inventoryFilled"); + else if (status.exhausted) + this.StopGathering("targetInvalidated"); }; /** @@ -236,14 +332,16 @@ ResourceGatherer.prototype.GetTargetGatherRate = function(target) { let cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply); - if (!cmpResourceSupply) + if (!cmpResourceSupply || cmpResourceSupply.GetCurrentAmount() <= 0) return 0; let type = cmpResourceSupply.GetType(); + if (type.generic == "treasure") + return 1; let rate = 0; if (type.specific) - rate = this.GetGatherRate(type.generic+"."+type.specific); + rate = this.GetGatherRate(type.generic + "." + type.specific); if (rate == 0 && type.generic) rate = this.GetGatherRate(type.generic); @@ -252,7 +350,7 @@ // 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 - // (else just add the diminishing returns data to the mirage data and remove the early return above) + // (else just add the diminishing returns data to the mirage data and remove the early return above). let diminishingReturns = cmpResourceSupply.GetDiminishingReturns(); if (diminishingReturns) rate *= diminishingReturns; @@ -261,6 +359,15 @@ }; /** + * @param {number} target - The entity ID of the target to check. + * @return {boolean} - Whether we can gather from the target. + */ +ResourceGatherer.prototype.CanGather = function(target) +{ + return this.GetTargetGatherRate(target) > 0; +}; + +/** * Returns whether this unit can carry more of the given type of resource. * (This ignores whether the unit is actually able to gather that * resource type or not.) @@ -324,6 +431,17 @@ Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() }); }; +/** + * @param {number} - The entity ID of the target to check. + * @return {boolean} - Whether this entity is in range of its target. + */ +ResourceGatherer.prototype.IsTargetInRange = function(target) +{ + let range = this.GetRange(); + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false); +}; + // Since we cache gather rates, we need to make sure we update them when tech changes. // and when our owner change because owners can had different techs. ResourceGatherer.prototype.OnValueModification = function(msg) Index: binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/UnitAI.js +++ binaries/data/mods/public/simulation/components/UnitAI.js @@ -193,6 +193,18 @@ // ignore }, + "OutOfRange": function(msg) { + // ignore + }, + + "TargetInvalidated": function(msg) { + // ignore + }, + + "InventoryFilled": function(msg) { + // ignore + }, + // Formation handlers: "FormationLeave": function(msg) { @@ -2287,55 +2299,39 @@ "GATHERING": { "enter": function() { - this.gatheringTarget = this.order.data.target || INVALID_ENTITY; // deleted in "leave". - - // Check if the resource is full. - // Will only be added if we're not already in. - let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); - let cmpSupply; - if (cmpOwnership) - cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); - if (!cmpSupply || !cmpSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity)) + let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); + if (!cmpResourceGatherer) { - this.SetNextState("FINDINGNEWTARGET"); + this.FinishOrder(); return true; } + let gatheringTarget = this.order.data.target || INVALID_ENTITY; + // If this order was forced, the player probably gave it, but now we've reached the target - // switch to an unforced order (can be interrupted by attacks) + // switch to an unforced order (can be interrupted by attacks). this.order.data.force = false; this.order.data.autoharvest = true; - // Calculate timing based on gather rates - // This allows the gather rate to control how often we gather, instead of how much. - let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); - let rate = cmpResourceGatherer.GetTargetGatherRate(this.gatheringTarget); - - if (!rate) + let callback = { + "outOfRange": "OnOutOfRange", + "targetInvalidated": "OnTargetInvalidated", + "inventoryFilled": "OnInventoryFilled" + }; + if (!cmpResourceGatherer.StartGathering(gatheringTarget, callback)) { - // Try to find another target if the current one stopped existing - if (!Engine.QueryInterface(this.gatheringTarget, IID_Identity)) - { - this.SetNextState("FINDINGNEWTARGET"); - return true; - } - - // No rate, give up on gathering - this.FinishOrder(); + this.SetNextState("FINDINGNEWTARGET"); return true; } - // Scale timing interval based on rate, and start timer - // The offset should be at least as long as the repeat time so we use the same value for both. - let offset = 1000 / rate; - this.StartTimer(offset, offset); + this.StartTimer(1000, 1000); // We want to start the gather animation as soon as possible, // but only if we're actually at the target and it's still alive // (else it'll look like we're chopping empty air). // (If it's not alive, the Timer handler will deal with sending us // off to a different target.) - if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer)) + if (this.CheckTargetRange(gatheringTarget, IID_ResourceGatherer)) { this.StopMoving(); this.SetDefaultAnimationVariant(); @@ -2348,99 +2344,49 @@ "leave": function() { this.StopTimer(); - // Don't use ownership because this is called after a conversion/resignation - // and the ownership would be invalid then. - let cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); - if (cmpSupply) - cmpSupply.RemoveGatherer(this.entity); - delete this.gatheringTarget; + let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); + if (cmpResourceGatherer) + cmpResourceGatherer.StopGathering(); this.ResetAnimation(); }, - "Timer": function(msg) { - let resourceTemplate = this.order.data.template; - let resourceType = this.order.data.type; - - let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); - if (!cmpOwnership) - return; - - // TODO: we are leaking information here - if the target died in FOW, we'll know it's dead - // straight away. - // Seems one would have to listen to ownership changed messages to make it work correctly - // but that's likely prohibitively expansive performance wise. - - let cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); - // If we can't gather from the target, find a new one. - if (!cmpSupply || !cmpSupply.IsAvailable(cmpOwnership.GetOwner(), this.entity) || - !this.CanGather(this.gatheringTarget)) - { - this.SetNextState("FINDINGNEWTARGET"); - return; - } - - if (!this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer)) + "InventoryFilled": function(msg) { + let nearestDropsite = this.FindNearestDropsite(this.order.data.type.generic); + if (nearestDropsite) { - // Try to follow the target - if (this.MoveToTargetRange(this.gatheringTarget, IID_ResourceGatherer)) - this.SetNextState("APPROACHING"); - // Our target is no longer visible - go to its last known position first - // and then hopefully it will become visible. - else if (!this.CheckTargetVisible(this.gatheringTarget) && this.order.data.lastPos) - this.PushOrderFront("Walk", { - "x": this.order.data.lastPos.x, - "z": this.order.data.lastPos.z, - "force": this.order.data.force - }); - else - this.SetNextState("FINDINGNEWTARGET"); + // Keep this Gather order on the stack so we'll + // continue gathering after returning. + this.PushOrderFront("ReturnResource", { "target": nearestDropsite, "force": false }); return; } - // Gather the resources: - - let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); + // Oh no, couldn't find any dropsites. Give up on gathering. + this.FinishOrder(); + }, - // Try to gather treasure - if (cmpResourceGatherer.TryInstantGather(this.gatheringTarget)) - return; + "OutOfRange": function(msg) { + // Try to follow the target + if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer)) + this.SetNextState("APPROACHING"); + // Our target is no longer visible - go to its last known position first + // and then hopefully it will become visible. + else if (!this.CheckTargetVisible(this.order.data.target) && this.order.data.lastPos) + this.PushOrderFront("Walk", { + "x": this.order.data.lastPos.x, + "z": this.order.data.lastPos.z, + "force": this.order.data.force + }); + else + this.SetNextState("FINDINGNEWTARGET"); + }, - // If we've already got some resources but they're the wrong type, - // drop them first to ensure we're only ever carrying one type - if (cmpResourceGatherer.IsCarryingAnythingExcept(resourceType.generic)) - cmpResourceGatherer.DropResources(); + "TargetInvalidated": function(msg) { + this.SetNextState("FINDINGNEWTARGET"); + }, + "Timer": function(msg) { this.FaceTowardsTarget(this.order.data.target); - - // Collect from the target - let status = cmpResourceGatherer.PerformGather(this.gatheringTarget); - - // If we've collected as many resources as possible, - // return to the nearest dropsite - if (status.filled) - { - let nearestDropsite = this.FindNearestDropsite(resourceType.generic); - if (nearestDropsite) - { - // (Keep this Gather order on the stack so we'll - // continue gathering after returning) - // However mark our target as invalid if it's exhausted, so we don't waste time - // trying to gather from it. - if (status.exhausted) - this.order.data.target = INVALID_ENTITY; - this.PushOrderFront("ReturnResource", { "target": nearestDropsite, "force": false }); - return; - } - - // Oh no, couldn't find any drop sites. Give up on gathering. - this.FinishOrder(); - return; - } - - // Find a new target if the current one is exhausted - if (status.exhausted) - this.SetNextState("FINDINGNEWTARGET"); }, }, @@ -4195,6 +4141,21 @@ this.UnitFsm.ProcessMessage(this, {"type": "PackFinished", "packed": msg.packed}); }; +UnitAI.prototype.OnOutOfRange = function(msg) +{ + this.UnitFsm.ProcessMessage(this, { "type": "OutOfRange", "data": msg }); +}; + +UnitAI.prototype.OnTargetInvalidated = function(msg) +{ + this.UnitFsm.ProcessMessage(this, { "type": "TargetInvalidated", "data": msg }); +}; + +UnitAI.prototype.OnInventoryFilled = function(msg) +{ + this.UnitFsm.ProcessMessage(this, { "type": "InventoryFilled", "data": msg }); +}; + //// Helper functions to be called by the FSM //// UnitAI.prototype.GetWalkSpeed = function() Index: binaries/data/mods/public/simulation/components/interfaces/ResourceGatherer.js =================================================================== --- binaries/data/mods/public/simulation/components/interfaces/ResourceGatherer.js +++ binaries/data/mods/public/simulation/components/interfaces/ResourceGatherer.js @@ -1,7 +1,7 @@ Engine.RegisterInterface("ResourceGatherer"); /** - * Message of the form { "to": [{ "type": string, "amount": number, "max":number }] } + * Message of the form { "to": [{ "type": string, "amount": number, "max": number }] } * sent from ResourceGatherer component whenever the amount of carried resources changes. */ Engine.RegisterMessageType("ResourceCarryingChanged"); Index: binaries/data/mods/public/simulation/components/tests/test_ResourceGatherer.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/tests/test_ResourceGatherer.js @@ -0,0 +1,291 @@ +Resources = { + "BuildSchema": () => { + let schema = ""; + for (let res of ["food", "wood"]) + { + for (let subtype in ["meat", "grain"]) + schema += "" + res + "." + subtype + ""; + schema += " treasure." + res + ""; + } + return "" + schema + ""; + } +}; + +Engine.LoadComponentScript("interfaces/ResourceGatherer.js"); +Engine.LoadComponentScript("interfaces/ResourceSupply.js"); +Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); +Engine.LoadComponentScript("interfaces/Timer.js"); +Engine.LoadComponentScript("interfaces/Trigger.js"); +Engine.LoadComponentScript("interfaces/UnitAI.js"); +Engine.LoadComponentScript("ResourceGatherer.js"); +Engine.LoadComponentScript("Timer.js"); +Engine.LoadHelperScript("Player.js"); + +AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { + "IsInTargetRange": () => true +}); + +let ApplyValueModificationsToEntity = (valueName, value, entity) => value; +Engine.RegisterGlobal("ApplyValueModificationsToEntity", ApplyValueModificationsToEntity); +let QueryOwnerInterface = () => {}; +Engine.RegisterGlobal("QueryOwnerInterface", QueryOwnerInterface); +let cmpTimer; + +let gathererID = 101; +let supplyID = 102; + +let template = { + "MaxDistance": "10", + "BaseSpeed": "1", + "Rates": { + "food": "1", + "wood": "2" + }, + "Capacities": { + "food": "10", + "wood": "20" + } +}; +let cmpResourceGatherer = ConstructComponent(gathererID, "ResourceGatherer", template); +function reset() +{ + cmpResourceGatherer = ConstructComponent(gathererID, "ResourceGatherer", template); + cmpResourceGatherer.RecalculateGatherRatesAndCapacities(); // Force updating values. + cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer", {}); +} + +// General getters. +TS_ASSERT_UNEVAL_EQUALS(cmpResourceGatherer.GetCarryingStatus(), []); +TS_ASSERT_EQUALS(cmpResourceGatherer.GetMainCarryingType(), undefined); +TS_ASSERT_EQUALS(cmpResourceGatherer.GetLastCarriedType(), undefined); +reset(); +TS_ASSERT_UNEVAL_EQUALS(cmpResourceGatherer.GetGatherRates(), { + "food": 1, + "wood": 2 +}); +TS_ASSERT_EQUALS(cmpResourceGatherer.GetGatherRate("food"), 1); +TS_ASSERT_EQUALS(cmpResourceGatherer.GetGatherRate("bogus"), 0); +TS_ASSERT_EQUALS(cmpResourceGatherer.GetCapacity("wood"), 20); +TS_ASSERT_EQUALS(cmpResourceGatherer.GetCapacity("bogus"), 0); +TS_ASSERT_UNEVAL_EQUALS(cmpResourceGatherer.GetRange(), { + "max": 10, + "min": 0 +}); +TS_ASSERT(cmpResourceGatherer.CanCarryMore("food")); +TS_ASSERT(!cmpResourceGatherer.CanCarryMore("bogus")); +TS_ASSERT(!cmpResourceGatherer.IsCarrying("food")); +TS_ASSERT(!cmpResourceGatherer.IsCarrying("bogus")); + + +// Test normal gathering. +reset(); +// No owner. +TS_ASSERT(!cmpResourceGatherer.StartGathering(supplyID)); + +AddMock(gathererID, IID_Ownership, { + "GetOwner": () => 1 +}); + +// Resource supply is full (or something else). +AddMock(supplyID, IID_ResourceSupply, { + "AddGatherer": () => false +}); +TS_ASSERT(!cmpResourceGatherer.StartGathering(supplyID)); + +// Supply is empty. +AddMock(supplyID, IID_ResourceSupply, { + "AddGatherer": () => true, + "GetCurrentAmount": () => 0 +}); +TS_ASSERT(!cmpResourceGatherer.StartGathering(supplyID)); + +// Supply is wrong type. +AddMock(supplyID, IID_ResourceSupply, { + "AddGatherer": () => true, + "GetCurrentAmount": () => 1, + "GetType": () => ({ "generic": "bogus" }), + "GetDiminishingReturns": () => 1 + +}); +TS_ASSERT(!cmpResourceGatherer.StartGathering(supplyID)); + + +// Supply is non-empty and right type. +AddMock(supplyID, IID_ResourceSupply, { + "AddGatherer": () => true, + "GetCurrentAmount": () => 2, + "GetType": () => ({ "generic": "food" }), + "GetDiminishingReturns": () => 1, + "TakeResources": (amount) => { + return { + "amount": amount, + "exhausted": false + }; + }, + "RemoveGatherer": () => {} + +}); +TS_ASSERT(cmpResourceGatherer.StartGathering(supplyID)); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_UNEVAL_EQUALS(cmpResourceGatherer.GetCarryingStatus(), + [{ "type": "food", "amount": 1, "max": 10 }]); +TS_ASSERT_EQUALS(cmpResourceGatherer.GetMainCarryingType(), "food"); +TS_ASSERT_UNEVAL_EQUALS(cmpResourceGatherer.GetLastCarriedType(), { "generic": "food" }); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_UNEVAL_EQUALS(cmpResourceGatherer.GetCarryingStatus(), + [{ "type": "food", "amount": 2, "max": 10 }]); +cmpResourceGatherer.StopGathering(); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_UNEVAL_EQUALS(cmpResourceGatherer.GetCarryingStatus(), + [{ "type": "food", "amount": 2, "max": 10 }]); +TS_ASSERT(cmpResourceGatherer.IsCarrying("food")); +TS_ASSERT(cmpResourceGatherer.CanCarryMore("food")); + + +// Test that when gathering a second type the first gathered type is ditched. +reset(); +AddMock(supplyID, IID_ResourceSupply, { + "AddGatherer": () => true, + "GetCurrentAmount": () => 2, + "GetType": () => ({ "generic": "food" }), + "GetDiminishingReturns": () => 1, + "TakeResources": (amount) => { + return { + "amount": amount, + "exhausted": false + }; + }, + "RemoveGatherer": () => {} + +}); +TS_ASSERT(cmpResourceGatherer.StartGathering(supplyID)); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_UNEVAL_EQUALS(cmpResourceGatherer.GetCarryingStatus(), + [{ "type": "food", "amount": 1, "max": 10 }]); +cmpResourceGatherer.StopGathering(); + +AddMock(supplyID, IID_ResourceSupply, { + "AddGatherer": () => true, + "GetCurrentAmount": () => 3, + "GetType": () => ({ "generic": "wood" }), + "GetDiminishingReturns": () => 1, + "TakeResources": (amount) => { + return { + "amount": amount, + "exhausted": false + }; + }, + "RemoveGatherer": () => {} + +}); +TS_ASSERT(cmpResourceGatherer.StartGathering(supplyID)); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_UNEVAL_EQUALS(cmpResourceGatherer.GetCarryingStatus(), + [{ "type": "wood", "amount": 2, "max": 20 }]); + +TS_ASSERT(!cmpResourceGatherer.IsCarrying("food")); +TS_ASSERT(cmpResourceGatherer.CanCarryMore("food")); +TS_ASSERT(cmpResourceGatherer.IsCarrying("wood")); +TS_ASSERT(cmpResourceGatherer.CanCarryMore("wood")); + + +// Test that we stop gathering when the target is exhausted. +reset(); +AddMock(supplyID, IID_ResourceSupply, { + "AddGatherer": () => true, + "GetCurrentAmount": () => 1, + "GetType": () => ({ "generic": "food" }), + "GetDiminishingReturns": () => 1, + "TakeResources": (amount) => { + return { + "amount": amount, + "exhausted": true + }; + }, + "RemoveGatherer": () => {} + +}); +TS_ASSERT(cmpResourceGatherer.StartGathering(supplyID)); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_UNEVAL_EQUALS(cmpResourceGatherer.GetCarryingStatus(), + [{ "type": "food", "amount": 1, "max": 10 }]); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_UNEVAL_EQUALS(cmpResourceGatherer.GetCarryingStatus(), + [{ "type": "food", "amount": 1, "max": 10 }]); + + +// Test that we stop gathering when we are filled. +reset(); +AddMock(supplyID, IID_ResourceSupply, { + "AddGatherer": () => true, + "GetCurrentAmount": () => 11, + "GetType": () => ({ "generic": "food" }), + "GetDiminishingReturns": () => 1, + "TakeResources": (amount) => { + return { + "amount": amount, + "exhausted": false + }; + }, + "RemoveGatherer": () => {} + +}); +TS_ASSERT(cmpResourceGatherer.StartGathering(supplyID)); +cmpTimer.OnUpdate({ "turnLength": 10 }); +TS_ASSERT_UNEVAL_EQUALS(cmpResourceGatherer.GetCarryingStatus(), + [{ "type": "food", "amount": 10, "max": 10 }]); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_UNEVAL_EQUALS(cmpResourceGatherer.GetCarryingStatus(), + [{ "type": "food", "amount": 10, "max": 10 }]); + + +// Test that starting to gather twice does not add resources at twice the speed. +reset(); +AddMock(supplyID, IID_ResourceSupply, { + "AddGatherer": () => true, + "GetCurrentAmount": () => 3, + "GetType": () => ({ "generic": "food" }), + "GetDiminishingReturns": () => 1, + "TakeResources": (amount) => { + return { + "amount": amount, + "exhausted": false + }; + }, + "RemoveGatherer": () => {} + +}); +TS_ASSERT(cmpResourceGatherer.StartGathering(supplyID)); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_UNEVAL_EQUALS(cmpResourceGatherer.GetCarryingStatus(), + [{ "type": "food", "amount": 1, "max": 10 }]); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_UNEVAL_EQUALS(cmpResourceGatherer.GetCarryingStatus(), + [{ "type": "food", "amount": 2, "max": 10 }]); +TS_ASSERT(cmpResourceGatherer.StartGathering(supplyID)); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_UNEVAL_EQUALS(cmpResourceGatherer.GetCarryingStatus(), + [{ "type": "food", "amount": 3, "max": 10 }]); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_UNEVAL_EQUALS(cmpResourceGatherer.GetCarryingStatus(), + [{ "type": "food", "amount": 4, "max": 10 }]); + + +// Test treasure gathering. +reset(); +AddMock(supplyID, IID_ResourceSupply, { + "AddGatherer": () => true, + "GetType": () => ({ "generic": "treasure" }), + "GetCurrentAmount": () => 10, + "TakeResources": (value) => { + return { + "amount": value, + "exhausted": true + }; + }, + "RemoveGatherer": () => {} +}); + +TS_ASSERT(cmpResourceGatherer.StartGathering(supplyID)); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_UNEVAL_EQUALS(cmpResourceGatherer.GetCarryingStatus(), []);