LCOV - code coverage report
Current view: top level - simulation/components - ResourceSupply.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 144 173 83.2 %
Date: 2023-04-02 12:52:40 Functions: 28 40 70.0 %

          Line data    Source code
       1             : function ResourceSupply() {}
       2             : 
       3           2 : ResourceSupply.prototype.Schema =
       4             :         "<a:help>Provides a supply of one particular type of resource.</a:help>" +
       5             :         "<a:example>" +
       6             :                 "<Max>1000</Max>" +
       7             :                 "<Initial>1000</Initial>" +
       8             :                 "<Type>food.meat</Type>" +
       9             :                 "<KillBeforeGather>false</KillBeforeGather>" +
      10             :                 "<MaxGatherers>25</MaxGatherers>" +
      11             :                 "<DiminishingReturns>0.8</DiminishingReturns>" +
      12             :                 "<Change>" +
      13             :                         "<AnyName>" +
      14             :                                 "<Value>2</Value>" +
      15             :                                 "<Interval>1000</Interval>" +
      16             :                         "</AnyName>" +
      17             :                         "<Growth>" +
      18             :                                 "<State>alive</State>" +
      19             :                                 "<Value>2</Value>" +
      20             :                                 "<Interval>1000</Interval>" +
      21             :                                 "<UpperLimit>500</UpperLimit>" +
      22             :                         "</Growth>" +
      23             :                         "<Rotting>" +
      24             :                                 "<State>dead notGathered</State>" +
      25             :                                 "<Value>-2</Value>" +
      26             :                                 "<Interval>1000</Interval>" +
      27             :                         "</Rotting>" +
      28             :                         "<Decay>" +
      29             :                                 "<State>dead</State>" +
      30             :                                 "<Value>-1</Value>" +
      31             :                                 "<Interval>1000</Interval>" +
      32             :                                 "<LowerLimit>500</LowerLimit>" +
      33             :                         "</Decay>" +
      34             :                 "</Change>" +
      35             :         "</a:example>" +
      36             :         "<element name='KillBeforeGather' a:help='Whether this entity must be killed (health reduced to 0) before its resources can be gathered'>" +
      37             :                 "<data type='boolean'/>" +
      38             :         "</element>" +
      39             :         "<element name='Max' a:help='Max amount of resources available from this entity.'>" +
      40             :                 "<choice><data type='nonNegativeInteger'/><value>Infinity</value></choice>" +
      41             :         "</element>" +
      42             :         "<optional>" +
      43             :                 "<element name='Initial' a:help='Initial amount of resources available from this entity, if this is not specified, Max is used.'>" +
      44             :                         "<choice><data type='nonNegativeInteger'/><value>Infinity</value></choice>" +
      45             :                 "</element>" +
      46             :         "</optional>" +
      47             :         "<element name='Type' a:help='Type and Subtype of resource available from this entity'>" +
      48             :                 Resources.BuildChoicesSchema(true) +
      49             :         "</element>" +
      50             :         "<element name='MaxGatherers' a:help='Amount of gatherers who can gather resources from this entity at the same time'>" +
      51             :                 "<data type='nonNegativeInteger'/>" +
      52             :         "</element>" +
      53             :         "<optional>" +
      54             :                 "<element name='DiminishingReturns' a:help='The relative rate of any new gatherer compared to the previous one (geometric sequence). Leave the element out for no diminishing returns.'>" +
      55             :                         "<ref name='positiveDecimal'/>" +
      56             :                 "</element>" +
      57             :         "</optional>" +
      58             :         "<optional>" +
      59             :                 "<element name='Change' a:help='Optional element containing all the modifications that affects a resource supply'>" +
      60             :                         "<oneOrMore>" +
      61             :                                 "<element a:help='Element defining whether and how a resource supply regenerates or decays'>" +
      62             :                                         "<anyName/>" +
      63             :                                         "<interleave>" +
      64             :                                                 "<element name='Value' a:help='The amount of resource added per interval.'>" +
      65             :                                                         "<data type='integer'/>" +
      66             :                                                 "</element>" +
      67             :                                                 "<element name='Interval' a:help='The interval in milliseconds.'>" +
      68             :                                                         "<data type='positiveInteger'/>" +
      69             :                                                 "</element>" +
      70             :                                                 "<optional>" +
      71             :                                                         "<element name='UpperLimit' a:help='The upper limit of the value after which the Change has no effect.'>" +
      72             :                                                                 "<data type='nonNegativeInteger'/>" +
      73             :                                                         "</element>" +
      74             :                                                 "</optional>" +
      75             :                                                 "<optional>" +
      76             :                                                         "<element name='LowerLimit' a:help='The bottom limit of the value after which the Change has no effect.'>" +
      77             :                                                                 "<data type='nonNegativeInteger'/>" +
      78             :                                                         "</element>" +
      79             :                                                 "</optional>" +
      80             :                                                 "<optional>" +
      81             :                                                         "<element name='State' a:help='What state the entity must be in for the effect to occur.'>" +
      82             :                                                                 "<list>" +
      83             :                                                                         "<oneOrMore>" +
      84             :                                                                                 "<choice>" +
      85             :                                                                                         "<value>alive</value>" +
      86             :                                                                                         "<value>dead</value>" +
      87             :                                                                                         "<value>gathered</value>" +
      88             :                                                                                         "<value>notGathered</value>" +
      89             :                                                                                 "</choice>" +
      90             :                                                                         "</oneOrMore>" +
      91             :                                                                 "</list>" +
      92             :                                                         "</element>" +
      93             :                                                 "</optional>" +
      94             :                                         "</interleave>" +
      95             :                                 "</element>" +
      96             :                         "</oneOrMore>" +
      97             :                 "</element>" +
      98             :         "</optional>";
      99             : 
     100           2 : ResourceSupply.prototype.Init = function()
     101             : {
     102          29 :         this.amount = +(this.template.Initial || this.template.Max);
     103             : 
     104             :         // Includes the ones that are tasked but not here yet, i.e. approaching.
     105          29 :         this.gatherers = [];
     106          29 :         this.activeGatherers = [];
     107             : 
     108          29 :         let [type, subtype] = this.template.Type.split('.');
     109          29 :         this.cachedType = { "generic": type, "specific": subtype };
     110             : 
     111          29 :         if (this.template.Change)
     112             :         {
     113          26 :                 this.timers = {};
     114          26 :                 this.cachedChanges = {};
     115             :         }
     116             : };
     117             : 
     118           2 : ResourceSupply.prototype.IsInfinite = function()
     119             : {
     120         148 :         return !isFinite(+this.template.Max);
     121             : };
     122             : 
     123           2 : ResourceSupply.prototype.GetKillBeforeGather = function()
     124             : {
     125           1 :         return this.template.KillBeforeGather == "true";
     126             : };
     127             : 
     128           2 : ResourceSupply.prototype.GetMaxAmount = function()
     129             : {
     130          70 :         return this.maxAmount;
     131             : };
     132             : 
     133           2 : ResourceSupply.prototype.GetCurrentAmount = function()
     134             : {
     135         105 :         return this.amount;
     136             : };
     137             : 
     138           2 : ResourceSupply.prototype.GetMaxGatherers = function()
     139             : {
     140          19 :         return +this.template.MaxGatherers;
     141             : };
     142             : 
     143           2 : ResourceSupply.prototype.GetNumGatherers = function()
     144             : {
     145          11 :         return this.gatherers.length;
     146             : };
     147             : 
     148             : /**
     149             :  * @return {number} - The number of currently active gatherers.
     150             :  */
     151           2 : ResourceSupply.prototype.GetNumActiveGatherers = function()
     152             : {
     153          43 :         return this.activeGatherers.length;
     154             : };
     155             : 
     156             : /**
     157             :  * @return {{ "generic": string, "specific": string }} An object containing the subtype and the generic type. All resources must have both.
     158             :  */
     159           2 : ResourceSupply.prototype.GetType = function()
     160             : {
     161           6 :         return this.cachedType;
     162             : };
     163             : 
     164             : /**
     165             :  * @param {number} gathererID - The gatherer's entity id.
     166             :  * @return {boolean} - Whether the ResourceSupply can have this additional gatherer or it is already gathering.
     167             :  */
     168           2 : ResourceSupply.prototype.IsAvailableTo = function(gathererID)
     169             : {
     170           5 :         return this.IsAvailable() || this.IsGatheringUs(gathererID);
     171             : };
     172             : 
     173             : /**
     174             :  * @return {boolean} - Whether this entity can have an additional gatherer.
     175             :  */
     176           2 : ResourceSupply.prototype.IsAvailable = function()
     177             : {
     178          19 :         return this.amount && this.gatherers.length < this.GetMaxGatherers();
     179             : };
     180             : 
     181             : /**
     182             :  * @param {number} entity - The entityID to check for.
     183             :  * @return {boolean} - Whether the given entity is already gathering at us.
     184             :  */
     185           2 : ResourceSupply.prototype.IsGatheringUs = function(entity)
     186             : {
     187          14 :         return this.gatherers.indexOf(entity) !== -1;
     188             : };
     189             : 
     190             : /**
     191             :  * Each additional gatherer decreases the rate following a geometric sequence, with diminishingReturns as ratio.
     192             :  * @return {number} The diminishing return if any, null otherwise.
     193             :  */
     194           2 : ResourceSupply.prototype.GetDiminishingReturns = function()
     195             : {
     196           3 :         if (!this.template.DiminishingReturns)
     197           3 :                 return null;
     198             : 
     199           0 :         let diminishingReturns = ApplyValueModificationsToEntity("ResourceSupply/DiminishingReturns", +this.template.DiminishingReturns, this.entity);
     200           0 :         if (!diminishingReturns)
     201           0 :                 return null;
     202             : 
     203           0 :         let numGatherers = this.GetNumGatherers();
     204           0 :         if (numGatherers > 1)
     205           0 :                 return diminishingReturns == 1 ? 1 : (1 - Math.pow(diminishingReturns, numGatherers)) / (1 - diminishingReturns) / numGatherers;
     206             : 
     207           0 :         return null;
     208             : };
     209             : 
     210             : /**
     211             :  * @param {number} amount The amount of resources that should be taken from the resource supply. The amount must be positive.
     212             :  * @return {{ "amount": number, "exhausted": boolean }} The current resource amount in the entity and whether it's exhausted or not.
     213             :  */
     214           2 : ResourceSupply.prototype.TakeResources = function(amount)
     215             : {
     216           7 :         if (this.IsInfinite())
     217           1 :                 return { "amount": amount, "exhausted": false };
     218             : 
     219           6 :         return {
     220             :                 "amount": Math.abs(this.Change(-amount)),
     221             :                 "exhausted": this.amount == 0
     222             :         };
     223             : };
     224             : 
     225             : /**
     226             :  * @param {number} change - The amount to change the resources with (can be negative).
     227             :  * @return {number} - The actual change in resourceSupply.
     228             :  */
     229           2 : ResourceSupply.prototype.Change = function(change)
     230             : {
     231             :         // Before changing the amount, activate Fogging if necessary to hide changes
     232          80 :         let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging);
     233          80 :         if (cmpFogging)
     234           4 :                 cmpFogging.Activate();
     235             : 
     236          80 :         let oldAmount = this.amount;
     237          80 :         this.amount = Math.min(Math.max(oldAmount + change, 0), this.maxAmount);
     238             : 
     239             :         // Remove entities that have been exhausted.
     240          80 :         if (this.amount == 0)
     241           1 :                 Engine.DestroyEntity(this.entity);
     242             : 
     243          80 :         let actualChange = this.amount - oldAmount;
     244          80 :         if (actualChange != 0)
     245             :         {
     246          79 :                 Engine.PostMessage(this.entity, MT_ResourceSupplyChanged, {
     247             :                         "from": oldAmount,
     248             :                         "to": this.amount
     249             :                 });
     250          79 :                 this.CheckTimers();
     251             :         }
     252          80 :         return actualChange;
     253             : };
     254             : 
     255             : /**
     256             :  * @param {number} newValue - The value to set the current amount to.
     257             :  */
     258           2 : ResourceSupply.prototype.SetAmount = function(newValue)
     259             : {
     260             :         // We currently don't support changing to or fro Infinity.
     261           1 :         if (this.IsInfinite() || newValue === Infinity)
     262           1 :                 return;
     263           0 :         this.Change(newValue - this.amount);
     264             : };
     265             : 
     266             : /**
     267             :  * @param {number} gathererID - The gatherer to add.
     268             :  * @return {boolean} - Whether the gatherer was successfully added to the entity's gatherers list
     269             :  *                      or the entity was already gathering us.
     270             :  */
     271           2 : ResourceSupply.prototype.AddGatherer = function(gathererID)
     272             : {
     273          13 :         if (!this.IsAvailable())
     274           2 :                 return false;
     275             : 
     276          11 :         if (this.IsGatheringUs(gathererID))
     277           0 :                 return true;
     278             : 
     279          11 :         this.gatherers.push(gathererID);
     280             : 
     281          11 :         return true;
     282             : };
     283             : 
     284             : /**
     285             :  * @param {number} player - The playerID owning the gatherer.
     286             :  * @param {number} entity - The entityID gathering.
     287             :  *
     288             :  * @return {boolean} - Whether the gatherer was successfully added to the active-gatherers list
     289             :  *                      or the entity was already in that list.
     290             :  */
     291           2 : ResourceSupply.prototype.AddActiveGatherer = function(entity)
     292             : {
     293           9 :         if (!this.AddGatherer(entity))
     294           0 :                 return false;
     295             : 
     296           9 :         if (this.activeGatherers.indexOf(entity) == -1)
     297             :         {
     298           9 :                 this.activeGatherers.push(entity);
     299           9 :                 this.CheckTimers();
     300             :         }
     301           9 :         return true;
     302             : };
     303             : 
     304             : /**
     305             :  * @param {number} gathererID - The gatherer's entity id.
     306             :  * @todo: Should this return false if the gatherer didn't gather from said resource?
     307             :  */
     308           2 : ResourceSupply.prototype.RemoveGatherer = function(gathererID)
     309             : {
     310          11 :         let index = this.gatherers.indexOf(gathererID);
     311          11 :         if (index != -1)
     312          11 :                 this.gatherers.splice(index, 1);
     313             : 
     314          11 :         index = this.activeGatherers.indexOf(gathererID);
     315          11 :         if (index == -1)
     316           2 :                 return;
     317           9 :         this.activeGatherers.splice(index, 1);
     318           9 :         this.CheckTimers();
     319             : };
     320             : 
     321             : /**
     322             :  * Checks whether a timer ought to be added or removed.
     323             :  */
     324           2 : ResourceSupply.prototype.CheckTimers = function()
     325             : {
     326         124 :         if (!this.template.Change || this.IsInfinite())
     327          12 :                 return;
     328             : 
     329         112 :         for (let changeKey in this.template.Change)
     330             :         {
     331         135 :                 if (!this.CheckState(changeKey))
     332             :                 {
     333          16 :                         this.StopTimer(changeKey);
     334          16 :                         continue;
     335             :                 }
     336         119 :                 let template = this.template.Change[changeKey];
     337         119 :                 if (this.amount < +(template.LowerLimit || -1) ||
     338             :                         this.amount > +(template.UpperLimit || this.GetMaxAmount()))
     339             :                 {
     340          18 :                         this.StopTimer(changeKey);
     341          18 :                         continue;
     342             :                 }
     343             : 
     344         101 :                 if (this.cachedChanges[changeKey] == 0)
     345             :                 {
     346           0 :                         this.StopTimer(changeKey);
     347           0 :                         continue;
     348             :                 }
     349             : 
     350         101 :                 if (!this.timers[changeKey])
     351          26 :                         this.StartTimer(changeKey);
     352             :         }
     353             : };
     354             : 
     355             : /**
     356             :  * This verifies whether the current state of the supply matches the ones needed
     357             :  * for the specific timer to run.
     358             :  *
     359             :  * @param {string} changeKey - The name of the Change to verify the state for.
     360             :  * @return {boolean} - Whether the timer may run.
     361             :  */
     362           2 : ResourceSupply.prototype.CheckState = function(changeKey)
     363             : {
     364         135 :         let template = this.template.Change[changeKey];
     365         135 :         if (!template.State)
     366          81 :                 return true;
     367             : 
     368          54 :         let states = template.State;
     369          54 :         let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
     370          54 :         if (states.indexOf("alive") != -1 && !cmpHealth && states.indexOf("dead") == -1 ||
     371             :                 states.indexOf("dead") != -1 && cmpHealth && states.indexOf("alive") == -1)
     372          11 :                 return false;
     373             : 
     374          43 :         let activeGatherers = this.GetNumActiveGatherers();
     375          43 :         if (states.indexOf("gathered") != -1 && activeGatherers == 0 && states.indexOf("notGathered") == -1 ||
     376             :                 states.indexOf("notGathered") != -1 && activeGatherers > 0 && states.indexOf("gathered") == -1)
     377           5 :                 return false;
     378             : 
     379          38 :         return true;
     380             : };
     381             : 
     382             : /**
     383             :  * @param {string} changeKey - The name of the Change to apply to the entity.
     384             :  */
     385           2 : ResourceSupply.prototype.StartTimer = function(changeKey)
     386             : {
     387          26 :         if (this.timers[changeKey])
     388           0 :                 return;
     389             : 
     390          26 :         let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     391          26 :         let interval = ApplyValueModificationsToEntity("ResourceSupply/Change/" + changeKey + "/Interval", +(this.template.Change[changeKey].Interval || 1000), this.entity);
     392          26 :         this.timers[changeKey] = cmpTimer.SetInterval(this.entity, IID_ResourceSupply, "TimerTick", interval, interval, changeKey);
     393             : };
     394             : 
     395             : /**
     396             :  * @param {string} changeKey - The name of the change to stop the timer for.
     397             :  */
     398           2 : ResourceSupply.prototype.StopTimer = function(changeKey)
     399             : {
     400          35 :         if (!this.timers[changeKey])
     401          21 :                 return;
     402             : 
     403          14 :         let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     404          14 :         cmpTimer.CancelTimer(this.timers[changeKey]);
     405          14 :         delete this.timers[changeKey];
     406             : };
     407             : 
     408             : /**
     409             :  * @param {string} changeKey - The name of the change to apply to the entity.
     410             :  */
     411           2 : ResourceSupply.prototype.TimerTick = function(changeKey)
     412             : {
     413          74 :         let template = this.template.Change[changeKey];
     414          74 :         if (!template || !this.Change(this.cachedChanges[changeKey]))
     415           1 :                 this.StopTimer(changeKey);
     416             : };
     417             : 
     418             : /**
     419             :  * Since the supposed changes can be affected by modifications, and applying those
     420             :  * are slow, do not calculate them every timer tick.
     421             :  */
     422           2 : ResourceSupply.prototype.RecalculateValues = function()
     423             : {
     424          29 :         this.maxAmount = ApplyValueModificationsToEntity("ResourceSupply/Max", +this.template.Max, this.entity);
     425          29 :         if (!this.template.Change || this.IsInfinite())
     426           5 :                 return;
     427             : 
     428          24 :         for (let changeKey in this.template.Change)
     429          28 :                 this.cachedChanges[changeKey] = ApplyValueModificationsToEntity("ResourceSupply/Change/" + changeKey + "/Value", +this.template.Change[changeKey].Value, this.entity);
     430             : 
     431          24 :         this.CheckTimers();
     432             : };
     433             : 
     434             : /**
     435             :  * @param {{ "component": string, "valueNames": string[] }} msg - Message containing a list of values that were changed.
     436             :  */
     437           2 : ResourceSupply.prototype.OnValueModification = function(msg)
     438             : {
     439           0 :         if (msg.component != "ResourceSupply")
     440           0 :                 return;
     441             : 
     442           0 :         this.RecalculateValues();
     443             : };
     444             : 
     445             : /**
     446             :  * @param {{ "from": number, "to": number }} msg - Message containing the old new owner.
     447             :  */
     448           2 : ResourceSupply.prototype.OnOwnershipChanged = function(msg)
     449             : {
     450          28 :         if (msg.to == INVALID_PLAYER)
     451             :         {
     452           0 :                 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     453           0 :                 for (let changeKey in this.timers)
     454           0 :                         cmpTimer.CancelTimer(this.timers[changeKey]);
     455             :         }
     456             :         else
     457          28 :                 this.RecalculateValues();
     458             : };
     459             : 
     460             : /**
     461             :  * @param {{ "entity": number, "newentity": number }} msg - Message to what the entity has been renamed.
     462             :  */
     463           2 : ResourceSupply.prototype.OnEntityRenamed = function(msg)
     464             : {
     465           1 :         let cmpResourceSupplyNew = Engine.QueryInterface(msg.newentity, IID_ResourceSupply);
     466           1 :         if (cmpResourceSupplyNew)
     467           1 :                 cmpResourceSupplyNew.SetAmount(this.GetCurrentAmount());
     468             : };
     469             : 
     470             : function ResourceSupplyMirage() {}
     471           2 : ResourceSupplyMirage.prototype.Init = function(cmpResourceSupply)
     472             : {
     473           0 :         this.maxAmount = cmpResourceSupply.GetMaxAmount();
     474           0 :         this.amount = cmpResourceSupply.GetCurrentAmount();
     475           0 :         this.type = cmpResourceSupply.GetType();
     476           0 :         this.isInfinite = cmpResourceSupply.IsInfinite();
     477           0 :         this.killBeforeGather = cmpResourceSupply.GetKillBeforeGather();
     478           0 :         this.maxGatherers = cmpResourceSupply.GetMaxGatherers();
     479           0 :         this.numGatherers = cmpResourceSupply.GetNumGatherers();
     480             : };
     481             : 
     482           2 : ResourceSupplyMirage.prototype.GetMaxAmount = function() { return this.maxAmount; };
     483           2 : ResourceSupplyMirage.prototype.GetCurrentAmount = function() { return this.amount; };
     484           2 : ResourceSupplyMirage.prototype.GetType = function() { return this.type; };
     485           2 : ResourceSupplyMirage.prototype.IsInfinite = function() { return this.isInfinite; };
     486           2 : ResourceSupplyMirage.prototype.GetKillBeforeGather = function() { return this.killBeforeGather; };
     487           2 : ResourceSupplyMirage.prototype.GetMaxGatherers = function() { return this.maxGatherers; };
     488           2 : ResourceSupplyMirage.prototype.GetNumGatherers = function() { return this.numGatherers; };
     489             : 
     490             : // Apply diminishing returns with more gatherers, for e.g. infinite farms. For most resources this has no effect
     491             : // (GetDiminishingReturns will return null). We can assume that for resources that are miraged this is the case.
     492           2 : ResourceSupplyMirage.prototype.GetDiminishingReturns = function() { return null; };
     493             : 
     494           2 : Engine.RegisterGlobal("ResourceSupplyMirage", ResourceSupplyMirage);
     495             : 
     496           2 : ResourceSupply.prototype.Mirage = function()
     497             : {
     498           0 :         let mirage = new ResourceSupplyMirage();
     499           0 :         mirage.Init(this);
     500           0 :         return mirage;
     501             : };
     502             : 
     503           2 : Engine.RegisterComponentType(IID_ResourceSupply, "ResourceSupply", ResourceSupply);

Generated by: LCOV version 1.14