LCOV - code coverage report
Current view: top level - simulation/components - BuildingAI.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 189 0.0 %
Date: 2023-04-02 12:52:40 Functions: 0 21 0.0 %

          Line data    Source code
       1             : // Number of rounds of firing per 2 seconds.
       2           0 : const roundCount = 10;
       3           0 : const attackType = "Ranged";
       4             : 
       5             : function BuildingAI() {}
       6             : 
       7           0 : BuildingAI.prototype.Schema =
       8             :         "<element name='DefaultArrowCount'>" +
       9             :                 "<data type='nonNegativeInteger'/>" +
      10             :         "</element>" +
      11             :         "<optional>" +
      12             :                 "<element name='MaxArrowCount' a:help='Limit the number of arrows to a certain amount'>" +
      13             :                         "<data type='nonNegativeInteger'/>" +
      14             :                 "</element>" +
      15             :         "</optional>" +
      16             :         "<element name='GarrisonArrowMultiplier'>" +
      17             :                 "<ref name='nonNegativeDecimal'/>" +
      18             :         "</element>" +
      19             :         "<element name='GarrisonArrowClasses' a:help='Add extra arrows for this class list'>" +
      20             :                 "<text/>" +
      21             :         "</element>";
      22             : 
      23           0 : BuildingAI.prototype.MAX_PREFERENCE_BONUS = 2;
      24             : 
      25           0 : BuildingAI.prototype.Init = function()
      26             : {
      27           0 :         this.currentRound = 0;
      28           0 :         this.archersGarrisoned = 0;
      29           0 :         this.arrowsLeft = 0;
      30           0 :         this.targetUnits = [];
      31             : };
      32             : 
      33           0 : BuildingAI.prototype.OnGarrisonedUnitsChanged = function(msg)
      34             : {
      35           0 :         let classes = this.template.GarrisonArrowClasses;
      36           0 :         for (let ent of msg.added)
      37             :         {
      38           0 :                 let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
      39           0 :                 if (cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), classes))
      40           0 :                         ++this.archersGarrisoned;
      41             :         }
      42           0 :         for (let ent of msg.removed)
      43             :         {
      44           0 :                 let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
      45           0 :                 if (cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), classes))
      46           0 :                         --this.archersGarrisoned;
      47             :         }
      48             : };
      49             : 
      50           0 : BuildingAI.prototype.OnOwnershipChanged = function(msg)
      51             : {
      52           0 :         this.targetUnits = [];
      53           0 :         this.SetupRangeQuery();
      54           0 :         this.SetupGaiaRangeQuery();
      55             : };
      56             : 
      57           0 : BuildingAI.prototype.OnDiplomacyChanged = function(msg)
      58             : {
      59           0 :         if (!IsOwnedByPlayer(msg.player, this.entity))
      60           0 :                 return;
      61             : 
      62             :         // Remove maybe now allied/neutral units.
      63           0 :         this.targetUnits = [];
      64           0 :         this.SetupRangeQuery();
      65           0 :         this.SetupGaiaRangeQuery();
      66             : };
      67             : 
      68           0 : BuildingAI.prototype.OnDestroy = function()
      69             : {
      70           0 :         if (this.timer)
      71             :         {
      72           0 :                 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
      73           0 :                 cmpTimer.CancelTimer(this.timer);
      74           0 :                 this.timer = undefined;
      75             :         }
      76             : 
      77             :         // Clean up range queries.
      78           0 :         let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
      79           0 :         if (this.enemyUnitsQuery)
      80           0 :                 cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery);
      81           0 :         if (this.gaiaUnitsQuery)
      82           0 :                 cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery);
      83             : };
      84             : 
      85             : /**
      86             :  * React on Attack value modifications, as it might influence the range.
      87             :  */
      88           0 : BuildingAI.prototype.OnValueModification = function(msg)
      89             : {
      90           0 :         if (msg.component != "Attack")
      91           0 :                 return;
      92             : 
      93           0 :         this.targetUnits = [];
      94           0 :         this.SetupRangeQuery();
      95           0 :         this.SetupGaiaRangeQuery();
      96             : };
      97             : 
      98             : /**
      99             :  * Setup the Range Query to detect units coming in & out of range.
     100             :  */
     101           0 : BuildingAI.prototype.SetupRangeQuery = function()
     102             : {
     103           0 :         var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
     104           0 :         if (!cmpAttack)
     105           0 :                 return;
     106             : 
     107           0 :         var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     108           0 :         if (this.enemyUnitsQuery)
     109             :         {
     110           0 :                 cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery);
     111           0 :                 this.enemyUnitsQuery = undefined;
     112             :         }
     113             : 
     114           0 :         var cmpPlayer = QueryOwnerInterface(this.entity);
     115           0 :         if (!cmpPlayer)
     116           0 :                 return;
     117             : 
     118           0 :         var enemies = cmpPlayer.GetEnemies();
     119             :         // Remove gaia.
     120           0 :         if (enemies.length && enemies[0] == 0)
     121           0 :                 enemies.shift();
     122             : 
     123           0 :         if (!enemies.length)
     124           0 :                 return;
     125             : 
     126           0 :         const range = cmpAttack.GetRange(attackType);
     127           0 :         const yOrigin = cmpAttack.GetAttackYOrigin(attackType);
     128             :         // This takes entity sizes into accounts, so no need to compensate for structure size.
     129           0 :         this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
     130             :                 this.entity, range.min, range.max, yOrigin,
     131             :                 enemies, IID_Resistance, cmpRangeManager.GetEntityFlagMask("normal"));
     132             : 
     133           0 :         cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery);
     134             : };
     135             : 
     136             : // Set up a range query for Gaia units within LOS range which can be attacked.
     137             : // This should be called whenever our ownership changes.
     138           0 : BuildingAI.prototype.SetupGaiaRangeQuery = function()
     139             : {
     140           0 :         var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
     141           0 :         if (!cmpAttack)
     142           0 :                 return;
     143             : 
     144           0 :         var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     145           0 :         if (this.gaiaUnitsQuery)
     146             :         {
     147           0 :                 cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery);
     148           0 :                 this.gaiaUnitsQuery = undefined;
     149             :         }
     150             : 
     151           0 :         var cmpPlayer = QueryOwnerInterface(this.entity);
     152           0 :         if (!cmpPlayer || !cmpPlayer.IsEnemy(0))
     153           0 :                 return;
     154             : 
     155           0 :         const range = cmpAttack.GetRange(attackType);
     156           0 :         const yOrigin = cmpAttack.GetAttackYOrigin(attackType);
     157             : 
     158             :         // This query is only interested in Gaia entities that can attack.
     159             :         // This takes entity sizes into accounts, so no need to compensate for structure size.
     160           0 :         this.gaiaUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
     161             :                 this.entity, range.min, range.max, yOrigin,
     162             :                 [0], IID_Attack, cmpRangeManager.GetEntityFlagMask("normal"));
     163             : 
     164           0 :         cmpRangeManager.EnableActiveQuery(this.gaiaUnitsQuery);
     165             : };
     166             : 
     167             : /**
     168             :  * Called when units enter or leave range.
     169             :  */
     170           0 : BuildingAI.prototype.OnRangeUpdate = function(msg)
     171             : {
     172             : 
     173           0 :         var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
     174           0 :         if (!cmpAttack)
     175           0 :                 return;
     176             : 
     177             :         // Target enemy units except non-dangerous animals.
     178           0 :         if (msg.tag == this.gaiaUnitsQuery)
     179             :         {
     180           0 :                 msg.added = msg.added.filter(e => {
     181           0 :                         let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
     182           0 :                         return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
     183             :                 });
     184             :         }
     185           0 :         else if (msg.tag != this.enemyUnitsQuery)
     186           0 :                 return;
     187             : 
     188             :         // Add new targets.
     189           0 :         for (let entity of msg.added)
     190           0 :                 if (cmpAttack.CanAttack(entity))
     191           0 :                         this.targetUnits.push(entity);
     192             : 
     193             :         // Remove targets outside of vision-range.
     194           0 :         for (let entity of msg.removed)
     195             :         {
     196           0 :                 let index = this.targetUnits.indexOf(entity);
     197           0 :                 if (index > -1)
     198           0 :                         this.targetUnits.splice(index, 1);
     199             :         }
     200             : 
     201           0 :         if (this.targetUnits.length)
     202           0 :                 this.StartTimer();
     203             : };
     204             : 
     205           0 : BuildingAI.prototype.StartTimer = function()
     206             : {
     207           0 :         if (this.timer)
     208           0 :                 return;
     209             : 
     210           0 :         var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
     211           0 :         if (!cmpAttack)
     212           0 :                 return;
     213             : 
     214           0 :         var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     215           0 :         var attackTimers = cmpAttack.GetTimers(attackType);
     216             : 
     217           0 :         this.timer = cmpTimer.SetInterval(this.entity, IID_BuildingAI, "FireArrows",
     218             :                 attackTimers.prepare, attackTimers.repeat / roundCount, null);
     219             : };
     220             : 
     221           0 : BuildingAI.prototype.GetDefaultArrowCount = function()
     222             : {
     223           0 :         var arrowCount = +this.template.DefaultArrowCount;
     224           0 :         return Math.round(ApplyValueModificationsToEntity("BuildingAI/DefaultArrowCount", arrowCount, this.entity));
     225             : };
     226             : 
     227           0 : BuildingAI.prototype.GetMaxArrowCount = function()
     228             : {
     229           0 :         if (!this.template.MaxArrowCount)
     230           0 :                 return Infinity;
     231             : 
     232           0 :         let maxArrowCount = +this.template.MaxArrowCount;
     233           0 :         return Math.round(ApplyValueModificationsToEntity("BuildingAI/MaxArrowCount", maxArrowCount, this.entity));
     234             : };
     235             : 
     236           0 : BuildingAI.prototype.GetGarrisonArrowMultiplier = function()
     237             : {
     238           0 :         var arrowMult = +this.template.GarrisonArrowMultiplier;
     239           0 :         return ApplyValueModificationsToEntity("BuildingAI/GarrisonArrowMultiplier", arrowMult, this.entity);
     240             : };
     241             : 
     242           0 : BuildingAI.prototype.GetGarrisonArrowClasses = function()
     243             : {
     244           0 :         var string = this.template.GarrisonArrowClasses;
     245           0 :         if (string)
     246           0 :                 return string.split(/\s+/);
     247           0 :         return [];
     248             : };
     249             : 
     250             : /**
     251             :  * Returns the number of arrows which needs to be fired.
     252             :  * DefaultArrowCount + Garrisoned Archers (i.e., any unit capable
     253             :  * of shooting arrows from inside buildings).
     254             :  */
     255           0 : BuildingAI.prototype.GetArrowCount = function()
     256             : {
     257           0 :         let count = this.GetDefaultArrowCount() +
     258             :                 Math.round(this.archersGarrisoned * this.GetGarrisonArrowMultiplier());
     259             : 
     260           0 :         return Math.min(count, this.GetMaxArrowCount());
     261             : };
     262             : 
     263           0 : BuildingAI.prototype.SetUnitAITarget = function(ent)
     264             : {
     265           0 :         this.unitAITarget = ent;
     266           0 :         if (ent)
     267           0 :                 this.StartTimer();
     268             : };
     269             : 
     270             : /**
     271             :  * Fire arrows with random temporal distribution on prefered targets.
     272             :  * Called 'roundCount' times every 'RepeatTime' seconds when there are units in the range.
     273             :  */
     274           0 : BuildingAI.prototype.FireArrows = function()
     275             : {
     276           0 :         if (!this.targetUnits.length && !this.unitAITarget)
     277             :         {
     278           0 :                 if (!this.timer)
     279           0 :                         return;
     280             : 
     281           0 :                 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     282           0 :                 cmpTimer.CancelTimer(this.timer);
     283           0 :                 this.timer = undefined;
     284           0 :                 return;
     285             :         }
     286             : 
     287           0 :         let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
     288           0 :         if (!cmpAttack)
     289           0 :                 return;
     290             : 
     291           0 :         if (this.currentRound > roundCount - 1)
     292           0 :                 this.currentRound = 0;
     293             : 
     294           0 :         if (this.currentRound == 0)
     295           0 :                 this.arrowsLeft = this.GetArrowCount();
     296             : 
     297           0 :         let arrowsToFire = 0;
     298           0 :         if (this.currentRound == roundCount - 1)
     299           0 :                 arrowsToFire = this.arrowsLeft;
     300             :         else
     301           0 :                 arrowsToFire = Math.min(
     302             :                     randIntInclusive(0, 2 * this.GetArrowCount() / roundCount),
     303             :                     this.arrowsLeft
     304             :                 );
     305             : 
     306           0 :         if (arrowsToFire <= 0)
     307             :         {
     308           0 :                 ++this.currentRound;
     309           0 :                 return;
     310             :         }
     311             : 
     312             :         // Add targets to a weighted list, to allow preferences.
     313           0 :         let targets = new WeightedList();
     314           0 :         let maxPreference = this.MAX_PREFERENCE_BONUS;
     315           0 :         let addTarget = function(target)
     316             :         {
     317           0 :                 let preference = cmpAttack.GetPreference(target);
     318           0 :                 let weight = 1;
     319             : 
     320           0 :                 if (preference !== null && preference !== undefined)
     321           0 :                         weight += maxPreference / (1 + preference);
     322             : 
     323           0 :                 targets.push(target, weight);
     324             :         };
     325             : 
     326             :         // Add the UnitAI target separately, as the UnitMotion and RangeManager implementations differ.
     327           0 :         if (this.unitAITarget && this.targetUnits.indexOf(this.unitAITarget) == -1)
     328           0 :                 addTarget(this.unitAITarget);
     329           0 :         for (let target of this.targetUnits)
     330           0 :                 addTarget(target);
     331             : 
     332             :         // The obstruction manager performs approximate range checks.
     333             :         // so we need to verify them here.
     334             :         // TODO: perhaps an optional 'precise' mode to range queries would be more performant.
     335           0 :         const cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
     336           0 :         const range = cmpAttack.GetRange(attackType);
     337           0 :         const yOrigin = cmpAttack.GetAttackYOrigin(attackType);
     338             : 
     339           0 :         let firedArrows = 0;
     340           0 :         while (firedArrows < arrowsToFire && targets.length())
     341             :         {
     342           0 :                 const selectedTarget = targets.randomItem();
     343           0 :                 if (this.CheckTargetVisible(selectedTarget) && cmpObstructionManager.IsInTargetParabolicRange(
     344             :                         this.entity,
     345             :                         selectedTarget,
     346             :                         range.min,
     347             :                         range.max,
     348             :                         yOrigin,
     349             :                         false))
     350             :                 {
     351           0 :                         cmpAttack.PerformAttack(attackType, selectedTarget);
     352           0 :                         PlaySound("attack_" + attackType.toLowerCase(), this.entity);
     353           0 :                         ++firedArrows;
     354           0 :                         continue;
     355             :                 }
     356             : 
     357             :                 // Could not attack target, try a different target.
     358           0 :                 targets.remove(selectedTarget);
     359             :         }
     360             : 
     361           0 :         this.arrowsLeft -= firedArrows;
     362           0 :         ++this.currentRound;
     363             : };
     364             : 
     365             : /**
     366             :  * Returns true if the target entity is visible through the FoW/SoD.
     367             :  */
     368           0 : BuildingAI.prototype.CheckTargetVisible = function(target)
     369             : {
     370           0 :         var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     371           0 :         if (!cmpOwnership)
     372           0 :                 return false;
     373             : 
     374             :         // Entities that are hidden and miraged are considered visible.
     375           0 :         var cmpFogging = Engine.QueryInterface(target, IID_Fogging);
     376           0 :         if (cmpFogging && cmpFogging.IsMiraged(cmpOwnership.GetOwner()))
     377           0 :                 return true;
     378             : 
     379             :         // Either visible directly, or visible in fog.
     380           0 :         let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     381           0 :         return cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner()) != "hidden";
     382             : };
     383             : 
     384           0 : Engine.RegisterComponentType(IID_BuildingAI, "BuildingAI", BuildingAI);

Generated by: LCOV version 1.14