LCOV - code coverage report
Current view: top level - simulation/components - Foundation.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 147 208 70.7 %
Date: 2023-04-02 12:52:40 Functions: 20 31 64.5 %

          Line data    Source code
       1             : function Foundation() {}
       2             : 
       3           1 : Foundation.prototype.Schema =
       4             :         "<element name='BuildTimeModifier' a:help='Effect for having multiple builders.'>" +
       5             :                 "<ref name='nonNegativeDecimal'/>" +
       6             :         "</element>";
       7             : 
       8           1 : Foundation.prototype.Init = function()
       9             : {
      10             :         // Foundations are initially 'uncommitted' and do not block unit movement at all
      11             :         // (to prevent players exploiting free foundations to confuse enemy units).
      12             :         // The first builder to reach the uncommitted foundation will tell friendly units
      13             :         // and animals to move out of the way, then will commit the foundation and enable
      14             :         // its obstruction once there's nothing in the way.
      15           5 :         this.committed = false;
      16             : 
      17           5 :         this.builders = new Map(); // Map of builder entities to their work per second
      18           5 :         this.totalBuilderRate = 0; // Total amount of work the builders do each second
      19           5 :         this.buildMultiplier = 1; // Multiplier for the amount of work builders do
      20             : 
      21           5 :         this.buildTimeModifier = +this.template.BuildTimeModifier;
      22             : 
      23           5 :         this.previewEntity = INVALID_ENTITY;
      24             : };
      25             : 
      26           1 : Foundation.prototype.Serialize = function()
      27             : {
      28           0 :         let ret = Object.assign({}, this);
      29           0 :         ret.previewEntity = INVALID_ENTITY;
      30           0 :         return ret;
      31             : };
      32             : 
      33           1 : Foundation.prototype.Deserialize = function(data)
      34             : {
      35           0 :         this.Init();
      36           0 :         Object.assign(this, data);
      37             : };
      38             : 
      39           1 : Foundation.prototype.OnDeserialized = function()
      40             : {
      41           0 :         this.CreateConstructionPreview();
      42             : };
      43             : 
      44           1 : Foundation.prototype.InitialiseConstruction = function(template)
      45             : {
      46           5 :         this.finalTemplateName = template;
      47             : 
      48             :         // Remember the cost here, so if it changes after construction begins (from auras or technologies)
      49             :         // we will use the correct values to refund partial construction costs.
      50           5 :         let cmpCost = Engine.QueryInterface(this.entity, IID_Cost);
      51           5 :         if (!cmpCost)
      52           0 :                 error("A foundation, from " + template + ", must have a cost component to know the build time");
      53             : 
      54           5 :         this.costs = cmpCost.GetResourceCosts();
      55             : 
      56           5 :         this.maxProgress = 0;
      57             : 
      58           5 :         this.initialised = true;
      59             : };
      60             : 
      61             : /**
      62             :  * Moving the revelation logic from Build to here makes the building sink if
      63             :  * it is attacked.
      64             :  */
      65           1 : Foundation.prototype.OnHealthChanged = function(msg)
      66             : {
      67          17 :         let cmpPosition = Engine.QueryInterface(this.previewEntity, IID_Position);
      68          17 :         if (cmpPosition)
      69           2 :                 cmpPosition.SetConstructionProgress(this.GetBuildProgress());
      70             : 
      71          17 :         Engine.PostMessage(this.entity, MT_FoundationProgressChanged, { "to": this.GetBuildPercentage() });
      72             : };
      73             : 
      74             : /**
      75             :  * Returns the current build progress in a [0,1] range.
      76             :  */
      77           1 : Foundation.prototype.GetBuildProgress = function()
      78             : {
      79          82 :         let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
      80          82 :         if (!cmpHealth)
      81           0 :                 return 0;
      82             : 
      83          82 :         return cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints();
      84             : };
      85             : 
      86           1 : Foundation.prototype.GetBuildPercentage = function()
      87             : {
      88          37 :         return Math.floor(this.GetBuildProgress() * 100);
      89             : };
      90             : 
      91             : /**
      92             :  * @return {number[]} - An array containing the entity IDs of assigned builders.
      93             :  */
      94           1 : Foundation.prototype.GetBuilders = function()
      95             : {
      96          17 :         return Array.from(this.builders.keys());
      97             : };
      98             : 
      99           1 : Foundation.prototype.GetNumBuilders = function()
     100             : {
     101          49 :         return this.builders.size;
     102             : };
     103             : 
     104           1 : Foundation.prototype.IsFinished = function()
     105             : {
     106          17 :         return (this.GetBuildProgress() == 1.0);
     107             : };
     108             : 
     109           1 : Foundation.prototype.OnOwnershipChanged = function(msg)
     110             : {
     111           0 :         if (msg.to != INVALID_PLAYER && this.previewEntity != INVALID_ENTITY)
     112             :         {
     113           0 :                 let cmpPreviewOwnership = Engine.QueryInterface(this.previewEntity, IID_Ownership);
     114           0 :                 if (cmpPreviewOwnership)
     115           0 :                         cmpPreviewOwnership.SetOwner(msg.to);
     116           0 :                 return;
     117             :         }
     118             : 
     119           0 :         if (msg.to != INVALID_PLAYER || !this.initialised)
     120           0 :                 return;
     121             : 
     122           0 :         if (this.previewEntity != INVALID_ENTITY)
     123             :         {
     124           0 :                 Engine.DestroyEntity(this.previewEntity);
     125           0 :                 this.previewEntity = INVALID_ENTITY;
     126             :         }
     127             : 
     128           0 :         if (this.IsFinished())
     129           0 :                 return;
     130             : 
     131           0 :         let cmpPlayer = QueryPlayerIDInterface(msg.from);
     132           0 :         let cmpStatisticsTracker = QueryPlayerIDInterface(msg.from, IID_StatisticsTracker);
     133             : 
     134             :         // Refund a portion of the construction cost, proportional
     135             :         // to the amount of build progress remaining.
     136           0 :         for (let r in this.costs)
     137             :         {
     138           0 :                 let scaled = Math.ceil(this.costs[r] * (1.0 - this.maxProgress));
     139           0 :                 if (scaled)
     140             :                 {
     141           0 :                         if (cmpPlayer)
     142           0 :                                 cmpPlayer.AddResource(r, scaled);
     143           0 :                         if (cmpStatisticsTracker)
     144           0 :                                 cmpStatisticsTracker.IncreaseResourceUsedCounter(r, -scaled);
     145             :                 }
     146             :         }
     147             : };
     148             : 
     149             : /**
     150             :  * @param {number[]} builders - An array containing the entity IDs of builders to assign.
     151             :  */
     152           1 : Foundation.prototype.AddBuilders = function(builders)
     153             : {
     154           0 :         let changed = false;
     155           0 :         for (let builder of builders)
     156           0 :                 changed = this.AddBuilderHelper(builder) || changed;
     157             : 
     158           0 :         if (changed)
     159           0 :                 this.HandleBuildersChanged();
     160             : };
     161             : 
     162             : /**
     163             :  * @param {number} builderEnt - The entity to add.
     164             :  * @return {boolean} - Whether the addition was successful.
     165             :  */
     166           1 : Foundation.prototype.AddBuilderHelper = function(builderEnt)
     167             : {
     168          13 :         if (this.builders.has(builderEnt))
     169           4 :                 return false;
     170             : 
     171           9 :         let cmpBuilder = Engine.QueryInterface(builderEnt, IID_Builder) ||
     172             :                 Engine.QueryInterface(this.entity, IID_AutoBuildable);
     173           9 :         if (!cmpBuilder)
     174           1 :                 return false;
     175             : 
     176           8 :         let buildRate = cmpBuilder.GetRate();
     177           8 :         this.builders.set(builderEnt, buildRate);
     178           8 :         this.totalBuilderRate += buildRate;
     179             : 
     180           8 :         return true;
     181             : };
     182             : 
     183             : /**
     184             :  * @param {number} builderEnt - The entity to add.
     185             :  */
     186           1 : Foundation.prototype.AddBuilder = function(builderEnt)
     187             : {
     188          13 :         if (this.AddBuilderHelper(builderEnt))
     189           8 :                 this.HandleBuildersChanged();
     190             : };
     191             : 
     192             : /**
     193             :  * @param {number} builderEnt - The entity to remove.
     194             :  */
     195           1 : Foundation.prototype.RemoveBuilder = function(builderEnt)
     196             : {
     197           9 :         if (!this.builders.has(builderEnt))
     198           4 :                 return;
     199             : 
     200           5 :         this.totalBuilderRate -= this.builders.get(builderEnt);
     201           5 :         this.builders.delete(builderEnt);
     202           5 :         this.HandleBuildersChanged();
     203             : };
     204             : 
     205             : /**
     206             :  * This has to be called whenever the number of builders change.
     207             :  */
     208           1 : Foundation.prototype.HandleBuildersChanged = function()
     209             : {
     210          13 :         this.SetBuildMultiplier();
     211             : 
     212          13 :         let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
     213          13 :         if (cmpVisual)
     214           3 :                 cmpVisual.SetVariable("numbuilders", this.GetNumBuilders());
     215             : 
     216          13 :         Engine.PostMessage(this.entity, MT_FoundationBuildersChanged, { "to": this.GetBuilders() });
     217             : };
     218             : 
     219             : /**
     220             :  * The build multiplier is a penalty that is applied to each builder.
     221             :  * For example, ten women build at a combined rate of 10^0.7 = 5.01 instead of 10.
     222             :  */
     223           1 : Foundation.prototype.CalculateBuildMultiplier = function(num)
     224             : {
     225             :         // Avoid division by zero, in particular 0/0 = NaN which isn't reliably serialized
     226          33 :         return num < 2 ? 1 : Math.pow(num, this.buildTimeModifier) / num;
     227             : };
     228             : 
     229           1 : Foundation.prototype.SetBuildMultiplier = function()
     230             : {
     231          13 :         this.buildMultiplier = this.CalculateBuildMultiplier(this.GetNumBuilders());
     232             : };
     233             : 
     234           1 : Foundation.prototype.GetBuildTime = function()
     235             : {
     236           8 :         let timeLeft = (1 - this.GetBuildProgress()) * Engine.QueryInterface(this.entity, IID_Cost).GetBuildTime();
     237           8 :         let rate = this.totalBuilderRate * this.buildMultiplier;
     238           8 :         let rateNew = (this.totalBuilderRate + 1) * this.CalculateBuildMultiplier(this.GetNumBuilders() + 1);
     239           8 :         return {
     240             :                 // Avoid division by zero, in particular 0/0 = NaN which isn't reliably serialized
     241             :                 "timeRemaining": rate ? timeLeft / rate : 0,
     242             :                 "timeRemainingNew": timeLeft / rateNew
     243             :         };
     244             : };
     245             : 
     246             : /**
     247             :  * @return {boolean} - Whether the foundation has been committed sucessfully.
     248             :  */
     249           1 : Foundation.prototype.Commit = function()
     250             : {
     251           5 :         if (this.committed)
     252           0 :                 return false;
     253             : 
     254           5 :         let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
     255           5 :         if (cmpObstruction && cmpObstruction.GetBlockMovementFlag(true))
     256             :         {
     257           4 :                 for (let ent of cmpObstruction.GetEntitiesDeletedUponConstruction())
     258           0 :                         Engine.DestroyEntity(ent);
     259             : 
     260           4 :                 let collisions = cmpObstruction.GetEntitiesBlockingConstruction();
     261           4 :                 if (collisions.length)
     262             :                 {
     263           0 :                         for (let ent of collisions)
     264             :                         {
     265           0 :                                 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
     266           0 :                                 if (cmpUnitAI)
     267           0 :                                         cmpUnitAI.LeaveFoundation(this.entity);
     268             : 
     269             :                                 // TODO: What if an obstruction has no UnitAI?
     270             :                         }
     271             : 
     272             :                         // TODO: maybe we should tell the builder to use a special
     273             :                         // animation to indicate they're waiting for people to get
     274             :                         // out the way
     275             : 
     276           0 :                         return false;
     277             :                 }
     278             :         }
     279             : 
     280             :         // The obstruction always blocks new foundations/construction,
     281             :         // but we've temporarily allowed units to walk all over it
     282             :         // (via CCmpTemplateManager). Now we need to remove that temporary
     283             :         // blocker-disabling, so that we'll perform standard unit blocking instead.
     284           5 :         if (cmpObstruction)
     285           4 :                 cmpObstruction.SetDisableBlockMovementPathfinding(false, false, -1);
     286             : 
     287           5 :         let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
     288           5 :         cmpTrigger.CallEvent("OnConstructionStarted", {
     289             :                 "foundation": this.entity,
     290             :                 "template": this.finalTemplateName
     291             :         });
     292             : 
     293           5 :         let cmpFoundationVisual = Engine.QueryInterface(this.entity, IID_Visual);
     294           5 :         if (cmpFoundationVisual)
     295           1 :                 cmpFoundationVisual.SelectAnimation("scaffold", false, 1.0);
     296             : 
     297           5 :         this.committed = true;
     298           5 :         this.CreateConstructionPreview();
     299           5 :         return true;
     300             : };
     301             : 
     302             : /**
     303             :  * Perform some number of seconds of construction work.
     304             :  * Returns true if the construction is completed.
     305             :  */
     306           1 : Foundation.prototype.Build = function(builderEnt, work)
     307             : {
     308             :         // Do nothing if we've already finished building
     309             :         // (The entity will be destroyed soon after completion so
     310             :         // this won't happen much.)
     311          17 :         if (this.IsFinished())
     312           0 :                 return;
     313             : 
     314          17 :         if (!this.committed && !this.Commit())
     315           0 :                 return;
     316             : 
     317          17 :         let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
     318          17 :         if (!cmpHealth)
     319             :         {
     320           0 :                 error("Foundation " + this.entity + " does not have a health component.");
     321           0 :                 return;
     322             :         }
     323          17 :         let deltaHP = work * this.GetBuildRate() * this.buildMultiplier;
     324          17 :         if (deltaHP > 0)
     325          17 :                 cmpHealth.Increase(deltaHP);
     326             : 
     327             :         // Update the total builder rate.
     328          17 :         this.totalBuilderRate += work - this.builders.get(builderEnt);
     329          17 :         this.builders.set(builderEnt, work);
     330             : 
     331             :         // Remember our max progress for partial refund in case of destruction.
     332          17 :         this.maxProgress = Math.max(this.maxProgress, this.GetBuildProgress());
     333             : 
     334          17 :         if (this.maxProgress >= 1.0)
     335             :         {
     336           4 :                 let cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
     337             : 
     338           4 :                 let building = ChangeEntityTemplate(this.entity, this.finalTemplateName);
     339             : 
     340           4 :                 const cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
     341           4 :                 const cmpBuildingIdentity = Engine.QueryInterface(building, IID_Identity);
     342           4 :                 if (cmpIdentity && cmpBuildingIdentity)
     343             :                 {
     344           0 :                         const oldPhenotype = cmpIdentity.GetPhenotype();
     345           0 :                         if (cmpBuildingIdentity.GetPhenotype() !== oldPhenotype)
     346             :                         {
     347           0 :                                 cmpBuildingIdentity.SetPhenotype(oldPhenotype);
     348           0 :                                 Engine.QueryInterface(building, IID_Visual)?.RecomputeActorName();
     349             :                         }
     350             :                 }
     351             : 
     352           4 :                 if (cmpPlayerStatisticsTracker)
     353           1 :                         cmpPlayerStatisticsTracker.IncreaseConstructedBuildingsCounter(building);
     354             : 
     355           4 :                 PlaySound("constructed", building);
     356             : 
     357           4 :                 Engine.PostMessage(this.entity, MT_ConstructionFinished,
     358             :                         { "entity": this.entity, "newentity": building });
     359             : 
     360           4 :                 for (let builder of this.GetBuilders())
     361             :                 {
     362           4 :                         let cmpUnitAIBuilder = Engine.QueryInterface(builder, IID_UnitAI);
     363           4 :                         if (cmpUnitAIBuilder)
     364           0 :                                 cmpUnitAIBuilder.ConstructionFinished({ "entity": this.entity, "newentity": building });
     365             :                 }
     366             :         }
     367             : };
     368             : 
     369           1 : Foundation.prototype.GetBuildRate = function()
     370             : {
     371          25 :         let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
     372          25 :         let cmpCost = Engine.QueryInterface(this.entity, IID_Cost);
     373             :         // Return infinity for instant structure conversion
     374          25 :         return cmpHealth.GetMaxHitpoints() / cmpCost.GetBuildTime();
     375             : };
     376             : 
     377             : /**
     378             :  * Create preview entity and copy various parameters from the foundation.
     379             :  */
     380           1 : Foundation.prototype.CreateConstructionPreview = function()
     381             : {
     382           5 :         if (this.previewEntity)
     383             :         {
     384           5 :                 Engine.DestroyEntity(this.previewEntity);
     385           5 :                 this.previewEntity = INVALID_ENTITY;
     386             :         }
     387             : 
     388           5 :         if (!this.committed)
     389           0 :                 return;
     390             : 
     391           5 :         let cmpFoundationVisual = Engine.QueryInterface(this.entity, IID_Visual);
     392           5 :         if (!cmpFoundationVisual || !cmpFoundationVisual.HasConstructionPreview())
     393           4 :                 return;
     394             : 
     395           1 :         this.previewEntity = Engine.AddLocalEntity("construction|"+this.finalTemplateName);
     396           1 :         let cmpFoundationOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
     397           1 :         let cmpPreviewOwnership = Engine.QueryInterface(this.previewEntity, IID_Ownership);
     398           1 :         if (cmpFoundationOwnership && cmpPreviewOwnership)
     399           1 :                 cmpPreviewOwnership.SetOwner(cmpFoundationOwnership.GetOwner());
     400             : 
     401             :         // TODO: the 'preview' would be invisible if it doesn't have the below component,
     402             :         // Maybe it makes more sense to simply delete it then?
     403             : 
     404             :         // Initially hide the preview underground
     405           1 :         let cmpPreviewPosition = Engine.QueryInterface(this.previewEntity, IID_Position);
     406           1 :         let cmpFoundationPosition = Engine.QueryInterface(this.entity, IID_Position);
     407           1 :         if (cmpPreviewPosition && cmpFoundationPosition)
     408             :         {
     409           1 :                 let rot = cmpFoundationPosition.GetRotation();
     410           1 :                 cmpPreviewPosition.SetYRotation(rot.y);
     411           1 :                 cmpPreviewPosition.SetXZRotation(rot.x, rot.z);
     412             : 
     413           1 :                 let pos = cmpFoundationPosition.GetPosition2D();
     414           1 :                 cmpPreviewPosition.JumpTo(pos.x, pos.y);
     415             : 
     416           1 :                 cmpPreviewPosition.SetConstructionProgress(this.GetBuildProgress());
     417             :         }
     418             : 
     419           1 :         let cmpPreviewVisual = Engine.QueryInterface(this.previewEntity, IID_Visual);
     420           1 :         if (cmpPreviewVisual && cmpFoundationVisual)
     421             :         {
     422           0 :                 cmpPreviewVisual.SetActorSeed(cmpFoundationVisual.GetActorSeed());
     423           0 :                 cmpPreviewVisual.SelectAnimation("scaffold", false, 1.0);
     424             :         }
     425             : };
     426             : 
     427           1 : Foundation.prototype.OnEntityRenamed = function(msg)
     428             : {
     429           0 :         let cmpFoundationNew = Engine.QueryInterface(msg.newentity, IID_Foundation);
     430           0 :         if (cmpFoundationNew)
     431           0 :                 cmpFoundationNew.AddBuilders(this.GetBuilders());
     432             : };
     433             : 
     434             : function FoundationMirage() {}
     435           1 : FoundationMirage.prototype.Init = function(cmpFoundation)
     436             : {
     437           0 :         this.numBuilders = cmpFoundation.GetNumBuilders();
     438           0 :         this.buildTime = cmpFoundation.GetBuildTime();
     439             : };
     440             : 
     441           1 : FoundationMirage.prototype.GetNumBuilders = function() { return this.numBuilders; };
     442           1 : FoundationMirage.prototype.GetBuildTime = function() { return this.buildTime; };
     443             : 
     444           1 : Engine.RegisterGlobal("FoundationMirage", FoundationMirage);
     445             : 
     446           1 : Foundation.prototype.Mirage = function()
     447             : {
     448           0 :         let mirage = new FoundationMirage();
     449           0 :         mirage.Init(this);
     450           0 :         return mirage;
     451             : };
     452             : 
     453           1 : Engine.RegisterComponentType(IID_Foundation, "Foundation", Foundation);

Generated by: LCOV version 1.14