LCOV - code coverage report
Current view: top level - simulation/components - ResourceGatherer.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 143 212 67.5 %
Date: 2023-04-02 12:52:40 Functions: 21 33 63.6 %

          Line data    Source code
       1             : function ResourceGatherer() {}
       2             : 
       3           2 : ResourceGatherer.prototype.Schema =
       4             :         "<a:help>Lets the unit gather resources from entities that have the ResourceSupply component.</a:help>" +
       5             :         "<a:example>" +
       6             :                 "<MaxDistance>2.0</MaxDistance>" +
       7             :                 "<BaseSpeed>1.0</BaseSpeed>" +
       8             :                 "<Rates>" +
       9             :                         "<food.fish>1</food.fish>" +
      10             :                         "<metal.ore>3</metal.ore>" +
      11             :                         "<stone.rock>3</stone.rock>" +
      12             :                         "<wood.tree>2</wood.tree>" +
      13             :                 "</Rates>" +
      14             :                 "<Capacities>" +
      15             :                         "<food>10</food>" +
      16             :                         "<metal>10</metal>" +
      17             :                         "<stone>10</stone>" +
      18             :                         "<wood>10</wood>" +
      19             :                 "</Capacities>" +
      20             :         "</a:example>" +
      21             :         "<element name='MaxDistance' a:help='Max resource-gathering distance'>" +
      22             :                 "<ref name='positiveDecimal'/>" +
      23             :         "</element>" +
      24             :         "<element name='BaseSpeed' a:help='Base resource-gathering rate (in resource units per second)'>" +
      25             :                 "<ref name='positiveDecimal'/>" +
      26             :         "</element>" +
      27             :         "<element name='Rates' a:help='Per-resource-type gather rate multipliers. If a resource type is not specified then it cannot be gathered by this unit'>" +
      28             :                 Resources.BuildSchema("positiveDecimal", [], true) +
      29             :         "</element>" +
      30             :         "<element name='Capacities' a:help='Per-resource-type maximum carrying capacity'>" +
      31             :                 Resources.BuildSchema("positiveDecimal") +
      32             :         "</element>";
      33             : 
      34             : /*
      35             :  * Call interval will be determined by gather rate,
      36             :  * so always gather integer amount.
      37             :  */
      38           2 : ResourceGatherer.prototype.GATHER_AMOUNT = 1;
      39             : 
      40           2 : ResourceGatherer.prototype.Init = function()
      41             : {
      42           7 :         this.capacities = {};
      43           7 :         this.carrying = {}; // { generic type: integer amount currently carried }
      44             :         // (Note that this component supports carrying multiple types of resources,
      45             :         // each with an independent capacity, but the rest of the game currently
      46             :         // ensures and assumes we'll only be carrying one type at once)
      47             : 
      48             :         // The last exact type gathered, so we can render appropriate props
      49           7 :         this.lastCarriedType = undefined; // { generic, specific }
      50             : };
      51             : 
      52             : /**
      53             :  * Returns data about what resources the unit is currently carrying,
      54             :  * in the form [ {"type":"wood", "amount":7, "max":10} ]
      55             :  */
      56           2 : ResourceGatherer.prototype.GetCarryingStatus = function()
      57             : {
      58          22 :         let ret = [];
      59          22 :         for (let type in this.carrying)
      60             :         {
      61          17 :                 ret.push({
      62             :                         "type": type,
      63             :                         "amount": this.carrying[type],
      64             :                         "max": +this.GetCapacity(type)
      65             :                 });
      66             :         }
      67          22 :         return ret;
      68             : };
      69             : 
      70             : /**
      71             :  * Used to instantly give resources to unit
      72             :  * @param resources The same structure as returned form GetCarryingStatus
      73             :  */
      74           2 : ResourceGatherer.prototype.GiveResources = function(resources)
      75             : {
      76           4 :         for (let resource of resources)
      77           5 :                 this.carrying[resource.type] = +resource.amount;
      78             : };
      79             : 
      80             : /**
      81             :  * Returns the generic type of one particular resource this unit is
      82             :  * currently carrying, or undefined if none.
      83             :  */
      84           2 : ResourceGatherer.prototype.GetMainCarryingType = function()
      85             : {
      86             :         // Return the first key, if any
      87           1 :         for (let type in this.carrying)
      88           1 :                 return type;
      89             : 
      90           0 :         return undefined;
      91             : };
      92             : 
      93             : /**
      94             :  * Returns the exact resource type we last picked up, as long as
      95             :  * we're still carrying something similar enough, in the form
      96             :  * { generic, specific }
      97             :  */
      98           2 : ResourceGatherer.prototype.GetLastCarriedType = function()
      99             : {
     100           1 :         if (this.lastCarriedType && this.lastCarriedType.generic in this.carrying)
     101           1 :                 return this.lastCarriedType;
     102             : 
     103           0 :         return undefined;
     104             : };
     105             : 
     106           2 : ResourceGatherer.prototype.SetLastCarriedType = function(lastCarriedType)
     107             : {
     108           0 :         this.lastCarriedType = lastCarriedType;
     109             : };
     110             : 
     111             : // Since this code is very performancecritical and applying technologies quite slow, cache it.
     112           2 : ResourceGatherer.prototype.RecalculateGatherRates = function()
     113             : {
     114           7 :         this.baseSpeed = ApplyValueModificationsToEntity("ResourceGatherer/BaseSpeed", +this.template.BaseSpeed, this.entity);
     115             : 
     116           7 :         this.rates = {};
     117           7 :         for (let r in this.template.Rates)
     118             :         {
     119          13 :                 let type = r.split(".");
     120             : 
     121          13 :                 if (!Resources.GetResource(type[0]).subtypes[type[1]])
     122             :                 {
     123           0 :                         error("Resource subtype not found: " + type[0] + "." + type[1]);
     124           0 :                         continue;
     125             :                 }
     126             : 
     127          13 :                 let rate = ApplyValueModificationsToEntity("ResourceGatherer/Rates/" + r, +this.template.Rates[r], this.entity);
     128          13 :                 this.rates[r] = rate * this.baseSpeed;
     129             :         }
     130             : };
     131             : 
     132           2 : ResourceGatherer.prototype.RecalculateCapacities = function()
     133             : {
     134           7 :         this.capacities = {};
     135           7 :         for (let r in this.template.Capacities)
     136          13 :                 this.capacities[r] = ApplyValueModificationsToEntity("ResourceGatherer/Capacities/" + r, +this.template.Capacities[r], this.entity);
     137             : };
     138             : 
     139           2 : ResourceGatherer.prototype.RecalculateCapacity = function(type)
     140             : {
     141           0 :         if (type in this.capacities)
     142           0 :                 this.capacities[type] = ApplyValueModificationsToEntity("ResourceGatherer/Capacities/" + type, +this.template.Capacities[type], this.entity);
     143             : };
     144             : 
     145           2 : ResourceGatherer.prototype.GetGatherRates = function()
     146             : {
     147           0 :         return this.rates;
     148             : };
     149             : 
     150           2 : ResourceGatherer.prototype.GetGatherRate = function(resourceType)
     151             : {
     152          12 :         if (!this.template.Rates[resourceType])
     153           3 :                 return 0;
     154             : 
     155           9 :         return this.rates[resourceType];
     156             : };
     157             : 
     158           2 : ResourceGatherer.prototype.GetCapacity = function(resourceType)
     159             : {
     160          64 :         if (!this.template.Capacities[resourceType])
     161           1 :                 return 0;
     162          63 :         return this.capacities[resourceType];
     163             : };
     164             : 
     165           2 : ResourceGatherer.prototype.GetRange = function()
     166             : {
     167           0 :         return { "max": +this.template.MaxDistance, "min": 0 };
     168             : };
     169             : 
     170             : /**
     171             :  * @param {number} target - The target to gather from.
     172             :  * @param {number} callerIID - The IID to notify on specific events.
     173             :  * @return {boolean} - Whether we started gathering.
     174             :  */
     175           2 : ResourceGatherer.prototype.StartGathering = function(target, callerIID)
     176             : {
     177          12 :         if (this.target)
     178           1 :                 this.StopGathering();
     179             : 
     180          12 :         let rate = this.GetTargetGatherRate(target);
     181          12 :         if (!rate)
     182           3 :                 return false;
     183             : 
     184           9 :         let cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
     185           9 :         if (!cmpResourceSupply || !cmpResourceSupply.AddActiveGatherer(this.entity))
     186           0 :                 return false;
     187             : 
     188           9 :         let resourceType = cmpResourceSupply.GetType();
     189             : 
     190             :         // If we've already got some resources but they're the wrong type,
     191             :         // drop them first to ensure we're only ever carrying one type.
     192           9 :         if (this.IsCarryingAnythingExcept(resourceType.generic))
     193           1 :                 this.DropResources();
     194             : 
     195           9 :         let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
     196           9 :         if (cmpVisual)
     197           0 :                 cmpVisual.SelectAnimation("gather_" + resourceType.specific, false, 1.0);
     198             : 
     199             :         // Calculate timing based on gather rates.
     200             :         // This allows the gather rate to control how often we gather, instead of how much.
     201           9 :         let timing = 1000 / rate;
     202             : 
     203           9 :         this.target = target;
     204           9 :         this.callerIID = callerIID;
     205             : 
     206           9 :         let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     207           9 :         this.timer = cmpTimer.SetInterval(this.entity, IID_ResourceGatherer, "PerformGather", timing, timing, null);
     208             : 
     209           9 :         return true;
     210             : };
     211             : 
     212             : /**
     213             :  * @param {string} reason - The reason why we stopped gathering used to notify the caller.
     214             :  */
     215           2 : ResourceGatherer.prototype.StopGathering = function(reason)
     216             : {
     217           7 :         if (!this.target)
     218           0 :                 return;
     219             : 
     220           7 :         let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     221           7 :         cmpTimer.CancelTimer(this.timer);
     222           7 :         delete this.timer;
     223             : 
     224           7 :         let cmpResourceSupply = Engine.QueryInterface(this.target, IID_ResourceSupply);
     225           7 :         if (cmpResourceSupply)
     226           7 :                 cmpResourceSupply.RemoveGatherer(this.entity);
     227             : 
     228           7 :         delete this.target;
     229             : 
     230           7 :         let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
     231           7 :         if (cmpVisual)
     232           0 :                 cmpVisual.SelectAnimation("idle", false, 1.0);
     233             : 
     234             :         // The callerIID component may start again,
     235             :         // replacing the callerIID, hence save that.
     236           7 :         let callerIID = this.callerIID;
     237           7 :         delete this.callerIID;
     238             : 
     239           7 :         if (reason && callerIID)
     240             :         {
     241           0 :                 let component = Engine.QueryInterface(this.entity, callerIID);
     242           0 :                 if (component)
     243           0 :                         component.ProcessMessage(reason, null);
     244             :         }
     245             : };
     246             : 
     247             : /**
     248             :  * Gather from our target entity.
     249             :  * @params - data and lateness are unused.
     250             :  */
     251           2 : ResourceGatherer.prototype.PerformGather = function(data, lateness)
     252             : {
     253          22 :         let cmpResourceSupply = Engine.QueryInterface(this.target, IID_ResourceSupply);
     254          22 :         if (!cmpResourceSupply || cmpResourceSupply.GetCurrentAmount() <= 0)
     255             :         {
     256           0 :                 this.StopGathering("TargetInvalidated");
     257           0 :                 return;
     258             :         }
     259             : 
     260          22 :         if (!this.IsTargetInRange(this.target))
     261             :         {
     262           0 :                 this.StopGathering("OutOfRange");
     263           0 :                 return;
     264             :         }
     265             : 
     266             :         // ToDo: Enable entities to keep facing a target.
     267          22 :         Engine.QueryInterface(this.entity, IID_UnitAI)?.FaceTowardsTarget(this.target);
     268             : 
     269          22 :         let type = cmpResourceSupply.GetType();
     270          22 :         if (!this.carrying[type.generic])
     271           8 :                 this.carrying[type.generic] = 0;
     272             : 
     273          22 :         let maxGathered = this.GetCapacity(type.generic) - this.carrying[type.generic];
     274          22 :         let status = cmpResourceSupply.TakeResources(Math.min(this.GATHER_AMOUNT, maxGathered));
     275          22 :         this.carrying[type.generic] += status.amount;
     276          22 :         this.lastCarriedType = type;
     277             : 
     278             :         // Update stats of how much the player collected.
     279             :         // (We have to do it here rather than at the dropsite, because we
     280             :         // need to know what subtype it was.)
     281          22 :         let cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
     282          22 :         if (cmpStatisticsTracker)
     283           0 :                 cmpStatisticsTracker.IncreaseResourceGatheredCounter(type.generic, status.amount, type.specific);
     284             : 
     285          22 :         if (!this.CanCarryMore(type.generic))
     286           1 :                 this.StopGathering("InventoryFilled");
     287          21 :         else if (status.exhausted)
     288           1 :                 this.StopGathering("TargetInvalidated");
     289             : };
     290             : 
     291             : /**
     292             :  * Compute the amount of resources collected per second from the target.
     293             :  * Returns 0 if resources cannot be collected (e.g. the target doesn't
     294             :  * exist, or is the wrong type).
     295             :  */
     296           2 : ResourceGatherer.prototype.GetTargetGatherRate = function(target)
     297             : {
     298          12 :         let cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply);
     299          12 :         if (!cmpResourceSupply || cmpResourceSupply.GetCurrentAmount() <= 0)
     300           0 :                 return 0;
     301             : 
     302          12 :         let type = cmpResourceSupply.GetType();
     303             : 
     304          12 :         let rate = 0;
     305          12 :         if (type.specific)
     306           9 :                 rate = this.GetGatherRate(type.generic + "." + type.specific);
     307          12 :         if (rate == 0 && type.generic)
     308           3 :                 rate = this.GetGatherRate(type.generic);
     309             : 
     310          12 :         let diminishingReturns = cmpResourceSupply.GetDiminishingReturns();
     311          12 :         if (diminishingReturns)
     312          10 :                 rate *= diminishingReturns;
     313             : 
     314          12 :         return rate;
     315             : };
     316             : 
     317             : /**
     318             :  * @param {number} target - The entity ID of the target to check.
     319             :  * @return {boolean} - Whether we can gather from the target.
     320             :  */
     321           2 : ResourceGatherer.prototype.CanGather = function(target)
     322             : {
     323           0 :         return this.GetTargetGatherRate(target) > 0;
     324             : };
     325             : 
     326             : /**
     327             :  * Returns whether this unit can carry more of the given type of resource.
     328             :  * (This ignores whether the unit is actually able to gather that
     329             :  * resource type or not.)
     330             :  */
     331           2 : ResourceGatherer.prototype.CanCarryMore = function(type)
     332             : {
     333          25 :         let amount = this.carrying[type] || 0;
     334          25 :         return amount < this.GetCapacity(type);
     335             : };
     336             : 
     337             : 
     338           2 : ResourceGatherer.prototype.IsCarrying = function(type)
     339             : {
     340           3 :         let amount = this.carrying[type] || 0;
     341           3 :         return amount > 0;
     342             : };
     343             : 
     344             : /**
     345             :  * Returns whether this unit is carrying any resources of a type that is
     346             :  * not the requested type. (This is to support cases where the unit is
     347             :  * only meant to be able to carry one type at once.)
     348             :  */
     349           2 : ResourceGatherer.prototype.IsCarryingAnythingExcept = function(exceptedType)
     350             : {
     351           9 :         for (let type in this.carrying)
     352           2 :                 if (type != exceptedType)
     353           1 :                         return true;
     354             : 
     355           8 :         return false;
     356             : };
     357             : 
     358             : /**
     359             :  * @param {number} target - The entity to check.
     360             :  * @param {boolean} checkCarriedResource - Whether we need to check the resource we are carrying.
     361             :  * @return {boolean} - Whether we can return carried resources.
     362             :  */
     363           2 : ResourceGatherer.prototype.CanReturnResource = function(target, checkCarriedResource)
     364             : {
     365           0 :         let cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
     366           0 :         if (!cmpResourceDropsite)
     367           0 :                 return false;
     368             : 
     369           0 :         if (checkCarriedResource)
     370             :         {
     371           0 :                 let type = this.GetMainCarryingType();
     372           0 :                 if (!type || !cmpResourceDropsite.AcceptsType(type))
     373           0 :                         return false;
     374             :         }
     375             : 
     376           0 :         let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     377           0 :         if (cmpOwnership && IsOwnedByPlayer(cmpOwnership.GetOwner(), target))
     378           0 :                 return true;
     379           0 :         let cmpPlayer = QueryOwnerInterface(this.entity);
     380           0 :         return cmpPlayer && cmpPlayer.HasSharedDropsites() && cmpResourceDropsite.IsShared() &&
     381             :                cmpOwnership && IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target);
     382             : };
     383             : 
     384             : /**
     385             :  * Transfer our carried resources to our owner immediately.
     386             :  * Only resources of the appropriate types will be transferred.
     387             :  * (This should typically be called after reaching a dropsite.)
     388             :  *
     389             :  * @param {number} target - The target entity ID to drop resources at.
     390             :  */
     391           2 : ResourceGatherer.prototype.CommitResources = function(target)
     392             : {
     393           4 :         let cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
     394           4 :         if (!cmpResourceDropsite)
     395           0 :                 return;
     396             : 
     397           4 :         let change = cmpResourceDropsite.ReceiveResources(this.carrying, this.entity);
     398           4 :         for (let type in change)
     399             :         {
     400           4 :                 this.carrying[type] -= change[type];
     401           4 :                 if (this.carrying[type] == 0)
     402           4 :                         delete this.carrying[type];
     403             :         }
     404             : };
     405             : 
     406             : /**
     407             :  * Drop all currently-carried resources.
     408             :  * (Currently they just vanish after being dropped - we don't bother depositing
     409             :  * them onto the ground.)
     410             :  */
     411           2 : ResourceGatherer.prototype.DropResources = function()
     412             : {
     413           3 :         this.carrying = {};
     414             : };
     415             : 
     416             : /**
     417             :  * @return {string} - A generic resource type if we were tasked to gather.
     418             :  */
     419           2 : ResourceGatherer.prototype.GetTaskedResourceType = function()
     420             : {
     421           0 :         return this.taskedResourceType;
     422             : };
     423             : 
     424             : /**
     425             :  * @param {string} type - A generic resource type.
     426             :  */
     427           2 : ResourceGatherer.prototype.AddToPlayerCounter = function(type)
     428             : {
     429             :         // We need to be removed from the player counter first.
     430           0 :         if (this.taskedResourceType)
     431           0 :                 return;
     432             : 
     433           0 :         let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
     434           0 :         if (cmpPlayer)
     435           0 :                 cmpPlayer.AddResourceGatherer(type);
     436             : 
     437           0 :         this.taskedResourceType = type;
     438             : };
     439             : 
     440             : /**
     441             :  * @param {number} playerid - Optionally a player ID.
     442             :  */
     443           2 : ResourceGatherer.prototype.RemoveFromPlayerCounter = function(playerid)
     444             : {
     445           0 :         if (!this.taskedResourceType)
     446           0 :                 return;
     447             : 
     448           0 :         let cmpPlayer = playerid != undefined ?
     449             :                 QueryPlayerIDInterface(playerid) :
     450             :                 QueryOwnerInterface(this.entity, IID_Player);
     451             : 
     452           0 :         if (cmpPlayer)
     453           0 :                 cmpPlayer.RemoveResourceGatherer(this.taskedResourceType);
     454             : 
     455           0 :         delete this.taskedResourceType;
     456             : };
     457             : 
     458             : /**
     459             :  * @param {number} - The entity ID of the target to check.
     460             :  * @return {boolean} - Whether this entity is in range of its target.
     461             :  */
     462           2 : ResourceGatherer.prototype.IsTargetInRange = function(target)
     463             : {
     464          22 :         return Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).
     465             :                 IsInTargetRange(this.entity, target, 0, +this.template.MaxDistance, false);
     466             : };
     467             : 
     468             : // Since we cache gather rates, we need to make sure we update them when tech changes.
     469             : // and when our owner change because owners can had different techs.
     470           2 : ResourceGatherer.prototype.OnValueModification = function(msg)
     471             : {
     472           0 :         if (msg.component != "ResourceGatherer")
     473           0 :                 return;
     474             : 
     475             :         // NB: at the moment, 0 A.D. always uses the fast path, the other is mod support.
     476           0 :         if (msg.valueNames.length === 1)
     477             :         {
     478           0 :                 if (msg.valueNames[0].indexOf("Capacities") !== -1)
     479           0 :                         this.RecalculateCapacity(msg.valueNames[0].substr(28));
     480             :                 else
     481           0 :                         this.RecalculateGatherRates();
     482             :         }
     483             :         else
     484             :         {
     485           0 :                 this.RecalculateGatherRates();
     486           0 :                 this.RecalculateCapacities();
     487             :         }
     488             : };
     489             : 
     490           2 : ResourceGatherer.prototype.OnOwnershipChanged = function(msg)
     491             : {
     492           0 :         if (msg.to == INVALID_PLAYER)
     493             :         {
     494           0 :                 this.RemoveFromPlayerCounter(msg.from);
     495           0 :                 return;
     496             :         }
     497           0 :         if (this.lastGathered && msg.from !== INVALID_PLAYER)
     498             :         {
     499           0 :                 const resource = this.taskedResourceType;
     500           0 :                 this.RemoveFromPlayerCounter(msg.from);
     501           0 :                 this.AddToPlayerCounter(resource);
     502             :         }
     503             : 
     504           0 :         this.RecalculateGatherRates();
     505           0 :         this.RecalculateCapacities();
     506             : };
     507             : 
     508           2 : ResourceGatherer.prototype.OnGlobalInitGame = function(msg)
     509             : {
     510           6 :         this.RecalculateGatherRates();
     511           6 :         this.RecalculateCapacities();
     512             : };
     513             : 
     514           2 : ResourceGatherer.prototype.OnMultiplierChanged = function(msg)
     515             : {
     516           0 :         let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
     517           0 :         if (cmpPlayer && msg.player == cmpPlayer.GetPlayerID())
     518           0 :                 this.RecalculateGatherRates();
     519             : };
     520             : 
     521           2 : Engine.RegisterComponentType(IID_ResourceGatherer, "ResourceGatherer", ResourceGatherer);

Generated by: LCOV version 1.14