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
12 "timeNotifications": this.timeNotifications,
13 "timeNotificationID": this.timeNotificationID
17 GuiInterface.prototype.Deserialize = function(data)
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();
36 this.enabledVisualRangeOverlayTypes = {};
40 * All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
41 * from GUI scripts, and executed here with arguments (player, arg).
43 * CAUTION: The input to the functions in this module is not network-synchronised, so it
44 * mustn't affect the simulation state (i.e. the data that is serialised and can affect
45 * the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
49 * Returns global information about the current game state.
50 * This is used by the GUI and also by AI scripts.
52 GuiInterface.prototype.GetSimulationState = function()
58 let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
59 for (let i = 0; i < numPlayers; ++i)
61 let cmpPlayer = QueryPlayerIDInterface(i);
62 let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits);
64 // Work out what phase we are in
66 let cmpTechnologyManager = QueryPlayerIDInterface(i, IID_TechnologyManager);
67 if (cmpTechnologyManager)
69 if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
71 else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
73 else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
77 // store player ally/neutral/enemy data as arrays
79 let mutualAllies = [];
83 for (let j = 0; j < numPlayers; ++j)
85 allies[j] = cmpPlayer.IsAlly(j);
86 mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
87 neutrals[j] = cmpPlayer.IsNeutral(j);
88 enemies[j] = cmpPlayer.IsEnemy(j);
92 "name": cmpPlayer.GetName(),
93 "civ": cmpPlayer.GetCiv(),
94 "color": cmpPlayer.GetColor(),
95 "controlsAll": cmpPlayer.CanControlAllUnits(),
96 "popCount": cmpPlayer.GetPopulationCount(),
97 "popLimit": cmpPlayer.GetPopulationLimit(),
98 "popMax": cmpPlayer.GetMaxPopulation(),
99 "panelEntities": cmpPlayer.GetPanelEntities(),
100 "resourceCounts": cmpPlayer.GetResourceCounts(),
101 "trainingBlocked": cmpPlayer.IsTrainingBlocked(),
102 "state": cmpPlayer.GetState(),
103 "team": cmpPlayer.GetTeam(),
104 "teamsLocked": cmpPlayer.GetLockTeams(),
105 "cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
106 "disabledTemplates": cmpPlayer.GetDisabledTemplates(),
107 "disabledTechnologies": cmpPlayer.GetDisabledTechnologies(),
108 "hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
109 "hasSharedLos": cmpPlayer.HasSharedLos(),
110 "spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(),
113 "isMutualAlly": mutualAllies,
114 "isNeutral": neutrals,
116 "entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
117 "entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
118 "entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
119 "researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
120 "researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null,
121 "researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
122 "classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
123 "typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null,
124 "canBarter": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).PlayerHasMarket(i),
125 "barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(i)
129 let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
131 ret.circularMap = cmpRangeManager.GetLosCircular();
133 let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
135 ret.mapSize = cmpTerrain.GetMapSize();
138 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
139 ret.timeElapsed = cmpTimer.GetTime();
141 // Add ceasefire info
142 let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
143 if (cmpCeasefireManager)
145 ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive();
146 ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0;
149 // Add cinema path info
150 let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager);
151 if (cmpCinemaManager)
152 ret.cinemaPlaying = cmpCinemaManager.IsPlaying();
154 // Add the game type and allied victory
155 let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
156 ret.gameType = cmpEndGameManager.GetGameType();
157 ret.alliedVictory = cmpEndGameManager.GetAlliedVictory();
159 // Add basic statistics to each player
160 for (let i = 0; i < numPlayers; ++i)
162 let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
163 if (cmpPlayerStatisticsTracker)
164 ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics();
171 * Returns global information about the current game state, plus statistics.
172 * This is used by the GUI at the end of a game, in the summary screen.
173 * Note: Amongst statistics, the team exploration map percentage is computed from
174 * scratch, so the extended simulation state should not be requested too often.
176 GuiInterface.prototype.GetExtendedSimulationState = function()
178 // Get basic simulation info
179 let ret = this.GetSimulationState();
181 // Add statistics to each player
182 let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
183 for (let i = 0; i < numPlayers; ++i)
185 let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
186 if (cmpPlayerStatisticsTracker)
187 ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences();
193 GuiInterface.prototype.GetRenamedEntities = function(player)
195 if (this.miragedEntities[player])
196 return this.renamedEntities.concat(this.miragedEntities[player]);
198 return this.renamedEntities;
201 GuiInterface.prototype.ClearRenamedEntities = function()
203 this.renamedEntities = [];
204 this.miragedEntities = [];
207 GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
209 if (!this.miragedEntities[player])
210 this.miragedEntities[player] = [];
212 this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
216 * Get common entity info, often used in the gui
218 GuiInterface.prototype.GetEntityState = function(player, ent)
220 let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
222 // All units must have a template; if not then it's a nonexistent entity id
223 let template = cmpTemplateManager.GetCurrentTemplateName(ent);
229 "template": template,
242 "isBarterMarket": null,
245 "garrisonHolder": null,
259 "resourceCarrying": null,
260 "resourceDropsite": null,
261 "resourceGatherRates": null,
262 "resourceSupply": null,
263 "resourceTrickle": null,
272 let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
276 let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
279 "rank": cmpIdentity.GetRank(),
280 "classes": cmpIdentity.GetClassesList(),
281 "visibleClasses": cmpIdentity.GetVisibleClassesList(),
282 "selectionGroupName": cmpIdentity.GetSelectionGroupName(),
283 "canDelete": !cmpIdentity.IsUndeletable()
286 let cmpPosition = Engine.QueryInterface(ent, IID_Position);
287 if (cmpPosition && cmpPosition.IsInWorld())
289 ret.position = cmpPosition.GetPosition();
290 ret.rotation = cmpPosition.GetRotation();
293 let cmpHealth = QueryMiragedInterface(ent, IID_Health);
296 ret.hitpoints = cmpHealth.GetHitpoints();
297 ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
298 ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints();
299 ret.needsHeal = !cmpHealth.IsUnhealable();
302 let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
305 ret.capturePoints = cmpCapturable.GetCapturePoints();
306 ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
309 let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
313 let cmpMarket = QueryMiragedInterface(ent, IID_Market);
316 "land": cmpMarket.HasType("land"),
317 "naval": cmpMarket.HasType("naval"),
320 let cmpPack = Engine.QueryInterface(ent, IID_Pack);
323 "packed": cmpPack.IsPacked(),
324 "progress": cmpPack.GetProgress(),
327 var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
330 "upgrades" : cmpUpgrade.GetUpgrades(),
331 "progress": cmpUpgrade.GetProgress(),
332 "template": cmpUpgrade.GetUpgradingTo()
335 let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
336 if (cmpProductionQueue)
338 "entities": cmpProductionQueue.GetEntitiesList(),
339 "technologies": cmpProductionQueue.GetTechnologiesList(),
340 "techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(),
341 "queue": cmpProductionQueue.GetQueue()
344 let cmpTrader = Engine.QueryInterface(ent, IID_Trader);
347 "goods": cmpTrader.GetGoods()
350 let cmpFogging = Engine.QueryInterface(ent, IID_Fogging);
353 "mirage": cmpFogging.IsMiraged(player) ? cmpFogging.GetMirage(player) : null
356 let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
360 "progress": cmpFoundation.GetBuildPercentage(),
361 "numBuilders": cmpFoundation.GetNumBuilders()
363 ret.buildRate = cmpFoundation.GetBuildRate();
364 ret.buildTime = cmpFoundation.GetBuildTime();
367 let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
370 ret.repairable = { "numBuilders": cmpRepairable.GetNumBuilders() };
371 ret.repairRate = cmpRepairable.GetRepairRate();
374 let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
376 ret.player = cmpOwnership.GetOwner();
378 let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
380 ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
382 let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
383 if (cmpGarrisonHolder)
384 ret.garrisonHolder = {
385 "entities": cmpGarrisonHolder.GetEntities(),
386 "buffHeal": cmpGarrisonHolder.GetHealRate(),
387 "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
388 "capacity": cmpGarrisonHolder.GetCapacity(),
389 "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
392 ret.canGarrison = !!Engine.QueryInterface(ent, IID_Garrisonable);
394 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
397 "state": cmpUnitAI.GetCurrentState(),
398 "orders": cmpUnitAI.GetOrders(),
399 "hasWorkOrders": cmpUnitAI.HasWorkOrders(),
400 "canGuard": cmpUnitAI.CanGuard(),
401 "isGuarding": cmpUnitAI.IsGuardOf(),
402 "canPatrol": cmpUnitAI.CanPatrol(),
403 "possibleStances": cmpUnitAI.GetPossibleStances(),
404 "isIdle":cmpUnitAI.IsIdle(),
407 let cmpGuard = Engine.QueryInterface(ent, IID_Guard);
410 "entities": cmpGuard.GetEntities(),
413 let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
414 if (cmpResourceGatherer)
416 ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
417 ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
420 let cmpGate = Engine.QueryInterface(ent, IID_Gate);
423 "locked": cmpGate.IsLocked(),
426 let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
429 "level": cmpAlertRaiser.GetLevel(),
430 "canIncreaseLevel": cmpAlertRaiser.CanIncreaseLevel(),
431 "hasRaisedAlert": cmpAlertRaiser.HasRaisedAlert(),
434 let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
435 ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
437 let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
440 let types = cmpAttack.GetAttackTypes();
444 for (let type of types)
446 ret.attack[type] = cmpAttack.GetAttackStrengths(type);
447 ret.attack[type].splash = cmpAttack.GetSplashDamage(type);
449 let range = cmpAttack.GetRange(type);
450 ret.attack[type].minRange = range.min;
451 ret.attack[type].maxRange = range.max;
453 let timers = cmpAttack.GetTimers(type);
454 ret.attack[type].prepareTime = timers.prepare;
455 ret.attack[type].repeatTime = timers.repeat;
457 if (type != "Ranged")
459 // not a ranged attack, set some defaults
460 ret.attack[type].elevationBonus = 0;
461 ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
465 ret.attack[type].elevationBonus = range.elevationBonus;
467 if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld())
469 // For units, take the range in front of it, no spread. So angle = 0
470 ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 0);
472 else if(cmpPosition && cmpPosition.IsInWorld())
474 // For buildings, take the average elevation around it. So angle = 2*pi
475 ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 2*Math.PI);
479 // not in world, set a default?
480 ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
485 let cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
487 ret.armour = cmpArmour.GetArmourStrengths();
489 let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
491 ret.auras = cmpAuras.GetDescriptions();
493 let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
496 "defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
497 "maxArrowCount": cmpBuildingAI.GetMaxArrowCount(),
498 "garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
499 "garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
500 "arrowCount": cmpBuildingAI.GetArrowCount()
503 let cmpDeathDamage = Engine.QueryInterface(ent, IID_DeathDamage);
505 ret.deathDamage = cmpDeathDamage.GetDeathDamageStrengths();
507 if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
508 ret.turretParent = cmpPosition.GetTurretParent();
510 let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
511 if (cmpResourceSupply)
512 ret.resourceSupply = {
513 "isInfinite": cmpResourceSupply.IsInfinite(),
514 "max": cmpResourceSupply.GetMaxAmount(),
515 "amount": cmpResourceSupply.GetCurrentAmount(),
516 "type": cmpResourceSupply.GetType(),
517 "killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
518 "maxGatherers": cmpResourceSupply.GetMaxGatherers(),
519 "numGatherers": cmpResourceSupply.GetNumGatherers()
522 let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
523 if (cmpResourceDropsite)
524 ret.resourceDropsite = {
525 "types": cmpResourceDropsite.GetTypes(),
526 "sharable": cmpResourceDropsite.IsSharable(),
527 "shared": cmpResourceDropsite.IsShared()
530 let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
533 "curr": cmpPromotion.GetCurrentXp(),
534 "req": cmpPromotion.GetRequiredXp()
537 if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket"))
538 ret.isBarterMarket = true;
540 let cmpHeal = Engine.QueryInterface(ent, IID_Heal);
543 "hp": cmpHeal.GetHP(),
544 "range": cmpHeal.GetRange().max,
545 "rate": cmpHeal.GetRate(),
546 "unhealableClasses": cmpHeal.GetUnhealableClasses(),
547 "healableClasses": cmpHeal.GetHealableClasses(),
550 let cmpLoot = Engine.QueryInterface(ent, IID_Loot);
553 ret.loot = cmpLoot.GetResources();
554 ret.loot.xp = cmpLoot.GetXp();
557 let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle);
558 if (cmpResourceTrickle)
559 ret.resourceTrickle = {
560 "interval": cmpResourceTrickle.GetTimer(),
561 "rates": cmpResourceTrickle.GetRates()
564 let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
567 "walk": cmpUnitMotion.GetWalkSpeed(),
568 "run": cmpUnitMotion.GetRunSpeed()
574 GuiInterface.prototype.GetMultipleEntityStates = function(player, ents)
576 return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) }));
579 GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
581 let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
582 let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
584 let rot = { "x": 0, "y": 0, "z": 0 };
587 "y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z),
591 let elevationBonus = cmd.elevationBonus || 0;
592 let range = cmd.range;
594 return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI);
597 GuiInterface.prototype.GetTemplateData = function(player, templateName)
599 let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
600 let template = cmpTemplateManager.GetTemplate(templateName);
605 let aurasTemplate = {};
608 return GetTemplateDataHelper(template, player, aurasTemplate, Resources, DamageTypes);
610 let auraNames = template.Auras._string.split(/\s+/);
612 for (let name of auraNames)
613 aurasTemplate[name] = AuraTemplates.Get(name);
615 return GetTemplateDataHelper(template, player, aurasTemplate, Resources, DamageTypes);
618 GuiInterface.prototype.IsTechnologyResearched = function(player, data)
623 let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager);
625 if (!cmpTechnologyManager)
628 return cmpTechnologyManager.IsTechnologyResearched(data.tech);
631 // Checks whether the requirements for this technology have been met
632 GuiInterface.prototype.CheckTechnologyRequirements = function(player, data)
634 let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager);
636 if (!cmpTechnologyManager)
639 return cmpTechnologyManager.CanResearch(data.tech);
642 // Returns technologies that are being actively researched, along with
643 // which entity is researching them and how far along the research is.
644 GuiInterface.prototype.GetStartedResearch = function(player)
646 let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
647 if (!cmpTechnologyManager)
651 for (let tech of cmpTechnologyManager.GetStartedTechs())
653 ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
654 let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
655 if (cmpProductionQueue)
656 ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
658 ret[tech].progress = 0;
663 // Returns the battle state of the player.
664 GuiInterface.prototype.GetBattleState = function(player)
666 let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
668 if (!cmpBattleDetection)
671 return cmpBattleDetection.GetState();
674 // Returns a list of ongoing attacks against the player.
675 GuiInterface.prototype.GetIncomingAttacks = function(player)
677 return QueryPlayerIDInterface(player, IID_AttackDetection).GetIncomingAttacks();
680 // Used to show a red square over GUI elements you can't yet afford.
681 GuiInterface.prototype.GetNeededResources = function(player, data)
683 return QueryPlayerIDInterface(data.player || player).GetNeededResources(data.cost);
687 * Add a timed notification.
688 * Warning: timed notifacations are serialised
689 * (to also display them on saved games or after a rejoin)
690 * so they should allways be added and deleted in a deterministic way.
692 GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000)
694 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
695 notification.endTime = duration + cmpTimer.GetTime();
696 notification.id = ++this.timeNotificationID;
698 // Let all players and observers receive the notification by default
699 if (!notification.players)
701 notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
702 notification.players[0] = -1;
705 this.timeNotifications.push(notification);
706 this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime);
708 cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID);
710 return this.timeNotificationID;
713 GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
715 this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID);
718 GuiInterface.prototype.GetTimeNotifications = function(player)
720 let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
721 // filter on players and time, since the delete timer might be executed with a delay
722 return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time);
725 GuiInterface.prototype.PushNotification = function(notification)
727 if (!notification.type || notification.type == "text")
728 this.AddTimeNotification(notification);
730 this.notifications.push(notification);
733 GuiInterface.prototype.GetNotifications = function()
735 let n = this.notifications;
736 this.notifications = [];
740 GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
742 return QueryPlayerIDInterface(wantedPlayer).GetFormations();
745 GuiInterface.prototype.GetFormationRequirements = function(player, data)
747 return GetFormationRequirements(data.formationTemplate);
750 GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
752 return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
755 GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
757 let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
758 let template = cmpTemplateManager.GetTemplate(data.templateName);
760 if (!template || !template.Formation)
764 "name": template.Formation.FormationName,
765 "tooltip": template.Formation.DisabledTooltip || "",
766 "icon": template.Formation.Icon
770 GuiInterface.prototype.IsFormationSelected = function(player, data)
772 for (let ent of data.ents)
774 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
775 // GetLastFormationName is named in a strange way as it (also) is
776 // the value of the current formation (see Formation.js LoadFormation)
777 if (cmpUnitAI && cmpUnitAI.GetLastFormationTemplate() == data.formationTemplate)
783 GuiInterface.prototype.IsStanceSelected = function(player, data)
785 for (let ent of data.ents)
787 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
788 if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance)
794 GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd)
796 let buildableEnts = [];
797 for (let ent of cmd.entities)
799 let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
803 for (let building of cmpBuilder.GetEntitiesList())
804 if (buildableEnts.indexOf(building) == -1)
805 buildableEnts.push(building);
807 return buildableEnts;
811 * Updates player colors on the minimap.
813 GuiInterface.prototype.UpdateDisplayedPlayerColors = function()
815 for (let ent of Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetGaiaAndNonGaiaEntities())
817 let cmpMinimap = Engine.QueryInterface(ent, IID_Minimap);
819 cmpMinimap.UpdateColor();
823 GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
825 let playerColors = {}; // cache of owner -> color map
827 for (let ent of cmd.entities)
829 let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
833 // Find the entity's owner's color:
835 let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
837 owner = cmpOwnership.GetOwner();
839 let color = playerColors[owner];
842 color = { "r":1, "g":1, "b":1 };
843 let cmpPlayer = QueryPlayerIDInterface(owner);
845 color = cmpPlayer.GetColor();
846 playerColors[owner] = color;
849 cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected);
851 let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
852 if (!cmpRangeOverlayManager || player != owner && player != -1)
855 cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false);
859 GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data)
861 this.enabledVisualRangeOverlayTypes[data.type] = data.enabled;
864 GuiInterface.prototype.GetEntitiesWithStatusBars = function()
866 return Array.from(this.entsWithAuraAndStatusBars);
869 GuiInterface.prototype.SetStatusBars = function(player, cmd)
871 let affectedEnts = new Set();
872 for (let ent of cmd.entities)
874 let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
877 cmpStatusBars.SetEnabled(cmd.enabled);
879 let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
883 for (let name of cmpAuras.GetAuraNames())
885 if (!cmpAuras.GetOverlayIcon(name))
887 for (let e of cmpAuras.GetAffectedEntities(name))
890 this.entsWithAuraAndStatusBars.add(ent);
892 this.entsWithAuraAndStatusBars.delete(ent);
896 for (let ent of affectedEnts)
898 let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
900 cmpStatusBars.RegenerateSprites();
904 GuiInterface.prototype.SetRangeOverlays = function(player, cmd)
906 for (let ent of cmd.entities)
908 let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
909 if (cmpRangeOverlayManager)
910 cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true);
914 GuiInterface.prototype.GetPlayerEntities = function(player)
916 return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player);
919 GuiInterface.prototype.GetNonGaiaEntities = function()
921 return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
925 * Displays the rally points of a given list of entities (carried in cmd.entities).
927 * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
928 * be rendered, in order to support instantaneously rendering a rally point marker at a specified location
929 * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
930 * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
931 * RallyPoint component.
933 GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
935 let cmpPlayer = QueryPlayerIDInterface(player);
937 // If there are some rally points already displayed, first hide them
938 for (let ent of this.entsRallyPointsDisplayed)
940 let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
941 if (cmpRallyPointRenderer)
942 cmpRallyPointRenderer.SetDisplayed(false);
945 this.entsRallyPointsDisplayed = [];
947 // Show the rally points for the passed entities
948 for (let ent of cmd.entities)
950 let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
951 if (!cmpRallyPointRenderer)
954 // entity must have a rally point component to display a rally point marker
955 // (regardless of whether cmd specifies a custom location)
956 let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
961 let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
962 if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
963 if (!cmpOwnership || cmpOwnership.GetOwner() != player)
966 // If the command was passed an explicit position, use that and
967 // override the real rally point position; otherwise use the real position
972 pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set
976 // Only update the position if we changed it (cmd.queued is set)
978 if (cmd.queued == true)
979 cmpRallyPointRenderer.AddPosition({ 'x': pos.x, 'y': pos.z }); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z
981 cmpRallyPointRenderer.SetPosition({ 'x': pos.x, 'y': pos.z }); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z
983 // rebuild the renderer when not set (when reading saved game or in case of building update)
984 else if (!cmpRallyPointRenderer.IsSet())
985 for (let posi of cmpRallyPoint.GetPositions())
986 cmpRallyPointRenderer.AddPosition({ 'x': posi.x, 'y': posi.z });
988 cmpRallyPointRenderer.SetDisplayed(true);
990 // remember which entities have their rally points displayed so we can hide them again
991 this.entsRallyPointsDisplayed.push(ent);
996 GuiInterface.prototype.AddTargetMarker = function(player, cmd)
998 let ent = Engine.AddLocalEntity(cmd.template);
1002 let cmpPosition = Engine.QueryInterface(ent, IID_Position);
1003 cmpPosition.JumpTo(cmd.x, cmd.z);
1007 * Display the building placement preview.
1008 * cmd.template is the name of the entity template, or "" to disable the preview.
1009 * cmd.x, cmd.z, cmd.angle give the location.
1011 * Returns result object from CheckPlacement:
1013 * "success": true iff the placement is valid, else false
1014 * "message": message to display in UI for invalid placement, else ""
1015 * "parameters": parameters to use in the message
1016 * "translateMessage": localisation info
1017 * "translateParameters": localisation info
1018 * "pluralMessage": we might return a plural translation instead (optional)
1019 * "pluralCount": localisation info (optional)
1022 GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
1028 "translateMessage": false,
1029 "translateParameters": [],
1032 // See if we're changing template
1033 if (!this.placementEntity || this.placementEntity[0] != cmd.template)
1035 // Destroy the old preview if there was one
1036 if (this.placementEntity)
1037 Engine.DestroyEntity(this.placementEntity[1]);
1039 // Load the new template
1040 if (cmd.template == "")
1041 this.placementEntity = undefined;
1043 this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
1046 if (this.placementEntity)
1048 let ent = this.placementEntity[1];
1050 // Move the preview into the right location
1051 let pos = Engine.QueryInterface(ent, IID_Position);
1054 pos.JumpTo(cmd.x, cmd.z);
1055 pos.SetYRotation(cmd.angle);
1058 let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
1059 cmpOwnership.SetOwner(player);
1061 // Check whether building placement is valid
1062 let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
1063 if (!cmpBuildRestrictions)
1064 error("cmpBuildRestrictions not defined");
1066 result = cmpBuildRestrictions.CheckPlacement();
1068 let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
1069 if (cmpRangeOverlayManager)
1070 cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes);
1072 // Set it to a red shade if this is an invalid location
1073 let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
1076 if (cmd.actorSeed !== undefined)
1077 cmpVisual.SetActorSeed(cmd.actorSeed);
1079 if (!result.success)
1080 cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
1082 cmpVisual.SetShadingColor(1, 1, 1, 1);
1090 * 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
1091 * specified. Returns an object with information about the list of entities that need to be newly constructed to complete
1092 * 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
1093 * them can be validly constructed.
1095 * It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
1096 * another depending on things like snapping and whether some of the entities inside them can be validly positioned.
1098 * - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
1099 * entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
1100 * to preview the completed tower on top of its foundation.
1102 * - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
1103 * any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
1104 * towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
1105 * snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
1108 * - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
1109 * as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
1110 * e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
1111 * constructed but come after said first invalid entity are also truncated away.
1113 * 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
1114 * were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
1115 * case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
1116 * argument (see below). Otherwise, it will return an object with the following information:
1119 * 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
1120 * 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
1121 * can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
1122 * but the wall construction was truncated before we could reach it, it won't be set here. Currently only
1124 * 'pieces': Array with the following data for each of the entities in the third list:
1126 * 'template': Template name of the entity.
1127 * 'x': X coordinate of the entity's position.
1128 * 'z': Z coordinate of the entity's position.
1129 * 'angle': Rotation around the Y axis of the entity (in radians).
1132 * 'cost': { The total cost required for constructing all the pieces as listed above.
1137 * 'population': ...,
1138 * 'populationBonus': ...,
1142 * @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
1143 * @param cmd.start Starting point of the wall segment being created.
1144 * @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
1145 * the starting point of the wall is available at this time (e.g. while the player is still in the process
1146 * of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
1148 * @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
1150 GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
1152 let wallSet = cmd.wallSet;
1157 "snapped": false, // did the start position snap to anything?
1158 "snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
1164 "snapped": false, // did the start position snap to anything?
1165 "snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
1168 // --------------------------------------------------------------------------------
1169 // do some entity cache management and check for snapping
1171 if (!this.placementWallEntities)
1172 this.placementWallEntities = {};
1176 // we're clearing the preview, clear the entity cache and bail
1177 for (let tpl in this.placementWallEntities)
1179 for (let ent of this.placementWallEntities[tpl].entities)
1180 Engine.DestroyEntity(ent);
1182 this.placementWallEntities[tpl].numUsed = 0;
1183 this.placementWallEntities[tpl].entities = [];
1184 // keep template data around
1190 // Move all existing cached entities outside of the world and reset their use count
1191 for (let tpl in this.placementWallEntities)
1193 for (let ent of this.placementWallEntities[tpl].entities)
1195 let pos = Engine.QueryInterface(ent, IID_Position);
1197 pos.MoveOutOfWorld();
1200 this.placementWallEntities[tpl].numUsed = 0;
1203 // Create cache entries for templates we haven't seen before
1204 for (let type in wallSet.templates)
1206 if (type == "curves")
1209 let tpl = wallSet.templates[type];
1210 if (!(tpl in this.placementWallEntities))
1212 this.placementWallEntities[tpl] = {
1215 "templateData": this.GetTemplateData(player, tpl),
1218 // ensure that the loaded template data contains a wallPiece component
1219 if (!this.placementWallEntities[tpl].templateData.wallPiece)
1221 error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
1227 // prevent division by zero errors further on if the start and end positions are the same
1228 if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
1229 end.pos = undefined;
1231 // 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
1232 // of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
1233 // data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
1234 if (cmd.snapEntities)
1236 let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error
1237 let startSnapData = this.GetFoundationSnapData(player, {
1240 "template": wallSet.templates.tower,
1241 "snapEntities": cmd.snapEntities,
1242 "snapRadius": snapRadius,
1247 start.pos.x = startSnapData.x;
1248 start.pos.z = startSnapData.z;
1249 start.angle = startSnapData.angle;
1250 start.snapped = true;
1252 if (startSnapData.ent)
1253 start.snappedEnt = startSnapData.ent;
1258 let endSnapData = this.GetFoundationSnapData(player, {
1261 "template": wallSet.templates.tower,
1262 "snapEntities": cmd.snapEntities,
1263 "snapRadius": snapRadius,
1268 end.pos.x = endSnapData.x;
1269 end.pos.z = endSnapData.z;
1270 end.angle = endSnapData.angle;
1273 if (endSnapData.ent)
1274 end.snappedEnt = endSnapData.ent;
1279 // clear the single-building preview entity (we'll be rolling our own)
1280 this.SetBuildingPlacementPreview(player, { "template": "" });
1282 // --------------------------------------------------------------------------------
1283 // calculate wall placement and position preview entities
1287 "cost": { "population": 0, "populationBonus": 0, "time": 0 },
1289 for (let res of Resources.GetCodes())
1290 result.cost[res] = 0;
1292 let previewEntities = [];
1294 previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js
1296 // For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
1297 // otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
1298 // an issue, because all preview entities have their obstruction components deactivated, meaning that their
1299 // obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
1300 // entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
1302 // Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
1303 // flag set), which is what we want. The only exception to this is when snapping to existing towers (or
1304 // foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
1305 // existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
1306 // we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
1307 // that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
1308 // assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
1310 // Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
1311 // constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
1312 // by the foundation it snaps to.
1314 if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
1316 let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
1317 if (previewEntities.length > 0 && startEntObstruction)
1318 previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
1320 // if we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group
1321 let startEntState = this.GetEntityState(player, start.snappedEnt);
1322 if (startEntState.foundation)
1324 let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
1326 previewEntities.unshift({
1327 "template": wallSet.templates.tower,
1329 "angle": cmpPosition.GetRotation().y,
1330 "controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined],
1331 "excludeFromResult": true, // preview only, must not appear in the result
1337 // Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
1338 // when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
1341 // To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
1342 // build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
1343 // foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
1344 // of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
1345 // the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
1346 // onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
1347 // which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
1348 // the foundation's angle.
1350 // The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
1351 // the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
1352 previewEntities.unshift({
1353 "template": wallSet.templates.tower,
1355 "angle": previewEntities.length > 0 ? previewEntities[0].angle : this.placementWallLastAngle
1361 // Analogous to the starting side case above
1362 if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
1364 let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
1366 // Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
1367 // same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
1368 // expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
1369 // the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
1370 // '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
1371 if (previewEntities.length > 0 && endEntObstruction)
1373 previewEntities[previewEntities.length-1].controlGroups = previewEntities[previewEntities.length-1].controlGroups || [];
1374 previewEntities[previewEntities.length-1].controlGroups.push(endEntObstruction.GetControlGroup());
1377 // if we're snapping to a foundation, add an extra preview tower and also set it to the same control group
1378 let endEntState = this.GetEntityState(player, end.snappedEnt);
1379 if (endEntState.foundation)
1381 let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
1383 previewEntities.push({
1384 "template": wallSet.templates.tower,
1386 "angle": cmpPosition.GetRotation().y,
1387 "controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined],
1388 "excludeFromResult": true
1393 previewEntities.push({
1394 "template": wallSet.templates.tower,
1396 "angle": previewEntities.length > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle
1400 let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
1403 error("[SetWallPlacementPreview] System Terrain component not found");
1407 let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
1408 if (!cmpRangeManager)
1410 error("[SetWallPlacementPreview] System RangeManager component not found");
1414 // Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
1415 // to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
1416 // but cannot validly be, constructed). See method-level documentation for more details.
1418 let allPiecesValid = true;
1419 let numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity
1421 for (let i = 0; i < previewEntities.length; ++i)
1423 let entInfo = previewEntities[i];
1426 let tpl = entInfo.template;
1427 let tplData = this.placementWallEntities[tpl].templateData;
1428 let entPool = this.placementWallEntities[tpl];
1430 if (entPool.numUsed >= entPool.entities.length)
1432 // allocate new entity
1433 ent = Engine.AddLocalEntity("preview|" + tpl);
1434 entPool.entities.push(ent);
1437 // reuse an existing one
1438 ent = entPool.entities[entPool.numUsed];
1442 error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
1446 // move piece to right location
1447 // TODO: consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities
1448 let cmpPosition = Engine.QueryInterface(ent, IID_Position);
1451 cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
1452 cmpPosition.SetYRotation(entInfo.angle);
1454 // if this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces
1455 if (tpl === wallSet.templates.tower)
1457 let terrainGroundPrev = null;
1458 let terrainGroundNext = null;
1461 terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i-1].pos.x, previewEntities[i-1].pos.z);
1463 if (i < previewEntities.length - 1)
1464 terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i+1].pos.x, previewEntities[i+1].pos.z);
1466 if (terrainGroundPrev != null || terrainGroundNext != null)
1468 let targetY = Math.max(terrainGroundPrev, terrainGroundNext);
1469 cmpPosition.SetHeightFixed(targetY);
1474 let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
1475 if (!cmpObstruction)
1477 error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
1481 // Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
1482 // more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
1483 // first-come first-served basis; the first value in the array is always assigned as the primary control group, and
1484 // any second value as the secondary control group.
1486 // By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
1487 // reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
1488 // reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
1491 let primaryControlGroup = ent;
1492 let secondaryControlGroup = INVALID_ENTITY;
1494 if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
1496 if (entInfo.controlGroups.length > 2)
1498 error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
1502 primaryControlGroup = entInfo.controlGroups[0];
1503 if (entInfo.controlGroups.length > 1)
1504 secondaryControlGroup = entInfo.controlGroups[1];
1507 cmpObstruction.SetControlGroup(primaryControlGroup);
1508 cmpObstruction.SetControlGroup2(secondaryControlGroup);
1510 // check whether this wall piece can be validly positioned here
1511 let validPlacement = false;
1513 let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
1514 cmpOwnership.SetOwner(player);
1516 // Check whether it's in a visible or fogged region
1517 // TODO: should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta
1518 let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden";
1521 let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
1522 if (!cmpBuildRestrictions)
1524 error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
1528 // TODO: Handle results of CheckPlacement
1529 validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success;
1531 // If a wall piece has two control groups, it's likely a segment that spans
1532 // between two existing towers. To avoid placing a duplicate wall segment,
1533 // check for collisions with entities that share both control groups.
1534 if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1)
1535 validPlacement = cmpObstruction.CheckDuplicateFoundation();
1538 allPiecesValid = allPiecesValid && validPlacement;
1540 // The requirement below that all pieces so far have to have valid positions, rather than only this single one,
1541 // ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
1542 // for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
1543 // through and past an existing building).
1545 // Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
1546 // on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
1548 if (!entInfo.excludeFromResult)
1549 ++numRequiredPieces;
1551 if (allPiecesValid && !entInfo.excludeFromResult)
1553 result.pieces.push({
1557 "angle": entInfo.angle,
1559 this.placementWallLastAngle = entInfo.angle;
1561 // grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
1562 // copied over, so we need to fetch it from the template instead).
1563 // TODO: we should really use a Cost object or at least some utility functions for this, this is mindless
1564 // boilerplate that's probably duplicated in tons of places.
1565 for (let res of Resources.GetCodes().concat(["population", "populationBonus", "time"]))
1566 result.cost[res] += tplData.cost[res];
1569 let canAfford = true;
1570 let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
1571 if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
1574 let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
1577 if (!allPiecesValid || !canAfford)
1578 cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
1580 cmpVisual.SetShadingColor(1, 1, 1, 1);
1586 // If any were entities required to build the wall, but none of them could be validly positioned, return failure
1587 // (see method-level documentation).
1588 if (numRequiredPieces > 0 && result.pieces.length == 0)
1591 if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
1592 result.startSnappedEnt = start.snappedEnt;
1594 // We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
1595 // i.e. are included in result.pieces (see docs for the result object).
1596 if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
1597 result.endSnappedEnt = end.snappedEnt;
1603 * Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
1604 * it to (if necessary/useful).
1606 * @param data.x The X position of the foundation to snap.
1607 * @param data.z The Z position of the foundation to snap.
1608 * @param data.template The template to get the foundation snapping data for.
1609 * @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
1610 * around the entity. Only takes effect when used in conjunction with data.snapRadius.
1611 * When this option is used and the foundation is found to snap to one of the entities passed in this list
1612 * (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
1613 * holding the ID of the entity that was snapped to.
1614 * @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
1615 * {data.x, data.z} must be located within to have it snap to that entity.
1617 GuiInterface.prototype.GetFoundationSnapData = function(player, data)
1619 let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template);
1622 warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
1626 if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
1628 // 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
1629 // (TODO: break unlikely ties by choosing the lowest entity ID)
1632 let minDistEntitySnapData = null;
1633 let radius2 = data.snapRadius * data.snapRadius;
1635 for (let ent of data.snapEntities)
1637 let cmpPosition = Engine.QueryInterface(ent, IID_Position);
1638 if (!cmpPosition || !cmpPosition.IsInWorld())
1641 let pos = cmpPosition.GetPosition();
1642 let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
1643 if (dist2 > radius2)
1646 if (minDist2 < 0 || dist2 < minDist2)
1649 minDistEntitySnapData = {
1652 "angle": cmpPosition.GetRotation().y,
1658 if (minDistEntitySnapData != null)
1659 return minDistEntitySnapData;
1662 if (template.BuildRestrictions.PlacementType == "shore")
1664 let angle = GetDockAngle(template, data.x, data.z);
1665 if (angle !== undefined)
1676 GuiInterface.prototype.PlaySound = function(player, data)
1681 PlaySound(data.name, data.entity);
1685 * Find any idle units.
1687 * @param data.idleClasses Array of class names to include.
1688 * @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined.
1689 * @param data.limit The number of idle units to return. May be left undefined (will return all idle units).
1690 * @param data.excludeUnits Array of units to exclude.
1692 * Returns an array of idle units.
1693 * If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class.
1695 GuiInterface.prototype.FindIdleUnits = function(player, data)
1698 // The general case is that only the 'first' idle unit is required; filtering would examine every unit.
1699 // This loop imitates a grouping/aggregation on the first matching idle class.
1700 let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
1701 for (let entity of cmpRangeManager.GetEntitiesByPlayer(player))
1703 let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits);
1707 // If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any.
1708 // By adding to the 'end', there is no pause if the series of units loops.
1709 var bucket = filtered.bucket;
1710 if(bucket == 0 && data.prevUnit && entity <= data.prevUnit)
1711 bucket = data.idleClasses.length;
1713 if (!idleUnits[bucket])
1714 idleUnits[bucket] = [];
1715 idleUnits[bucket].push(entity);
1717 // If enough units have been collected in the first bucket, go ahead and return them.
1718 if (data.limit && bucket == 0 && idleUnits[0].length == data.limit)
1719 return idleUnits[0];
1722 let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []);
1723 if (data.limit && reduced.length > data.limit)
1724 return reduced.slice(0, data.limit);
1730 * Discover if the player has idle units.
1732 * @param data.idleClasses Array of class names to include.
1733 * @param data.excludeUnits Array of units to exclude.
1735 * Returns a boolean of whether the player has any idle units
1737 GuiInterface.prototype.HasIdleUnits = function(player, data)
1739 let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
1740 return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle);
1744 * Whether to filter an idle unit
1746 * @param unit The unit to filter.
1747 * @param idleclasses Array of class names to include.
1748 * @param excludeUnits Array of units to exclude.
1750 * Returns an object with the following fields:
1751 * - idle - true if the unit is considered idle by the filter, false otherwise.
1752 * - bucket - if idle, set to the index of the first matching idle class, undefined otherwise.
1754 GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits)
1756 let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
1757 if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned())
1758 return { "idle": false };
1760 let cmpIdentity = Engine.QueryInterface(unit, IID_Identity);
1762 return { "idle": false };
1764 let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem));
1765 if (bucket == -1 || excludeUnits.indexOf(unit) > -1)
1766 return { "idle": false };
1768 return { "idle": true, "bucket": bucket };
1771 GuiInterface.prototype.GetTradingRouteGain = function(player, data)
1773 if (!data.firstMarket || !data.secondMarket)
1776 return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template);
1779 GuiInterface.prototype.GetTradingDetails = function(player, data)
1781 let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
1782 if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
1785 let firstMarket = cmpEntityTrader.GetFirstMarket();
1786 let secondMarket = cmpEntityTrader.GetSecondMarket();
1788 if (data.target === firstMarket)
1792 "hasBothMarkets": cmpEntityTrader.HasBothMarkets()
1794 if (cmpEntityTrader.HasBothMarkets())
1795 result.gain = cmpEntityTrader.GetGoods().amount;
1797 else if (data.target === secondMarket)
1800 "type": "is second",
1801 "gain": cmpEntityTrader.GetGoods().amount,
1804 else if (!firstMarket)
1806 result = { "type": "set first" };
1808 else if (!secondMarket)
1811 "type": "set second",
1812 "gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
1817 // Else both markets are not null and target is different from them
1818 result = { "type": "set first" };
1823 GuiInterface.prototype.CanAttack = function(player, data)
1825 let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
1826 return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
1830 * Returns batch build time.
1832 GuiInterface.prototype.GetBatchTime = function(player, data)
1834 let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
1835 if (!cmpProductionQueue)
1838 return cmpProductionQueue.GetBatchTime(data.batchSize);
1841 GuiInterface.prototype.IsMapRevealed = function(player)
1843 return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player);
1846 GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
1848 Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled);
1851 GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled)
1853 Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled);
1856 GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
1858 Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled);
1861 GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
1863 for (let ent of data.entities)
1865 let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
1867 cmpUnitMotion.SetDebugOverlay(data.enabled);
1871 GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
1873 Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled);
1876 GuiInterface.prototype.GetTraderNumber = function(player)
1878 let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
1879 let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader));
1881 let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
1882 let shipTrader = { "total": 0, "trading": 0 };
1884 for (let ent of traders)
1886 let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
1887 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
1888 if (!cmpIdentity || !cmpUnitAI)
1891 if (cmpIdentity.HasClass("Ship"))
1894 if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
1895 ++shipTrader.trading;
1900 if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
1901 ++landTrader.trading;
1902 if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
1904 let holder = cmpUnitAI.order.data.target;
1905 let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
1906 if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
1907 ++landTrader.garrisoned;
1912 return { "landTrader": landTrader, "shipTrader": shipTrader };
1915 GuiInterface.prototype.GetTradingGoods = function(player)
1917 return QueryPlayerIDInterface(player).GetTradingGoods();
1920 GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
1922 this.renamedEntities.push(msg);
1925 // List the GuiInterface functions that can be safely called by GUI scripts.
1926 // (GUI scripts are non-deterministic and untrusted, so these functions must be
1927 // appropriately careful. They are called with a first argument "player", which is
1928 // trusted and indicates the player associated with the current client; no data should
1929 // be returned unless this player is meant to be able to see it.)
1930 let exposedFunctions = {
1932 "GetSimulationState": 1,
1933 "GetExtendedSimulationState": 1,
1934 "GetRenamedEntities": 1,
1935 "ClearRenamedEntities": 1,
1936 "GetEntityState": 1,
1937 "GetMultipleEntityStates": 1,
1938 "GetAverageRangeForBuildings": 1,
1939 "GetTemplateData": 1,
1940 "IsTechnologyResearched": 1,
1941 "CheckTechnologyRequirements": 1,
1942 "GetStartedResearch": 1,
1943 "GetBattleState": 1,
1944 "GetIncomingAttacks": 1,
1945 "GetNeededResources": 1,
1946 "GetNotifications": 1,
1947 "GetTimeNotifications": 1,
1949 "GetAvailableFormations": 1,
1950 "GetFormationRequirements": 1,
1951 "CanMoveEntsIntoFormation": 1,
1952 "IsFormationSelected": 1,
1953 "GetFormationInfoFromTemplate": 1,
1954 "IsStanceSelected": 1,
1956 "UpdateDisplayedPlayerColors": 1,
1957 "SetSelectionHighlight": 1,
1958 "GetAllBuildableEntities": 1,
1960 "GetPlayerEntities": 1,
1961 "GetNonGaiaEntities": 1,
1962 "DisplayRallyPoint": 1,
1963 "AddTargetMarker": 1,
1964 "SetBuildingPlacementPreview": 1,
1965 "SetWallPlacementPreview": 1,
1966 "GetFoundationSnapData": 1,
1970 "GetTradingRouteGain": 1,
1971 "GetTradingDetails": 1,
1976 "SetPathfinderDebugOverlay": 1,
1977 "SetPathfinderHierDebugOverlay": 1,
1978 "SetObstructionDebugOverlay": 1,
1979 "SetMotionDebugOverlay": 1,
1980 "SetRangeDebugOverlay": 1,
1981 "EnableVisualRangeOverlayType": 1,
1982 "SetRangeOverlays": 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);
1993 throw new Error("Invalid GuiInterface Call name \""+name+"\"");
1996 Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);