LCOV - code coverage report
Current view: top level - simulation/components - UnitMotionFlying.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 162 199 81.4 %
Date: 2023-04-02 12:52:40 Functions: 13 22 59.1 %

          Line data    Source code
       1             : // (A serious implementation of this might want to use C++ instead of JS
       2             : // for performance; this is just for fun.)
       3           1 : const SHORT_FINAL = 2.5;
       4             : function UnitMotionFlying() {}
       5             : 
       6           1 : UnitMotionFlying.prototype.Schema =
       7             :         "<element name='MaxSpeed'>" +
       8             :                 "<ref name='nonNegativeDecimal'/>" +
       9             :         "</element>" +
      10             :         "<element name='TakeoffSpeed'>" +
      11             :                 "<ref name='nonNegativeDecimal'/>" +
      12             :         "</element>" +
      13             :         "<optional>" +
      14             :                 "<element name='StationaryDistance' a:help='Allows the object to be stationary when reaching a target. Value defines the maximum distance at which a target is considered reached.'>" +
      15             :                         "<ref name='positiveDecimal'/>" +
      16             :                 "</element>" +
      17             :         "</optional>" +
      18             :         "<element name='LandingSpeed'>" +
      19             :                 "<ref name='nonNegativeDecimal'/>" +
      20             :         "</element>" +
      21             :         "<element name='AccelRate'>" +
      22             :                 "<ref name='nonNegativeDecimal'/>" +
      23             :         "</element>" +
      24             :         "<element name='SlowingRate'>" +
      25             :                 "<ref name='nonNegativeDecimal'/>" +
      26             :         "</element>" +
      27             :         "<element name='BrakingRate'>" +
      28             :                 "<ref name='nonNegativeDecimal'/>" +
      29             :         "</element>" +
      30             :         "<element name='TurnRate'>" +
      31             :                 "<ref name='nonNegativeDecimal'/>" +
      32             :         "</element>" +
      33             :         "<element name='OvershootTime'>" +
      34             :                 "<ref name='nonNegativeDecimal'/>" +
      35             :         "</element>" +
      36             :         "<element name='FlyingHeight'>" +
      37             :                 "<data type='decimal'/>" +
      38             :         "</element>" +
      39             :         "<element name='ClimbRate'>" +
      40             :                 "<ref name='nonNegativeDecimal'/>" +
      41             :         "</element>" +
      42             :         "<element name='DiesInWater'>" +
      43             :                 "<data type='boolean'/>" +
      44             :         "</element>" +
      45             :         "<element name='PassabilityClass'>" +
      46             :                 "<text/>" +
      47             :         "</element>";
      48             : 
      49           1 : UnitMotionFlying.prototype.Init = function()
      50             : {
      51           1 :         this.hasTarget = false;
      52           1 :         this.reachedTarget = false;
      53           1 :         this.targetX = 0;
      54           1 :         this.targetZ = 0;
      55           1 :         this.targetMinRange = 0;
      56           1 :         this.targetMaxRange = 0;
      57           1 :         this.speed = 0;
      58           1 :         this.landing = false;
      59           1 :         this.onGround = true;
      60           1 :         this.pitch = 0;
      61           1 :         this.roll = 0;
      62           1 :         this.waterDeath = false;
      63           1 :         this.passabilityClass = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).GetPassabilityClass(this.template.PassabilityClass);
      64             : };
      65             : 
      66           1 : UnitMotionFlying.prototype.OnUpdate = function(msg)
      67             : {
      68          17 :         let turnLength = msg.turnLength;
      69          17 :         if (!this.hasTarget)
      70           3 :                 return;
      71          14 :         let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
      72          14 :         let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
      73          14 :         let pos = cmpPosition.GetPosition();
      74          14 :         let angle = cmpPosition.GetRotation().y;
      75          14 :         let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
      76          14 :         let cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
      77          14 :         let ground = Math.max(cmpTerrain.GetGroundLevel(pos.x, pos.z), cmpWaterManager.GetWaterLevel(pos.x, pos.z));
      78          14 :         let newangle = angle;
      79          14 :         let canTurn = true;
      80          14 :         let distanceToTargetSquared = Math.euclidDistance2DSquared(pos.x, pos.z, this.targetX, this.targetZ);
      81          14 :         if (this.landing)
      82             :         {
      83           6 :                 if (this.speed > 0 && this.onGround)
      84             :                 {
      85           3 :                         if (pos.y <= cmpWaterManager.GetWaterLevel(pos.x, pos.z) && this.template.DiesInWater == "true")
      86           0 :                                 this.waterDeath = true;
      87           3 :                         this.pitch = 0;
      88             :                         // Deaccelerate forwards...at a very reduced pace.
      89           3 :                         if (this.waterDeath)
      90           0 :                                 this.speed = Math.max(0, this.speed - turnLength * this.template.BrakingRate * 10);
      91             :                         else
      92           3 :                                 this.speed = Math.max(0, this.speed - turnLength * this.template.BrakingRate);
      93           3 :                         canTurn = false;
      94             :                         // Clamp to ground if below it, or descend if above.
      95           3 :                         if (pos.y < ground)
      96           0 :                                 pos.y = ground;
      97           3 :                         else if (pos.y > ground)
      98           0 :                                 pos.y = Math.max(ground, pos.y - turnLength * this.template.ClimbRate);
      99             :                 }
     100           3 :                 else if (this.speed == 0 && this.onGround)
     101             :                 {
     102           1 :                         let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
     103           1 :                         if (this.waterDeath && cmpHealth)
     104           0 :                                 cmpHealth.Kill();
     105             :                         else
     106             :                         {
     107           1 :                                 this.pitch = 0;
     108             :                                 // We've stopped.
     109           1 :                                 if (cmpGarrisonHolder)
     110           1 :                                         cmpGarrisonHolder.AllowGarrisoning(true, "UnitMotionFlying");
     111           1 :                                 canTurn = false;
     112           1 :                                 this.hasTarget = false;
     113           1 :                                 this.landing = false;
     114             :                                 // Summon planes back from the edge of the map.
     115           1 :                                 let terrainSize = cmpTerrain.GetMapSize();
     116           1 :                                 let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
     117           1 :                                 if (cmpRangeManager.GetLosCircular())
     118             :                                 {
     119           1 :                                         let mapRadius = terrainSize/2;
     120           1 :                                         let x = pos.x - mapRadius;
     121           1 :                                         let z = pos.z - mapRadius;
     122           1 :                                         let div = (mapRadius - 12) / Math.sqrt(x*x + z*z);
     123           1 :                                         if (div < 1)
     124             :                                         {
     125           1 :                                                 pos.x = mapRadius + x*div;
     126           1 :                                                 pos.z = mapRadius + z*div;
     127           1 :                                                 newangle += Math.PI;
     128           1 :                                                 distanceToTargetSquared = Math.euclidDistance2DSquared(pos.x, pos.z, this.targetX, this.targetZ);
     129             :                                         }
     130             :                                 }
     131             :                                 else
     132             :                                 {
     133           0 :                                         pos.x = Math.max(Math.min(pos.x, terrainSize - 12), 12);
     134           0 :                                         pos.z = Math.max(Math.min(pos.z, terrainSize - 12), 12);
     135           0 :                                         newangle += Math.PI;
     136           0 :                                         distanceToTargetSquared = Math.euclidDistance2DSquared(pos.x, pos.z, this.targetX, this.targetZ);
     137             :                                 }
     138             :                         }
     139             :                 }
     140             :                 else
     141             :                 {
     142             :                         // Final Approach.
     143             :                         // We need to slow down to land!
     144           2 :                         this.speed = Math.max(this.template.LandingSpeed, this.speed - turnLength * this.template.SlowingRate);
     145           2 :                         canTurn = false;
     146           2 :                         let targetHeight = ground;
     147             :                         // Steep, then gradual descent.
     148           2 :                         if ((pos.y - targetHeight) / this.template.FlyingHeight > 1 / SHORT_FINAL)
     149           2 :                                 this.pitch = -Math.PI / 18;
     150             :                         else
     151           0 :                                 this.pitch = Math.PI / 18;
     152           2 :                         let descentRate = ((pos.y - targetHeight) / this.template.FlyingHeight * this.template.ClimbRate + SHORT_FINAL) * SHORT_FINAL;
     153           2 :                         if (pos.y < targetHeight)
     154           0 :                                 pos.y = Math.max(targetHeight, pos.y + turnLength * descentRate);
     155           2 :                         else if (pos.y > targetHeight)
     156           2 :                                 pos.y = Math.max(targetHeight, pos.y - turnLength * descentRate);
     157           2 :                         if (targetHeight == pos.y)
     158             :                         {
     159           1 :                                 this.onGround = true;
     160           1 :                                 if (targetHeight == cmpWaterManager.GetWaterLevel(pos.x, pos.z) && this.template.DiesInWater)
     161           0 :                                         this.waterDeath = true;
     162             :                         }
     163             :                 }
     164             :         }
     165             :         else
     166             :         {
     167           8 :                 if (this.template.StationaryDistance && distanceToTargetSquared <= +this.template.StationaryDistance * +this.template.StationaryDistance)
     168             :                 {
     169           0 :                         cmpPosition.SetXZRotation(0, 0);
     170           0 :                         this.pitch = 0;
     171           0 :                         this.roll = 0;
     172           0 :                         this.reachedTarget = true;
     173           0 :                         cmpPosition.TurnTo(Math.atan2(this.targetX - pos.x, this.targetZ - pos.z));
     174           0 :                         Engine.PostMessage(this.entity, MT_MotionUpdate, { "updateString": "likelySuccess" });
     175           0 :                         return;
     176             :                 }
     177             :                 // If we haven't reached max speed yet then we're still on the ground;
     178             :                 // otherwise we're taking off or flying.
     179             :                 // this.onGround in case of a go-around after landing (but not fully stopped).
     180             : 
     181           8 :                 if (this.speed < this.template.TakeoffSpeed && this.onGround)
     182             :                 {
     183           2 :                         if (cmpGarrisonHolder)
     184           2 :                                 cmpGarrisonHolder.AllowGarrisoning(false, "UnitMotionFlying");
     185           2 :                         this.pitch = 0;
     186             :                         // Accelerate forwards.
     187           2 :                         this.speed = Math.min(this.template.MaxSpeed, this.speed + turnLength * this.template.AccelRate);
     188           2 :                         canTurn = false;
     189             :                         // Clamp to ground if below it, or descend if above.
     190           2 :                         if (pos.y < ground)
     191           0 :                                 pos.y = ground;
     192           2 :                         else if (pos.y > ground)
     193           0 :                                 pos.y = Math.max(ground, pos.y - turnLength * this.template.ClimbRate);
     194             :                 }
     195             :                 else
     196             :                 {
     197           6 :                         this.onGround = false;
     198             :                         // Climb/sink to max height above ground.
     199           6 :                         this.speed = Math.min(this.template.MaxSpeed, this.speed + turnLength * this.template.AccelRate);
     200           6 :                         let targetHeight = ground + (+this.template.FlyingHeight);
     201           6 :                         if (Math.abs(pos.y-targetHeight) > this.template.FlyingHeight/5)
     202             :                         {
     203           3 :                                 this.pitch = Math.PI / 9;
     204           3 :                                 canTurn = false;
     205             :                         }
     206             :                         else
     207           3 :                                 this.pitch = 0;
     208           6 :                         if (pos.y < targetHeight)
     209           3 :                                 pos.y = Math.min(targetHeight, pos.y + turnLength * this.template.ClimbRate);
     210           3 :                         else if (pos.y > targetHeight)
     211             :                         {
     212           0 :                                 pos.y = Math.max(targetHeight, pos.y - turnLength * this.template.ClimbRate);
     213           0 :                                 this.pitch = -1 * this.pitch;
     214             :                         }
     215             :                 }
     216             :         }
     217             : 
     218             :         // If we're in range of the target then tell people that we've reached it.
     219             :         // (TODO: quantisation breaks this)
     220          14 :         if (!this.reachedTarget &&
     221             :                 this.targetMinRange * this.targetMinRange <= distanceToTargetSquared &&
     222             :                 distanceToTargetSquared <= this.targetMaxRange * this.targetMaxRange)
     223             :         {
     224           0 :                 this.reachedTarget = true;
     225           0 :                 Engine.PostMessage(this.entity, MT_MotionUpdate, { "updateString": "likelySuccess" });
     226             :         }
     227             : 
     228             :         // If we're facing away from the target, and are still fairly close to it,
     229             :         // then carry on going straight so we overshoot in a straight line.
     230          14 :         let isBehindTarget = ((this.targetX - pos.x) * Math.sin(angle) + (this.targetZ - pos.z) * Math.cos(angle) < 0);
     231             :         // Overshoot the target: carry on straight.
     232          14 :         if (isBehindTarget && distanceToTargetSquared < this.template.MaxSpeed * this.template.MaxSpeed * this.template.OvershootTime * this.template.OvershootTime)
     233           0 :                 canTurn = false;
     234             : 
     235          14 :         if (canTurn)
     236             :         {
     237             :                 // Turn towards the target.
     238           3 :                 let targetAngle = Math.atan2(this.targetX - pos.x, this.targetZ - pos.z);
     239           3 :                 let delta = targetAngle - angle;
     240             :                 // Wrap delta to -pi..pi.
     241           3 :                 delta = (delta + Math.PI) % (2*Math.PI);
     242           3 :                 if (delta < 0)
     243           0 :                         delta += 2 * Math.PI;
     244           3 :                 delta -= Math.PI;
     245             :                 // Clamp to max rate.
     246           3 :                 let deltaClamped = Math.min(Math.max(delta, -this.template.TurnRate * turnLength), this.template.TurnRate * turnLength);
     247             :                 // Calculate new orientation, in a peculiar way in order to make sure the
     248             :                 // result gets close to targetAngle (rather than being n*2*pi out).
     249           3 :                 newangle = targetAngle + deltaClamped - delta;
     250           3 :                 if (newangle - angle > Math.PI / 18)
     251           0 :                         this.roll = Math.PI / 9;
     252           3 :                 else if (newangle - angle < -Math.PI / 18)
     253           2 :                         this.roll = -Math.PI / 9;
     254             :                 else
     255           1 :                         this.roll = newangle - angle;
     256             :         }
     257             :         else
     258          11 :                 this.roll = 0;
     259             : 
     260          14 :         pos.x += this.speed * turnLength * Math.sin(angle);
     261          14 :         pos.z += this.speed * turnLength * Math.cos(angle);
     262          14 :         cmpPosition.SetHeightFixed(pos.y);
     263          14 :         cmpPosition.TurnTo(newangle);
     264          14 :         cmpPosition.SetXZRotation(this.pitch, this.roll);
     265          14 :         cmpPosition.MoveTo(pos.x, pos.z);
     266             : };
     267             : 
     268           1 : UnitMotionFlying.prototype.MoveToPointRange = function(x, z, minRange, maxRange)
     269             : {
     270           1 :         this.hasTarget = true;
     271           1 :         this.landing = false;
     272           1 :         this.reachedTarget = false;
     273           1 :         this.targetX = x;
     274           1 :         this.targetZ = z;
     275           1 :         this.targetMinRange = minRange;
     276           1 :         this.targetMaxRange = maxRange;
     277             : 
     278           1 :         return true;
     279             : };
     280             : 
     281           1 : UnitMotionFlying.prototype.MoveToTargetRange = function(target, minRange, maxRange)
     282             : {
     283           1 :         let cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
     284           1 :         if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
     285           0 :                 return false;
     286             : 
     287           1 :         let targetPos = cmpTargetPosition.GetPosition2D();
     288             : 
     289           1 :         this.hasTarget = true;
     290           1 :         this.reachedTarget = false;
     291           1 :         this.targetX = targetPos.x;
     292           1 :         this.targetZ = targetPos.y;
     293           1 :         this.targetMinRange = minRange;
     294           1 :         this.targetMaxRange = maxRange;
     295             : 
     296           1 :         return true;
     297             : };
     298             : 
     299           1 : UnitMotionFlying.prototype.SetMemberOfFormation = function()
     300             : {
     301             :         // Ignored.
     302             : };
     303             : 
     304           1 : UnitMotionFlying.prototype.GetWalkSpeed = function()
     305             : {
     306           0 :         return +this.template.MaxSpeed;
     307             : };
     308             : 
     309           1 : UnitMotionFlying.prototype.SetSpeedMultiplier = function(multiplier)
     310             : {
     311             :         // Ignore this, the speed is always the walk speed.
     312             : };
     313             : 
     314           1 : UnitMotionFlying.prototype.GetRunMultiplier = function()
     315             : {
     316           2 :         return 1;
     317             : };
     318             : 
     319             : /**
     320             :  * Estimate the next position of the unit. Just linearly extrapolate.
     321             :  * TODO: Reuse the movement code for a better estimate.
     322             :  */
     323           1 : UnitMotionFlying.prototype.EstimateFuturePosition = function(dt)
     324             : {
     325           0 :         let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
     326           0 :         if (!cmpPosition || !cmpPosition.IsInWorld())
     327           0 :                 return Vector2D();
     328           0 :         let position = cmpPosition.GetPosition2D();
     329             : 
     330           0 :         return Vector2D.add(position, Vector2D.sub(position, cmpPosition.GetPreviousPosition2D()).mult(dt/Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetLatestTurnLength()));
     331             : };
     332             : 
     333           1 : UnitMotionFlying.prototype.IsMoveRequested = function()
     334             : {
     335           0 :         return this.hasTarget;
     336             : };
     337             : 
     338           1 : UnitMotionFlying.prototype.GetCurrentSpeed = function()
     339             : {
     340          20 :         return this.speed;
     341             : };
     342             : 
     343           1 : UnitMotionFlying.prototype.GetSpeedMultiplier = function()
     344             : {
     345           4 :         return this.speed / +this.template.MaxSpeed;
     346             : };
     347             : 
     348           1 : UnitMotionFlying.prototype.GetAcceleration = function()
     349             : {
     350           0 :         return +this.template.AccelRate;
     351             : };
     352             : 
     353           1 : UnitMotionFlying.prototype.SetAcceleration = function()
     354             : {
     355             :         // Acceleration is set by the template. Ignore.
     356             : };
     357             : 
     358           1 : UnitMotionFlying.prototype.GetPassabilityClassName = function()
     359             : {
     360           3 :         return this.passabilityClassName ? this.passabilityClassName : this.template.PassabilityClass;
     361             : };
     362             : 
     363           1 : UnitMotionFlying.prototype.SetPassabilityClassName = function(passClassName)
     364             : {
     365           1 :         this.passabilityClassName = passClassName;
     366           1 :         const cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder);
     367           1 :         if (cmpPathfinder)
     368           1 :                 this.passabilityClass = cmpPathfinder.GetPassabilityClass(passClassName);
     369             : };
     370             : 
     371           1 : UnitMotionFlying.prototype.GetPassabilityClass = function()
     372             : {
     373           1 :         return this.passabilityClass;
     374             : };
     375             : 
     376           1 : UnitMotionFlying.prototype.FaceTowardsPoint = function(x, z)
     377             : {
     378             :         // Ignore this - angle is controlled by the target-seeking code instead.
     379             : };
     380             : 
     381           1 : UnitMotionFlying.prototype.SetFacePointAfterMove = function()
     382             : {
     383             :         // Ignore this - angle is controlled by the target-seeking code instead.
     384             : };
     385             : 
     386           1 : UnitMotionFlying.prototype.StopMoving = function()
     387             : {
     388             :         // Invert.
     389           1 :         if (!this.waterDeath)
     390           1 :                 this.landing = !this.landing;
     391             : 
     392             : };
     393             : 
     394           1 : UnitMotionFlying.prototype.SetDebugOverlay = function(enabled)
     395             : {
     396             : };
     397             : 
     398           1 : Engine.RegisterComponentType(IID_UnitMotion, "UnitMotionFlying", UnitMotionFlying);

Generated by: LCOV version 1.14