Allow entities to upgrade into other entities.
[0ad.git] / binaries / data / mods / public / simulation / components / GuiInterface.js
blob18222d103955dbee1889a27f5a5ed4ca6fbff439
1 function GuiInterface() {}
3 GuiInterface.prototype.Schema =
4         "<a:component type='system'/><empty/>";
6 GuiInterface.prototype.Serialize = function()
8         // This component isn't network-synchronised for the biggest part
9         // So most of the attributes shouldn't be serialized
10         // Return an object with a small selection of deterministic data
11         return {
12                 "timeNotifications": this.timeNotifications,
13                 "timeNotificationID": this.timeNotificationID
14         };
17 GuiInterface.prototype.Deserialize = function(data)
19         this.Init();
20         this.timeNotifications = data.timeNotifications;
21         this.timeNotificationID = data.timeNotificationID;
24 GuiInterface.prototype.Init = function()
26         this.placementEntity = undefined; // = undefined or [templateName, entityID]
27         this.placementWallEntities = undefined;
28         this.placementWallLastAngle = 0;
29         this.notifications = [];
30         this.renamedEntities = [];
31         this.miragedEntities = [];
32         this.timeNotificationID = 1;
33         this.timeNotifications = [];
34         this.entsRallyPointsDisplayed = [];
35         this.entsWithAuraAndStatusBars = new Set();
39  * All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
40  * from GUI scripts, and executed here with arguments (player, arg).
41  *
42  * CAUTION: The input to the functions in this module is not network-synchronised, so it
43  * mustn't affect the simulation state (i.e. the data that is serialised and can affect
44  * the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
45  */
47 /**
48  * Returns global information about the current game state.
49  * This is used by the GUI and also by AI scripts.
50  */
51 GuiInterface.prototype.GetSimulationState = function()
53         let ret = {
54                 "players": []
55         };
57         let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
58         let numPlayers = cmpPlayerManager.GetNumPlayers();
60         for (let i = 0; i < numPlayers; ++i)
61         {
62                 let playerEnt = cmpPlayerManager.GetPlayerByID(i);
63                 let cmpPlayerEntityLimits = Engine.QueryInterface(playerEnt, IID_EntityLimits);
64                 let cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
66                 // Work out what phase we are in
67                 let phase = "";
68                 let cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager);
69                 if (cmpTechnologyManager)
70                 {
71                         if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
72                                 phase = "city";
73                         else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
74                                 phase = "town";
75                         else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
76                                 phase = "village";
77                 }
79                 // store player ally/neutral/enemy data as arrays
80                 let allies = [];
81                 let mutualAllies = [];
82                 let neutrals = [];
83                 let enemies = [];
85                 for (let j = 0; j < numPlayers; ++j)
86                 {
87                         allies[j] = cmpPlayer.IsAlly(j);
88                         mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
89                         neutrals[j] = cmpPlayer.IsNeutral(j);
90                         enemies[j] = cmpPlayer.IsEnemy(j);
91                 }
93                 ret.players.push({
94                         "name": cmpPlayer.GetName(),
95                         "civ": cmpPlayer.GetCiv(),
96                         "color": cmpPlayer.GetColor(),
97                         "controlsAll": cmpPlayer.CanControlAllUnits(),
98                         "popCount": cmpPlayer.GetPopulationCount(),
99                         "popLimit": cmpPlayer.GetPopulationLimit(),
100                         "popMax": cmpPlayer.GetMaxPopulation(),
101                         "heroes": cmpPlayer.GetHeroes(),
102                         "resourceCounts": cmpPlayer.GetResourceCounts(),
103                         "trainingBlocked": cmpPlayer.IsTrainingBlocked(),
104                         "state": cmpPlayer.GetState(),
105                         "team": cmpPlayer.GetTeam(),
106                         "teamsLocked": cmpPlayer.GetLockTeams(),
107                         "cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
108                         "disabledTemplates": cmpPlayer.GetDisabledTemplates(),
109                         "hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
110                         "phase": phase,
111                         "isAlly": allies,
112                         "isMutualAlly": mutualAllies,
113                         "isNeutral": neutrals,
114                         "isEnemy": enemies,
115                         "entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
116                         "entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
117                         "entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
118                         "researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
119                         "researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedResearch() : null,
120                         "researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
121                         "classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
122                         "typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null
123                 });
124         }
126         let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
127         if (cmpRangeManager)
128                 ret.circularMap = cmpRangeManager.GetLosCircular();
130         let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
131         if (cmpTerrain)
132                 ret.mapSize = 4 * cmpTerrain.GetTilesPerSide();
134         // Add timeElapsed
135         let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
136         ret.timeElapsed = cmpTimer.GetTime();
138         // Add ceasefire info
139         let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
140         if (cmpCeasefireManager)
141         {
142                 ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive();
143                 ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0;
144         }
146         // Add the game type
147         let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
148         ret.gameType = cmpEndGameManager.GetGameType();
150         // Add bartering prices
151         let cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
152         ret.barterPrices = cmpBarter.GetPrices();
154         // Add basic statistics to each player
155         for (let i = 0; i < numPlayers; ++i)
156         {
157                 let playerEnt = cmpPlayerManager.GetPlayerByID(i);
158                 let cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker);
159                 if (cmpPlayerStatisticsTracker)
160                         ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics();
161         }
163         return ret;
167  * Returns global information about the current game state, plus statistics.
168  * This is used by the GUI at the end of a game, in the summary screen.
169  * Note: Amongst statistics, the team exploration map percentage is computed from
170  * scratch, so the extended simulation state should not be requested too often.
171  */
172 GuiInterface.prototype.GetExtendedSimulationState = function()
174         // Get basic simulation info
175         let ret = this.GetSimulationState();
177         // Add statistics to each player
178         let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
179         let n = cmpPlayerManager.GetNumPlayers();
180         for (let i = 0; i < n; ++i)
181         {
182                 let playerEnt = cmpPlayerManager.GetPlayerByID(i);
183                 let cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker);
184                 if (cmpPlayerStatisticsTracker)
185                         ret.players[i].statistics = cmpPlayerStatisticsTracker.GetStatistics();
186         }
188         return ret;
191 GuiInterface.prototype.GetRenamedEntities = function(player)
193         if (this.miragedEntities[player])
194                 return this.renamedEntities.concat(this.miragedEntities[player]);
195         else
196                 return this.renamedEntities;
199 GuiInterface.prototype.ClearRenamedEntities = function()
201         this.renamedEntities = [];
202         this.miragedEntities = [];
205 GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
207         if (!this.miragedEntities[player])
208                 this.miragedEntities[player] = [];
210         this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
214  * Get common entity info, often used in the gui
215  */
216 GuiInterface.prototype.GetEntityState = function(player, ent)
218         let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
220         // All units must have a template; if not then it's a nonexistent entity id
221         let template = cmpTemplateManager.GetCurrentTemplateName(ent);
222         if (!template)
223                 return null;
225         let ret = {
226                 "id": ent,
227                 "template": template,
229                 "alertRaiser": null,
230                 "builder": null,
231                 "identity": null,
232                 "fogging": null,
233                 "foundation": null,
234                 "garrisonHolder": null,
235                 "gate": null,
236                 "guard": null,
237                 "market": null,
238                 "mirage": null,
239                 "pack": null,
240                 "upgrade" : null,
241                 "player": -1,
242                 "position": null,
243                 "production": null,
244                 "rallyPoint": null,
245                 "resourceCarrying": null,
246                 "rotation": null,
247                 "trader": null,
248                 "unitAI": null,
249                 "visibility": null,
250         };
252         let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
253         if (cmpMirage)
254                 ret.mirage = true;
256         let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
257         if (cmpIdentity)
258                 ret.identity = {
259                         "rank": cmpIdentity.GetRank(),
260                         "classes": cmpIdentity.GetClassesList(),
261                         "visibleClasses": cmpIdentity.GetVisibleClassesList(),
262                         "selectionGroupName": cmpIdentity.GetSelectionGroupName()
263                 };
265         let cmpPosition = Engine.QueryInterface(ent, IID_Position);
266         if (cmpPosition && cmpPosition.IsInWorld())
267         {
268                 ret.position = cmpPosition.GetPosition();
269                 ret.rotation = cmpPosition.GetRotation();
270         }
272         let cmpHealth = QueryMiragedInterface(ent, IID_Health);
273         if (cmpHealth)
274         {
275                 ret.hitpoints = cmpHealth.GetHitpoints();
276                 ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
277                 ret.needsRepair = cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints());
278                 ret.needsHeal = !cmpHealth.IsUnhealable();
279                 ret.canDelete = !cmpHealth.IsUndeletable();
280         }
282         let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
283         if (cmpCapturable)
284         {
285                 ret.capturePoints = cmpCapturable.GetCapturePoints();
286                 ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
287         }
289         let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
290         if (cmpBuilder)
291                 ret.builder = true;
293         let cmpMarket = QueryMiragedInterface(ent, IID_Market);
294         if (cmpMarket)
295                 ret.market = {
296                         "land": cmpMarket.HasType("land"),
297                         "naval": cmpMarket.HasType("naval"),
298                 };
300         let cmpPack = Engine.QueryInterface(ent, IID_Pack);
301         if (cmpPack)
302                 ret.pack = {
303                         "packed": cmpPack.IsPacked(),
304                         "progress": cmpPack.GetProgress(),
305                 };
307         var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
308         if (cmpUpgrade)
309                 ret.upgrade = {
310                         "upgrades" : cmpUpgrade.GetUpgrades(),
311                         "progress": cmpUpgrade.GetProgress(),
312                         "template": cmpUpgrade.GetUpgradingTo()
313                 };
315         let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
316         if (cmpProductionQueue)
317                 ret.production = {
318                         "entities": cmpProductionQueue.GetEntitiesList(),
319                         "technologies": cmpProductionQueue.GetTechnologiesList(),
320                         "techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(),
321                         "queue": cmpProductionQueue.GetQueue()
322                 };
324         let cmpTrader = Engine.QueryInterface(ent, IID_Trader);
325         if (cmpTrader)
326                 ret.trader = {
327                         "goods": cmpTrader.GetGoods()
328                 };
330         let cmpFogging = Engine.QueryInterface(ent, IID_Fogging);
331         if (cmpFogging)
332         {
333                 if (cmpFogging.IsMiraged(player))
334                         ret.fogging = { "mirage": cmpFogging.GetMirage(player) };
335                 else
336                         ret.fogging = { "mirage": null };
337         }
339         let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
340         if (cmpFoundation)
341                 ret.foundation = {
342                         "progress": cmpFoundation.GetBuildPercentage(),
343                         "numBuilders": cmpFoundation.GetNumBuilders()
344                 };
346         let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
347         if (cmpRepairable)
348                 ret.repairable = { "numBuilders": cmpRepairable.GetNumBuilders() };
350         let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
351         if (cmpOwnership)
352                 ret.player = cmpOwnership.GetOwner();
354         let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
355         if (cmpRallyPoint)
356                 ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
358         let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
359         if (cmpGarrisonHolder)
360                 ret.garrisonHolder = {
361                         "entities": cmpGarrisonHolder.GetEntities(),
362                         "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
363                         "capacity": cmpGarrisonHolder.GetCapacity(),
364                         "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
365                 };
367         let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
368         if (cmpUnitAI)
369         {
370                 ret.unitAI = {
371                         "state": cmpUnitAI.GetCurrentState(),
372                         "orders": cmpUnitAI.GetOrders(),
373                         "hasWorkOrders": cmpUnitAI.HasWorkOrders(),
374                         "canGuard": cmpUnitAI.CanGuard(),
375                         "isGuarding": cmpUnitAI.IsGuardOf(),
376                         "possibleStances": cmpUnitAI.GetPossibleStances(),
377                         "isIdle":cmpUnitAI.IsIdle(),
378                 };
379                 // Add some information needed for ungarrisoning
380                 if (cmpUnitAI.IsGarrisoned() && ret.player !== undefined)
381                         ret.template = "p" + ret.player + "&" + ret.template;
382         }
384         let cmpGuard = Engine.QueryInterface(ent, IID_Guard);
385         if (cmpGuard)
386                 ret.guard = {
387                         "entities": cmpGuard.GetEntities(),
388                 };
390         let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
391         if (cmpResourceGatherer)
392                 ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
394         let cmpGate = Engine.QueryInterface(ent, IID_Gate);
395         if (cmpGate)
396                 ret.gate = {
397                         "locked": cmpGate.IsLocked(),
398                 };
400         let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
401         if (cmpAlertRaiser)
402                 ret.alertRaiser = {
403                         "level": cmpAlertRaiser.GetLevel(),
404                         "canIncreaseLevel": cmpAlertRaiser.CanIncreaseLevel(),
405                         "hasRaisedAlert": cmpAlertRaiser.HasRaisedAlert(),
406                 };
408         let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
409         ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
411         return ret;
415  * Get additionnal entity info, rarely used in the gui
416  */
417 GuiInterface.prototype.GetExtendedEntityState = function(player, ent)
419         let ret = {
420                 "armour": null,
421                 "attack": null,
422                 "barterMarket": null,
423                 "buildingAI": null,
424                 "healer": null,
425                 "obstruction": null,
426                 "turretParent":null,
427                 "promotion": null,
428                 "repairRate": null,
429                 "buildRate": null,
430                 "resourceDropsite": null,
431                 "resourceGatherRates": null,
432                 "resourceSupply": null,
433         };
435         let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
437         let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
438         if (cmpAttack)
439         {
440                 let types = cmpAttack.GetAttackTypes();
441                 if (types.length)
442                         ret.attack = {};
443                 for (let type of types)
444                 {
445                         ret.attack[type] = cmpAttack.GetAttackStrengths(type);
446                         let range = cmpAttack.GetRange(type);
447                         ret.attack[type].minRange = range.min;
448                         ret.attack[type].maxRange = range.max;
449                         let timers = cmpAttack.GetTimers(type);
450                         ret.attack[type].prepareTime = timers.prepare;
451                         ret.attack[type].repeatTime = timers.repeat;
452                         if (type != "Ranged")
453                         {
454                                 // not a ranged attack, set some defaults
455                                 ret.attack[type].elevationBonus = 0;
456                                 ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
457                                 continue;
458                         }
460                         ret.attack[type].elevationBonus = range.elevationBonus;
461                         let cmpPosition = Engine.QueryInterface(ent, IID_Position);
462                         let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
463                         let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
464                         if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld())
465                         {
466                                 // For units, take the rage in front of it, no spread. So angle = 0
467                                 ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 0);
468                         }
469                         else if(cmpPosition && cmpPosition.IsInWorld())
470                         {
471                                 // For buildings, take the average elevation around it. So angle = 2*pi
472                                 ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 2*Math.PI);
473                         }
474                         else
475                         {
476                                 // not in world, set a default?
477                                 ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
478                         }
479                 }
480         }
482         let cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
483         if (cmpArmour)
484                 ret.armour = cmpArmour.GetArmourStrengths();
486         let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
487         if (cmpAuras)
488                 ret.auras = cmpAuras.GetDescriptions();
490         let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
491         if (cmpBuildingAI)
492                 ret.buildingAI = {
493                         "defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
494                         "garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
495                         "garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
496                         "arrowCount": cmpBuildingAI.GetArrowCount()
497                 };
499         let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
500         if (cmpObstruction)
501                 ret.obstruction = {
502                         "controlGroup": cmpObstruction.GetControlGroup(),
503                         "controlGroup2": cmpObstruction.GetControlGroup2(),
504                 };
506         let cmpPosition = Engine.QueryInterface(ent, IID_Position);
507         if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
508                 ret.turretParent = cmpPosition.GetTurretParent();
510         let cmpRepairable = Engine.QueryInterface(ent, IID_Repairable);
511         if (cmpRepairable)
512                 ret.repairRate = cmpRepairable.GetRepairRate();
514         let cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
515         if (cmpFoundation)
516                 ret.buildRate = cmpFoundation.GetBuildRate();
518         let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
519         if (cmpResourceSupply)
520                 ret.resourceSupply = {
521                         "isInfinite": cmpResourceSupply.IsInfinite(),
522                         "max": cmpResourceSupply.GetMaxAmount(),
523                         "amount": cmpResourceSupply.GetCurrentAmount(),
524                         "type": cmpResourceSupply.GetType(),
525                         "killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
526                         "maxGatherers": cmpResourceSupply.GetMaxGatherers(),
527                         "numGatherers": cmpResourceSupply.GetNumGatherers()
528                 };
530         let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
531         if (cmpResourceGatherer)
532                 ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
534         let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
535         if (cmpResourceDropsite)
536                 ret.resourceDropsite = {
537                         "types": cmpResourceDropsite.GetTypes(),
538                         "sharable": cmpResourceDropsite.IsSharable(),
539                         "shared": cmpResourceDropsite.IsShared()
540                 };
542         let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
543         if (cmpPromotion)
544                 ret.promotion = {
545                         "curr": cmpPromotion.GetCurrentXp(),
546                         "req": cmpPromotion.GetRequiredXp()
547                 };
549         if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket"))
550         {
551                 let cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
552                 ret.barterMarket = { "prices": cmpBarter.GetPrices() };
553         }
555         let cmpHeal = Engine.QueryInterface(ent, IID_Heal);
556         if (cmpHeal)
557                 ret.heal = {
558                         "hp": cmpHeal.GetHP(),
559                         "range": cmpHeal.GetRange().max,
560                         "rate": cmpHeal.GetRate(),
561                         "unhealableClasses": cmpHeal.GetUnhealableClasses(),
562                         "healableClasses": cmpHeal.GetHealableClasses(),
563                 };
565         return ret;
568 GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
570         let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
571         let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
573         let rot = { "x": 0, "y": 0, "z": 0 };
574         let pos = {
575                 "x": cmd.x,
576                 "y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z),
577                 "z": cmd.z
578         };
580         let elevationBonus = cmd.elevationBonus || 0;
581         let range = cmd.range;
583         return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI);
586 GuiInterface.prototype.GetTemplateData = function(player, extendedName)
588         let name = extendedName;
589         // Special case for garrisoned units which have a extended template
590         if (extendedName.indexOf("&") != -1)
591                 name = extendedName.slice(extendedName.indexOf("&")+1);
593         let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
594         let template = cmpTemplateManager.GetTemplate(name);
596         if (!template)
597                 return null;
599         let aurasTemplate = {};
601         if (!template.Auras)
602                 return GetTemplateDataHelper(template, player, aurasTemplate);
604         // Add aura name and description loaded from JSON file
605         let auraNames = template.Auras._string.split(/\s+/);
606         let cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager);
607         for (let name of auraNames)
608         {
609                 let auraTemplate = cmpDataTemplateManager.GetAuraTemplate(name);
610                 if (!auraTemplate)
611                 {
612                         // The following warning is perhaps useless since it's yet done in DataTemplateManager
613                         warn("Tried to get data for invalid aura: " + name);
614                         continue;
615                 }
616                 aurasTemplate[name] = {};
617                 aurasTemplate[name].auraName = auraTemplate.auraName || null;
618                 aurasTemplate[name].auraDescription = auraTemplate.auraDescription || null;
619         }
620         return GetTemplateDataHelper(template, player, aurasTemplate);
623 GuiInterface.prototype.GetTechnologyData = function(player, name)
625         let cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager);
626         let template = cmpDataTemplateManager.GetTechnologyTemplate(name);
628         if (!template)
629         {
630                 warn("Tried to get data for invalid technology: " + name);
631                 return null;
632         }
634         let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
635         return GetTechnologyDataHelper(template, cmpPlayer.GetCiv());
638 GuiInterface.prototype.IsTechnologyResearched = function(player, data)
640         if (!data.tech)
641                 return true;
643         let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager);
645         if (!cmpTechnologyManager)
646                 return false;
648         return cmpTechnologyManager.IsTechnologyResearched(data.tech);
651 // Checks whether the requirements for this technology have been met
652 GuiInterface.prototype.CheckTechnologyRequirements = function(player, data)
654         let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager);
656         if (!cmpTechnologyManager)
657                 return false;
659         return cmpTechnologyManager.CanResearch(data.tech);
662 // Returns technologies that are being actively researched, along with
663 // which entity is researching them and how far along the research is.
664 GuiInterface.prototype.GetStartedResearch = function(player)
666         let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
667         if (!cmpTechnologyManager)
668                 return {};
670         let ret = {};
671         for (let tech in cmpTechnologyManager.GetTechsStarted())
672         {
673                 ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
674                 let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
675                 if (cmpProductionQueue)
676                         ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
677                 else
678                         ret[tech].progress = 0;
679         }
680         return ret;
683 // Returns the battle state of the player.
684 GuiInterface.prototype.GetBattleState = function(player)
686         let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
688         if (!cmpBattleDetection)
689                 return false;
691         return cmpBattleDetection.GetState();
694 // Returns a list of ongoing attacks against the player.
695 GuiInterface.prototype.GetIncomingAttacks = function(player)
697         return QueryPlayerIDInterface(player, IID_AttackDetection).GetIncomingAttacks();
700 // Used to show a red square over GUI elements you can't yet afford.
701 GuiInterface.prototype.GetNeededResources = function(player, data)
703         return QueryPlayerIDInterface(data.player || player).GetNeededResources(data.cost);
707  * Add a timed notification.
708  * Warning: timed notifacations are serialised
709  * (to also display them on saved games or after a rejoin)
710  * so they should allways be added and deleted in a deterministic way.
711  */
712 GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000)
714         let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
715         notification.endTime = duration + cmpTimer.GetTime();
716         notification.id = ++this.timeNotificationID;
718         // Let all players and observers receive the notification by default
719         if (notification.players == undefined)
720         {
721                 let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
722                 let numPlayers = cmpPlayerManager.GetNumPlayers();
723                 notification.players = [-1];
724                 for (let i = 1; i < numPlayers; ++i)
725                         notification.players.push(i);
726         }
727         this.timeNotifications.push(notification);
728         this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime);
730         cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID);
732         return this.timeNotificationID;
735 GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
737         this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID);
740 GuiInterface.prototype.GetTimeNotifications = function(player)
742         let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
743         // filter on players and time, since the delete timer might be executed with a delay
744         return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time);
747 GuiInterface.prototype.PushNotification = function(notification)
749         if (!notification.type || notification.type == "text")
750                 this.AddTimeNotification(notification);
751         else
752                 this.notifications.push(notification);
755 GuiInterface.prototype.GetNotifications = function()
757         let n = this.notifications;
758         this.notifications = [];
759         return n;
762 GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
764         return QueryPlayerIDInterface(wantedPlayer).GetFormations();
767 GuiInterface.prototype.GetFormationRequirements = function(player, data)
769         return GetFormationRequirements(data.formationTemplate);
772 GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
774         return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
777 GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
779         let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
780         let template = cmpTemplateManager.GetTemplate(data.templateName);
782         if (!template || !template.Formation)
783                 return {};
785         return {
786                 "name": template.Formation.FormationName,
787                 "tooltip": template.Formation.DisabledTooltip || "",
788                 "icon": template.Formation.Icon
789         };
792 GuiInterface.prototype.IsFormationSelected = function(player, data)
794         for each (let ent in data.ents)
795         {
796                 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
797                 // GetLastFormationName is named in a strange way as it (also) is
798                 // the value of the current formation (see Formation.js LoadFormation)
799                 if (cmpUnitAI && cmpUnitAI.GetLastFormationTemplate() == data.formationTemplate)
800                         return true;
801         }
802         return false;
805 GuiInterface.prototype.IsStanceSelected = function(player, data)
807         for each (let ent in data.ents)
808         {
809                 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
810                 if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance)
811                         return true;
812         }
813         return false;
816 GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd)
818         let buildableEnts = [];
819         for each (let ent in cmd.entities)
820         {
821                 let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
822                 if (!cmpBuilder)
823                         continue;
825                 for (let building of cmpBuilder.GetEntitiesList())
826                         if (buildableEnts.indexOf(building) == -1)
827                                 buildableEnts.push(building);
828         }
829         return buildableEnts;
832 GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
834         let playerColors = {}; // cache of owner -> color map
836         for each (let ent in cmd.entities)
837         {
838                 let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
839                 if (!cmpSelectable)
840                         continue;
842                 // Find the entity's owner's color:
843                 let owner = -1;
844                 let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
845                 if (cmpOwnership)
846                         owner = cmpOwnership.GetOwner();
848                 let color = playerColors[owner];
849                 if (!color)
850                 {
851                         color = { "r":1, "g":1, "b":1 };
852                         let cmpPlayer = QueryPlayerIDInterface(owner);
853                         if (cmpPlayer)
854                                 color = cmpPlayer.GetColor();
855                         playerColors[owner] = color;
856                 }
858                 cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected);
859         }
862 GuiInterface.prototype.GetEntitiesWithStatusBars = function()
864         return [...this.entsWithAuraAndStatusBars];
867 GuiInterface.prototype.SetStatusBars = function(player, cmd)
869         let affectedEnts = new Set();
870         for (let ent of cmd.entities)
871         {
872                 let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
873                 if (!cmpStatusBars)
874                         continue;
875                 cmpStatusBars.SetEnabled(cmd.enabled);
877                 let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
878                 if (!cmpAuras)
879                         continue;
881                 for (let name of cmpAuras.GetAuraNames())
882                 {
883                         if (!cmpAuras.GetOverlayIcon(name))
884                                 continue;
885                         for (let e of cmpAuras.GetAffectedEntities(name))
886                                 affectedEnts.add(e);
887                         if (cmd.enabled)
888                                 this.entsWithAuraAndStatusBars.add(ent);
889                         else
890                                 this.entsWithAuraAndStatusBars.delete(ent);
891                 }
892         }
894         for (let ent of affectedEnts)
895         {
896                 let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
897                 if (cmpStatusBars)
898                         cmpStatusBars.RegenerateSprites();
899         }
902 GuiInterface.prototype.GetPlayerEntities = function(player)
904         return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player);
907 GuiInterface.prototype.GetNonGaiaEntities = function()
909     return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
913  * Displays the rally points of a given list of entities (carried in cmd.entities).
915  * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
916  * be rendered, in order to support instantaneously rendering a rally point marker at a specified location
917  * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
918  * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
919  * RallyPoint component.
920  */
921 GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
923         let cmpPlayer = QueryPlayerIDInterface(player);
925         // If there are some rally points already displayed, first hide them
926         for each (let ent in this.entsRallyPointsDisplayed)
927         {
928                 let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
929                 if (cmpRallyPointRenderer)
930                         cmpRallyPointRenderer.SetDisplayed(false);
931         }
933         this.entsRallyPointsDisplayed = [];
935         // Show the rally points for the passed entities
936         for each (let ent in cmd.entities)
937         {
938                 let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
939                 if (!cmpRallyPointRenderer)
940                         continue;
942                 // entity must have a rally point component to display a rally point marker
943                 // (regardless of whether cmd specifies a custom location)
944                 let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
945                 if (!cmpRallyPoint)
946                         continue;
948                 // Verify the owner
949                 let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
950                 if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
951                         if (!cmpOwnership || cmpOwnership.GetOwner() != player)
952                                 continue;
954                 // If the command was passed an explicit position, use that and
955                 // override the real rally point position; otherwise use the real position
956                 let pos;
957                 if (cmd.x && cmd.z)
958                         pos = cmd;
959                 else
960                         pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set
962                 if (pos)
963                 {
964                         // Only update the position if we changed it (cmd.queued is set)
965                         if ("queued" in cmd)
966                                 if (cmd.queued == true)
967                                         cmpRallyPointRenderer.AddPosition({ 'x': pos.x, 'y': pos.z }); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z
968                                 else
969                                         cmpRallyPointRenderer.SetPosition({ 'x': pos.x, 'y': pos.z }); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z
971                         // rebuild the renderer when not set (when reading saved game or in case of building update)
972                         else if (!cmpRallyPointRenderer.IsSet())
973                                 for each (let posi in cmpRallyPoint.GetPositions())
974                                         cmpRallyPointRenderer.AddPosition({ 'x': posi.x, 'y': posi.z });
976                         cmpRallyPointRenderer.SetDisplayed(true);
978                         // remember which entities have their rally points displayed so we can hide them again
979                         this.entsRallyPointsDisplayed.push(ent);
980                 }
981         }
985  * Display the building placement preview.
986  * cmd.template is the name of the entity template, or "" to disable the preview.
987  * cmd.x, cmd.z, cmd.angle give the location.
989  * Returns result object from CheckPlacement:
990  *      {
991  *              "success":             true iff the placement is valid, else false
992  *              "message":             message to display in UI for invalid placement, else ""
993  *              "parameters":          parameters to use in the message
994  *              "translateMessage":    localisation info
995  *              "translateParameters": localisation info
996  *              "pluralMessage":       we might return a plural translation instead (optional)
997  *              "pluralCount":         localisation info (optional)
998  *  }
999  */
1000 GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
1002         let result = {
1003                 "success": false,
1004                 "message": "",
1005                 "parameters": {},
1006                 "translateMessage": false,
1007                 "translateParameters": [],
1008         };
1010         // See if we're changing template
1011         if (!this.placementEntity || this.placementEntity[0] != cmd.template)
1012         {
1013                 // Destroy the old preview if there was one
1014                 if (this.placementEntity)
1015                         Engine.DestroyEntity(this.placementEntity[1]);
1017                 // Load the new template
1018                 if (cmd.template == "")
1019                         this.placementEntity = undefined;
1020                 else
1021                         this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
1022         }
1024         if (this.placementEntity)
1025         {
1026                 let ent = this.placementEntity[1];
1028                 // Move the preview into the right location
1029                 let pos = Engine.QueryInterface(ent, IID_Position);
1030                 if (pos)
1031                 {
1032                         pos.JumpTo(cmd.x, cmd.z);
1033                         pos.SetYRotation(cmd.angle);
1034                 }
1036                 let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
1037                 cmpOwnership.SetOwner(player);
1039                 // Check whether building placement is valid
1040                 let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
1041                 if (!cmpBuildRestrictions)
1042                         error("cmpBuildRestrictions not defined");
1043                 else
1044                         result = cmpBuildRestrictions.CheckPlacement();
1046                 // Set it to a red shade if this is an invalid location
1047                 let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
1048                 if (cmpVisual)
1049                 {
1050                         if (cmd.actorSeed !== undefined)
1051                                 cmpVisual.SetActorSeed(cmd.actorSeed);
1053                         if (!result.success)
1054                                 cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
1055                         else
1056                                 cmpVisual.SetShadingColor(1, 1, 1, 1);
1057                 }
1058         }
1060         return result;
1064  * Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not
1065  * specified. Returns an object with information about the list of entities that need to be newly constructed to complete
1066  * at least a part of the wall, or false if there are entities required to build at least part of the wall but none of
1067  * them can be validly constructed.
1069  * It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
1070  * another depending on things like snapping and whether some of the entities inside them can be validly positioned.
1071  * We have:
1072  *    - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
1073  *      entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
1074  *      to preview the completed tower on top of its foundation.
1076  *    - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
1077  *      any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
1078  *      towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
1079  *      snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
1080  *      constructed.
1082  *    - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
1083  *      as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
1084  *      e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
1085  *      constructed but come after said first invalid entity are also truncated away.
1087  * With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there
1088  * were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
1089  * case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
1090  * argument (see below). Otherwise, it will return an object with the following information:
1092  * result: {
1093  *   'startSnappedEnt':   ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
1094  *   'endSnappedEnt':     ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
1095  *                        can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
1096  *                        but the wall construction was truncated before we could reach it, it won't be set here. Currently only
1097  *                        supports towers.
1098  *   'pieces':            Array with the following data for each of the entities in the third list:
1099  *    [{
1100  *       'template':      Template name of the entity.
1101  *       'x':             X coordinate of the entity's position.
1102  *       'z':             Z coordinate of the entity's position.
1103  *       'angle':         Rotation around the Y axis of the entity (in radians).
1104  *     },
1105  *     ...]
1106  *   'cost': {            The total cost required for constructing all the pieces as listed above.
1107  *     'food': ...,
1108  *     'wood': ...,
1109  *     'stone': ...,
1110  *     'metal': ...,
1111  *     'population': ...,
1112  *     'populationBonus': ...,
1113  *   }
1114  * }
1116  * @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
1117  * @param cmd.start Starting point of the wall segment being created.
1118  * @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
1119  *                 the starting point of the wall is available at this time (e.g. while the player is still in the process
1120  *                 of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
1121  *                 previewed.
1122  * @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
1123  */
1124 GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
1126         let wallSet = cmd.wallSet;
1128         let start = {
1129                 "pos": cmd.start,
1130                 "angle": 0,
1131                 "snapped": false,                       // did the start position snap to anything?
1132                 "snappedEnt": INVALID_ENTITY,           // if we snapped, was it to an entity? if yes, holds that entity's ID
1133         };
1135         let end = {
1136                 "pos": cmd.end,
1137                 "angle": 0,
1138                 "snapped": false,                       // did the start position snap to anything?
1139                 "snappedEnt": INVALID_ENTITY,           // if we snapped, was it to an entity? if yes, holds that entity's ID
1140         };
1142         // --------------------------------------------------------------------------------
1143         // do some entity cache management and check for snapping
1145         if (!this.placementWallEntities)
1146                 this.placementWallEntities = {};
1148         if (!wallSet)
1149         {
1150                 // we're clearing the preview, clear the entity cache and bail
1151                 for (let tpl in this.placementWallEntities)
1152                 {
1153                         for each (let ent in this.placementWallEntities[tpl].entities)
1154                                 Engine.DestroyEntity(ent);
1156                         this.placementWallEntities[tpl].numUsed = 0;
1157                         this.placementWallEntities[tpl].entities = [];
1158                         // keep template data around
1159                 }
1161                 return false;
1162         }
1163         else
1164         {
1165                 // Move all existing cached entities outside of the world and reset their use count
1166                 for (let tpl in this.placementWallEntities)
1167                 {
1168                         for each (let ent in this.placementWallEntities[tpl].entities)
1169                         {
1170                                 let pos = Engine.QueryInterface(ent, IID_Position);
1171                                 if (pos)
1172                                         pos.MoveOutOfWorld();
1173                         }
1175                         this.placementWallEntities[tpl].numUsed = 0;
1176                 }
1178                 // Create cache entries for templates we haven't seen before
1179                 for each (let tpl in wallSet.templates)
1180                 {
1181                         if (!(tpl in this.placementWallEntities))
1182                         {
1183                                 this.placementWallEntities[tpl] = {
1184                                         "numUsed": 0,
1185                                         "entities": [],
1186                                         "templateData": this.GetTemplateData(player, tpl),
1187                                 };
1189                                 // ensure that the loaded template data contains a wallPiece component
1190                                 if (!this.placementWallEntities[tpl].templateData.wallPiece)
1191                                 {
1192                                         error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
1193                                         return false;
1194                                 }
1195                         }
1196                 }
1197         }
1199         // prevent division by zero errors further on if the start and end positions are the same
1200         if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
1201                 end.pos = undefined;
1203         // See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list
1204         // of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
1205         // data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
1206         if (cmd.snapEntities)
1207         {
1208                 let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error
1209                 let startSnapData = this.GetFoundationSnapData(player, {
1210                         "x": start.pos.x,
1211                         "z": start.pos.z,
1212                         "template": wallSet.templates.tower,
1213                         "snapEntities": cmd.snapEntities,
1214                         "snapRadius": snapRadius,
1215                 });
1217                 if (startSnapData)
1218                 {
1219                         start.pos.x = startSnapData.x;
1220                         start.pos.z = startSnapData.z;
1221                         start.angle = startSnapData.angle;
1222                         start.snapped = true;
1224                         if (startSnapData.ent)
1225                                 start.snappedEnt = startSnapData.ent;
1226                 }
1228                 if (end.pos)
1229                 {
1230                         let endSnapData = this.GetFoundationSnapData(player, {
1231                                 "x": end.pos.x,
1232                                 "z": end.pos.z,
1233                                 "template": wallSet.templates.tower,
1234                                 "snapEntities": cmd.snapEntities,
1235                                 "snapRadius": snapRadius,
1236                         });
1238                         if (endSnapData)
1239                         {
1240                                 end.pos.x = endSnapData.x;
1241                                 end.pos.z = endSnapData.z;
1242                                 end.angle = endSnapData.angle;
1243                                 end.snapped = true;
1245                                 if (endSnapData.ent)
1246                                         end.snappedEnt = endSnapData.ent;
1247                         }
1248                 }
1249         }
1251         // clear the single-building preview entity (we'll be rolling our own)
1252         this.SetBuildingPlacementPreview(player, { "template": "" });
1254         // --------------------------------------------------------------------------------
1255         // calculate wall placement and position preview entities
1257         let result = {
1258                 "pieces": [],
1259                 "cost": { "food": 0, "wood": 0, "stone": 0, "metal": 0, "population": 0, "populationBonus": 0, "time": 0 },
1260         };
1262         let previewEntities = [];
1263         if (end.pos)
1264                 previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js
1266         // For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
1267         // otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
1268         // an issue, because all preview entities have their obstruction components deactivated, meaning that their
1269         // obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
1270         // entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
1272         // Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
1273         // flag set), which is what we want. The only exception to this is when snapping to existing towers (or
1274         // foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
1275         // existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
1276         // we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
1277         // that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
1278         // assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
1280         // Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
1281         // constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
1282         // by the foundation it snaps to.
1284         if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
1285         {
1286                 let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
1287                 if (previewEntities.length > 0 && startEntObstruction)
1288                         previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
1290                 // if we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group
1291                 let startEntState = this.GetEntityState(player, start.snappedEnt);
1292                 if (startEntState.foundation)
1293                 {
1294                         let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
1295                         if (cmpPosition)
1296                                 previewEntities.unshift({
1297                                         "template": wallSet.templates.tower,
1298                                         "pos": start.pos,
1299                                         "angle": cmpPosition.GetRotation().y,
1300                                         "controlGroups": [(startEntObstruction ? startEntObstruction.GetControlGroup() : undefined)],
1301                                         "excludeFromResult": true, // preview only, must not appear in the result
1302                                 });
1303                 }
1304         }
1305         else
1306         {
1307                 // Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
1308                 // when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
1309                 // wall piece.
1311                 // To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
1312                 // build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
1313                 // foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
1314                 // of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
1315                 // the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
1316                 // onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
1317                 // which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
1318                 // the foundation's angle.
1320                 // The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
1321                 // the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
1322                 previewEntities.unshift({
1323                         "template": wallSet.templates.tower,
1324                         "pos": start.pos,
1325                         "angle": (previewEntities.length > 0 ? previewEntities[0].angle : this.placementWallLastAngle)
1326                 });
1327         }
1329         if (end.pos)
1330         {
1331                 // Analogous to the starting side case above
1332                 if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
1333                 {
1334                         let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
1336                         // Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
1337                         // same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
1338                         // expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
1339                         // the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
1340                         // '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
1341                         if (previewEntities.length > 0 && endEntObstruction)
1342                         {
1343                                 previewEntities[previewEntities.length-1].controlGroups = (previewEntities[previewEntities.length-1].controlGroups || []);
1344                                 previewEntities[previewEntities.length-1].controlGroups.push(endEntObstruction.GetControlGroup());
1345                         }
1347                         // if we're snapping to a foundation, add an extra preview tower and also set it to the same control group
1348                         let endEntState = this.GetEntityState(player, end.snappedEnt);
1349                         if (endEntState.foundation)
1350                         {
1351                                 let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
1352                                 if (cmpPosition)
1353                                         previewEntities.push({
1354                                                 "template": wallSet.templates.tower,
1355                                                 "pos": end.pos,
1356                                                 "angle": cmpPosition.GetRotation().y,
1357                                                 "controlGroups": [(endEntObstruction ? endEntObstruction.GetControlGroup() : undefined)],
1358                                                 "excludeFromResult": true
1359                                         });
1360                         }
1361                 }
1362                 else
1363                         previewEntities.push({
1364                                 "template": wallSet.templates.tower,
1365                                 "pos": end.pos,
1366                                 "angle": (previewEntities.length > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle)
1367                         });
1368         }
1370         let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
1371         if (!cmpTerrain)
1372         {
1373                 error("[SetWallPlacementPreview] System Terrain component not found");
1374                 return false;
1375         }
1377         let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
1378         if (!cmpRangeManager)
1379         {
1380                 error("[SetWallPlacementPreview] System RangeManager component not found");
1381                 return false;
1382         }
1384         // Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
1385         // to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
1386         // but cannot validly be, constructed). See method-level documentation for more details.
1388         let allPiecesValid = true;
1389         let numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity
1391         for (let i = 0; i < previewEntities.length; ++i)
1392         {
1393                 let entInfo = previewEntities[i];
1395                 let ent = null;
1396                 let tpl = entInfo.template;
1397                 let tplData = this.placementWallEntities[tpl].templateData;
1398                 let entPool = this.placementWallEntities[tpl];
1400                 if (entPool.numUsed >= entPool.entities.length)
1401                 {
1402                         // allocate new entity
1403                         ent = Engine.AddLocalEntity("preview|" + tpl);
1404                         entPool.entities.push(ent);
1405                 }
1406                 else
1407                          // reuse an existing one
1408                         ent = entPool.entities[entPool.numUsed];
1410                 if (!ent)
1411                 {
1412                         error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
1413                         continue;
1414                 }
1416                 // move piece to right location
1417                 // TODO: consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities
1418                 let cmpPosition = Engine.QueryInterface(ent, IID_Position);
1419                 if (cmpPosition)
1420                 {
1421                         cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
1422                         cmpPosition.SetYRotation(entInfo.angle);
1424                         // if this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces
1425                         if (tpl === wallSet.templates.tower)
1426                         {
1427                                 let terrainGroundPrev = null;
1428                                 let terrainGroundNext = null;
1430                                 if (i > 0)
1431                                         terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i-1].pos.x, previewEntities[i-1].pos.z);
1433                                 if (i < previewEntities.length - 1)
1434                                         terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i+1].pos.x, previewEntities[i+1].pos.z);
1436                                 if (terrainGroundPrev != null || terrainGroundNext != null)
1437                                 {
1438                                         let targetY = Math.max(terrainGroundPrev, terrainGroundNext);
1439                                         cmpPosition.SetHeightFixed(targetY);
1440                                 }
1441                         }
1442                 }
1444                 let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
1445                 if (!cmpObstruction)
1446                 {
1447                         error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
1448                         continue;
1449                 }
1451                 // Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
1452                 // more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
1453                 // first-come first-served basis; the first value in the array is always assigned as the primary control group, and
1454                 // any second value as the secondary control group.
1456                 // By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
1457                 // reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
1458                 // reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
1459                 // once snapped to.
1461                 let primaryControlGroup = ent;
1462                 let secondaryControlGroup = INVALID_ENTITY;
1464                 if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
1465                 {
1466                         if (entInfo.controlGroups.length > 2)
1467                         {
1468                                 error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
1469                                 break;
1470                         }
1472                         primaryControlGroup = entInfo.controlGroups[0];
1473                         if (entInfo.controlGroups.length > 1)
1474                                 secondaryControlGroup = entInfo.controlGroups[1];
1475                 }
1477                 cmpObstruction.SetControlGroup(primaryControlGroup);
1478                 cmpObstruction.SetControlGroup2(secondaryControlGroup);
1480                 // check whether this wall piece can be validly positioned here
1481                 let validPlacement = false;
1483                 let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
1484                 cmpOwnership.SetOwner(player);
1486                 // Check whether it's in a visible or fogged region
1487                 // TODO: should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta
1488                 let visible = (cmpRangeManager.GetLosVisibility(ent, player) != "hidden");
1489                 if (visible)
1490                 {
1491                         let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
1492                         if (!cmpBuildRestrictions)
1493                         {
1494                                 error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
1495                                 continue;
1496                         }
1498                         // TODO: Handle results of CheckPlacement
1499                         validPlacement = (cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success);
1501                         // If a wall piece has two control groups, it's likely a segment that spans
1502                         // between two existing towers. To avoid placing a duplicate wall segment,
1503                         // check for collisions with entities that share both control groups.
1504                         if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1)
1505                                 validPlacement = cmpObstruction.CheckDuplicateFoundation();
1506                 }
1508                 allPiecesValid = allPiecesValid && validPlacement;
1510                 // The requirement below that all pieces so far have to have valid positions, rather than only this single one,
1511                 // ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
1512                 // for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
1513                 // through and past an existing building).
1515                 // Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
1516                 // on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
1518                 if (!entInfo.excludeFromResult)
1519                         ++numRequiredPieces;
1521                 if (allPiecesValid && !entInfo.excludeFromResult)
1522                 {
1523                         result.pieces.push({
1524                                 "template": tpl,
1525                                 "x": entInfo.pos.x,
1526                                 "z": entInfo.pos.z,
1527                                 "angle": entInfo.angle,
1528                         });
1529                         this.placementWallLastAngle = entInfo.angle;
1531                         // grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
1532                         // copied over, so we need to fetch it from the template instead).
1533                         // TODO: we should really use a Cost object or at least some utility functions for this, this is mindless
1534                         // boilerplate that's probably duplicated in tons of places.
1535                         result.cost.food += tplData.cost.food;
1536                         result.cost.wood += tplData.cost.wood;
1537                         result.cost.stone += tplData.cost.stone;
1538                         result.cost.metal += tplData.cost.metal;
1539                         result.cost.population += tplData.cost.population;
1540                         result.cost.populationBonus += tplData.cost.populationBonus;
1541                         result.cost.time += tplData.cost.time;
1542                 }
1544                 let canAfford = true;
1545                 let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
1546                 if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
1547                         canAfford = false;
1549                 let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
1550                 if (cmpVisual)
1551                 {
1552                         if (!allPiecesValid || !canAfford)
1553                                 cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
1554                         else
1555                                 cmpVisual.SetShadingColor(1, 1, 1, 1);
1556                 }
1558                 ++entPool.numUsed;
1559         }
1561         // If any were entities required to build the wall, but none of them could be validly positioned, return failure
1562         // (see method-level documentation).
1563         if (numRequiredPieces > 0 && result.pieces.length == 0)
1564                 return false;
1566         if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
1567                 result.startSnappedEnt = start.snappedEnt;
1569         // We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
1570         // i.e. are included in result.pieces (see docs for the result object).
1571         if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
1572                 result.endSnappedEnt = end.snappedEnt;
1574         return result;
1578  * Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
1579  * it to (if necessary/useful).
1581  * @param data.x            The X position of the foundation to snap.
1582  * @param data.z            The Z position of the foundation to snap.
1583  * @param data.template     The template to get the foundation snapping data for.
1584  * @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
1585  *                            around the entity. Only takes effect when used in conjunction with data.snapRadius.
1586  *                          When this option is used and the foundation is found to snap to one of the entities passed in this list
1587  *                            (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
1588  *                            holding the ID of the entity that was snapped to.
1589  * @param data.snapRadius   Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
1590  *                            {data.x, data.z} must be located within to have it snap to that entity.
1591  */
1592 GuiInterface.prototype.GetFoundationSnapData = function(player, data)
1594         let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template);
1595         if (!template)
1596         {
1597                 warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
1598                 return false;
1599         }
1601         if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
1602         {
1603                 // see if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest
1604                 // (TODO: break unlikely ties by choosing the lowest entity ID)
1606                 let minDist2 = -1;
1607                 let minDistEntitySnapData = null;
1608                 let radius2 = data.snapRadius * data.snapRadius;
1610                 for each (let ent in data.snapEntities)
1611                 {
1612                         let cmpPosition = Engine.QueryInterface(ent, IID_Position);
1613                         if (!cmpPosition || !cmpPosition.IsInWorld())
1614                                 continue;
1616                         let pos = cmpPosition.GetPosition();
1617                         let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
1618                         if (dist2 > radius2)
1619                                 continue;
1621                         if (minDist2 < 0 || dist2 < minDist2)
1622                         {
1623                                 minDist2 = dist2;
1624                                 minDistEntitySnapData = {
1625                                                 "x": pos.x,
1626                                                 "z": pos.z,
1627                                                 "angle": cmpPosition.GetRotation().y,
1628                                                 "ent": ent
1629                                 };
1630                         }
1631                 }
1633                 if (minDistEntitySnapData != null)
1634                         return minDistEntitySnapData;
1635         }
1637         if (template.BuildRestrictions.Category == "Dock")
1638         {
1639                 let angle = GetDockAngle(template, data.x, data.z);
1640                 if (angle !== undefined)
1641                         return {
1642                                 "x": data.x,
1643                                 "z": data.z,
1644                                 "angle": angle
1645                         };
1646         }
1648         return false;
1651 GuiInterface.prototype.PlaySound = function(player, data)
1653         if (!data.entity)
1654                 return;
1656         PlaySound(data.name, data.entity);
1660  * Find any idle units.
1662  * @param data.idleClasses              Array of class names to include.
1663  * @param data.prevUnit         The previous idle unit, if calling a second time to iterate through units.  May be left undefined.
1664  * @param data.limit                    The number of idle units to return.  May be left undefined (will return all idle units).
1665  * @param data.excludeUnits     Array of units to exclude.
1667  * Returns an array of idle units.
1668  * If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class.
1669  */
1670 GuiInterface.prototype.FindIdleUnits = function(player, data)
1672         let idleUnits = [];
1673         // The general case is that only the 'first' idle unit is required; filtering would examine every unit.
1674         // This loop imitates a grouping/aggregation on the first matching idle class.
1675         let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
1676         for (let entity of cmpRangeManager.GetEntitiesByPlayer(player))
1677         {
1678                 let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits);
1679                 if (!filtered.idle)
1680                         continue;
1682                 // If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any.
1683                 // By adding to the 'end', there is no pause if the series of units loops.
1684                 var bucket = filtered.bucket;
1685                 if(bucket == 0 && data.prevUnit && entity <= data.prevUnit)
1686                         bucket = data.idleClasses.length;
1688                 if (!idleUnits[bucket])
1689                         idleUnits[bucket] = [];
1690                 idleUnits[bucket].push(entity);
1692                 // If enough units have been collected in the first bucket, go ahead and return them.
1693                 if (data.limit && bucket == 0 && idleUnits[0].length == data.limit)
1694                         return idleUnits[0];
1695         }
1697         let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []);
1698         if (data.limit && reduced.length > data.limit)
1699                 return reduced.slice(0, data.limit);
1701         return reduced;
1705  * Discover if the player has idle units.
1707  * @param data.idleClasses      Array of class names to include.
1708  * @param data.excludeUnits     Array of units to exclude.
1710  * Returns a boolean of whether the player has any idle units
1711  */
1712 GuiInterface.prototype.HasIdleUnits = function(player, data)
1714         let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
1715         return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle);
1719  * Whether to filter an idle unit
1721  * @param unit                  The unit to filter.
1722  * @param idleclasses   Array of class names to include.
1723  * @param excludeUnits  Array of units to exclude.
1725  * Returns an object with the following fields:
1726  *      - idle - true if the unit is considered idle by the filter, false otherwise.
1727  *      - bucket - if idle, set to the index of the first matching idle class, undefined otherwise.
1728  */
1729 GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits)
1731         let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
1732         if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned())
1733                 return { "idle": false };
1735         let cmpIdentity = Engine.QueryInterface(unit, IID_Identity);
1736         if(!cmpIdentity)
1737                 return { "idle": false };
1739         let bucket = idleClasses.findIndex(elem => cmpIdentity.HasClass(elem));
1740         if (bucket == -1 || excludeUnits.indexOf(unit) > -1)
1741                 return { "idle": false };
1743         return { "idle": true, "bucket": bucket };
1746 GuiInterface.prototype.GetTradingRouteGain = function(player, data)
1748         if (!data.firstMarket || !data.secondMarket)
1749                 return null;
1751         return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template);
1754 GuiInterface.prototype.GetTradingDetails = function(player, data)
1756         let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
1757         if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
1758                 return null;
1760         let firstMarket = cmpEntityTrader.GetFirstMarket();
1761         let secondMarket = cmpEntityTrader.GetSecondMarket();
1762         let result = null;
1763         if (data.target === firstMarket)
1764         {
1765                 result = {
1766                         "type": "is first",
1767                         "hasBothMarkets": cmpEntityTrader.HasBothMarkets()
1768                 };
1769                 if (cmpEntityTrader.HasBothMarkets())
1770                         result.gain = cmpEntityTrader.GetGoods().amount;
1771         }
1772         else if (data.target === secondMarket)
1773         {
1774                 result = {
1775                         "type": "is second",
1776                         "gain": cmpEntityTrader.GetGoods().amount,
1777                 };
1778         }
1779         else if (!firstMarket)
1780         {
1781                 result = { "type": "set first" };
1782         }
1783         else if (!secondMarket)
1784         {
1785                 result = {
1786                         "type": "set second",
1787                         "gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
1788                 };
1789         }
1790         else
1791         {
1792                 // Else both markets are not null and target is different from them
1793                 result = { "type": "set first" };
1794         }
1795         return result;
1798 GuiInterface.prototype.CanCapture = function(player, data)
1800         let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
1801         if (!cmpAttack)
1802                 return false;
1804         let owner = QueryOwnerInterface(data.entity).GetPlayerID();
1806         let cmpCapturable = QueryMiragedInterface(data.target, IID_Capturable);
1807         if (cmpCapturable && cmpCapturable.CanCapture(owner) && cmpAttack.GetAttackTypes().indexOf("Capture") != -1)
1808                 return cmpAttack.CanAttack(data.target);
1810         return false;
1813 GuiInterface.prototype.CanAttack = function(player, data)
1815         let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
1816         if (!cmpAttack)
1817                 return false;
1819         let cmpEntityPlayer = QueryOwnerInterface(data.entity, IID_Player);
1820         let cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player);
1821         if (!cmpEntityPlayer || !cmpTargetPlayer)
1822                 return false;
1824         // if the owner is an enemy, it's up to the attack component to decide
1825         if (cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()))
1826                 return cmpAttack.CanAttack(data.target);
1828         return false;
1832  * Returns batch build time.
1833  */
1834 GuiInterface.prototype.GetBatchTime = function(player, data)
1836         let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
1837         if (!cmpProductionQueue)
1838                 return 0;
1840         return cmpProductionQueue.GetBatchTime(data.batchSize);
1843 GuiInterface.prototype.IsMapRevealed = function(player)
1845         return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player);
1848 GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
1850         Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled);
1853 GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled)
1855         Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled);
1858 GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
1860         Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled);
1863 GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
1865         for each (let ent in data.entities)
1866         {
1867                 let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
1868                 if (cmpUnitMotion)
1869                         cmpUnitMotion.SetDebugOverlay(data.enabled);
1870         }
1873 GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
1875         Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled);
1878 GuiInterface.prototype.GetTraderNumber = function(player)
1880         let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
1881         let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader));
1883         let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
1884         let shipTrader = { "total": 0, "trading": 0 };
1886         for each (let ent in traders)
1887         {
1888                 let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
1889                 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
1890                 if (!cmpIdentity || !cmpUnitAI)
1891                         continue;
1893                 if (cmpIdentity.HasClass("Ship"))
1894                 {
1895                         ++shipTrader.total;
1896                         if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
1897                                 ++shipTrader.trading;
1898                 }
1899                 else
1900                 {
1901                         ++landTrader.total;
1902                         if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
1903                                 ++landTrader.trading;
1904                         if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
1905                         {
1906                                 let holder = cmpUnitAI.order.data.target;
1907                                 let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
1908                                 if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
1909                                         ++landTrader.garrisoned;
1910                         }
1911                 }
1912         }
1914         return { "landTrader": landTrader, "shipTrader": shipTrader };
1917 GuiInterface.prototype.GetTradingGoods = function(player)
1919         return QueryPlayerIDInterface(player).GetTradingGoods();
1922 GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
1924         this.renamedEntities.push(msg);
1927 // List the GuiInterface functions that can be safely called by GUI scripts.
1928 // (GUI scripts are non-deterministic and untrusted, so these functions must be
1929 // appropriately careful. They are called with a first argument "player", which is
1930 // trusted and indicates the player associated with the current client; no data should
1931 // be returned unless this player is meant to be able to see it.)
1932 let exposedFunctions = {
1934         "GetSimulationState": 1,
1935         "GetExtendedSimulationState": 1,
1936         "GetRenamedEntities": 1,
1937         "ClearRenamedEntities": 1,
1938         "GetEntityState": 1,
1939         "GetExtendedEntityState": 1,
1940         "GetAverageRangeForBuildings": 1,
1941         "GetTemplateData": 1,
1942         "GetTechnologyData": 1,
1943         "IsTechnologyResearched": 1,
1944         "CheckTechnologyRequirements": 1,
1945         "GetStartedResearch": 1,
1946         "GetBattleState": 1,
1947         "GetIncomingAttacks": 1,
1948         "GetNeededResources": 1,
1949         "GetNotifications": 1,
1950         "GetTimeNotifications": 1,
1952         "GetAvailableFormations": 1,
1953         "GetFormationRequirements": 1,
1954         "CanMoveEntsIntoFormation": 1,
1955         "IsFormationSelected": 1,
1956         "GetFormationInfoFromTemplate": 1,
1957         "IsStanceSelected": 1,
1959         "SetSelectionHighlight": 1,
1960         "GetAllBuildableEntities": 1,
1961         "SetStatusBars": 1,
1962         "GetPlayerEntities": 1,
1963         "GetNonGaiaEntities": 1,
1964         "DisplayRallyPoint": 1,
1965         "SetBuildingPlacementPreview": 1,
1966         "SetWallPlacementPreview": 1,
1967         "GetFoundationSnapData": 1,
1968         "PlaySound": 1,
1969         "FindIdleUnits": 1,
1970         "HasIdleUnits": 1,
1971         "GetTradingRouteGain": 1,
1972         "GetTradingDetails": 1,
1973         "CanCapture": 1,
1974         "CanAttack": 1,
1975         "GetBatchTime": 1,
1977         "IsMapRevealed": 1,
1978         "SetPathfinderDebugOverlay": 1,
1979         "SetPathfinderHierDebugOverlay": 1,
1980         "SetObstructionDebugOverlay": 1,
1981         "SetMotionDebugOverlay": 1,
1982         "SetRangeDebugOverlay": 1,
1984         "GetTraderNumber": 1,
1985         "GetTradingGoods": 1,
1988 GuiInterface.prototype.ScriptCall = function(player, name, args)
1990         if (exposedFunctions[name])
1991                 return this[name](player, args);
1992         else
1993                 throw new Error("Invalid GuiInterface Call name \""+name+"\"");
1996 Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);