LCOV - code coverage report
Current view: top level - simulation/components - Capturable.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 141 197 71.6 %
Date: 2023-04-02 12:52:40 Functions: 17 30 56.7 %

          Line data    Source code
       1             : function Capturable() {}
       2             : 
       3           1 : Capturable.prototype.Schema =
       4             :         "<element name='CapturePoints' a:help='Maximum capture points.'>" +
       5             :                 "<ref name='positiveDecimal'/>" +
       6             :         "</element>" +
       7             :         "<element name='RegenRate' a:help='Number of capture points that are regenerated per second in favour of the owner.'>" +
       8             :                 "<ref name='nonNegativeDecimal'/>" +
       9             :         "</element>" +
      10             :         "<element name='GarrisonRegenRate' a:help='Factor how much each garrisoned entity will add capture points to the regeneration per second in favour of the owner.'>" +
      11             :                 "<ref name='nonNegativeDecimal'/>" +
      12             :         "</element>";
      13             : 
      14           1 : Capturable.prototype.Init = function()
      15             : {
      16          19 :         this.maxCapturePoints = +this.template.CapturePoints;
      17          19 :         this.garrisonRegenRate = +this.template.GarrisonRegenRate;
      18          19 :         this.regenRate = +this.template.RegenRate;
      19          19 :         this.capturePoints = [];
      20             : };
      21             : 
      22             : // Interface functions
      23             : 
      24             : /**
      25             :  * Returns the current capture points array.
      26             :  */
      27           1 : Capturable.prototype.GetCapturePoints = function()
      28             : {
      29          10 :         return this.capturePoints;
      30             : };
      31             : 
      32           1 : Capturable.prototype.GetMaxCapturePoints = function()
      33             : {
      34           0 :         return this.maxCapturePoints;
      35             : };
      36             : 
      37           1 : Capturable.prototype.GetGarrisonRegenRate = function()
      38             : {
      39          57 :         return this.garrisonRegenRate;
      40             : };
      41             : 
      42             : /**
      43             :  * Set the new capture points, used for cloning entities.
      44             :  * The caller should assure that the sum of capture points
      45             :  * matches the max.
      46             :  * @param {number[]} - Array with for all players the new value.
      47             :  */
      48           1 : Capturable.prototype.SetCapturePoints = function(capturePointsArray)
      49             : {
      50          18 :         this.capturePoints = capturePointsArray;
      51             : };
      52             : 
      53             : /**
      54             :  * Compute the amount of capture points to be reduced and reduce them.
      55             :  * @param {number} amount - Number of capture points to be taken.
      56             :  * @param {number} captor - The entity capturing us.
      57             :  * @param {number} captorOwner - Owner of the captor.
      58             :  * @return {Object} - Object of the form { "captureChange": number }, where number indicates the actual amount of capture points taken.
      59             :  */
      60           1 : Capturable.prototype.Capture = function(amount, captor, captorOwner)
      61             : {
      62           0 :         if (captorOwner == INVALID_PLAYER || !this.CanCapture(captorOwner))
      63           0 :                 return {};
      64             : 
      65             :         // TODO: implement loot
      66             : 
      67           0 :         return { "captureChange": this.Reduce(amount, captorOwner) };
      68             : };
      69             : 
      70             : /**
      71             :  * Reduces the amount of capture points of an entity,
      72             :  * in favour of the player of the source.
      73             :  * @param {number} amount - Number of capture points to be taken.
      74             :  * @param {number} playerID - ID of player the capture points should be awarded to.
      75             :  * @return {number} - The number of capture points actually taken.
      76             :  */
      77           1 : Capturable.prototype.Reduce = function(amount, playerID)
      78             : {
      79          16 :         if (amount <= 0)
      80           2 :                 return 0;
      81             : 
      82          14 :         let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
      83          14 :         if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
      84           0 :                 return 0;
      85             : 
      86          14 :         let cmpPlayerSource = QueryPlayerIDInterface(playerID);
      87          14 :         if (!cmpPlayerSource)
      88           0 :                 return 0;
      89             : 
      90             :         // Before changing the value, activate Fogging if necessary to hide changes.
      91          14 :         let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging);
      92          14 :         if (cmpFogging)
      93          14 :                 cmpFogging.Activate();
      94             : 
      95          56 :         let numberOfEnemies = this.capturePoints.filter((v, i) => v > 0 && cmpPlayerSource.IsEnemy(i)).length;
      96             : 
      97          14 :         if (numberOfEnemies == 0)
      98           1 :                 return 0;
      99             : 
     100             :         // Distribute the capture points over all enemies.
     101          13 :         let distributedAmount = amount / numberOfEnemies;
     102          13 :         let removedAmount = 0;
     103          13 :         while (distributedAmount > 0.0001)
     104             :         {
     105          13 :                 numberOfEnemies = 0;
     106          13 :                 for (let i in this.capturePoints)
     107             :                 {
     108          52 :                         if (!this.capturePoints[i] || !cmpPlayerSource.IsEnemy(i))
     109          34 :                                 continue;
     110          18 :                         if (this.capturePoints[i] > distributedAmount)
     111             :                         {
     112          14 :                                 removedAmount += distributedAmount;
     113          14 :                                 this.capturePoints[i] -= distributedAmount;
     114          14 :                                 ++numberOfEnemies;
     115             :                         }
     116             :                         else
     117             :                         {
     118           4 :                                 removedAmount += this.capturePoints[i];
     119           4 :                                 this.capturePoints[i] = 0;
     120             :                         }
     121             :                 }
     122          13 :                 distributedAmount = numberOfEnemies ? (amount - removedAmount) / numberOfEnemies : 0;
     123             :         }
     124             : 
     125             :         // Give all capture points taken to the player.
     126          39 :         let takenCapturePoints = this.maxCapturePoints - this.capturePoints.reduce((a, b) => a + b);
     127          13 :         this.capturePoints[playerID] += takenCapturePoints;
     128             : 
     129          13 :         this.CheckTimer();
     130          13 :         this.RegisterCapturePointsChanged();
     131          13 :         return takenCapturePoints;
     132             : };
     133             : 
     134             : /**
     135             :  * Check if the source can (re)capture points from this building.
     136             :  * @param {number} playerID - PlayerID of the source.
     137             :  * @return {boolean} - Whether the source can (re)capture points from this building.
     138             :  */
     139           1 : Capturable.prototype.CanCapture = function(playerID)
     140             : {
     141           0 :         let cmpPlayerSource = QueryPlayerIDInterface(playerID);
     142             : 
     143           0 :         if (!cmpPlayerSource)
     144           0 :                 warn(playerID + " has no player component defined on its id.");
     145           0 :         let capturePoints = this.GetCapturePoints();
     146           0 :         let sourceEnemyCapturePoints = 0;
     147           0 :         for (let i in this.GetCapturePoints())
     148           0 :                 if (cmpPlayerSource.IsEnemy(i))
     149           0 :                         sourceEnemyCapturePoints += capturePoints[i];
     150           0 :         return sourceEnemyCapturePoints > 0;
     151             : };
     152             : 
     153             : // Private functions
     154             : 
     155             : /**
     156             :  * This has to be called whenever the capture points are changed.
     157             :  * It notifies other components of the change, and switches ownership when needed.
     158             :  */
     159           1 : Capturable.prototype.RegisterCapturePointsChanged = function()
     160             : {
     161          16 :         let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     162          16 :         if (!cmpOwnership)
     163           0 :                 return;
     164             : 
     165          16 :         Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.capturePoints });
     166             : 
     167          16 :         let owner = cmpOwnership.GetOwner();
     168          16 :         if (owner == INVALID_PLAYER || this.capturePoints[owner] > 0)
     169          13 :                 return;
     170             : 
     171             :         // If all capture points have been taken from the owner, convert it to player with the most capture points.
     172           3 :         let cmpLostPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
     173           3 :         if (cmpLostPlayerStatisticsTracker)
     174           0 :                 cmpLostPlayerStatisticsTracker.LostEntity(this.entity);
     175             : 
     176          12 :         cmpOwnership.SetOwner(this.capturePoints.reduce((bestPlayer, playerCapturePoints, player, capturePoints) => playerCapturePoints > capturePoints[bestPlayer] ? player : bestPlayer, 0));
     177             : 
     178           3 :         let cmpCapturedPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
     179           3 :         if (cmpCapturedPlayerStatisticsTracker)
     180           0 :                 cmpCapturedPlayerStatisticsTracker.CapturedEntity(this.entity);
     181             : };
     182             : 
     183           1 : Capturable.prototype.GetRegenRate = function()
     184             : {
     185          57 :         const cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
     186          57 :         if (!cmpGarrisonHolder)
     187           0 :                 return this.regenRate;
     188             : 
     189          57 :         let total = this.regenRate;
     190          57 :         const garrisonRegenRate = this.GetGarrisonRegenRate();
     191          57 :         for (const entity of cmpGarrisonHolder.GetEntities())
     192             :         {
     193         228 :                 const captureStrength = Engine.QueryInterface(entity, IID_Attack)?.GetAttackEffectsData("Capture")?.Capture;
     194         228 :                 if (!captureStrength)
     195         114 :                         continue;
     196             : 
     197         114 :                 total += captureStrength * garrisonRegenRate;
     198             :         }
     199             : 
     200          57 :         return total;
     201             : };
     202             : 
     203           1 : Capturable.prototype.TimerTick = function()
     204             : {
     205           5 :         let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     206           5 :         if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
     207           0 :                 return;
     208             : 
     209           5 :         let owner = cmpOwnership.GetOwner();
     210           5 :         let modifiedCapturePoints = 0;
     211             : 
     212             :         // Special handle for the territory decay.
     213             :         // Reduce capture points from the owner in favour of all neighbours (also allies).
     214           5 :         let cmpTerritoryDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay);
     215           5 :         if (cmpTerritoryDecay && cmpTerritoryDecay.IsDecaying())
     216             :         {
     217           1 :                 let neighbours = cmpTerritoryDecay.GetConnectedNeighbours();
     218           3 :                 let totalNeighbours = neighbours.reduce((a, b) => a + b);
     219           1 :                 let decay = Math.min(cmpTerritoryDecay.GetDecayRate(), this.capturePoints[owner]);
     220           1 :                 this.capturePoints[owner] -= decay;
     221             : 
     222           1 :                 if (totalNeighbours)
     223           1 :                         for (let p in neighbours)
     224           4 :                                 this.capturePoints[p] += decay * neighbours[p] / totalNeighbours;
     225             :                 // Decay to gaia as default.
     226             :                 else
     227           0 :                         this.capturePoints[0] += decay;
     228             : 
     229           1 :                 modifiedCapturePoints += decay;
     230           1 :                 this.RegisterCapturePointsChanged();
     231             :         }
     232             : 
     233           5 :         let regenRate = this.GetRegenRate();
     234           5 :         if (regenRate < 0)
     235           1 :                 modifiedCapturePoints += this.Reduce(-regenRate, 0);
     236           4 :         else if (regenRate > 0)
     237           4 :                 modifiedCapturePoints += this.Reduce(regenRate, owner);
     238             : 
     239           5 :         if (modifiedCapturePoints)
     240           4 :                 return;
     241             : 
     242             :         // Nothing changed, stop the timer.
     243           1 :         let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     244           1 :         cmpTimer.CancelTimer(this.timer);
     245           1 :         delete this.timer;
     246           1 :         Engine.PostMessage(this.entity, MT_CaptureRegenStateChanged, { "regenerating": false, "regenRate": 0, "territoryDecay": 0 });
     247             : };
     248             : 
     249             : /**
     250             :  * Start the regeneration timer when no timer exists.
     251             :  * When nothing can be modified (f.e. because it is fully regenerated), the
     252             :  * timer stops automatically after one execution.
     253             :  */
     254           1 : Capturable.prototype.CheckTimer = function()
     255             : {
     256          31 :         if (this.timer)
     257           0 :                 return;
     258             : 
     259          31 :         let regenRate = this.GetRegenRate();
     260          31 :         let cmpDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay);
     261          31 :         let decay = cmpDecay && cmpDecay.IsDecaying() ? cmpDecay.GetDecayRate() : 0;
     262          31 :         if (regenRate == 0 && decay == 0)
     263           0 :                 return;
     264             : 
     265          31 :         let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     266          31 :         this.timer = cmpTimer.SetInterval(this.entity, IID_Capturable, "TimerTick", 1000, 1000, null);
     267          31 :         Engine.PostMessage(this.entity, MT_CaptureRegenStateChanged, { "regenerating": true, "regenRate": regenRate, "territoryDecay": decay });
     268             : };
     269             : 
     270             : /**
     271             :  * Update all chached values that could be affected by modifications.
     272             : */
     273           1 : Capturable.prototype.UpdateCachedValues = function()
     274             : {
     275           1 :         this.garrisonRegenRate = ApplyValueModificationsToEntity("Capturable/GarrisonRegenRate", +this.template.GarrisonRegenRate, this.entity);
     276           1 :         this.regenRate = ApplyValueModificationsToEntity("Capturable/RegenRate", +this.template.RegenRate, this.entity);
     277           1 :         this.maxCapturePoints = ApplyValueModificationsToEntity("Capturable/CapturePoints", +this.template.CapturePoints, this.entity);
     278             : };
     279             : 
     280             : /**
     281             :  * Update all chached values that could be affected by modifications.
     282             :  * Check timer and send changed messages when required.
     283             :  * @param {boolean} message - Whether not to send a CapturePointsChanged message. When false, caller should take care of sending that message.
     284             : */
     285           1 : Capturable.prototype.UpdateCachedValuesAndNotify = function(sendMessage = true)
     286             : {
     287           0 :         let oldMaxCapturePoints = this.maxCapturePoints;
     288           0 :         let oldGarrisonRegenRate = this.garrisonRegenRate;
     289           0 :         let oldRegenRate = this.regenRate;
     290             : 
     291           0 :         this.UpdateCachedValues();
     292             : 
     293           0 :         if (oldMaxCapturePoints != this.maxCapturePoints)
     294             :         {
     295           0 :                 let scale = this.maxCapturePoints / oldMaxCapturePoints;
     296           0 :                 for (let i in this.capturePoints)
     297           0 :                         this.capturePoints[i] *= scale;
     298           0 :                 if (sendMessage)
     299           0 :                         Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.capturePoints });
     300             :         }
     301             : 
     302           0 :         if (oldGarrisonRegenRate != this.garrisonRegenRate || oldRegenRate != this.regenRate)
     303           0 :                 this.CheckTimer();
     304             : };
     305             : 
     306             : // Message Listeners
     307             : 
     308           1 : Capturable.prototype.OnValueModification = function(msg)
     309             : {
     310           0 :         if (msg.component == "Capturable")
     311           0 :                 this.UpdateCachedValuesAndNotify();
     312             : };
     313             : 
     314           1 : Capturable.prototype.OnGarrisonedUnitsChanged = function(msg)
     315             : {
     316           0 :         this.CheckTimer();
     317             : };
     318             : 
     319           1 : Capturable.prototype.OnTerritoryDecayChanged = function(msg)
     320             : {
     321           0 :         if (msg.to)
     322           0 :                 this.CheckTimer();
     323             : };
     324             : 
     325           1 : Capturable.prototype.OnDiplomacyChanged = function(msg)
     326             : {
     327           0 :         this.CheckTimer();
     328             : };
     329             : 
     330           1 : Capturable.prototype.OnOwnershipChanged = function(msg)
     331             : {
     332           1 :         if (msg.to == INVALID_PLAYER)
     333           0 :                 return;
     334             : 
     335             :         // Initialise the capture points when created.
     336           1 :         if (!this.capturePoints.length)
     337             :         {
     338           1 :                 this.UpdateCachedValues();
     339           1 :                 let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
     340           1 :                 for (let i = 0; i < numPlayers; ++i)
     341             :                 {
     342           4 :                         if (i == msg.to)
     343           1 :                                 this.capturePoints[i] = this.maxCapturePoints;
     344             :                         else
     345           3 :                                 this.capturePoints[i] = 0;
     346             :                 }
     347           1 :                 this.CheckTimer();
     348           1 :                 return;
     349             :         }
     350             : 
     351             :         // When already initialised, this happens on defeat or wololo,
     352             :         // transfer the points of the old owner to the new one.
     353           0 :         if (this.capturePoints[msg.from])
     354             :         {
     355           0 :                 this.capturePoints[msg.to] += this.capturePoints[msg.from];
     356           0 :                 this.capturePoints[msg.from] = 0;
     357           0 :                 this.UpdateCachedValuesAndNotify(false);
     358           0 :                 this.RegisterCapturePointsChanged();
     359           0 :                 return;
     360             :         }
     361             : 
     362           0 :         this.UpdateCachedValuesAndNotify();
     363             : };
     364             : 
     365             : /**
     366             :  * When a player is defeated, reassign the capture points of non-owned entities to gaia.
     367             :  * Those owned by the defeated player are dealt with onOwnershipChanged.
     368             :  */
     369           1 : Capturable.prototype.OnGlobalPlayerDefeated = function(msg)
     370             : {
     371           1 :         if (!this.capturePoints[msg.playerId])
     372           0 :                 return;
     373           1 :         let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     374           1 :         if (cmpOwnership && (cmpOwnership.GetOwner() == INVALID_PLAYER ||
     375             :                              cmpOwnership.GetOwner() == msg.playerId))
     376           0 :                 return;
     377           1 :         this.capturePoints[0] += this.capturePoints[msg.playerId];
     378           1 :         this.capturePoints[msg.playerId] = 0;
     379           1 :         this.RegisterCapturePointsChanged();
     380           1 :         this.CheckTimer();
     381             : };
     382             : 
     383             : function CapturableMirage() {}
     384           1 : CapturableMirage.prototype.Init = function(cmpCapturable)
     385             : {
     386           0 :         this.capturePoints = clone(cmpCapturable.GetCapturePoints());
     387           0 :         this.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
     388             : };
     389             : 
     390           1 : CapturableMirage.prototype.GetCapturePoints = function() { return this.capturePoints; };
     391           1 : CapturableMirage.prototype.GetMaxCapturePoints = function() { return this.maxCapturePoints; };
     392           1 : CapturableMirage.prototype.CanCapture = Capturable.prototype.CanCapture;
     393             : 
     394           1 : Engine.RegisterGlobal("CapturableMirage", CapturableMirage);
     395             : 
     396           1 : Capturable.prototype.Mirage = function()
     397             : {
     398           0 :         let mirage = new CapturableMirage();
     399           0 :         mirage.Init(this);
     400           0 :         return mirage;
     401             : };
     402             : 
     403           1 : Engine.RegisterComponentType(IID_Capturable, "Capturable", Capturable);

Generated by: LCOV version 1.14